diff options
Diffstat (limited to 'src/nvim/api')
-rw-r--r-- | src/nvim/api/autocmd.c | 10 | ||||
-rw-r--r-- | src/nvim/api/buffer.c | 34 | ||||
-rw-r--r-- | src/nvim/api/keysets.lua | 41 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 145 | ||||
-rw-r--r-- | src/nvim/api/ui.c | 6 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 105 | ||||
-rw-r--r-- | src/nvim/api/vimscript.c | 395 |
7 files changed, 670 insertions, 66 deletions
diff --git a/src/nvim/api/autocmd.c b/src/nvim/api/autocmd.c index 76e531e7aa..30a5d60c39 100644 --- a/src/nvim/api/autocmd.c +++ b/src/nvim/api/autocmd.c @@ -405,6 +405,7 @@ cleanup: /// - match: (string) the expanded value of |<amatch>| /// - buf: (number) the expanded value of |<abuf>| /// - file: (string) the expanded value of |<afile>| +/// - data: (any) arbitrary data passed to |nvim_exec_autocmds()| /// - command (string) optional: Vim command to execute on event. Cannot be used with /// {callback} /// - once (boolean) optional: defaults to false. Run the autocommand @@ -749,6 +750,8 @@ void nvim_del_augroup_by_name(String name, Error *err) /// {pattern}. /// - modeline (bool) optional: defaults to true. Process the /// modeline after the autocommands |<nomodeline>|. +/// - data (any): arbitrary data to send to the autocommand callback. See +/// |nvim_create_autocmd()| for details. /// @see |:doautocmd| void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) FUNC_API_SINCE(9) @@ -760,6 +763,7 @@ void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) bool set_buf = false; char *pattern = NULL; + Object *data = NULL; bool set_pattern = false; Array event_array = ARRAY_DICT_INIT; @@ -818,6 +822,10 @@ void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) set_pattern = true; } + if (opts->data.type != kObjectTypeNil) { + data = &opts->data; + } + modeline = api_object_to_bool(opts->modeline, "modeline", true, err); if (set_pattern && set_buf) { @@ -829,7 +837,7 @@ void nvim_exec_autocmds(Object event, Dict(exec_autocmds) *opts, Error *err) FOREACH_ITEM(event_array, event_str, { GET_ONE_EVENT(event_nr, event_str, cleanup) - did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL); + did_aucmd |= apply_autocmds_group(event_nr, pattern, NULL, true, au_group, buf, NULL, data); }) if (did_aucmd && modeline) { diff --git a/src/nvim/api/buffer.c b/src/nvim/api/buffer.c index 45dadae1dd..9842975d62 100644 --- a/src/nvim/api/buffer.c +++ b/src/nvim/api/buffer.c @@ -262,7 +262,7 @@ void nvim__buf_redraw_range(Buffer buffer, Integer first, Integer last, Error *e /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start First line index -/// @param end Last line index (exclusive) +/// @param end Last line index, exclusive /// @param strict_indexing Whether out-of-bounds should be an error. /// @param[out] err Error details, if any /// @return Array of lines, or empty array for unloaded buffer. @@ -358,7 +358,7 @@ static bool check_string_array(Array arr, bool disallow_nl, Error *err) /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start First line index -/// @param end Last line index (exclusive) +/// @param end Last line index, exclusive /// @param strict_indexing Whether out-of-bounds should be an error. /// @param replacement Array of lines to use as replacement /// @param[out] err Error details, if any @@ -514,24 +514,25 @@ end: /// Sets (replaces) a range in the buffer /// -/// This is recommended over nvim_buf_set_lines when only modifying parts of a -/// line, as extmarks will be preserved on non-modified parts of the touched +/// This is recommended over |nvim_buf_set_lines()| when only modifying parts of +/// a line, as extmarks will be preserved on non-modified parts of the touched /// lines. /// -/// Indexing is zero-based and end-exclusive. +/// Indexing is zero-based. Row indices are end-inclusive, and column indices +/// are end-exclusive. /// -/// To insert text at a given index, set `start` and `end` ranges to the same -/// index. To delete a range, set `replacement` to an array containing -/// an empty string, or simply an empty array. +/// To insert text at a given `(row, column)` location, use `start_row = end_row +/// = row` and `start_col = end_col = col`. To delete the text in a range, use +/// `replacement = {}`. /// -/// Prefer nvim_buf_set_lines when adding or deleting entire lines only. +/// Prefer |nvim_buf_set_lines()| if you are only adding or deleting entire lines. /// /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start_row First line index -/// @param start_col First column -/// @param end_row Last line index -/// @param end_col Last column +/// @param start_col Starting column (byte offset) on first line +/// @param end_row Last line index, inclusive +/// @param end_col Ending column (byte offset) on last line, exclusive /// @param replacement Array of lines to use as replacement /// @param[out] err Error details, if any void nvim_buf_set_text(uint64_t channel_id, Buffer buffer, Integer start_row, Integer start_col, @@ -760,16 +761,17 @@ end: /// This differs from |nvim_buf_get_lines()| in that it allows retrieving only /// portions of a line. /// -/// Indexing is zero-based. Column indices are end-exclusive. +/// Indexing is zero-based. Row indices are end-inclusive, and column indices +/// are end-exclusive. /// /// Prefer |nvim_buf_get_lines()| when retrieving entire lines. /// /// @param channel_id /// @param buffer Buffer handle, or 0 for current buffer /// @param start_row First line index -/// @param start_col Starting byte offset of first line -/// @param end_row Last line index -/// @param end_col Ending byte offset of last line (exclusive) +/// @param start_col Starting column (byte offset) on first line +/// @param end_row Last line index, inclusive +/// @param end_col Ending column (byte offset) on last line, exclusive /// @param opts Optional parameters. Currently unused. /// @param[out] err Error details, if any /// @return Array of lines, or empty array for unloaded buffer. diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index 5baffaf505..6924e2ef8f 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -80,6 +80,7 @@ return { "maxwidth"; "fillchar"; "highlights"; + "use_winbar"; "use_tabline"; }; option = { @@ -145,6 +146,7 @@ return { "group"; "modeline"; "pattern"; + "data"; }; get_autocmds = { "event"; @@ -155,5 +157,44 @@ return { create_augroup = { "clear"; }; + cmd = { + "cmd"; + "range"; + "count"; + "reg"; + "bang"; + "args"; + "magic"; + "mods"; + "nargs"; + "addr"; + "nextcmd"; + }; + cmd_magic = { + "file"; + "bar"; + }; + cmd_mods = { + "silent"; + "emsg_silent"; + "sandbox"; + "noautocmd"; + "browse"; + "confirm"; + "hide"; + "keepalt"; + "keepjumps"; + "keepmarks"; + "keeppatterns"; + "lockmarks"; + "noswapfile"; + "tab"; + "verbose"; + "vertical"; + "split"; + }; + cmd_opts = { + "output"; + }; } diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index a2ee4f91bc..7bd68f277b 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -19,6 +19,7 @@ #include "nvim/decoration.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" +#include "nvim/ex_cmds_defs.h" #include "nvim/extmark.h" #include "nvim/fileio.h" #include "nvim/getchar.h" @@ -657,8 +658,7 @@ void modify_keymap(uint64_t channel_id, Buffer buffer, bool is_unmap, String mod mode_val = get_map_mode(&p, true); // mapmode-ic } else { mode_val = get_map_mode(&p, false); - if ((mode_val == VISUAL + SELECTMODE + NORMAL + OP_PENDING) - && mode.size > 0) { + if (mode_val == (MODE_VISUAL | MODE_SELECT | MODE_NORMAL | MODE_OP_PENDING) && mode.size > 0) { // get_map_mode() treats unrecognized mode shortnames as ":map". // This is an error unless the given shortname was empty string "". api_set_error(err, kErrorTypeValidation, "Invalid mode shortname: \"%s\"", p); @@ -1418,14 +1418,14 @@ bool set_mark(buf_T *buf, String name, Integer line, Integer col, Error *err) } /// Get default statusline highlight for window -const char *get_default_stl_hl(win_T *wp) +const char *get_default_stl_hl(win_T *wp, bool use_winbar) { if (wp == NULL) { return "TabLineFill"; - } else if (wp == curwin) { - return "StatusLine"; + } else if (use_winbar) { + return (wp == curwin) ? "WinBar" : "WinBarNC"; } else { - return "StatusLineNC"; + return (wp == curwin) ? "StatusLine" : "StatusLineNC"; } } @@ -1691,3 +1691,136 @@ int init_sign_text(char **sign_text, char *text) return OK; } + +/// Check if a string contains only whitespace characters. +bool string_iswhite(String str) +{ + for (size_t i = 0; i < str.size; i++) { + if (!ascii_iswhite(str.data[i])) { + // Found a non-whitespace character + return false; + } else if (str.data[i] == NUL) { + // Terminate at first occurence of a NUL character + break; + } + } + return true; +} + +/// Build cmdline string for command, used by `nvim_cmd()`. +/// +/// @return OK or FAIL. +void build_cmdline_str(char **cmdlinep, exarg_T *eap, CmdParseInfo *cmdinfo, char **args, + size_t argc) +{ + StringBuilder cmdline = KV_INITIAL_VALUE; + + // Add command modifiers + if (cmdinfo->cmdmod.tab != 0) { + kv_printf(cmdline, "%dtab ", cmdinfo->cmdmod.tab - 1); + } + if (cmdinfo->verbose != -1) { + kv_printf(cmdline, "%ldverbose ", cmdinfo->verbose); + } + + if (cmdinfo->emsg_silent) { + kv_concat(cmdline, "silent! "); + } else if (cmdinfo->silent) { + kv_concat(cmdline, "silent "); + } + + switch (cmdinfo->cmdmod.split & (WSP_ABOVE | WSP_BELOW | WSP_TOP | WSP_BOT)) { + case WSP_ABOVE: + kv_concat(cmdline, "aboveleft "); + break; + case WSP_BELOW: + kv_concat(cmdline, "belowright "); + break; + case WSP_TOP: + kv_concat(cmdline, "topleft "); + break; + case WSP_BOT: + kv_concat(cmdline, "botright "); + break; + default: + break; + } + +#define CMDLINE_APPEND_IF(cond, str) \ + do { \ + if (cond) { \ + kv_concat(cmdline, str); \ + } \ + } while (0) + + CMDLINE_APPEND_IF(cmdinfo->cmdmod.split & WSP_VERT, "vertical "); + CMDLINE_APPEND_IF(cmdinfo->sandbox, "sandbox "); + CMDLINE_APPEND_IF(cmdinfo->noautocmd, "noautocmd "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.browse, "browse "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.confirm, "confirm "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.hide, "hide "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.keepalt, "keepalt "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.keepjumps, "keepjumps "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.keepmarks, "keepmarks "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.keeppatterns, "keeppatterns "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.lockmarks, "lockmarks "); + CMDLINE_APPEND_IF(cmdinfo->cmdmod.noswapfile, "noswapfile "); +#undef CMDLINE_APPEND_IF + + // Command range / count. + if (eap->argt & EX_RANGE) { + if (eap->addr_count == 1) { + kv_printf(cmdline, "%ld", eap->line2); + } else if (eap->addr_count > 1) { + kv_printf(cmdline, "%ld,%ld", eap->line1, eap->line2); + eap->addr_count = 2; // Make sure address count is not greater than 2 + } + } + + // Keep the index of the position where command name starts, so eap->cmd can point to it. + size_t cmdname_idx = cmdline.size; + kv_printf(cmdline, "%s", eap->cmd); + + // Command bang. + if (eap->argt & EX_BANG && eap->forceit) { + kv_printf(cmdline, "!"); + } + + // Command register. + if (eap->argt & EX_REGSTR && eap->regname) { + kv_printf(cmdline, " %c", eap->regname); + } + + // Iterate through each argument and store the starting index and length of each argument + size_t *argidx = xcalloc(argc, sizeof(size_t)); + eap->argc = argc; + eap->arglens = xcalloc(argc, sizeof(size_t)); + for (size_t i = 0; i < argc; i++) { + argidx[i] = cmdline.size + 1; // add 1 to account for the space. + eap->arglens[i] = STRLEN(args[i]); + kv_printf(cmdline, " %s", args[i]); + } + + // Now that all the arguments are appended, use the command index and argument indices to set the + // values of eap->cmd, eap->arg and eap->args. + eap->cmd = cmdline.items + cmdname_idx; + eap->args = xcalloc(argc, sizeof(char *)); + for (size_t i = 0; i < argc; i++) { + eap->args[i] = cmdline.items + argidx[i]; + } + // If there isn't an argument, make eap->arg point to end of cmdline. + eap->arg = argc > 0 ? eap->args[0] : cmdline.items + cmdline.size; + + // Finally, make cmdlinep point to the cmdline string. + *cmdlinep = cmdline.items; + xfree(argidx); + + // Replace, :make and :grep with 'makeprg' and 'grepprg'. + char *p = replace_makeprg(eap, eap->arg, cmdlinep); + if (p != eap->arg) { + // If replace_makeprg modified the cmdline string, correct the argument pointers. + assert(argc == 1); + eap->arg = p; + eap->args[0] = p; + } +} diff --git a/src/nvim/api/ui.c b/src/nvim/api/ui.c index b4d20ed975..52f76f4650 100644 --- a/src/nvim/api/ui.c +++ b/src/nvim/api/ui.c @@ -349,7 +349,11 @@ void nvim_ui_try_resize_grid(uint64_t channel_id, Integer grid, Integer width, I return; } - ui_grid_resize((handle_T)grid, (int)width, (int)height, err); + if (grid == DEFAULT_GRID_HANDLE) { + nvim_ui_try_resize(channel_id, width, height, err); + } else { + ui_grid_resize((handle_T)grid, (int)width, (int)height, err); + } } /// Tells Nvim the number of elements displaying in the popumenu, to decide diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 5e7e51762a..15992a98be 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -126,22 +126,43 @@ Dictionary nvim__get_hl_defs(Integer ns_id, Error *err) /// Sets a highlight group. /// -/// Note: Unlike the `:highlight` command which can update a highlight group, -/// this function completely replaces the definition. For example: -/// ``nvim_set_hl(0, 'Visual', {})`` will clear the highlight group 'Visual'. +/// @note Unlike the `:highlight` command which can update a highlight group, +/// this function completely replaces the definition. For example: +/// ``nvim_set_hl(0, 'Visual', {})`` will clear the highlight group +/// 'Visual'. +/// +/// @note The fg and bg keys also accept the string values `"fg"` or `"bg"` +/// which act as aliases to the corresponding foreground and background +/// values of the Normal group. If the Normal group has not been defined, +/// using these values results in an error. /// /// @param ns_id Namespace id for this highlight |nvim_create_namespace()|. /// Use 0 to set a highlight group globally |:highlight|. /// @param name Highlight group name, e.g. "ErrorMsg" -/// @param val Highlight definition map, like |synIDattr()|. In -/// addition, the following keys are recognized: +/// @param val Highlight definition map, accepts the following keys: +/// - fg (or foreground): color name or "#RRGGBB", see note. +/// - bg (or background): color name or "#RRGGBB", see note. +/// - sp (or special): color name or "#RRGGBB" +/// - blend: integer between 0 and 100 +/// - bold: boolean +/// - standout: boolean +/// - underline: boolean +/// - underlineline: boolean +/// - undercurl: boolean +/// - underdot: boolean +/// - underdash: boolean +/// - strikethrough: boolean +/// - italic: boolean +/// - reverse: boolean +/// - nocombine: boolean +/// - link: name of another highlight group to link to, see |:hi-link|. +/// Additionally, the following keys are recognized: /// - default: Don't override existing definition |:hi-default| /// - ctermfg: Sets foreground of cterm color |highlight-ctermfg| /// - ctermbg: Sets background of cterm color |highlight-ctermbg| -/// - cterm: cterm attribute map, like -/// |highlight-args|. -/// Note: Attributes default to those set for `gui` -/// if not set. +/// - cterm: cterm attribute map, like |highlight-args|. If not set, +/// cterm attributes will match those from the attribute map +/// documented above. /// @param[out] err Error details, if any /// // TODO(bfredl): val should take update vs reset flag @@ -1327,14 +1348,14 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err) draining = true; goto theend; } - if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 1)) { + if (!(State & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 1)) { ResetRedobuff(); AppendCharToRedobuff('a'); // Dot-repeat. } // vim.paste() decides if client should cancel. Errors do NOT cancel: we // want to drain remaining chunks (rather than divert them to main input). cancel = (rv.type == kObjectTypeBoolean && !rv.data.boolean); - if (!cancel && !(State & CMDLINE)) { // Dot-repeat. + if (!cancel && !(State & MODE_CMDLINE)) { // Dot-repeat. for (size_t i = 0; i < lines.size; i++) { String s = lines.items[i].data.string; assert(s.size <= INT_MAX); @@ -1345,7 +1366,7 @@ Boolean nvim_paste(String data, Boolean crlf, Integer phase, Error *err) } } } - if (!(State & (CMDLINE | INSERT)) && (phase == -1 || phase == 3)) { + if (!(State & (MODE_CMDLINE | MODE_INSERT)) && (phase == -1 || phase == 3)) { AppendCharToRedobuff(ESC); // Dot-repeat. } theend: @@ -1601,12 +1622,13 @@ ArrayOf(Dictionary) nvim_get_keymap(uint64_t channel_id, String mode) /// or "!" for |:map!|, or empty string for |:map|. /// @param lhs Left-hand-side |{lhs}| of the mapping. /// @param rhs Right-hand-side |{rhs}| of the mapping. -/// @param opts Optional parameters map. Accepts all |:map-arguments| -/// as keys excluding |<buffer>| but including |noremap| and "desc". -/// "desc" can be used to give a description to keymap. -/// When called from Lua, also accepts a "callback" key that takes -/// a Lua function to call when the mapping is executed. -/// Values are Booleans. Unknown key is an error. +/// @param opts Optional parameters map: keys are |:map-arguments|, values +/// are booleans (default false). Accepts all |:map-arguments| as +/// keys excluding |<buffer>| but including |noremap| and "desc". +/// Unknown key is an error. "desc" can be used to give a +/// description to the mapping. When called from Lua, also accepts a +/// "callback" key that takes a Lua function to call when the +/// mapping is executed. /// @param[out] err Error details, if any. void nvim_set_keymap(uint64_t channel_id, String mode, String lhs, String rhs, Dict(keymap) *opts, Error *err) @@ -2118,8 +2140,8 @@ Array nvim__inspect_cell(Integer grid, Integer row, Integer col, Error *err) } } - if (row < 0 || row >= g->Rows - || col < 0 || col >= g->Columns) { + if (row < 0 || row >= g->rows + || col < 0 || col >= g->cols) { return ret; } size_t off = g->line_offset[(size_t)row] + (size_t)col; @@ -2252,8 +2274,9 @@ Array nvim_get_mark(String name, Dictionary opts, Error *err) /// - fillchar: (string) Character to fill blank spaces in the statusline (see /// 'fillchars'). Treated as single-width even if it isn't. /// - highlights: (boolean) Return highlight information. +/// - use_winbar: (boolean) Evaluate winbar instead of statusline. /// - use_tabline: (boolean) Evaluate tabline instead of statusline. When |TRUE|, {winid} -/// is ignored. +/// is ignored. Mutually exclusive with {use_winbar}. /// /// @param[out] err Error details, if any. /// @return Dictionary containing statusline information, with these keys: @@ -2272,9 +2295,18 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * int maxwidth; int fillchar = 0; Window window = 0; + bool use_winbar = false; bool use_tabline = false; bool highlights = false; + if (str.size < 2 || memcmp(str.data, "%!", 2)) { + const char *const errmsg = check_stl_option((char_u *)str.data); + if (errmsg) { + api_set_error(err, kErrorTypeValidation, "%s", errmsg); + return result; + } + } + if (HAS_KEY(opts->winid)) { if (opts->winid.type != kObjectTypeInteger) { api_set_error(err, kErrorTypeValidation, "winid must be an integer"); @@ -2283,7 +2315,6 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * window = (Window)opts->winid.data.integer; } - if (HAS_KEY(opts->fillchar)) { if (opts->fillchar.type != kObjectTypeString || opts->fillchar.data.string.size == 0 || ((size_t)utf_ptr2len(opts->fillchar.data.string.data) @@ -2293,7 +2324,6 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * } fillchar = utf_ptr2char(opts->fillchar.data.string.data); } - if (HAS_KEY(opts->highlights)) { highlights = api_object_to_bool(opts->highlights, "highlights", false, err); @@ -2301,7 +2331,13 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * return result; } } + if (HAS_KEY(opts->use_winbar)) { + use_winbar = api_object_to_bool(opts->use_winbar, "use_winbar", 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); @@ -2309,6 +2345,10 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * return result; } } + if (use_winbar && use_tabline) { + api_set_error(err, kErrorTypeValidation, "use_winbar and use_tabline are mutually exclusive"); + return result; + } win_T *wp, *ewp; @@ -2318,7 +2358,6 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * fillchar = ' '; } else { wp = find_window_by_handle(window, err); - if (wp == NULL) { api_set_error(err, kErrorTypeException, "unknown winid %d", window); return result; @@ -2326,8 +2365,12 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * ewp = wp; if (fillchar == 0) { - int attr; - fillchar = fillchar_status(&attr, wp); + if (use_winbar) { + fillchar = wp->w_p_fcs_chars.wbr; + } else { + int attr; + fillchar = fillchar_status(&attr, wp); + } } } @@ -2339,7 +2382,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * maxwidth = (int)opts->maxwidth.data.integer; } else { - maxwidth = (use_tabline || global_stl_height() > 0) ? Columns : wp->w_width; + maxwidth = (use_tabline || (!use_winbar && global_stl_height() > 0)) ? Columns : wp->w_width; } char buf[MAXPATHL]; @@ -2374,7 +2417,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * // 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); + grpname = get_default_stl_hl(wp, use_winbar); PUT(hl_info, "start", INTEGER_OBJ(0)); PUT(hl_info, "group", CSTR_TO_OBJ(grpname)); @@ -2388,22 +2431,18 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * PUT(hl_info, "start", INTEGER_OBJ((char *)sp->start - buf)); if (sp->userhl == 0) { - grpname = get_default_stl_hl(wp); + grpname = get_default_stl_hl(wp, use_winbar); } 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/api/vimscript.c b/src/nvim/api/vimscript.c index c4a301839f..b8f7b33cd5 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -10,10 +10,14 @@ #include "nvim/api/private/helpers.h" #include "nvim/api/vimscript.h" #include "nvim/ascii.h" +#include "nvim/autocmd.h" #include "nvim/eval.h" #include "nvim/eval/typval.h" #include "nvim/eval/userfunc.h" #include "nvim/ex_cmds2.h" +#include "nvim/ops.h" +#include "nvim/strings.h" +#include "nvim/vim.h" #include "nvim/viml/parser/expressions.h" #include "nvim/viml/parser/parser.h" #include "nvim/window.h" @@ -22,16 +26,19 @@ # include "api/vimscript.c.generated.h" #endif -/// Executes Vimscript (multiline block of Ex-commands), like anonymous +#define IS_USER_CMDIDX(idx) ((int)(idx) < 0) + +/// Executes Vimscript (multiline block of Ex commands), like anonymous /// |:source|. /// /// Unlike |nvim_command()| this function supports heredocs, script-scope (s:), /// etc. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @see |execute()| /// @see |nvim_command()| +/// @see |nvim_cmd()| /// /// @param src Vimscript code /// @param output Capture and return all (non-error, non-shell |:!|) output @@ -89,13 +96,16 @@ theend: return (String)STRING_INIT; } -/// Executes an ex-command. +/// Executes an Ex command. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// -/// @see |nvim_exec()| +/// Prefer using |nvim_cmd()| or |nvim_exec()| over this. To evaluate multiple lines of Vim script +/// or an Ex command directly, use |nvim_exec()|. To construct an Ex command using a structured +/// format and then execute it, use |nvim_cmd()|. To modify an Ex command before evaluating it, use +/// |nvim_parse_cmd()| in conjunction with |nvim_cmd()|. /// -/// @param command Ex-command string +/// @param command Ex command string /// @param[out] err Error details (Vim error), if any void nvim_command(String command, Error *err) FUNC_API_SINCE(1) @@ -108,7 +118,7 @@ void nvim_command(String command, Error *err) /// Evaluates a VimL |expression|. /// Dictionaries and Lists are recursively expanded. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param expr VimL expression string /// @param[out] err Error details, if any @@ -216,7 +226,7 @@ free_vim_args: /// Calls a VimL function with the given arguments. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param fn Function to call /// @param args Function arguments packed in an Array @@ -230,7 +240,7 @@ Object nvim_call_function(String fn, Array args, Error *err) /// Calls a VimL |Dictionary-function| with the given arguments. /// -/// On execution error: fails with VimL error, does not update v:errmsg. +/// On execution error: fails with VimL error, updates v:errmsg. /// /// @param dict Dictionary, or String evaluating to a VimL |self| dict /// @param fn Name of the function defined on the VimL dict @@ -978,3 +988,370 @@ end: xfree(cmdline); return result; } + +/// Executes an Ex command. +/// +/// Unlike |nvim_command()| this command takes a structured Dictionary instead of a String. This +/// allows for easier construction and manipulation of an Ex command. This also allows for things +/// such as having spaces inside a command argument, expanding filenames in a command that otherwise +/// doesn't expand filenames, etc. +/// +/// On execution error: fails with VimL error, updates v:errmsg. +/// +/// @see |nvim_exec()| +/// @see |nvim_command()| +/// +/// @param cmd Command to execute. Must be a Dictionary that can contain the same values as +/// the return value of |nvim_parse_cmd()| except "addr", "nargs" and "nextcmd" +/// which are ignored if provided. All values except for "cmd" are optional. +/// @param opts Optional parameters. +/// - output: (boolean, default false) Whether to return command output. +/// @param[out] err Error details, if any. +/// @return Command output (non-error, non-shell |:!|) if `output` is true, else empty string. +String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error *err) + FUNC_API_SINCE(10) +{ + exarg_T ea; + memset(&ea, 0, sizeof(ea)); + ea.verbose_save = -1; + ea.save_msg_silent = -1; + + CmdParseInfo cmdinfo; + memset(&cmdinfo, 0, sizeof(cmdinfo)); + cmdinfo.verbose = -1; + + char *cmdline = NULL; + char *cmdname = NULL; + char **args = NULL; + size_t argc = 0; + + String retv = (String)STRING_INIT; + +#define OBJ_TO_BOOL(var, value, default, varname) \ + do { \ + var = api_object_to_bool(value, varname, default, err); \ + if (ERROR_SET(err)) { \ + goto end; \ + } \ + } while (0) + +#define VALIDATION_ERROR(...) \ + do { \ + api_set_error(err, kErrorTypeValidation, __VA_ARGS__); \ + goto end; \ + } while (0) + + bool output; + OBJ_TO_BOOL(output, opts->output, false, "'output'"); + + // First, parse the command name and check if it exists and is valid. + if (!HAS_KEY(cmd->cmd) || cmd->cmd.type != kObjectTypeString + || cmd->cmd.data.string.data[0] == NUL) { + VALIDATION_ERROR("'cmd' must be a non-empty String"); + } + + cmdname = string_to_cstr(cmd->cmd.data.string); + ea.cmd = cmdname; + + char *p = find_ex_command(&ea, NULL); + + // If this looks like an undefined user command and there are CmdUndefined + // autocommands defined, trigger the matching autocommands. + if (p != NULL && ea.cmdidx == CMD_SIZE && ASCII_ISUPPER(*ea.cmd) + && has_event(EVENT_CMDUNDEFINED)) { + p = xstrdup(cmdname); + int ret = apply_autocmds(EVENT_CMDUNDEFINED, p, p, true, NULL); + xfree(p); + // If the autocommands did something and didn't cause an error, try + // finding the command again. + p = (ret && !aborting()) ? find_ex_command(&ea, NULL) : ea.cmd; + } + + if (p == NULL || ea.cmdidx == CMD_SIZE) { + VALIDATION_ERROR("Command not found: %s", cmdname); + } + if (is_cmd_ni(ea.cmdidx)) { + VALIDATION_ERROR("Command not implemented: %s", cmdname); + } + + // Get the command flags so that we can know what type of arguments the command uses. + // Not required for a user command since `find_ex_command` already deals with it in that case. + if (!IS_USER_CMDIDX(ea.cmdidx)) { + ea.argt = get_cmd_argt(ea.cmdidx); + } + + // Parse command arguments since it's needed to get the command address type. + if (HAS_KEY(cmd->args)) { + if (cmd->args.type != kObjectTypeArray) { + VALIDATION_ERROR("'args' must be an Array"); + } + // Check if every argument is valid + for (size_t i = 0; i < cmd->args.data.array.size; i++) { + Object elem = cmd->args.data.array.items[i]; + if (elem.type != kObjectTypeString) { + VALIDATION_ERROR("Command argument must be a String"); + } else if (string_iswhite(elem.data.string)) { + VALIDATION_ERROR("Command argument must have non-whitespace characters"); + } + } + + argc = cmd->args.data.array.size; + bool argc_valid; + + // Check if correct number of arguments is used. + switch (ea.argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) { + case EX_EXTRA | EX_NOSPC | EX_NEEDARG: + argc_valid = argc == 1; + break; + case EX_EXTRA | EX_NOSPC: + argc_valid = argc <= 1; + break; + case EX_EXTRA | EX_NEEDARG: + argc_valid = argc >= 1; + break; + case EX_EXTRA: + argc_valid = true; + break; + default: + argc_valid = argc == 0; + break; + } + + if (!argc_valid) { + argc = 0; // Ensure that args array isn't erroneously freed at the end. + VALIDATION_ERROR("Incorrect number of arguments supplied"); + } + + if (argc != 0) { + args = xcalloc(argc, sizeof(char *)); + + for (size_t i = 0; i < argc; i++) { + args[i] = string_to_cstr(cmd->args.data.array.items[i].data.string); + } + } + } + + // Simply pass the first argument (if it exists) as the arg pointer to `set_cmd_addr_type()` + // since it only ever checks the first argument. + set_cmd_addr_type(&ea, argc > 0 ? (char_u *)args[0] : NULL); + + if (HAS_KEY(cmd->range)) { + if (!(ea.argt & EX_RANGE)) { + VALIDATION_ERROR("Command cannot accept a range"); + } else if (cmd->range.type != kObjectTypeArray) { + VALIDATION_ERROR("'range' must be an Array"); + } else if (cmd->range.data.array.size > 2) { + VALIDATION_ERROR("'range' cannot contain more than two elements"); + } + + Array range = cmd->range.data.array; + ea.addr_count = (int)range.size; + + for (size_t i = 0; i < range.size; i++) { + Object elem = range.items[i]; + if (elem.type != kObjectTypeInteger || elem.data.integer < 0) { + VALIDATION_ERROR("'range' element must be a non-negative Integer"); + } + } + + if (range.size > 0) { + ea.line1 = range.items[0].data.integer; + ea.line2 = range.items[range.size - 1].data.integer; + } + + if (invalid_range(&ea) != NULL) { + VALIDATION_ERROR("Invalid range provided"); + } + } + if (ea.addr_count == 0) { + if (ea.argt & EX_DFLALL) { + set_cmd_dflall_range(&ea); // Default range for range=% + } else { + ea.line1 = ea.line2 = get_cmd_default_range(&ea); // Default range. + + if (ea.addr_type == ADDR_OTHER) { + // Default is 1, not cursor. + ea.line2 = 1; + } + } + } + + if (HAS_KEY(cmd->count)) { + if (!(ea.argt & EX_COUNT)) { + VALIDATION_ERROR("Command cannot accept a count"); + } else if (cmd->count.type != kObjectTypeInteger || cmd->count.data.integer < 0) { + VALIDATION_ERROR("'count' must be a non-negative Integer"); + } + set_cmd_count(&ea, cmd->count.data.integer, true); + } + + if (HAS_KEY(cmd->reg)) { + if (!(ea.argt & EX_REGSTR)) { + VALIDATION_ERROR("Command cannot accept a register"); + } else if (cmd->reg.type != kObjectTypeString || cmd->reg.data.string.size != 1) { + VALIDATION_ERROR("'reg' must be a single character"); + } + char regname = cmd->reg.data.string.data[0]; + if (regname == '=') { + VALIDATION_ERROR("Cannot use register \"="); + } else if (!valid_yank_reg(regname, ea.cmdidx != CMD_put && !IS_USER_CMDIDX(ea.cmdidx))) { + VALIDATION_ERROR("Invalid register: \"%c", regname); + } + ea.regname = (uint8_t)regname; + } + + OBJ_TO_BOOL(ea.forceit, cmd->bang, false, "'bang'"); + if (ea.forceit && !(ea.argt & EX_BANG)) { + VALIDATION_ERROR("Command cannot accept a bang"); + } + + if (HAS_KEY(cmd->magic)) { + if (cmd->magic.type != kObjectTypeDictionary) { + VALIDATION_ERROR("'magic' must be a Dictionary"); + } + + Dict(cmd_magic) magic = { 0 }; + if (!api_dict_to_keydict(&magic, KeyDict_cmd_magic_get_field, + cmd->magic.data.dictionary, err)) { + goto end; + } + + OBJ_TO_BOOL(cmdinfo.magic.file, magic.file, ea.argt & EX_XFILE, "'magic.file'"); + OBJ_TO_BOOL(cmdinfo.magic.bar, magic.bar, ea.argt & EX_TRLBAR, "'magic.bar'"); + } else { + cmdinfo.magic.file = ea.argt & EX_XFILE; + cmdinfo.magic.bar = ea.argt & EX_TRLBAR; + } + + if (HAS_KEY(cmd->mods)) { + if (cmd->mods.type != kObjectTypeDictionary) { + VALIDATION_ERROR("'mods' must be a Dictionary"); + } + + Dict(cmd_mods) mods = { 0 }; + if (!api_dict_to_keydict(&mods, KeyDict_cmd_mods_get_field, cmd->mods.data.dictionary, err)) { + goto end; + } + + if (HAS_KEY(mods.tab)) { + if (mods.tab.type != kObjectTypeInteger || mods.tab.data.integer < 0) { + VALIDATION_ERROR("'mods.tab' must be a non-negative Integer"); + } + cmdinfo.cmdmod.tab = (int)mods.tab.data.integer + 1; + } + + if (HAS_KEY(mods.verbose)) { + if (mods.verbose.type != kObjectTypeInteger || mods.verbose.data.integer <= 0) { + VALIDATION_ERROR("'mods.verbose' must be a non-negative Integer"); + } + cmdinfo.verbose = mods.verbose.data.integer; + } + + bool vertical; + OBJ_TO_BOOL(vertical, mods.vertical, false, "'mods.vertical'"); + cmdinfo.cmdmod.split |= (vertical ? WSP_VERT : 0); + + if (HAS_KEY(mods.split)) { + if (mods.split.type != kObjectTypeString) { + VALIDATION_ERROR("'mods.split' must be a String"); + } + + if (STRCMP(mods.split.data.string.data, "aboveleft") == 0 + || STRCMP(mods.split.data.string.data, "leftabove") == 0) { + cmdinfo.cmdmod.split |= WSP_ABOVE; + } else if (STRCMP(mods.split.data.string.data, "belowright") == 0 + || STRCMP(mods.split.data.string.data, "rightbelow") == 0) { + cmdinfo.cmdmod.split |= WSP_BELOW; + } else if (STRCMP(mods.split.data.string.data, "topleft") == 0) { + cmdinfo.cmdmod.split |= WSP_TOP; + } else if (STRCMP(mods.split.data.string.data, "botright") == 0) { + cmdinfo.cmdmod.split |= WSP_BOT; + } else { + VALIDATION_ERROR("Invalid value for 'mods.split'"); + } + } + + OBJ_TO_BOOL(cmdinfo.silent, mods.silent, false, "'mods.silent'"); + OBJ_TO_BOOL(cmdinfo.emsg_silent, mods.emsg_silent, false, "'mods.emsg_silent'"); + OBJ_TO_BOOL(cmdinfo.sandbox, mods.sandbox, false, "'mods.sandbox'"); + OBJ_TO_BOOL(cmdinfo.noautocmd, mods.noautocmd, false, "'mods.noautocmd'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.browse, mods.browse, false, "'mods.browse'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.confirm, mods.confirm, false, "'mods.confirm'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.hide, mods.hide, false, "'mods.hide'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.keepalt, mods.keepalt, false, "'mods.keepalt'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.keepjumps, mods.keepjumps, false, "'mods.keepjumps'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.keepmarks, mods.keepmarks, false, "'mods.keepmarks'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.keeppatterns, mods.keeppatterns, false, "'mods.keeppatterns'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.lockmarks, mods.lockmarks, false, "'mods.lockmarks'"); + OBJ_TO_BOOL(cmdinfo.cmdmod.noswapfile, mods.noswapfile, false, "'mods.noswapfile'"); + + if (cmdinfo.sandbox && !(ea.argt & EX_SBOXOK)) { + VALIDATION_ERROR("Command cannot be run in sandbox"); + } + } + + // Finally, build the command line string that will be stored inside ea.cmdlinep. + // This also sets the values of ea.cmd, ea.arg, ea.args and ea.arglens. + build_cmdline_str(&cmdline, &ea, &cmdinfo, args, argc); + ea.cmdlinep = &cmdline; + + garray_T capture_local; + const int save_msg_silent = msg_silent; + garray_T * const save_capture_ga = capture_ga; + + if (output) { + ga_init(&capture_local, 1, 80); + capture_ga = &capture_local; + } + + try_start(); + if (output) { + msg_silent++; + } + + WITH_SCRIPT_CONTEXT(channel_id, { + execute_cmd(&ea, &cmdinfo); + }); + + if (output) { + capture_ga = save_capture_ga; + msg_silent = save_msg_silent; + } + try_end(err); + + if (ERROR_SET(err)) { + goto clear_ga; + } + + if (output && capture_local.ga_len > 1) { + retv = (String){ + .data = capture_local.ga_data, + .size = (size_t)capture_local.ga_len, + }; + // redir usually (except :echon) prepends a newline. + if (retv.data[0] == '\n') { + memmove(retv.data, retv.data + 1, retv.size - 1); + retv.data[retv.size - 1] = '\0'; + retv.size = retv.size - 1; + } + goto end; + } +clear_ga: + if (output) { + ga_clear(&capture_local); + } +end: + xfree(cmdline); + xfree(cmdname); + xfree(ea.args); + xfree(ea.arglens); + for (size_t i = 0; i < argc; i++) { + xfree(args[i]); + } + xfree(args); + + return retv; + +#undef OBJ_TO_BOOL +#undef VALIDATION_ERROR +} |