diff options
author | Famiu Haque <famiuhaque@protonmail.com> | 2022-04-20 17:02:18 +0600 |
---|---|---|
committer | Famiu Haque <famiuhaque@protonmail.com> | 2022-05-31 20:55:05 +0600 |
commit | 46536f53e82967dcac8d030ee3394cdb156f9603 (patch) | |
tree | 4444067831639a6a1eb1916ca9cd5002261932ec | |
parent | e9803e1de6497ee21f77f45cf2670c2fe4e8ab22 (diff) | |
download | rneovim-46536f53e82967dcac8d030ee3394cdb156f9603.tar.gz rneovim-46536f53e82967dcac8d030ee3394cdb156f9603.tar.bz2 rneovim-46536f53e82967dcac8d030ee3394cdb156f9603.zip |
feat: add preview functionality to user commands
Adds a Lua-only `preview` flag to user commands which allows the command to be incrementally previewed like `:substitute` when 'inccommand' is set.
-rw-r--r-- | runtime/doc/api.txt | 2 | ||||
-rw-r--r-- | runtime/doc/map.txt | 106 | ||||
-rw-r--r-- | runtime/doc/options.txt | 10 | ||||
-rw-r--r-- | runtime/doc/vim_diff.txt | 2 | ||||
-rw-r--r-- | src/nvim/api/keysets.lua | 1 | ||||
-rw-r--r-- | src/nvim/api/private/helpers.c | 11 | ||||
-rw-r--r-- | src/nvim/api/vim.c | 1 | ||||
-rw-r--r-- | src/nvim/api/vimscript.c | 2 | ||||
-rw-r--r-- | src/nvim/buffer_updates.c | 9 | ||||
-rw-r--r-- | src/nvim/change.c | 4 | ||||
-rw-r--r-- | src/nvim/ex_cmds.c | 251 | ||||
-rw-r--r-- | src/nvim/ex_cmds.lua | 55 | ||||
-rw-r--r-- | src/nvim/ex_cmds_defs.h | 11 | ||||
-rw-r--r-- | src/nvim/ex_docmd.c | 89 | ||||
-rw-r--r-- | src/nvim/ex_docmd.h | 2 | ||||
-rw-r--r-- | src/nvim/ex_getln.c | 302 | ||||
-rw-r--r-- | src/nvim/fold.c | 5 | ||||
-rw-r--r-- | src/nvim/generators/gen_ex_cmds.lua | 13 | ||||
-rw-r--r-- | src/nvim/globals.h | 3 | ||||
-rw-r--r-- | src/nvim/lua/executor.c | 31 | ||||
-rw-r--r-- | src/nvim/vim.h | 1 | ||||
-rw-r--r-- | test/functional/api/command_spec.lua | 14 | ||||
-rw-r--r-- | test/functional/ui/float_spec.lua | 12 | ||||
-rw-r--r-- | test/functional/ui/inccommand_spec.lua | 36 | ||||
-rw-r--r-- | test/functional/ui/inccommand_user_spec.lua | 329 |
25 files changed, 958 insertions, 344 deletions
diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 965b8e6492..6c8c35486e 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -760,6 +760,8 @@ nvim_create_user_command({name}, {command}, {*opts}) when a Lua function is used for {command}. • force: (boolean, default true) Override any previous definition. + • preview: (function) Preview callback for + 'inccommand' |:command-preview| nvim_del_current_line() *nvim_del_current_line()* Deletes the current line. diff --git a/runtime/doc/map.txt b/runtime/doc/map.txt index 98da68b76a..9776304c8e 100644 --- a/runtime/doc/map.txt +++ b/runtime/doc/map.txt @@ -1430,6 +1430,112 @@ Possible values are (second column is the short name used in listing): -addr=other ? other kind of range +Incremental preview ~ + *:command-preview* {nvim-api} +Commands can show an 'inccommand' (as-you-type) preview by defining a preview +handler (only from Lua, see |nvim_create_user_command()|). + +The preview callback must be a Lua function with this signature: > + + function cmdpreview(opts, ns, buf) +< +where "opts" has the same form as that given to |nvim_create_user_command()| +callbacks, "ns" is the preview namespace id for highlights, and "buf" is the +buffer that your preview routine will directly modify to show the previewed +results (for "inccommand=split", or nil for "inccommand=nosplit"). + +Your command preview routine must implement this protocol: + +1. Modify the current buffer as required for the preview (see + |nvim_buf_set_text()| and |nvim_buf_set_lines()|). +2. If preview buffer is provided, add necessary text to the preview buffer. +3. Add required highlights to the current buffer. If preview buffer is + provided, add required highlights to the preview buffer as well. All + highlights must be added to the preview namespace which is provided as an + argument to the preview callback (see |nvim_buf_add_highlight()| and + |nvim_buf_set_extmark()| for help on how to add highlights to a namespace). +4. Return an integer (0, 1, 2) which controls how Nvim behaves as follows: + 0: No preview is shown. + 1: Preview is shown without preview window (even with "inccommand=split"). + 2: Preview is shown and preview window is opened (if "inccommand=split"). + For "inccommand=nosplit" this is the same as 1. + +After preview ends, Nvim discards all changes to the buffer and all highlights +in the preview namespace. + +Here's an example of a command to trim trailing whitespace from lines that +supports incremental command preview: +> + -- Trims trailing whitespace in the current buffer. + -- Also performs 'inccommand' preview if invoked as a preview callback + -- (preview_ns is non-nil). + local function trim_space(opts, preview_ns, preview_buf) + local line1 = opts.line1 + local line2 = opts.line2 + local buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, 0) + local new_lines = {} + local preview_buf_line = 0 + + for i, line in ipairs(lines) do + local startidx, endidx = string.find(line, '%s+$') + + if startidx ~= nil then + -- Highlight the match if in command preview mode + if preview_ns ~= nil then + vim.api.nvim_buf_add_highlight( + buf, preview_ns, 'Substitute', line1 + i - 2, startidx - 1, + endidx + ) + + -- Add lines and highlight to the preview buffer + -- if inccommand=split + if preview_buf ~= nil then + local prefix = string.format('|%d| ', line1 + i - 1) + + vim.api.nvim_buf_set_lines( + preview_buf, preview_buf_line, preview_buf_line, 0, + { prefix .. line } + ) + vim.api.nvim_buf_add_highlight( + preview_buf, preview_ns, 'Substitute', preview_buf_line, + #prefix + startidx - 1, #prefix + endidx + ) + + preview_buf_line = preview_buf_line + 1 + end + end + end + + if not preview_ns then + new_lines[#new_lines+1] = string.gsub(line, '%s+$', '') + end + end + + -- Don't make any changes to the buffer if previewing + if not preview_ns then + vim.api.nvim_buf_set_lines(buf, line1 - 1, line2, 0, new_lines) + end + + -- When called as a preview callback, return the value of the + -- preview type + if preview_ns ~= nil then + return 2 + end + end + + -- Create the user command + vim.api.nvim_create_user_command( + 'TrimTrailingWhitespace', + trim_space, + { nargs = '?', range = '%', addr = 'lines', preview = trim_space } + ) +< +Note that in the above example, the same function is used as both the command +callback and the preview callback, but you could instead use separate +functions. + + Special cases ~ *:command-bang* *:command-bar* *:command-register* *:command-buffer* diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index a21d3bbce7..eebbc3f73a 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -3266,8 +3266,9 @@ A jump table for the options with a short description can be found at |Q_op|. 'inccommand' 'icm' string (default "nosplit") global - When nonempty, shows the effects of |:substitute|, |:smagic|, and - |:snomagic| as you type. + When nonempty, shows the effects of |:substitute|, |:smagic|, + |:snomagic| and user commands with the |:command-preview| flag as you + type. Possible values: nosplit Shows the effects of a command incrementally in the @@ -3275,8 +3276,9 @@ A jump table for the options with a short description can be found at |Q_op|. split Like "nosplit", but also shows partial off-screen results in a preview window. - If the preview is too slow (exceeds 'redrawtime') then 'inccommand' is - automatically disabled until |Command-line-mode| is done. + If the preview for built-in commands is too slow (exceeds + 'redrawtime') then 'inccommand' is automatically disabled until + |Command-line-mode| is done. *'include'* *'inc'* 'include' 'inc' string (default "^\s*#\s*include") diff --git a/runtime/doc/vim_diff.txt b/runtime/doc/vim_diff.txt index c079b83c29..8e67cb0923 100644 --- a/runtime/doc/vim_diff.txt +++ b/runtime/doc/vim_diff.txt @@ -183,6 +183,7 @@ Commands: |:sign-define| accepts a `numhl` argument, to highlight the line number |:match| can be invoked before highlight group is defined |:source| works with Lua and anonymous (no file) scripts + User commands can support |:command-preview| to show results as you type Events: |RecordingEnter| @@ -235,6 +236,7 @@ Options: "horizdown", "vertleft", "vertright", "verthoriz" 'foldcolumn' supports up to 9 dynamic/fixed columns 'inccommand' shows interactive results for |:substitute|-like commands + and |:command-preview| commands 'laststatus' global statusline support 'pumblend' pseudo-transparent popupmenu 'scrollback' diff --git a/src/nvim/api/keysets.lua b/src/nvim/api/keysets.lua index d4882abffe..881a83e606 100644 --- a/src/nvim/api/keysets.lua +++ b/src/nvim/api/keysets.lua @@ -53,6 +53,7 @@ return { "force"; "keepscript"; "nargs"; + "preview"; "range"; "register"; }; diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index 3cccbc3cdf..6981ecc455 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1438,6 +1438,7 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, char *rep = NULL; LuaRef luaref = LUA_NOREF; LuaRef compl_luaref = LUA_NOREF; + LuaRef preview_luaref = LUA_NOREF; if (!uc_validate_name(name.data)) { api_set_error(err, kErrorTypeValidation, "Invalid command name"); @@ -1592,6 +1593,14 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, goto err; } + if (opts->preview.type == kObjectTypeLuaRef) { + argt |= EX_PREVIEW; + preview_luaref = api_new_luaref(opts->preview.data.luaref); + } else if (HAS_KEY(opts->preview)) { + api_set_error(err, kErrorTypeValidation, "Invalid value for 'preview'"); + goto err; + } + switch (command.type) { case kObjectTypeLuaRef: luaref = api_new_luaref(command.data.luaref); @@ -1611,7 +1620,7 @@ void create_user_command(String name, Object command, Dict(user_command) *opts, } if (uc_add_command(name.data, name.size, rep, argt, def, flags, compl, compl_arg, compl_luaref, - addr_type_arg, luaref, force) != OK) { + preview_luaref, addr_type_arg, luaref, force) != OK) { api_set_error(err, kErrorTypeException, "Failed to create user command"); // Do not goto err, since uc_add_command now owns luaref, compl_luaref, and compl_arg } diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 5c3c16d6b0..8555d1bb71 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -2511,6 +2511,7 @@ Dictionary nvim_eval_statusline(String str, Dict(eval_statusline) *opts, Error * /// - desc: (string) Used for listing the command when a Lua function is used for /// {command}. /// - force: (boolean, default true) Override any previous definition. +/// - preview: (function) Preview callback for 'inccommand' |:command-preview| /// @param[out] err Error details, if any. void nvim_create_user_command(String name, Object command, Dict(user_command) *opts, Error *err) FUNC_API_SINCE(9) diff --git a/src/nvim/api/vimscript.c b/src/nvim/api/vimscript.c index e71f1a11ec..99ab247c2a 100644 --- a/src/nvim/api/vimscript.c +++ b/src/nvim/api/vimscript.c @@ -1311,7 +1311,7 @@ String nvim_cmd(uint64_t channel_id, Dict(cmd) *cmd, Dict(cmd_opts) *opts, Error } WITH_SCRIPT_CONTEXT(channel_id, { - execute_cmd(&ea, &cmdinfo); + execute_cmd(&ea, &cmdinfo, false); }); if (output) { diff --git a/src/nvim/buffer_updates.c b/src/nvim/buffer_updates.c index cb08ba0cfb..47b88945c7 100644 --- a/src/nvim/buffer_updates.c +++ b/src/nvim/buffer_updates.c @@ -187,7 +187,7 @@ void buf_updates_unload(buf_T *buf, bool can_reload) } void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added, - int64_t num_removed, bool send_tick) + int64_t num_removed) { size_t deleted_codepoints, deleted_codeunits; size_t deleted_bytes = ml_flush_deleted_bytes(buf, &deleted_codepoints, @@ -197,6 +197,9 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added, return; } + // Don't send b:changedtick during 'inccommand' preview if "buf" is the current buffer. + bool send_tick = !(cmdpreview && buf == curbuf); + // if one the channels doesn't work, put its ID here so we can remove it later uint64_t badchannelid = 0; @@ -253,7 +256,7 @@ void buf_updates_send_changes(buf_T *buf, linenr_T firstline, int64_t num_added, for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i); bool keep = true; - if (cb.on_lines != LUA_NOREF && (cb.preview || !(State & MODE_CMDPREVIEW))) { + if (cb.on_lines != LUA_NOREF && (cb.preview || !cmdpreview)) { Array args = ARRAY_DICT_INIT; Object items[8]; args.size = 6; // may be increased to 8 below @@ -312,7 +315,7 @@ void buf_updates_send_splice(buf_T *buf, int start_row, colnr_T start_col, bcoun for (size_t i = 0; i < kv_size(buf->update_callbacks); i++) { BufUpdateCallbacks cb = kv_A(buf->update_callbacks, i); bool keep = true; - if (cb.on_bytes != LUA_NOREF && (cb.preview || !(State & MODE_CMDPREVIEW))) { + if (cb.on_bytes != LUA_NOREF && (cb.preview || !cmdpreview)) { FIXED_TEMP_ARRAY(args, 11); // the first argument is always the buffer handle diff --git a/src/nvim/change.c b/src/nvim/change.c index 9fd5083fd3..fa1de69e2c 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -351,7 +351,7 @@ void changed_bytes(linenr_T lnum, colnr_T col) changedOneline(curbuf, lnum); changed_common(lnum, col, lnum + 1, 0L); // notify any channels that are watching - buf_updates_send_changes(curbuf, lnum, 1, 1, true); + buf_updates_send_changes(curbuf, lnum, 1, 1); // Diff highlighting in other diff windows may need to be updated too. if (curwin->w_p_diff) { @@ -501,7 +501,7 @@ void changed_lines(linenr_T lnum, colnr_T col, linenr_T lnume, long xtra, bool d if (do_buf_event) { int64_t num_added = (int64_t)(lnume + xtra - lnum); int64_t num_removed = lnume - lnum; - buf_updates_send_changes(curbuf, lnum, num_added, num_removed, true); + buf_updates_send_changes(curbuf, lnum, num_added, num_removed); } } diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index f6bdfc6175..93ff7bd752 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -14,7 +14,6 @@ #include <string.h> #include "nvim/api/buffer.h" -#include "nvim/api/extmark.h" #include "nvim/api/private/defs.h" #include "nvim/ascii.h" #include "nvim/buffer.h" @@ -111,8 +110,6 @@ typedef struct { # include "ex_cmds.c.generated.h" #endif -static int preview_bufnr = 0; - /// ":ascii" and "ga" implementation void do_ascii(const exarg_T *const eap) { @@ -1013,7 +1010,7 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) disable_fold_update--; // send update regarding the new lines that were added - buf_updates_send_changes(curbuf, dest + 1, num_lines, 0, true); + buf_updates_send_changes(curbuf, dest + 1, num_lines, 0); /* * Now we delete the original text -- webb @@ -1055,7 +1052,7 @@ int do_move(linenr_T line1, linenr_T line2, linenr_T dest) } // send nvim_buf_lines_event regarding lines that were deleted - buf_updates_send_changes(curbuf, line1 + extra, 0, num_lines, true); + buf_updates_send_changes(curbuf, line1 + extra, 0, num_lines); return OK; } @@ -3438,8 +3435,8 @@ static int check_regexp_delim(int c) /// The usual escapes are supported as described in the regexp docs. /// /// @param do_buf_event If `true`, send buffer updates. -/// @return buffer used for 'inccommand' preview -static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle_T bufnr) +/// @return 0, 1 or 2. See show_cmdpreview() for more information on what the return value means. +static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T cmdpreview_bufnr) { long i = 0; regmmatch_T regmatch; @@ -3467,14 +3464,10 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle char *sub_firstline; // allocated copy of first sub line bool endcolumn = false; // cursor in last column when done PreviewLines preview_lines = { KV_INITIAL_VALUE, 0 }; - static int pre_src_id = 0; // Source id for the preview highlight static int pre_hl_id = 0; - buf_T *orig_buf = curbuf; // save to reset highlighting pos_T old_cursor = curwin->w_cursor; int start_nsubs; int save_ma = 0; - int save_b_changed = curbuf->b_changed; - bool preview = (State & MODE_CMDPREVIEW); bool did_save = false; @@ -3494,7 +3487,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle && vim_strchr("0123456789cegriIp|\"", *cmd) == NULL) { // don't accept alphanumeric for separator if (check_regexp_delim(*cmd) == FAIL) { - return NULL; + return 0; } // undocumented vi feature: @@ -3504,7 +3497,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle cmd++; if (vim_strchr("/?&", *cmd) == NULL) { emsg(_(e_backslash)); - return NULL; + return 0; } if (*cmd != '&') { which_pat = RE_SEARCH; // use last '/' pattern @@ -3540,7 +3533,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle MB_PTR_ADV(cmd); } - if (!eap->skip && !preview) { + if (!eap->skip && !cmdpreview) { sub_set_replacement((SubReplacementString) { .sub = xstrdup(sub), .timestamp = os_time(), @@ -3550,7 +3543,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle } else if (!eap->skip) { // use previous pattern and substitution if (old_sub.sub == NULL) { // there is no previous command emsg(_(e_nopresub)); - return NULL; + return 0; } pat = NULL; // search_regcomp() will use previous pattern sub = old_sub.sub; @@ -3560,8 +3553,8 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle endcolumn = (curwin->w_curswant == MAXCOL); } - if (sub != NULL && sub_joining_lines(eap, pat, sub, cmd, !preview)) { - return NULL; + if (sub != NULL && sub_joining_lines(eap, pat, sub, cmd, !cmdpreview)) { + return 0; } cmd = sub_parse_flags(cmd, &subflags, &which_pat); @@ -3575,7 +3568,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle i = getdigits_long((char_u **)&cmd, true, 0); if (i <= 0 && !eap->skip && subflags.do_error) { emsg(_(e_zerocount)); - return NULL; + return 0; } eap->line1 = eap->line2; eap->line2 += i - 1; @@ -3592,26 +3585,26 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle eap->nextcmd = (char *)check_nextcmd((char_u *)cmd); if (eap->nextcmd == NULL) { emsg(_(e_trailing)); - return NULL; + return 0; } } if (eap->skip) { // not executing commands, only parsing - return NULL; + return 0; } if (!subflags.do_count && !MODIFIABLE(curbuf)) { // Substitution is not allowed in non-'modifiable' buffer emsg(_(e_modifiable)); - return NULL; + return 0; } - if (search_regcomp((char_u *)pat, RE_SUBST, which_pat, (preview ? 0 : SEARCH_HIS), + if (search_regcomp((char_u *)pat, RE_SUBST, which_pat, (cmdpreview ? 0 : SEARCH_HIS), ®match) == FAIL) { if (subflags.do_error) { emsg(_(e_invcmd)); } - return NULL; + return 0; } // the 'i' or 'I' flag overrules 'ignorecase' and 'smartcase' @@ -3638,10 +3631,10 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle sub_copy = sub; } else { char *source = sub; - sub = (char *)regtilde((char_u *)sub, p_magic, preview); + sub = (char *)regtilde((char_u *)sub, p_magic, cmdpreview); // When previewing, the new pattern allocated by regtilde() needs to be freed // in this function because it will not be used or freed by regtilde() later. - sub_needs_free = preview && sub != source; + sub_needs_free = cmdpreview && sub != source; } // Check for a match on each line. @@ -3650,7 +3643,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle for (linenr_T lnum = eap->line1; lnum <= line2 && !got_quit && !aborting() - && (!preview || preview_lines.lines_needed <= (linenr_T)p_cwh + && (!cmdpreview || preview_lines.lines_needed <= (linenr_T)p_cwh || lnum <= curwin->w_botline); lnum++) { long nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, @@ -3817,7 +3810,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle } } - if (subflags.do_ask && !preview) { + if (subflags.do_ask && !cmdpreview) { int typed = 0; // change State to MODE_CONFIRM, so that the mouse works @@ -4049,7 +4042,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle // Save the line numbers for the preview buffer // NOTE: If the pattern matches a final newline, the next line will // be shown also, but should not be highlighted. Intentional for now. - if (preview && !has_second_delim) { + if (cmdpreview && !has_second_delim) { current_match.start.col = regmatch.startpos[0].col; if (current_match.end.lnum == 0) { current_match.end.lnum = sub_firstlnum + nmatch - 1; @@ -4064,7 +4057,7 @@ static buf_T *do_sub(exarg_T *eap, proftime_T timeout, bool do_buf_event, handle // 3. Substitute the string. During 'inccommand' preview only do this if // there is a replace pattern. - if (!preview || has_second_delim) { + if (!cmdpreview || has_second_delim) { long lnum_start = lnum; // save the start lnum save_ma = curbuf->b_p_ma; if (subflags.do_count) { @@ -4310,7 +4303,7 @@ skip: #define PUSH_PREVIEW_LINES() \ do { \ - if (preview) { \ + if (cmdpreview) { \ linenr_T match_lines = current_match.end.lnum \ - current_match.start.lnum +1; \ if (preview_lines.subresults.size > 0) { \ @@ -4366,8 +4359,7 @@ skip: int64_t num_added = last_line - first_line; int64_t num_removed = num_added - i; - buf_updates_send_changes(curbuf, first_line, num_added, num_removed, - do_buf_event); + buf_updates_send_changes(curbuf, first_line, num_added, num_removed); } xfree(sub_firstline); // may have to free allocated copy of the line @@ -4394,7 +4386,7 @@ skip: beginline(BL_WHITE | BL_FIX); } } - if (!preview && !do_sub_msg(subflags.do_count) && subflags.do_ask) { + if (!cmdpreview && !do_sub_msg(subflags.do_count) && subflags.do_ask) { msg(""); } } else { @@ -4431,34 +4423,23 @@ skip: subflags.do_all = save_do_all; subflags.do_ask = save_do_ask; + int retv = 0; + // Show 'inccommand' preview if there are matched lines. - buf_T *preview_buf = NULL; - size_t subsize = preview_lines.subresults.size; - if (preview && !aborting()) { + if (cmdpreview && !aborting()) { if (got_quit || profile_passed_limit(timeout)) { // Too slow, disable. set_string_option_direct("icm", -1, (char_u *)"", OPT_FREE, SID_NONE); } else if (*p_icm != NUL && pat != NULL) { - if (pre_src_id == 0) { - // Get a unique new src_id, saved in a static - pre_src_id = (int)nvim_create_namespace((String)STRING_INIT); - } if (pre_hl_id == 0) { pre_hl_id = syn_check_group(S_LEN("Substitute")); } - curbuf->b_changed = save_b_changed; // preserve 'modified' during preview - preview_buf = show_sub(eap, old_cursor, &preview_lines, - pre_hl_id, pre_src_id, bufnr); - if (subsize > 0) { - extmark_clear(orig_buf, pre_src_id, eap->line1 - 1, 0, - kv_last(preview_lines.subresults).end.lnum - 1, MAXCOL); - } + retv = show_sub(eap, old_cursor, &preview_lines, pre_hl_id, cmdpreview_ns, cmdpreview_bufnr); } } kv_destroy(preview_lines.subresults); - - return preview_buf; + return retv; #undef ADJUST_SUB_FIRSTLNUM #undef PUSH_PREVIEW_LINES } @@ -5854,52 +5835,26 @@ void ex_helpclose(exarg_T *eap) } } -/// Tries to enter to an existing window of given buffer. If no existing buffer -/// is found, creates a new split. -/// -/// @return OK/FAIL. -int sub_preview_win(buf_T *preview_buf) -{ - if (preview_buf != NULL) { - FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { - if (wp->w_buffer == preview_buf) { - win_enter(wp, false); - - return OK; - } - } - } - return win_split((int)p_cwh, WSP_BOT); -} - /// Shows the effects of the :substitute command being typed ('inccommand'). /// If inccommand=split, shows a preview window and later restores the layout. -static buf_T *show_sub(exarg_T *eap, pos_T old_cusr, PreviewLines *preview_lines, int hl_id, - int src_id, handle_T bufnr) +/// +/// @return 1 if preview window isn't needed, 2 if preview window is needed. +static int show_sub(exarg_T *eap, pos_T old_cusr, PreviewLines *preview_lines, int hl_id, + long cmdpreview_ns, handle_T cmdpreview_bufnr) FUNC_ATTR_NONNULL_ALL { - win_T *save_curwin = curwin; - cmdmod_T save_cmdmod = cmdmod; char *save_shm_p = (char *)vim_strsave(p_shm); PreviewLines lines = *preview_lines; buf_T *orig_buf = curbuf; - // We keep a special-purpose buffer around, but don't assume it exists. - buf_T *preview_buf = bufnr ? buflist_findnr(bufnr) : 0; - cmdmod.split = 0; // disable :leftabove/botright modifiers - cmdmod.tab = 0; // disable :tab modifier - cmdmod.noswapfile = true; // disable swap for preview buffer + buf_T *cmdpreview_buf = NULL; + // disable file info message set_string_option_direct("shm", -1, (char_u *)"F", OPT_FREE, SID_NONE); - bool outside_curline = (eap->line1 != old_cusr.lnum - || eap->line2 != old_cusr.lnum); - bool preview = outside_curline && (*p_icm != 'n'); - if (preview_buf == curbuf) { // Preview buffer cannot preview itself! - preview = false; - preview_buf = NULL; - } + // Update the topline to ensure that main window is on the correct line + update_topline(curwin); // Place cursor on nearest matching line, to undo do_sub() cursor placement. for (size_t i = 0; i < lines.subresults.size; i++) { @@ -5914,27 +5869,17 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr, PreviewLines *preview_lines // Width of the "| lnum|..." column which displays the line numbers. linenr_T highest_num_line = 0; int col_width = 0; + // Use preview window only when inccommand=split and range is not just the current line + bool preview = (*p_icm != 'n') && (eap->line1 != old_cusr.lnum || eap->line2 != old_cusr.lnum); - if (preview && sub_preview_win(preview_buf) != FAIL) { - buf_open_scratch(preview_buf ? bufnr : 0, "[Preview]"); - buf_clear(); - preview_buf = curbuf; - curbuf->b_p_bl = false; - curbuf->b_p_ma = true; - curbuf->b_p_ul = -1; - curbuf->b_p_tw = 0; // Reset 'textwidth' (was set by ftplugin) - curwin->w_p_cul = false; - curwin->w_p_cuc = false; - curwin->w_p_spell = false; - curwin->w_p_fen = false; + if (preview) { + cmdpreview_buf = buflist_findnr(cmdpreview_bufnr); + assert(cmdpreview_buf != NULL); if (lines.subresults.size > 0) { highest_num_line = kv_last(lines.subresults).end.lnum; col_width = log10(highest_num_line) + 1 + 3; } - } else { - // Failed to split the window, don't show 'inccommand' preview. - preview_buf = NULL; } char *str = NULL; // construct the line to show in here @@ -5944,10 +5889,13 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr, PreviewLines *preview_lines linenr_T linenr_origbuf = 0; // last line added to original buffer linenr_T next_linenr = 0; // next line to show for the match + // Temporarily switch to preview buffer + aco_save_T aco; + for (size_t matchidx = 0; matchidx < lines.subresults.size; matchidx++) { SubResult match = lines.subresults.items[matchidx]; - if (preview_buf) { + if (cmdpreview_buf) { lpos_T p_start = { 0, match.start.col }; // match starts here in preview lpos_T p_end = { 0, match.end.col }; // ... and ends here @@ -5986,115 +5934,50 @@ static buf_T *show_sub(exarg_T *eap, pos_T old_cusr, PreviewLines *preview_lines // Put "|lnum| line" into `str` and append it to the preview buffer. snprintf(str, line_size, "|%*ld| %s", col_width - 3, next_linenr, line); + // Temporarily switch to preview buffer + aucmd_prepbuf(&aco, cmdpreview_buf); if (linenr_preview == 0) { ml_replace(1, str, true); } else { ml_append(linenr_preview, str, (colnr_T)line_size, false); } + aucmd_restbuf(&aco); linenr_preview += 1; } linenr_origbuf = match.end.lnum; - bufhl_add_hl_pos_offset(preview_buf, src_id, hl_id, p_start, - p_end, col_width); + bufhl_add_hl_pos_offset(cmdpreview_buf, cmdpreview_ns, hl_id, p_start, p_end, col_width); } - bufhl_add_hl_pos_offset(orig_buf, src_id, hl_id, match.start, - match.end, 0); + bufhl_add_hl_pos_offset(orig_buf, cmdpreview_ns, hl_id, match.start, match.end, 0); } - xfree(str); - redraw_later(curwin, SOME_VALID); - win_enter(save_curwin, false); // Return to original window - update_topline(curwin); - - // Update screen now. - int save_rd = RedrawingDisabled; - RedrawingDisabled = 0; - update_screen(SOME_VALID); - RedrawingDisabled = save_rd; + xfree(str); set_string_option_direct("shm", -1, (char_u *)save_shm_p, OPT_FREE, SID_NONE); xfree(save_shm_p); - cmdmod = save_cmdmod; - - return preview_buf; + return preview ? 2 : 1; } -/// Closes any open windows for inccommand preview buffer. -void close_preview_windows(void) +/// :substitute command. +void ex_substitute(exarg_T *eap) { - block_autocmds(); - buf_T *buf = preview_bufnr ? buflist_findnr(preview_bufnr) : NULL; - if (buf != NULL) { - close_windows(buf, false); - } - unblock_autocmds(); + (void)do_sub(eap, profile_zero(), 0, 0); + return; } -/// :substitute command -/// -/// If 'inccommand' is empty: calls do_sub(). -/// If 'inccommand' is set: shows a "live" preview then removes the changes. -/// from undo history. -void ex_substitute(exarg_T *eap) +/// :substitute command preview callback. +int ex_substitute_preview(exarg_T *eap, long cmdpreview_ns, handle_T cmdpreview_bufnr) { - bool preview = (State & MODE_CMDPREVIEW); - if (*p_icm == NUL || !preview) { // 'inccommand' is disabled - close_preview_windows(); - (void)do_sub(eap, profile_zero(), true, preview_bufnr); - - return; + // Only preview once the pattern delimiter has been typed + if (*eap->arg && !ASCII_ISALNUM(*eap->arg)) { + char *save_eap = eap->arg; + int retv = do_sub(eap, profile_setlimit(p_rdt), cmdpreview_ns, cmdpreview_bufnr); + eap->arg = save_eap; + return retv; } - block_autocmds(); // Disable events during command preview. - - char *save_eap = eap->arg; - garray_T save_view; - win_size_save(&save_view); // Save current window sizes. - save_search_patterns(); - int save_changedtick = buf_get_changedtick(curbuf); - time_t save_b_u_time_cur = curbuf->b_u_time_cur; - u_header_T *save_b_u_newhead = curbuf->b_u_newhead; - long save_b_p_ul = curbuf->b_p_ul; - int save_w_p_cul = curwin->w_p_cul; - int save_w_p_cuc = curwin->w_p_cuc; - - curbuf->b_p_ul = LONG_MAX; // make sure we can undo all changes - curwin->w_p_cul = false; // Disable 'cursorline' - curwin->w_p_cuc = false; // Disable 'cursorcolumn' - - // Don't show search highlighting during live substitution - bool save_hls = p_hls; - p_hls = false; - buf_T *preview_buf = do_sub(eap, profile_setlimit(p_rdt), false, - preview_bufnr); - p_hls = save_hls; - - if (preview_buf != NULL) { - preview_bufnr = preview_buf->handle; - } - - if (save_changedtick != buf_get_changedtick(curbuf)) { - // Undo invisibly. This also moves the cursor! - if (!u_undo_and_forget(1)) { - abort(); - } - // Restore newhead. It is meaningless when curhead is valid, but we must - // restore it so that undotree() is identical before/after the preview. - curbuf->b_u_newhead = save_b_u_newhead; - curbuf->b_u_time_cur = save_b_u_time_cur; - buf_set_changedtick(curbuf, save_changedtick); - } - - curbuf->b_p_ul = save_b_p_ul; - curwin->w_p_cul = save_w_p_cul; // Restore 'cursorline' - curwin->w_p_cuc = save_w_p_cuc; // Restore 'cursorcolumn' - eap->arg = save_eap; - restore_search_patterns(); - win_size_restore(&save_view); - ga_clear(&save_view); - unblock_autocmds(); + return 0; } /// Skip over the pattern argument of ":vimgrep /pat/[g][j]". diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index 427e018141..b18bdefc2a 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -4,28 +4,29 @@ local module = {} -- Description of the values below is contained in ex_cmds_defs.h file. -- "EX_" prefix is omitted. -local RANGE = 0x001 -local BANG = 0x002 -local EXTRA = 0x004 -local XFILE = 0x008 -local NOSPC = 0x010 -local DFLALL = 0x020 -local WHOLEFOLD = 0x040 -local NEEDARG = 0x080 -local TRLBAR = 0x100 -local REGSTR = 0x200 -local COUNT = 0x400 -local NOTRLCOM = 0x800 -local ZEROR = 0x1000 -local CTRLV = 0x2000 -local CMDARG = 0x4000 -local BUFNAME = 0x8000 -local BUFUNL = 0x10000 -local ARGOPT = 0x20000 -local SBOXOK = 0x40000 -local CMDWIN = 0x80000 -local MODIFY = 0x100000 -local FLAGS = 0x200000 +local RANGE = 0x001 +local BANG = 0x002 +local EXTRA = 0x004 +local XFILE = 0x008 +local NOSPC = 0x010 +local DFLALL = 0x020 +local WHOLEFOLD = 0x040 +local NEEDARG = 0x080 +local TRLBAR = 0x100 +local REGSTR = 0x200 +local COUNT = 0x400 +local NOTRLCOM = 0x800 +local ZEROR = 0x1000 +local CTRLV = 0x2000 +local CMDARG = 0x4000 +local BUFNAME = 0x8000 +local BUFUNL = 0x10000 +local ARGOPT = 0x20000 +local SBOXOK = 0x40000 +local CMDWIN = 0x80000 +local MODIFY = 0x100000 +local FLAGS = 0x200000 +local PREVIEW = 0x8000000 local FILES = bit.bor(XFILE, EXTRA) local WORD1 = bit.bor(EXTRA, NOSPC) local FILE1 = bit.bor(FILES, NOSPC) @@ -33,6 +34,7 @@ local FILE1 = bit.bor(FILES, NOSPC) module.flags = { RANGE = RANGE, DFLALL = DFLALL, + PREVIEW = PREVIEW } -- The following table is described in ex_cmds_defs.h file. @@ -2305,9 +2307,10 @@ module.cmds = { }, { command='substitute', - flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN), + flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN, PREVIEW), addr_type='ADDR_LINES', func='ex_substitute', + preview_func='ex_substitute_preview', }, { command='sNext', @@ -2479,9 +2482,10 @@ module.cmds = { }, { command='smagic', - flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN), + flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN, PREVIEW), addr_type='ADDR_LINES', func='ex_submagic', + preview_func='ex_submagic_preview', }, { command='smap', @@ -2509,9 +2513,10 @@ module.cmds = { }, { command='snomagic', - flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN), + flags=bit.bor(RANGE, WHOLEFOLD, EXTRA, CMDWIN, PREVIEW), addr_type='ADDR_LINES', func='ex_submagic', + preview_func='ex_submagic_preview', }, { command='snoremap', diff --git a/src/nvim/ex_cmds_defs.h b/src/nvim/ex_cmds_defs.h index d8dd3da9e6..a5c9c6be2d 100644 --- a/src/nvim/ex_cmds_defs.h +++ b/src/nvim/ex_cmds_defs.h @@ -63,6 +63,7 @@ #define EX_MODIFY 0x100000 // forbidden in non-'modifiable' buffer #define EX_FLAGS 0x200000 // allow flags after count in argument #define EX_KEEPSCRIPT 0x4000000 // keep sctx of where command was invoked +#define EX_PREVIEW 0x8000000 // allow incremental command preview #define EX_FILES (EX_XFILE | EX_EXTRA) // multiple extra files allowed #define EX_FILE1 (EX_FILES | EX_NOSPC) // 1 file, defaults to current file #define EX_WORD1 (EX_EXTRA | EX_NOSPC) // one extra word allowed @@ -91,6 +92,7 @@ typedef struct exarg exarg_T; #define BAD_DROP (-2) // erase it typedef void (*ex_func_T)(exarg_T *eap); +typedef int (*ex_preview_func_T)(exarg_T *eap, long cmdpreview_ns, handle_T cmdpreview_bufnr); // NOTE: These possible could be removed and changed so that // Callback could take a "command" style string, and simply @@ -125,10 +127,11 @@ typedef char *(*LineGetter)(int, void *, int, bool); /// Structure for command definition. typedef struct cmdname { - char *cmd_name; ///< Name of the command. - ex_func_T cmd_func; ///< Function with implementation of this command. - uint32_t cmd_argt; ///< Relevant flags from the declared above. - cmd_addr_T cmd_addr_type; ///< Flag for address type + char *cmd_name; ///< Name of the command. + ex_func_T cmd_func; ///< Function with implementation of this command. + ex_preview_func_T cmd_preview_func; ///< Preview callback function of this command. + uint32_t cmd_argt; ///< Relevant flags from the declared above. + cmd_addr_T cmd_addr_type; ///< Flag for address type. } CommandDefinition; // A list used for saving values of "emsg_silent". Used by ex_try() to save the diff --git a/src/nvim/ex_docmd.c b/src/nvim/ex_docmd.c index e6ee0046af..4b462994be 100644 --- a/src/nvim/ex_docmd.c +++ b/src/nvim/ex_docmd.c @@ -294,7 +294,6 @@ int do_cmdline_cmd(const char *cmd) /// DOCMD_KEYTYPED - Don't reset KeyTyped. /// DOCMD_EXCRESET - Reset the exception environment (used for debugging). /// DOCMD_KEEPLINE - Store first typed line (for repeating with "."). -/// DOCMD_PREVIEW - During 'inccommand' preview. /// /// @param cookie argument for fgetline() /// @@ -593,11 +592,6 @@ int do_cmdline(char *cmdline, LineGetter fgetline, void *cookie, int flags) next_cmdline = do_one_cmd(&cmdline_copy, flags, &cstack, cmd_getline, cmd_cookie); recursive--; - // Ignore trailing '|'-separated commands in preview-mode ('inccommand'). - if ((State & MODE_CMDPREVIEW) && (flags & DOCMD_PREVIEW)) { - next_cmdline = NULL; - } - if (cmd_cookie == (void *)&cmd_loop_cookie) { // Use "current_line" from "cmd_loop_cookie", it may have been // incremented when defining a function. @@ -1578,9 +1572,11 @@ bool parse_cmdline(char *cmdline, exarg_T *eap, CmdParseInfo *cmdinfo, char **er /// /// @param eap Ex-command arguments /// @param cmdinfo Command parse information -void execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo) +/// @param preview Execute command preview callback instead of actual command +int execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo, bool preview) { char *errormsg = NULL; + int retv = 0; #define ERROR(msg) \ do { \ @@ -1698,11 +1694,17 @@ void execute_cmd(exarg_T *eap, CmdParseInfo *cmdinfo) // Execute the command if (IS_USER_CMDIDX(eap->cmdidx)) { // Execute a user-defined command. - do_ucmd(eap); + retv = do_ucmd(eap, preview); } else { - // Call the function to execute the command. + // Call the function to execute the command or the preview callback. eap->errmsg = NULL; - (cmdnames[eap->cmdidx].cmd_func)(eap); + + if (preview) { + retv = (cmdnames[eap->cmdidx].cmd_preview_func)(eap, cmdpreview_get_ns(), + cmdpreview_get_bufnr()); + } else { + (cmdnames[eap->cmdidx].cmd_func)(eap); + } if (eap->errmsg != NULL) { errormsg = _(eap->errmsg); } @@ -1718,6 +1720,7 @@ end: if (eap->did_sandbox) { sandbox--; } + return retv; #undef ERROR } @@ -2350,7 +2353,7 @@ static char *do_one_cmd(char **cmdlinep, int flags, cstack_T *cstack, LineGetter /* * Execute a user-defined command. */ - do_ucmd(&ea); + do_ucmd(&ea, false); } else { /* * Call the function to execute the command. @@ -5541,8 +5544,8 @@ char *uc_validate_name(char *name) /// /// @return OK if the command is created, FAIL otherwise. int uc_add_command(char *name, size_t name_len, char *rep, uint32_t argt, long def, int flags, - int compl, char *compl_arg, LuaRef compl_luaref, cmd_addr_T addr_type, - LuaRef luaref, bool force) + int compl, char *compl_arg, LuaRef compl_luaref, LuaRef preview_luaref, + cmd_addr_T addr_type, LuaRef luaref, bool force) FUNC_ATTR_NONNULL_ARG(1, 3) { ucmd_T *cmd = NULL; @@ -5597,6 +5600,7 @@ int uc_add_command(char *name, size_t name_len, char *rep, uint32_t argt, long d XFREE_CLEAR(cmd->uc_compl_arg); NLUA_CLEAR_REF(cmd->uc_luaref); NLUA_CLEAR_REF(cmd->uc_compl_luaref); + NLUA_CLEAR_REF(cmd->uc_preview_luaref); break; } @@ -5629,6 +5633,7 @@ int uc_add_command(char *name, size_t name_len, char *rep, uint32_t argt, long d nlua_set_sctx(&cmd->uc_script_ctx); cmd->uc_compl_arg = (char_u *)compl_arg; cmd->uc_compl_luaref = compl_luaref; + cmd->uc_preview_luaref = preview_luaref; cmd->uc_addr_type = addr_type; cmd->uc_luaref = luaref; @@ -5639,6 +5644,7 @@ fail: xfree(compl_arg); NLUA_CLEAR_REF(luaref); NLUA_CLEAR_REF(compl_luaref); + NLUA_CLEAR_REF(preview_luaref); return FAIL; } @@ -6071,8 +6077,7 @@ static void ex_command(exarg_T *eap) } else if (compl > 0 && (argt & EX_EXTRA) == 0) { emsg(_(e_complete_used_without_nargs)); } else { - uc_add_command(name, name_len, p, argt, def, flags, compl, - compl_arg, LUA_NOREF, + uc_add_command(name, name_len, p, argt, def, flags, compl, compl_arg, LUA_NOREF, LUA_NOREF, addr_type_arg, LUA_NOREF, eap->forceit); } } @@ -6092,6 +6097,7 @@ void free_ucmd(ucmd_T *cmd) xfree(cmd->uc_compl_arg); NLUA_CLEAR_REF(cmd->uc_compl_luaref); NLUA_CLEAR_REF(cmd->uc_luaref); + NLUA_CLEAR_REF(cmd->uc_preview_luaref); } /// Clear all user commands for "gap". @@ -6622,7 +6628,7 @@ size_t uc_mods(char *buf) return result; } -static void do_ucmd(exarg_T *eap) +static int do_ucmd(exarg_T *eap, bool preview) { char *buf; char *p; @@ -6643,9 +6649,14 @@ static void do_ucmd(exarg_T *eap) cmd = USER_CMD_GA(&curbuf->b_ucmds, eap->useridx); } + if (preview) { + assert(cmd->uc_preview_luaref > 0); + return nlua_do_ucmd(cmd, eap, true); + } + if (cmd->uc_luaref > 0) { - nlua_do_ucmd(cmd, eap); - return; + nlua_do_ucmd(cmd, eap, false); + return 0; } /* @@ -6740,6 +6751,8 @@ static void do_ucmd(exarg_T *eap) } xfree(buf); xfree(split_buf); + + return 0; } static char *expand_user_command_name(int idx) @@ -6796,7 +6809,8 @@ char *get_user_cmd_flags(expand_T *xp, int idx) { static char *user_cmd_flags[] = { "addr", "bang", "bar", "buffer", "complete", "count", - "nargs", "range", "register", "keepscript" }; + "nargs", "range", "register", + "keepscript" }; if (idx >= (int)ARRAY_SIZE(user_cmd_flags)) { return NULL; @@ -8568,6 +8582,18 @@ static void ex_submagic(exarg_T *eap) p_magic = magic_save; } +/// ":smagic" and ":snomagic" preview callback. +static int ex_submagic_preview(exarg_T *eap, long cmdpreview_ns, handle_T cmdpreview_bufnr) +{ + int magic_save = p_magic; + + p_magic = (eap->cmdidx == CMD_smagic); + int retv = ex_substitute_preview(eap, cmdpreview_ns, cmdpreview_bufnr); + p_magic = magic_save; + + return retv; +} + /// ":join". static void ex_join(exarg_T *eap) { @@ -8809,7 +8835,7 @@ static void ex_redir(exarg_T *eap) /// ":redraw": force redraw static void ex_redraw(exarg_T *eap) { - if (State & MODE_CMDPREVIEW) { + if (cmdpreview) { return; // Ignore :redraw during 'inccommand' preview. #9777 } int r = RedrawingDisabled; @@ -8843,7 +8869,7 @@ static void ex_redraw(exarg_T *eap) /// ":redrawstatus": force redraw of status line(s) and window bar(s) static void ex_redrawstatus(exarg_T *eap) { - if (State & MODE_CMDPREVIEW) { + if (cmdpreview) { return; // Ignore :redrawstatus during 'inccommand' preview. #9777 } int r = RedrawingDisabled; @@ -10107,22 +10133,16 @@ bool cmd_can_preview(char *cmd) if (*ea.cmd == '*') { ea.cmd = skipwhite(ea.cmd + 1); } - char *end = find_ex_command(&ea, NULL); + find_ex_command(&ea, NULL); - switch (ea.cmdidx) { - case CMD_substitute: - case CMD_smagic: - case CMD_snomagic: - // Only preview once the pattern delimiter has been typed - if (*end && !ASCII_ISALNUM(*end)) { - return true; - } - break; - default: - break; + if (ea.cmdidx == CMD_SIZE) { + return false; + } else if (!IS_USER_CMDIDX(ea.cmdidx)) { + // find_ex_command sets the flags for user commands automatically + ea.argt = cmdnames[(int)ea.cmdidx].cmd_argt; } - return false; + return (ea.argt & EX_PREVIEW); } /// Gets a map of maps describing user-commands defined for buffer `buf` or @@ -10149,6 +10169,7 @@ Dictionary commands_array(buf_T *buf) PUT(d, "bar", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_TRLBAR))); PUT(d, "register", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_REGSTR))); PUT(d, "keepscript", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_KEEPSCRIPT))); + PUT(d, "preview", BOOLEAN_OBJ(!!(cmd->uc_argt & EX_PREVIEW))); switch (cmd->uc_argt & (EX_EXTRA | EX_NOSPC | EX_NEEDARG)) { case 0: diff --git a/src/nvim/ex_docmd.h b/src/nvim/ex_docmd.h index 24656f3851..dbe095ab13 100644 --- a/src/nvim/ex_docmd.h +++ b/src/nvim/ex_docmd.h @@ -12,7 +12,6 @@ #define DOCMD_KEYTYPED 0x08 // don't reset KeyTyped #define DOCMD_EXCRESET 0x10 // reset exception environment (for debugging #define DOCMD_KEEPLINE 0x20 // keep typed line for repeating with "." -#define DOCMD_PREVIEW 0x40 // during 'inccommand' preview // defines for eval_vars() #define VALID_PATH 1 @@ -42,6 +41,7 @@ typedef struct ucmd { sctx_T uc_script_ctx; // SCTX where the command was defined char_u *uc_compl_arg; // completion argument if any LuaRef uc_compl_luaref; // Reference to Lua completion function + LuaRef uc_preview_luaref; // Reference to Lua preview function LuaRef uc_luaref; // Reference to Lua function } ucmd_T; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index 13cfd76adf..a94d6edce2 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -11,7 +11,9 @@ #include <stdlib.h> #include <string.h> +#include "nvim/api/extmark.h" #include "nvim/api/private/helpers.h" +#include "nvim/api/vim.h" #include "nvim/arabic.h" #include "nvim/ascii.h" #include "nvim/assert.h" @@ -69,6 +71,7 @@ #include "nvim/syntax.h" #include "nvim/tag.h" #include "nvim/ui.h" +#include "nvim/undo.h" #include "nvim/vim.h" #include "nvim/viml/parser/expressions.h" #include "nvim/viml/parser/parser.h" @@ -251,6 +254,9 @@ static CheckhealthComp healthchecks = { GA_INIT(sizeof(char_u *), 10), 0 }; # include "ex_getln.c.generated.h" #endif +static handle_T cmdpreview_bufnr = 0; +static long cmdpreview_ns = 0; + static int cmd_hkmap = 0; // Hebrew mapping during command line static void save_viewstate(viewstate_T *vs) @@ -740,6 +746,8 @@ static uint8_t *command_line_enter(int firstc, long count, int indent, bool init static int cmdline_level = 0; cmdline_level++; + bool save_cmdpreview = cmdpreview; + cmdpreview = false; CommandLineState state = { .firstc = firstc, .count = count, @@ -951,11 +959,6 @@ static uint8_t *command_line_enter(int firstc, long count, int indent, bool init ExpandCleanup(&s->xpc); ccline.xpc = NULL; - if (s->gotesc) { - // There might be a preview window open for inccommand. Close it. - close_preview_windows(); - } - finish_incsearch_highlighting(s->gotesc, &s->is_state, false); if (ccline.cmdbuff != NULL) { @@ -998,6 +1001,10 @@ static uint8_t *command_line_enter(int firstc, long count, int indent, bool init set_string_option_direct("icm", -1, s->save_p_icm, OPT_FREE, SID_NONE); State = s->save_State; + if (cmdpreview != save_cmdpreview) { + cmdpreview = save_cmdpreview; // restore preview state + redraw_all_later(SOME_VALID); + } setmouse(); ui_cursor_shape(); // may show different cursor shape sb_text_end_cmdline(); @@ -2306,6 +2313,267 @@ static int empty_pattern(char_u *p) return n == 0 || (n >= 2 && p[n - 2] == '\\' && p[n - 1] == '|'); } +handle_T cmdpreview_get_bufnr(void) +{ + return cmdpreview_bufnr; +} + +long cmdpreview_get_ns(void) +{ + return cmdpreview_ns; +} + +/// Sets up command preview buffer. +/// +/// @return Pointer to command preview buffer if succeeded, NULL if failed. +static buf_T *cmdpreview_open_buf(void) +{ + buf_T *cmdpreview_buf = cmdpreview_bufnr ? buflist_findnr(cmdpreview_bufnr) : NULL; + + // If preview buffer doesn't exist, open one. + if (cmdpreview_buf == NULL) { + Error err = ERROR_INIT; + handle_T bufnr = nvim_create_buf(false, true, &err); + + if (ERROR_SET(&err)) { + return NULL; + } + + cmdpreview_buf = buflist_findnr(bufnr); + } + + // Preview buffer cannot preview itself! + if (cmdpreview_buf == curbuf) { + return NULL; + } + + // Rename preview buffer. + aco_save_T aco; + aucmd_prepbuf(&aco, cmdpreview_buf); + int retv = rename_buffer("[Preview]"); + aucmd_restbuf(&aco); + + if (retv == FAIL) { + return NULL; + } + + // Temporarily switch to preview buffer to set it up for previewing. + aucmd_prepbuf(&aco, cmdpreview_buf); + buf_clear(); + curbuf->b_p_ma = true; + curbuf->b_p_ul = -1; + curbuf->b_p_tw = 0; // Reset 'textwidth' (was set by ftplugin) + aucmd_restbuf(&aco); + cmdpreview_bufnr = cmdpreview_buf->handle; + + return cmdpreview_buf; +} + +/// Open command preview window if it's not already open. +/// Returns to original window after opening command preview window. +/// +/// @param cmdpreview_buf Pointer to command preview buffer +/// +/// @return Pointer to command preview window if succeeded, NULL if failed. +static win_T *cmdpreview_open_win(buf_T *cmdpreview_buf) +{ + win_T *save_curwin = curwin; + bool win_found = false; + + // Try to find an existing preview window. + FOR_ALL_WINDOWS_IN_TAB(wp, curtab) { + if (wp->w_buffer == cmdpreview_buf) { + win_enter(wp, false); + win_found = true; + break; + } + } + + // If an existing window is not found, create one. + if (!win_found && win_split((int)p_cwh, WSP_BOT) == FAIL) { + return NULL; + } + + win_T *preview_win = curwin; + Error err = ERROR_INIT; + + // Switch to preview buffer + try_start(); + int result = do_buffer(DOBUF_GOTO, DOBUF_FIRST, FORWARD, cmdpreview_buf->handle, 0); + if (try_end(&err) || result == FAIL) { + api_clear_error(&err); + return NULL; + } + + curwin->w_p_cul = false; + curwin->w_p_cuc = false; + curwin->w_p_spell = false; + curwin->w_p_fen = false; + + win_enter(save_curwin, false); + return preview_win; +} + +/// Closes any open command preview windows. +static void cmdpreview_close_win(void) +{ + buf_T *buf = cmdpreview_bufnr ? buflist_findnr(cmdpreview_bufnr) : NULL; + if (buf != NULL) { + close_windows(buf, false); + } +} + +/// Show 'inccommand' preview. It works like this: +/// 1. Store current undo information so we can revert to current state later. +/// 2. Execute the preview callback with the parsed command, preview buffer number and preview +/// namespace number as arguments. The preview callback sets the highlight and does the +/// changes required for the preview if needed. +/// 3. Preview callback returns 0, 1 or 2. 0 means no preview is shown. 1 means preview is shown +/// but preview window doesn't need to be opened. 2 means preview is shown and preview window +/// needs to be opened if inccommand=split. +/// 4. Use the return value of the preview callback to determine whether to +/// open the preview window or not and open preview window if needed. +/// 5. If the return value of the preview callback is not 0, update the screen while the effects +/// of the preview are still in place. +/// 6. Revert all changes made by the preview callback. +static void cmdpreview_show(CommandLineState *s) +{ + // Parse the command line and return if it fails. + exarg_T ea; + CmdParseInfo cmdinfo; + // Copy the command line so we can modify it. + char *cmdline = xstrdup((char *)ccline.cmdbuff); + char *errormsg = NULL; + + parse_cmdline(cmdline, &ea, &cmdinfo, &errormsg); + if (errormsg != NULL) { + goto end; + } + + // Swap invalid command range if needed + if ((ea.argt & EX_RANGE) && ea.line1 > ea.line2) { + linenr_T lnum = ea.line1; + ea.line1 = ea.line2; + ea.line2 = lnum; + } + + time_t save_b_u_time_cur = curbuf->b_u_time_cur; + long save_b_u_seq_cur = curbuf->b_u_seq_cur; + u_header_T *save_b_u_newhead = curbuf->b_u_newhead; + long save_b_p_ul = curbuf->b_p_ul; + int save_b_changed = curbuf->b_changed; + int save_w_p_cul = curwin->w_p_cul; + int save_w_p_cuc = curwin->w_p_cuc; + bool save_hls = p_hls; + varnumber_T save_changedtick = buf_get_changedtick(curbuf); + buf_T *cmdpreview_buf; + win_T *cmdpreview_win; + cmdmod_T save_cmdmod = cmdmod; + + cmdpreview = true; + emsg_silent++; // Block error reporting as the command may be incomplete + msg_silent++; // Block messages, namely ones that prompt + block_autocmds(); // Block events + garray_T save_view; + win_size_save(&save_view); // Save current window sizes + save_search_patterns(); // Save search patterns + curbuf->b_p_ul = LONG_MAX; // Make sure we can undo all changes + curwin->w_p_cul = false; // Disable 'cursorline' so it doesn't mess up the highlights + curwin->w_p_cuc = false; // Disable 'cursorcolumn' so it doesn't mess up the highlights + p_hls = false; // Don't show search highlighting during live substitution + cmdmod.split = 0; // Disable :leftabove/botright modifiers + cmdmod.tab = 0; // Disable :tab modifier + cmdmod.noswapfile = true; // Disable swap for preview buffer + + // Open preview buffer if inccommand=split. + if (*p_icm == 'n') { + cmdpreview_bufnr = 0; + } else if ((cmdpreview_buf = cmdpreview_open_buf()) == NULL) { + abort(); + } + + // Setup preview namespace if it's not already set. + if (!cmdpreview_ns) { + cmdpreview_ns = (int)nvim_create_namespace((String)STRING_INIT); + } + + // Execute the preview callback and use its return value to determine whether to show preview or + // open the preview window. The preview callback also handles doing the changes and highlights for + // the preview. + Error err = ERROR_INIT; + try_start(); + int cmdpreview_type = execute_cmd(&ea, &cmdinfo, true); + if (try_end(&err)) { + api_clear_error(&err); + cmdpreview_type = 0; + } + + // If inccommand=split and preview callback returns 2, open preview window. + if (*p_icm != 'n' && cmdpreview_type == 2 + && (cmdpreview_win = cmdpreview_open_win(cmdpreview_buf)) == NULL) { + abort(); + } + + // If preview callback is nonzero, update screen now. + if (cmdpreview_type != 0) { + int save_rd = RedrawingDisabled; + RedrawingDisabled = 0; + update_screen(SOME_VALID); + RedrawingDisabled = save_rd; + } + + // Close preview window if it's open. + if (*p_icm != 'n' && cmdpreview_type == 2 && cmdpreview_win != NULL) { + cmdpreview_close_win(); + } + // Clear preview highlights. + extmark_clear(curbuf, (uint32_t)cmdpreview_ns, 0, 0, MAXLNUM, MAXCOL); + + curbuf->b_changed = save_b_changed; // Preserve 'modified' during preview + + if (curbuf->b_u_seq_cur != save_b_u_seq_cur) { + // Undo invisibly. This also moves the cursor! + while (curbuf->b_u_seq_cur != save_b_u_seq_cur) { + if (!u_undo_and_forget(1)) { + abort(); + } + } + // Restore newhead. It is meaningless when curhead is valid, but we must + // restore it so that undotree() is identical before/after the preview. + curbuf->b_u_newhead = save_b_u_newhead; + curbuf->b_u_time_cur = save_b_u_time_cur; + } + if (save_changedtick != buf_get_changedtick(curbuf)) { + buf_set_changedtick(curbuf, save_changedtick); + } + + cmdmod = save_cmdmod; // Restore cmdmod + p_hls = save_hls; // Restore 'hlsearch' + curwin->w_p_cul = save_w_p_cul; // Restore 'cursorline' + curwin->w_p_cuc = save_w_p_cuc; // Restore 'cursorcolumn' + curbuf->b_p_ul = save_b_p_ul; // Restore 'undolevels' + restore_search_patterns(); // Restore search patterns + win_size_restore(&save_view); // Restore window sizes + ga_clear(&save_view); + unblock_autocmds(); // Unblock events + msg_silent--; // Unblock messages + emsg_silent--; // Unblock error reporting + + // Restore the window "view". + curwin->w_cursor = s->is_state.save_cursor; + restore_viewstate(&s->is_state.old_viewstate); + update_topline(curwin); + + redrawcmdline(); + + // If preview callback returned 0, update screen to clear remnants of an earlier preview. + if (cmdpreview_type == 0) { + update_screen(SOME_VALID); + } +end: + xfree(cmdline); +} + static int command_line_changed(CommandLineState *s) { // Trigger CmdlineChanged autocommands. @@ -2345,27 +2613,9 @@ static int command_line_changed(CommandLineState *s) && cmdline_star == 0 // not typing a password && cmd_can_preview((char *)ccline.cmdbuff) && !vpeekc_any()) { - // Show 'inccommand' preview. It works like this: - // 1. Do the command. - // 2. Command implementation detects MODE_CMDPREVIEW state, then: - // - Update the screen while the effects are in place. - // - Immediately undo the effects. - State |= MODE_CMDPREVIEW; - emsg_silent++; // Block error reporting as the command may be incomplete - msg_silent++; // Block messages, namely ones that prompt - do_cmdline((char *)ccline.cmdbuff, NULL, NULL, DOCMD_KEEPLINE|DOCMD_NOWAIT|DOCMD_PREVIEW); - msg_silent--; // Unblock messages - emsg_silent--; // Unblock error reporting - - // Restore the window "view". - curwin->w_cursor = s->is_state.save_cursor; - restore_viewstate(&s->is_state.old_viewstate); - update_topline(curwin); - - redrawcmdline(); - } else if (State & MODE_CMDPREVIEW) { - State = (State & ~MODE_CMDPREVIEW); - close_preview_windows(); + cmdpreview_show(s); + } else if (cmdpreview) { + cmdpreview = false; update_screen(SOME_VALID); // Clear 'inccommand' preview. } else { if (s->xpc.xp_context == EXPAND_NOTHING && (KeyTyped || vpeekc() == NUL)) { diff --git a/src/nvim/fold.c b/src/nvim/fold.c index 234c11227d..d5277b9910 100644 --- a/src/nvim/fold.c +++ b/src/nvim/fold.c @@ -754,8 +754,7 @@ void deleteFold(win_T *const wp, const linenr_T start, const linenr_T end, const // the modification of the *first* line of the fold, but we send through a // notification that includes every line that was part of the fold int64_t num_changed = last_lnum - first_lnum; - buf_updates_send_changes(wp->w_buffer, first_lnum, num_changed, - num_changed, true); + buf_updates_send_changes(wp->w_buffer, first_lnum, num_changed, num_changed); } } @@ -1614,7 +1613,7 @@ static void foldCreateMarkers(win_T *wp, pos_T start, pos_T end) // u_save() is unable to save the buffer line, but we send the // nvim_buf_lines_event anyway since it won't do any harm. int64_t num_changed = 1 + end.lnum - start.lnum; - buf_updates_send_changes(buf, start.lnum, num_changed, num_changed, true); + buf_updates_send_changes(buf, start.lnum, num_changed, num_changed); } // foldAddMarker() {{{2 diff --git a/src/nvim/generators/gen_ex_cmds.lua b/src/nvim/generators/gen_ex_cmds.lua index 27cfe194fa..255c415a4d 100644 --- a/src/nvim/generators/gen_ex_cmds.lua +++ b/src/nvim/generators/gen_ex_cmds.lua @@ -65,20 +65,31 @@ for _, cmd in ipairs(defs) do assert(cmd.addr_type ~= 'ADDR_OTHER' and cmd.addr_type ~= 'ADDR_NONE', string.format('ex_cmds.lua:%s: Missing misplaced DFLALL\n', cmd.command)) end + if bit.band(cmd.flags, flags.PREVIEW) == flags.PREVIEW then + assert(cmd.preview_func ~= nil, + string.format('ex_cmds.lua:%s: Missing preview_func\n', cmd.command)) + end local enumname = cmd.enum or ('CMD_' .. cmd.command) local byte_cmd = cmd.command:sub(1, 1):byte() if byte_a <= byte_cmd and byte_cmd <= byte_z then table.insert(cmds, cmd.command) end + local preview_func + if cmd.preview_func then + preview_func = string.format("(ex_preview_func_T)&%s", cmd.preview_func) + else + preview_func = "NULL" + end enumfile:write(' ' .. enumname .. ',\n') defsfile:write(string.format([[ [%s] = { .cmd_name = "%s", .cmd_func = (ex_func_T)&%s, + .cmd_preview_func = %s, .cmd_argt = %uL, .cmd_addr_type = %s }, -]], enumname, cmd.command, cmd.func, cmd.flags, cmd.addr_type)) +]], enumname, cmd.command, cmd.func, preview_func, cmd.flags, cmd.addr_type)) end for i = #cmds, 1, -1 do local cmd = cmds[i] diff --git a/src/nvim/globals.h b/src/nvim/globals.h index b0006ebaca..fb3aa91c85 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -638,6 +638,9 @@ EXTERN int motion_force INIT(=0); // motion force for pending operator EXTERN bool exmode_active INIT(= false); // true if Ex mode is active EXTERN bool ex_no_reprint INIT(=false); // No need to print after z or p. +// 'inccommand' command preview state +EXTERN bool cmdpreview INIT(= false); + EXTERN int reg_recording INIT(= 0); // register for recording or zero EXTERN int reg_executing INIT(= 0); // register being executed or zero // Flag set when peeking a character and found the end of executed register diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index a826dd07d3..deff2347e0 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -1840,11 +1840,12 @@ cleanup: xfree(info); } -void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap) +/// @param preview Invoke the callback as a |:command-preview| handler. +int nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap, bool preview) { lua_State *const lstate = global_lstate; - nlua_pushref(lstate, cmd->uc_luaref); + nlua_pushref(lstate, preview ? cmd->uc_preview_luaref : cmd->uc_luaref); lua_newtable(lstate); lua_pushboolean(lstate, eap->forceit == 1); @@ -1969,7 +1970,31 @@ void nlua_do_ucmd(ucmd_T *cmd, exarg_T *eap) lua_setfield(lstate, -2, "smods"); - if (nlua_pcall(lstate, 1, 0)) { + if (preview) { + lua_pushinteger(lstate, cmdpreview_get_ns()); + + handle_T cmdpreview_bufnr = cmdpreview_get_bufnr(); + if (cmdpreview_bufnr != 0) { + lua_pushinteger(lstate, cmdpreview_bufnr); + } else { + lua_pushnil(lstate); + } + } + + if (nlua_pcall(lstate, preview ? 3 : 1, preview ? 1 : 0)) { nlua_error(lstate, _("Error executing Lua callback: %.*s")); + return 0; } + + int retv = 0; + + if (preview) { + if (lua_isnumber(lstate, -1) && (retv = (int)lua_tointeger(lstate, -1)) >= 0 && retv <= 2) { + lua_pop(lstate, 1); + } else { + retv = 0; + } + } + + return retv; } diff --git a/src/nvim/vim.h b/src/nvim/vim.h index 7e82af2d93..31ac5a67ff 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -70,7 +70,6 @@ enum { NUMBUFLEN = 65, }; #define MODE_EXTERNCMD 0x5000 // executing an external command #define MODE_SHOWMATCH (0x6000 | MODE_INSERT) // show matching paren #define MODE_CONFIRM 0x7000 // ":confirm" prompt -#define MODE_CMDPREVIEW 0x8000 // Showing 'inccommand' command "live" preview. /// Directions. typedef enum { diff --git a/test/functional/api/command_spec.lua b/test/functional/api/command_spec.lua index d6d75e93e4..253371557a 100644 --- a/test/functional/api/command_spec.lua +++ b/test/functional/api/command_spec.lua @@ -16,8 +16,8 @@ local feed = helpers.feed local funcs = helpers.funcs describe('nvim_get_commands', function() - local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', range=NIL, register=false, keepscript=false, script_id=0, } - local cmd_dict2 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='pwd', name='Pwd', nargs='?', range=NIL, register=false, keepscript=false, script_id=0, } + local cmd_dict = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='echo "Hello World"', name='Hello', nargs='1', preview=false, range=NIL, register=false, keepscript=false, script_id=0, } + local cmd_dict2 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='pwd', name='Pwd', nargs='?', preview=false, range=NIL, register=false, keepscript=false, script_id=0, } before_each(clear) it('gets empty list if no commands were defined', function() @@ -59,11 +59,11 @@ describe('nvim_get_commands', function() end) it('gets various command attributes', function() - local cmd0 = { addr='arguments', bang=false, bar=false, complete='dir', complete_arg=NIL, count='10', definition='pwd <args>', name='TestCmd', nargs='1', range='10', register=false, keepscript=false, script_id=0, } - local cmd1 = { addr=NIL, bang=false, bar=false, complete='custom', complete_arg='ListUsers', count=NIL, definition='!finger <args>', name='Finger', nargs='+', range=NIL, register=false, keepscript=false, script_id=1, } - local cmd2 = { addr=NIL, bang=true, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R2_foo(<q-args>)', name='Cmd2', nargs='*', range=NIL, register=false, keepscript=false, script_id=2, } - local cmd3 = { addr=NIL, bang=false, bar=true, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R3_ohyeah()', name='Cmd3', nargs='0', range=NIL, register=false, keepscript=false, script_id=3, } - local cmd4 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R4_just_great()', name='Cmd4', nargs='0', range=NIL, register=true, keepscript=false, script_id=4, } + local cmd0 = { addr='arguments', bang=false, bar=false, complete='dir', complete_arg=NIL, count='10', definition='pwd <args>', name='TestCmd', nargs='1', preview=false, range='10', register=false, keepscript=false, script_id=0, } + local cmd1 = { addr=NIL, bang=false, bar=false, complete='custom', complete_arg='ListUsers', count=NIL, definition='!finger <args>', name='Finger', nargs='+', preview=false, range=NIL, register=false, keepscript=false, script_id=1, } + local cmd2 = { addr=NIL, bang=true, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R2_foo(<q-args>)', name='Cmd2', nargs='*', preview=false, range=NIL, register=false, keepscript=false, script_id=2, } + local cmd3 = { addr=NIL, bang=false, bar=true, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R3_ohyeah()', name='Cmd3', nargs='0', preview=false, range=NIL, register=false, keepscript=false, script_id=3, } + local cmd4 = { addr=NIL, bang=false, bar=false, complete=NIL, complete_arg=NIL, count=NIL, definition='call \128\253R4_just_great()', name='Cmd4', nargs='0', preview=false, range=NIL, register=true, keepscript=false, script_id=4, } source([[ let s:foo = 1 command -complete=custom,ListUsers -nargs=+ Finger !finger <args> diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index ca5e269f92..fdd1504b13 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -3660,10 +3660,10 @@ describe('float window', function() screen:expect{grid=[[ ## grid 1 [2:----------------------------------------]| - {5:[No Name] }| - [5:----------------------------------------]| - [5:----------------------------------------]| - [5:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| + [2:----------------------------------------]| {5:[Preview] }| [3:----------------------------------------]| ## grid 2 @@ -3674,10 +3674,6 @@ describe('float window', function() {17:f}{1:oo }| {17:b}{1:ar }| {1: }| - ## grid 5 - |1| {17:f}oo | - |2| {17:b}ar | - {0:~ }| ]], float_pos=expected_pos} else screen:expect([[ diff --git a/test/functional/ui/inccommand_spec.lua b/test/functional/ui/inccommand_spec.lua index a1ff778da1..a95cb0e83a 100644 --- a/test/functional/ui/inccommand_spec.lua +++ b/test/functional/ui/inccommand_spec.lua @@ -255,42 +255,6 @@ describe(":substitute, 'inccommand' preserves", function() end) end - for _, case in pairs{"", "split", "nosplit"} do - it("visual selection for non-previewable command (inccommand="..case..") #5888", function() - local screen = Screen.new(30,10) - common_setup(screen, case, default_text) - feed('1G2V') - - feed(':s') - screen:expect([[ - {vis:Inc substitution on} | - t{vis:wo lines} | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :'<,'>s^ | - ]]) - - feed('o') - screen:expect([[ - {vis:Inc substitution on} | - t{vis:wo lines} | - | - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - {15:~ }| - :'<,'>so^ | - ]]) - end) - end - for _, case in ipairs({'', 'split', 'nosplit'}) do it('previous substitute string ~ (inccommand='..case..') #12109', function() local screen = Screen.new(30,10) diff --git a/test/functional/ui/inccommand_user_spec.lua b/test/functional/ui/inccommand_user_spec.lua new file mode 100644 index 0000000000..e2cd82943e --- /dev/null +++ b/test/functional/ui/inccommand_user_spec.lua @@ -0,0 +1,329 @@ +local helpers = require('test.functional.helpers')(after_each) +local Screen = require('test.functional.ui.screen') +local clear = helpers.clear +local exec_lua = helpers.exec_lua +local insert = helpers.insert +local feed = helpers.feed +local command = helpers.command + +-- Implements a :Replace command that works like :substitute. +local setup_replace_cmd = [[ + local function show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) + -- Find the width taken by the largest line number, used for padding the line numbers + local highest_lnum = math.max(matches[#matches][1], 1) + local highest_lnum_width = math.floor(math.log10(highest_lnum)) + local preview_buf_line = 0 + + vim.g.prevns = preview_ns + vim.g.prevbuf = preview_buf + + for _, match in ipairs(matches) do + local lnum = match[1] + local line_matches = match[2] + local prefix + + if use_preview_win then + prefix = string.format( + '|%s%d| ', + string.rep(' ', highest_lnum_width - math.floor(math.log10(lnum))), + lnum + ) + + vim.api.nvim_buf_set_lines( + preview_buf, + preview_buf_line, + preview_buf_line, + 0, + { prefix .. vim.api.nvim_buf_get_lines(buf, lnum - 1, lnum, false)[1] } + ) + end + + for _, line_match in ipairs(line_matches) do + vim.api.nvim_buf_add_highlight( + buf, + preview_ns, + 'Substitute', + lnum - 1, + line_match[1], + line_match[2] + ) + + if use_preview_win then + vim.api.nvim_buf_add_highlight( + preview_buf, + preview_ns, + 'Substitute', + preview_buf_line, + #prefix + line_match[1], + #prefix + line_match[2] + ) + end + end + + preview_buf_line = preview_buf_line + 1 + end + + if use_preview_win then + return 2 + else + return 1 + end + end + + local function do_replace(opts, preview, preview_ns, preview_buf) + local pat1 = opts.fargs[1] or '' + local pat2 = opts.fargs[2] or '' + local line1 = opts.line1 + local line2 = opts.line2 + + local buf = vim.api.nvim_get_current_buf() + local lines = vim.api.nvim_buf_get_lines(buf, line1 - 1, line2, 0) + local matches = {} + + for i, line in ipairs(lines) do + local startidx, endidx = 0, 0 + local line_matches = {} + local num = 1 + + while startidx ~= -1 do + local match = vim.fn.matchstrpos(line, pat1, 0, num) + startidx, endidx = match[2], match[3] + + if startidx ~= -1 then + line_matches[#line_matches+1] = { startidx, endidx } + end + + num = num + 1 + end + + if #line_matches > 0 then + matches[#matches+1] = { line1 + i - 1, line_matches } + end + end + + local new_lines = {} + + for _, match in ipairs(matches) do + local lnum = match[1] + local line_matches = match[2] + local line = lines[lnum - line1 + 1] + local pat_width_differences = {} + + -- If previewing, only replace the text in current buffer if pat2 isn't empty + -- Otherwise, always replace the text + if pat2 ~= '' or not preview then + if preview then + for _, line_match in ipairs(line_matches) do + local startidx, endidx = unpack(line_match) + local pat_match = line:sub(startidx + 1, endidx) + + pat_width_differences[#pat_width_differences+1] = + #vim.fn.substitute(pat_match, pat1, pat2, 'g') - #pat_match + end + end + + new_lines[lnum] = vim.fn.substitute(line, pat1, pat2, 'g') + end + + -- Highlight the matches if previewing + if preview then + local idx_offset = 0 + for i, line_match in ipairs(line_matches) do + local startidx, endidx = unpack(line_match) + -- Starting index of replacement text + local repl_startidx = startidx + idx_offset + -- Ending index of the replacement text (if pat2 isn't empty) + local repl_endidx + + if pat2 ~= '' then + repl_endidx = endidx + idx_offset + pat_width_differences[i] + else + repl_endidx = endidx + idx_offset + end + + if pat2 ~= '' then + idx_offset = idx_offset + pat_width_differences[i] + end + + line_matches[i] = { repl_startidx, repl_endidx } + end + end + end + + for lnum, line in pairs(new_lines) do + vim.api.nvim_buf_set_lines(buf, lnum - 1, lnum, false, { line }) + end + + if preview then + local lnum = vim.api.nvim_win_get_cursor(0)[1] + -- Use preview window only if preview buffer is provided and range isn't just the current line + local use_preview_win = (preview_buf ~= nil) and (line1 ~= lnum or line2 ~= lnum) + return show_replace_preview(buf, use_preview_win, preview_ns, preview_buf, matches) + end + end + + local function replace(opts) + do_replace(opts, false) + end + + local function replace_preview(opts, preview_ns, preview_buf) + return do_replace(opts, true, preview_ns, preview_buf) + end + + -- ":<range>Replace <pat1> <pat2>" + -- Replaces all occurences of <pat1> in <range> with <pat2> + vim.api.nvim_create_user_command( + 'Replace', + replace, + { nargs = '*', range = '%', addr = 'lines', + preview = replace_preview } + ) +]] + +describe("'inccommand' for user commands", function() + local screen + + before_each(function() + clear() + screen = Screen.new(40, 17) + screen:set_default_attr_ids({ + [1] = {background = Screen.colors.Yellow1}, + [2] = {foreground = Screen.colors.Blue1, bold = true}, + [3] = {reverse = true}, + [4] = {reverse = true, bold = true} + }) + screen:attach() + exec_lua(setup_replace_cmd) + command('set cmdwinheight=5') + insert[[ + text on line 1 + more text on line 2 + oh no, even more text + will the text ever stop + oh well + did the text stop + why won't it stop + make the text stop + ]] + end) + + it('works with inccommand=nosplit', function() + command('set inccommand=nosplit') + feed(':Replace text cats') + screen:expect([[ + {1:cats} on line 1 | + more {1:cats} on line 2 | + oh no, even more {1:cats} | + will the {1:cats} ever stop | + oh well | + did the {1:cats} stop | + why won't it stop | + make the {1:cats} stop | + | + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + :Replace text cats^ | + ]]) + end) + + it('works with inccommand=split', function() + command('set inccommand=split') + feed(':Replace text cats') + screen:expect([[ + {1:cats} on line 1 | + more {1:cats} on line 2 | + oh no, even more {1:cats} | + will the {1:cats} ever stop | + oh well | + did the {1:cats} stop | + why won't it stop | + make the {1:cats} stop | + | + {4:[No Name] [+] }| + |1| {1:cats} on line 1 | + |2| more {1:cats} on line 2 | + |3| oh no, even more {1:cats} | + |4| will the {1:cats} ever stop | + |6| did the {1:cats} stop | + {3:[Preview] }| + :Replace text cats^ | + ]]) + end) + + it('properly closes preview when inccommand=split', function() + command('set inccommand=split') + feed(':Replace text cats<Esc>') + screen:expect([[ + text on line 1 | + more text on line 2 | + oh no, even more text | + will the text ever stop | + oh well | + did the text stop | + why won't it stop | + make the text stop | + ^ | + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + | + ]]) + end) + + it('properly executes command when inccommand=split', function() + command('set inccommand=split') + feed(':Replace text cats<CR>') + screen:expect([[ + cats on line 1 | + more cats on line 2 | + oh no, even more cats | + will the cats ever stop | + oh well | + did the cats stop | + why won't it stop | + make the cats stop | + ^ | + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + :Replace text cats | + ]]) + end) + + it('shows preview window only when range is not current line', function() + command('set inccommand=split') + feed('gg:.Replace text cats') + screen:expect([[ + {1:cats} on line 1 | + more text on line 2 | + oh no, even more text | + will the text ever stop | + oh well | + did the text stop | + why won't it stop | + make the text stop | + | + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + :.Replace text cats^ | + ]]) + end) +end) |