From 9086938f7bd6d6ccb7f4a30fb78aeaf0d84e4471 Mon Sep 17 00:00:00 2001 From: Famiu Haque Date: Tue, 19 Oct 2021 01:44:17 +0600 Subject: [PATCH] feat(api): evaluate statusline string #16020 Adds API function `nvim_eval_statusline` to allow evaluating a statusline string and obtaining information regarding it. Closes https://github.com/neovim/neovim/issues/15849 --- src/nvim/api/keysets.lua | 7 ++ src/nvim/api/private/helpers.c | 12 +++ src/nvim/api/vim.c | 163 +++++++++++++++++++++++++++++++ src/nvim/screen.c | 2 +- test/functional/api/vim_spec.lua | 75 ++++++++++++++ 5 files changed, 258 insertions(+), 1 deletion(-) diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index a430d56168..144c252687 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -51,5 +51,12 @@ return { runtime = { "is_lua"; }; + eval_statusline = { + "winid"; + "maxwidth"; + "fillchar"; + "highlights"; + "use_tabline"; + }; } diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index b2529db5be..f1259c8a18 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1681,3 +1681,15 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err) } return res; } + +/// Get default statusline highlight for window +const char *get_default_stl_hl(win_T *wp) +{ + if (wp == NULL) { + return "TabLineFill"; + } else if (wp == curwin) { + return "StatusLine"; + } else { + return "StatusLineNC"; + } +} diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 847e142939..a3143efe0c 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -17,6 +17,7 @@ #include "nvim/api/window.h" #include "nvim/ascii.h" #include "nvim/buffer.h" +#include "nvim/buffer_defs.h" #include "nvim/context.h" #include "nvim/decoration.h" #include "nvim/edit.h" @@ -2906,3 +2907,165 @@ Array nvim_get_mark(String name, Error *err) return rv; } +/// Evaluates statusline string. +/// +/// @param str Statusline string (see 'statusline'). +/// @param opts Optional parameters. +/// - winid: (number) |window-ID| of the window to use as context for statusline. +/// - maxwidth: (number) Maximum width of statusline. +/// - fillchar: (string) Character to fill blank spaces in the statusline (see +/// 'fillchars'). +/// - highlights: (boolean) Return highlight information. +/// - use_tabline: (boolean) Evaluate tabline instead of statusline. When |TRUE|, {winid} +/// is ignored. +/// +/// @param[out] err Error details, if any. +/// @return Dictionary containing statusline information, with these keys: +/// - str: (string) Characters that will be displayed on the statusline. +/// - width: (number) Display width of the statusline. +/// - highlights: Array containing highlight information of the statusline. Only included when +/// the "highlights" key in {opts} is |TRUE|. Each element of the array is a +/// |Dictionary| with these keys: +/// - start: (number) Byte index (0-based) of first character that uses the highlight. +/// - group: (string) Name of highlight group. +Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error *err) + FUNC_API_SINCE(8) FUNC_API_FAST +{ + Dictionary result = ARRAY_DICT_INIT; + + Window window = 0; + int maxwidth = 0; + char fillchar = 0; + bool use_tabline = false; + bool highlights = false; + + if (HAS_KEY(opts->winid)) { + if (opts->winid.type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, "winid must be an integer"); + return result; + } + + window = (Window)opts->winid.data.integer; + } + + if (HAS_KEY(opts->maxwidth)) { + if (opts->maxwidth.type != kObjectTypeInteger) { + api_set_error(err, kErrorTypeValidation, "maxwidth must be an integer"); + return result; + } + + maxwidth = (int)opts->maxwidth.data.integer; + } + + if (HAS_KEY(opts->fillchar)) { + if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size > 1) { + api_set_error(err, kErrorTypeValidation, "fillchar must be an ASCII character"); + return result; + } + + fillchar = opts->fillchar.data.string.data[0]; + } + + if (HAS_KEY(opts->highlights)) { + highlights = api_object_to_bool(opts->highlights, "highlights", false, err); + + if (ERROR_SET(err)) { + return result; + } + } + + if (HAS_KEY(opts->use_tabline)) { + use_tabline = api_object_to_bool(opts->use_tabline, "use_tabline", false, err); + + if (ERROR_SET(err)) { + return result; + } + } + + win_T *wp, *ewp; + + if (use_tabline) { + wp = NULL; + ewp = curwin; + fillchar = ' '; + } else { + wp = find_window_by_handle(window, err); + ewp = wp; + + if (fillchar == 0) { + int attr; + fillchar = (char)fillchar_status(&attr, wp); + } + } + + if (maxwidth == 0) { + maxwidth = use_tabline ? Columns : wp->w_width; + } + + char buf[MAXPATHL]; + stl_hlrec_t *hltab; + stl_hlrec_t **hltab_ptr = highlights ? &hltab : NULL; + + // Temporarily reset 'cursorbind' to prevent side effects from moving the cursor away and back. + int p_crb_save = ewp->w_p_crb; + ewp->w_p_crb = false; + + int width = build_stl_str_hl( + ewp, + (char_u *)buf, + sizeof(buf), + (char_u *)str.data, + false, + (char_u)fillchar, + maxwidth, + hltab_ptr, + NULL); + + PUT(result, "width", INTEGER_OBJ(width)); + + // Restore original value of 'cursorbind' + ewp->w_p_crb = p_crb_save; + + if (highlights) { + Array hl_values = ARRAY_DICT_INIT; + const char *grpname; + char user_group[6]; + + // If first character doesn't have a defined highlight, + // add the default highlight at the beginning of the highlight list + if (hltab->start == NULL || ((char *)hltab->start - buf) != 0) { + Dictionary hl_info = ARRAY_DICT_INIT; + grpname = get_default_stl_hl(wp); + + PUT(hl_info, "start", INTEGER_OBJ(0)); + PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); + + ADD(hl_values, DICTIONARY_OBJ(hl_info)); + } + + for (stl_hlrec_t *sp = hltab; sp->start != NULL; sp++) { + Dictionary hl_info = ARRAY_DICT_INIT; + + PUT(hl_info, "start", INTEGER_OBJ((char *)sp->start - buf)); + + if (sp->userhl == 0) { + grpname = get_default_stl_hl(wp); + } else if (sp->userhl < 0) { + grpname = (char *)syn_id2name(-sp->userhl); + } else { + snprintf(user_group, sizeof(user_group), "User%d", sp->userhl); + grpname = user_group; + } + + PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); + + ADD(hl_values, DICTIONARY_OBJ(hl_info)); + } + + PUT(result, "highlights", ARRAY_OBJ(hl_values)); + } + + PUT(result, "str", CSTR_TO_OBJ((char *)buf)); + + return result; +} diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 63f3267d8a..c65fd3a90b 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -7379,7 +7379,7 @@ void get_trans_bufname(buf_T *buf) /* * Get the character to use in a status line. Get its attributes in "*attr". */ -static int fillchar_status(int *attr, win_T *wp) +int fillchar_status(int *attr, win_T *wp) { int fill; bool is_curwin = (wp == curwin); diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index d8914a3ab7..f030cfe00e 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -2367,4 +2367,79 @@ describe('API', function() eq({2, 2, buf.id, mark[4]}, mark) end) end) + describe('nvim_eval_statusline', function() + it('works', function() + eq({ + str = '%StatusLineStringWithHighlights', + width = 31 + }, + meths.eval_statusline( + '%%StatusLineString%#WarningMsg#WithHighlights', + {})) + end) + it('doesn\'t exceed maxwidth', function() + eq({ + str = 'Should be trun>', + width = 15 + }, + meths.eval_statusline( + 'Should be truncated%<', + { maxwidth = 15 })) + end) + describe('highlight parsing', function() + it('works', function() + eq({ + str = "TextWithWarningHighlightTextWithUserHighlight", + width = 45, + highlights = { + { start = 0, group = 'WarningMsg' }, + { start = 24, group = 'User1' } + }, + }, + meths.eval_statusline( + '%#WarningMsg#TextWithWarningHighlight%1*TextWithUserHighlight', + { highlights = true })) + end) + it('works with no highlight', function() + eq({ + str = "TextWithNoHighlight", + width = 19, + highlights = { + { start = 0, group = 'StatusLine' }, + }, + }, + meths.eval_statusline( + 'TextWithNoHighlight', + { highlights = true })) + end) + it('works with inactive statusline', function() + command('split') + + eq({ + str = 'TextWithNoHighlightTextWithWarningHighlight', + width = 43, + highlights = { + { start = 0, group = 'StatusLineNC' }, + { start = 19, group = 'WarningMsg' } + } + }, + meths.eval_statusline( + 'TextWithNoHighlight%#WarningMsg#TextWithWarningHighlight', + { winid = meths.list_wins()[2].id, highlights = true })) + end) + it('works with tabline', function() + eq({ + str = 'TextWithNoHighlightTextWithWarningHighlight', + width = 43, + highlights = { + { start = 0, group = 'TabLineFill' }, + { start = 19, group = 'WarningMsg' } + } + }, + meths.eval_statusline( + 'TextWithNoHighlight%#WarningMsg#TextWithWarningHighlight', + { use_tabline = true, highlights = true })) + end) + end) + end) end)