neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

commit f2373a89d78356423a84e9636d6949b5322ecc69
parent a4f318574a3a84b2119e9d613940937891d14f15
Author: glepnir <glephunter@gmail.com>
Date:   Wed, 28 May 2025 17:10:00 +0800

vim-patch:9.1.1408: not easily possible to complete from register content (#34198)

Problem:  not easily possible to complete from register content
Solution: add register-completion submode using i_CTRL-X_CTRL-R
          (glepnir)

closes: vim/vim#17354

https://github.com/vim/vim/commit/0546068aaef2b1a40faa2945ef7eba249739f219
Diffstat:
Mruntime/doc/index.txt | 1+
Mruntime/doc/insert.txt | 15+++++++++++++++
Mruntime/doc/options.txt | 3++-
Mruntime/doc/usr_24.txt | 1+
Mruntime/doc/vi_diff.txt | 1+
Mruntime/lua/vim/_meta/options.lua | 3++-
Msrc/nvim/edit.c | 4++++
Msrc/nvim/insexpand.c | 90++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Msrc/nvim/options.lua | 3++-
Mtest/functional/editor/completion_spec.lua | 2+-
Mtest/functional/ui/float_spec.lua | 4++--
Mtest/functional/ui/messages_spec.lua | 2+-
Mtest/old/testdir/test_ins_complete.vim | 81+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
13 files changed, 198 insertions(+), 12 deletions(-)

diff --git a/runtime/doc/index.txt b/runtime/doc/index.txt @@ -146,6 +146,7 @@ commands in CTRL-X submode *i_CTRL-X_index* |i_CTRL-X_CTRL-N| CTRL-X CTRL-N next completion |i_CTRL-X_CTRL-O| CTRL-X CTRL-O omni completion |i_CTRL-X_CTRL-P| CTRL-X CTRL-P previous completion +|i_CTRL-X_CTRL-R| CTRL-X CTRL-R complete words from registers |i_CTRL-X_CTRL-S| CTRL-X CTRL-S spelling suggestions |i_CTRL-X_CTRL-T| CTRL-X CTRL-T complete identifiers from thesaurus |i_CTRL-X_CTRL-Y| CTRL-X CTRL-Y scroll down diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt @@ -629,6 +629,7 @@ Completion can be done for: 11. omni completion |i_CTRL-X_CTRL-O| 12. Spelling suggestions |i_CTRL-X_s| 13. keywords in 'complete' |i_CTRL-N| |i_CTRL-P| +14. words from registers |i_CTRL-X_CTRL-R| Additionally, |i_CTRL-X_CTRL-Z| stops completion without changing the text. @@ -999,6 +1000,20 @@ CTRL-X CTRL-V Guess what kind of item is in front of the cursor and completion, for example: > :imap <Tab> <C-X><C-V> +Completing words from registers *compl-register-words* + *i_CTRL-X_CTRL-R* +CTRL-X CTRL-R Guess what kind of item is in front of the cursor from + all registers and find the first match for it. + Further use of CTRL-R (without CTRL-X) will insert the + register content, see |i_CTRL-R|. + 'ignorecase' applies to the matching. + + CTRL-N Search forwards for next match. This match replaces + the previous one. + + CTRL-P Search backwards for previous match. This match + replaces the previous one. + User defined completion *compl-function* Completion is done by a function that can be defined by the user with the diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt @@ -3345,7 +3345,8 @@ A jump table for the options with a short description can be found at |Q_op|. 'ignorecase' 'ic' boolean (default off) global Ignore case in search patterns, |cmdline-completion|, when - searching in the tags file, and |expr-==|. + searching in the tags file, |expr-==| and for Insert-mode completion + |ins-completion|. Also see 'smartcase' and 'tagcase'. Can be overruled by using "\c" or "\C" in the pattern, see |/ignorecase|. diff --git a/runtime/doc/usr_24.txt b/runtime/doc/usr_24.txt @@ -187,6 +187,7 @@ with a certain type of item: CTRL-X CTRL-D macro definitions (also in included files) CTRL-X CTRL-I current and included files CTRL-X CTRL-K words from a dictionary + CTRL-X CTRL-R words from registers CTRL-X CTRL-T words from a thesaurus CTRL-X CTRL-] tags CTRL-X CTRL-V Vim command line diff --git a/runtime/doc/vi_diff.txt b/runtime/doc/vi_diff.txt @@ -230,6 +230,7 @@ Insert-mode completion. |ins-completion| |i_CTRL-X_CTRL-D| definitions or macros |i_CTRL-X_CTRL-O| Omni completion: clever completion specifically for a file type + |i_CTRL-X_CTRL-R| words from registers etc. Long line support. |'wrap'| |'linebreak'| diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua @@ -3186,7 +3186,8 @@ vim.o.iconstring = "" vim.go.iconstring = vim.o.iconstring --- Ignore case in search patterns, `cmdline-completion`, when ---- searching in the tags file, and `expr-==`. +--- searching in the tags file, `expr-==` and for Insert-mode completion +--- `ins-completion`. --- Also see 'smartcase' and 'tagcase'. --- Can be overruled by using "\c" or "\C" in the pattern, see --- `/ignorecase`. diff --git a/src/nvim/edit.c b/src/nvim/edit.c @@ -796,6 +796,10 @@ static int insert_handle_key(InsertState *s) break; case Ctrl_R: // insert the contents of a register + if (ctrl_x_mode_register() && !ins_compl_active()) { + insert_do_complete(s); + break; + } ins_reg(); auto_format(false, true); s->inserted_space = false; diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c @@ -53,6 +53,7 @@ #include "nvim/memory.h" #include "nvim/message.h" #include "nvim/move.h" +#include "nvim/ops.h" #include "nvim/option.h" #include "nvim/option_defs.h" #include "nvim/option_vars.h" @@ -104,6 +105,7 @@ enum { CTRL_X_EVAL = 16, ///< for builtin function complete() CTRL_X_CMDLINE_CTRL_X = 17, ///< CTRL-X typed in CTRL_X_CMDLINE CTRL_X_BUFNAMES = 18, + CTRL_X_REGISTER = 19, ///< complete words from registers }; #define CTRL_X_MSG(i) ctrl_x_msgs[(i) & ~CTRL_X_WANT_IDENT] @@ -111,7 +113,7 @@ enum { /// Message for CTRL-X mode, index is ctrl_x_mode. static char *ctrl_x_msgs[] = { N_(" Keyword completion (^N^P)"), // CTRL_X_NORMAL, ^P/^N compl. - N_(" ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)"), + N_(" ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)"), NULL, // CTRL_X_SCROLL: depends on state N_(" Whole line completion (^L^N^P)"), N_(" File name completion (^F^N^P)"), @@ -128,6 +130,8 @@ static char *ctrl_x_msgs[] = { N_(" Keyword Local completion (^N^P)"), NULL, // CTRL_X_EVAL doesn't use msg. N_(" Command-line completion (^V^N^P)"), + NULL, + N_(" Register completion (^N^P)"), }; static char *ctrl_x_mode_names[] = { @@ -149,6 +153,8 @@ static char *ctrl_x_mode_names[] = { NULL, // CTRL_X_LOCAL_MSG only used in "ctrl_x_msgs" "eval", "cmdline", + NULL, // CTRL_X_BUFNAME + "register", }; /// Structure used to store one match for insert completion. @@ -423,6 +429,12 @@ bool ctrl_x_mode_line_or_eval(void) return ctrl_x_mode == CTRL_X_WHOLE_LINE || ctrl_x_mode == CTRL_X_EVAL; } +bool ctrl_x_mode_register(void) + FUNC_ATTR_PURE +{ + return ctrl_x_mode == CTRL_X_REGISTER; +} + /// Whether other than default completion has been selected. bool ctrl_x_mode_not_default(void) FUNC_ATTR_PURE @@ -514,7 +526,7 @@ bool vim_is_ctrl_x_key(int c) FUNC_ATTR_WARN_UNUSED_RESULT { // Always allow ^R - let its results then be checked - if (c == Ctrl_R) { + if (c == Ctrl_R && ctrl_x_mode != CTRL_X_REGISTER) { return true; } @@ -534,7 +546,7 @@ bool vim_is_ctrl_x_key(int c) || c == Ctrl_N || c == Ctrl_T || c == Ctrl_V || c == Ctrl_Q || c == Ctrl_U || c == Ctrl_O || c == Ctrl_S || c == Ctrl_K || c == 's' - || c == Ctrl_Z; + || c == Ctrl_Z || c == Ctrl_R; case CTRL_X_SCROLL: return c == Ctrl_Y || c == Ctrl_E; case CTRL_X_WHOLE_LINE: @@ -564,6 +576,8 @@ bool vim_is_ctrl_x_key(int c) return (c == Ctrl_P || c == Ctrl_N); case CTRL_X_BUFNAMES: return (c == Ctrl_P || c == Ctrl_N); + case CTRL_X_REGISTER: + return (c == Ctrl_R || c == Ctrl_P || c == Ctrl_N); } internal_error("vim_is_ctrl_x_key()"); return false; @@ -2244,8 +2258,12 @@ static bool set_ctrl_x_mode(const int c) ctrl_x_mode = CTRL_X_DICTIONARY; break; case Ctrl_R: - // Register insertion without exiting CTRL-X mode - // Simply allow ^R to happen without affecting ^X mode + // When CTRL-R is followed by '=', don't trigger register completion + // This allows expressions like <C-R>=func()<CR> to work normally + if (vpeekc() == '=') { + break; + } + ctrl_x_mode = CTRL_X_REGISTER; break; case Ctrl_T: // complete words from a thesaurus @@ -4027,6 +4045,63 @@ static int get_next_default_completion(ins_compl_next_state_T *st, pos_T *start_ return found_new_match; } +/// Get completion matches from register contents. +/// Extracts words from all available registers and adds them to the completion list. +static void get_register_completion(void) +{ + Direction dir = compl_direction; + + for (int i = 0; i < NUM_REGISTERS; i++) { + int regname = get_register_name(i); + // Skip invalid or black hole register + if (!valid_yank_reg(regname, false) || regname == '_') { + continue; + } + + yankreg_T *reg = copy_register(regname); + + if (reg->y_array == NULL || reg->y_size == 0) { + free_register(reg); + xfree(reg); + continue; + } + + for (size_t j = 0; j < reg->y_size; j++) { + char *str = reg->y_array[j].data; + if (str == NULL) { + continue; + } + + char *p = str; + while (*p != NUL) { + p = find_word_start(p); + if (*p == NUL) { + break; + } + + char *word_end = find_word_end(p); + int len = (int)(word_end - p); + + // Add the word to the completion list + if (len > 0 && (!compl_orig_text.data + || (p_ic ? STRNICMP(p, compl_orig_text.data, + compl_orig_text.size) == 0 + : strncmp(p, compl_orig_text.data, + compl_orig_text.size) == 0))) { + if (ins_compl_add_infercase(p, len, p_ic, NULL, + dir, false, 0) == OK) { + dir = FORWARD; + } + } + p = word_end; + } + } + + free_register(reg); + xfree(reg); + } +} + /// get the next set of completion matches for "type". /// @return true if a new match is found, otherwise false. static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_T *ini) @@ -4071,6 +4146,9 @@ static bool get_next_completion_match(int type, ins_compl_next_state_T *st, pos_ case CTRL_X_BUFNAMES: get_next_bufname_token(); break; + case CTRL_X_REGISTER: + get_register_completion(); + break; default: // normal ^P/^N and ^X^L found_new_match = get_next_default_completion(st, ini); @@ -5077,6 +5155,8 @@ static int compl_get_info(char *line, int startcol, colnr_T curs_col, bool *line return FAIL; } *line_invalid = true; // "line" may have become invalid + } else if (ctrl_x_mode_register()) { + return get_normal_compl_info(line, startcol, curs_col); } else { internal_error("ins_complete()"); return FAIL; diff --git a/src/nvim/options.lua b/src/nvim/options.lua @@ -4315,7 +4315,8 @@ local options = { defaults = false, desc = [=[ Ignore case in search patterns, |cmdline-completion|, when - searching in the tags file, and |expr-==|. + searching in the tags file, |expr-==| and for Insert-mode completion + |ins-completion|. Also see 'smartcase' and 'tagcase'. Can be overruled by using "\c" or "\C" in the pattern, see |/ignorecase|. diff --git a/test/functional/editor/completion_spec.lua b/test/functional/editor/completion_spec.lua @@ -124,7 +124,7 @@ describe('completion', function() foo | ^ | {1:~ }|*5 - {5:-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)} | + {5:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)} | ]]) feed('<C-n>') screen:expect([[ diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua @@ -3519,7 +3519,7 @@ describe('float window', function() | {0:~ }|*7 ## grid 3 - {3:-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)} | + {3:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)} | ## grid 4 {1: }| {2:~ }|*2 @@ -3543,7 +3543,7 @@ describe('float window', function() {1: } {1:^ } | {2:~ }{0: }{2:~ }{0: }|*2 {0:~ }|*5 - {3:-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)} | + {3:-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)} | ]], } end diff --git a/test/functional/ui/messages_spec.lua b/test/functional/ui/messages_spec.lua @@ -815,7 +815,7 @@ describe('ui/ext_messages', function() ^ | {1:~ }|*2 ]], - showmode = { { '-- ^X mode (^]^D^E^F^I^K^L^N^O^Ps^U^V^Y)', 5, 11 } }, + showmode = { { '-- ^X mode (^]^D^E^F^I^K^L^N^O^P^Rs^U^V^Y)', 5, 11 } }, } feed('<c-p>') diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim @@ -3792,4 +3792,85 @@ func Test_complete_match() delfunc TestComplete endfunc +func Test_register_completion() + let @a = "completion test apple application" + let @b = "banana behavior better best" + let @c = "complete completion compliment computer" + let g:save_reg = '' + func GetItems() + let g:result = complete_info(['pum_visible']) + endfunc + + new + call setline(1, "comp") + call cursor(1, 4) + call feedkeys("a\<C-X>\<C-R>\<C-N>\<C-N>\<Esc>", 'tx') + call assert_equal("compliment", getline(1)) + + inoremap <buffer><F2> <C-R>=GetItems()<CR> + call feedkeys("S\<C-X>\<C-R>\<F2>\<ESC>", 'tx') + call assert_equal(1, g:result['pum_visible']) + + call setline(1, "app") + call cursor(1, 3) + call feedkeys("a\<C-X>\<C-R>\<C-N>\<Esc>", 'tx') + call assert_equal("application", getline(1)) + + " Test completion with case differences + set ignorecase + let @e = "TestCase UPPERCASE lowercase" + call setline(1, "testc") + call cursor(1, 5) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("TestCase", getline(1)) + + " Test clipboard registers if available + if has('clipboard_working') + let g:save_reg = getreg('*') + call setreg('*', "clipboard selection unique words") + call setline(1, "uni") + call cursor(1, 3) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("unique", getline(1)) + call setreg('*', g:save_reg) + + let g:save_reg = getreg('+') + call setreg('+', "system clipboard special content") + call setline(1, "spe") + call cursor(1, 3) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("special", getline(1)) + call setreg('+', g:save_reg) + + call setreg('*', g:save_reg) + call setreg('a', "normal register") + call setreg('*', "clipboard mixed content") + call setline(1, "mix") + call cursor(1, 3) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("mixed", getline(1)) + call setreg('*', g:save_reg) + endif + + " Test black hole register should be skipped + let @_ = "blackhole content should not appear" + call setline(1, "black") + call cursor(1, 5) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("black", getline(1)) + + let @1 = "recent yank zero" + call setline(1, "ze") + call cursor(1, 2) + call feedkeys("a\<C-X>\<C-R>\<Esc>", 'tx') + call assert_equal("zero", getline(1)) + + " Clean up + bwipe! + delfunc GetItems + unlet g:result + unlet g:save_reg + set ignorecase& +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable