From edd4ac3e895ce16034c7e098f1d68e0155d97886 Mon Sep 17 00:00:00 2001 From: glepnir Date: Wed, 29 Jan 2025 18:53:51 +0100 Subject: [PATCH] patch 9.1.1056: Vim doesn't highlight to be inserted text when completing Problem: Vim doesn't highlight to be inserted text when completing Solution: Add support for the "preinsert" 'completeopt' value (glepnir) Support automatically inserting the currently selected candidate word that does not belong to the latter part of the leader. fixes: #3433 closes: #16403 Signed-off-by: glepnir Signed-off-by: Christian Brabandt --- runtime/doc/options.txt | 8 +- runtime/doc/version9.txt | 4 +- src/edit.c | 5 +- src/insexpand.c | 66 ++++++++++++++--- src/option.h | 1 + src/optionstr.c | 2 +- src/proto/insexpand.pro | 3 +- src/testdir/gen_opt_test.vim | 2 +- src/testdir/test_ins_complete.vim | 117 ++++++++++++++++++++++++++++++ src/version.c | 2 + 10 files changed, 194 insertions(+), 16 deletions(-) diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index 880ab0d53e..648b18cbea 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -1,4 +1,4 @@ -*options.txt* For Vim version 9.1. Last change: 2025 Jan 26 +*options.txt* For Vim version 9.1. Last change: 2025 Jan 29 VIM REFERENCE MANUAL by Bram Moolenaar @@ -2168,6 +2168,12 @@ A jump table for the options with a short description can be found at |Q_op|. scores when "fuzzy" is enabled. Candidates will appear in their original order. + preinsert + Preinsert the portion of the first candidate word that is + not part of the current completion leader and using the + |hl-ComplMatchIns| highlight group. Does not work when + "fuzzy" is also included. + *'completepopup'* *'cpp'* 'completepopup' 'cpp' string (default empty) global diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt index e2243f6041..87c603960a 100644 --- a/runtime/doc/version9.txt +++ b/runtime/doc/version9.txt @@ -41625,7 +41625,9 @@ Changed~ the "matches" key - |v:stacktrace| The stack trace of the exception most recently caught and not finished -- New option value "nosort" for 'completeopt' +- New option value for 'completeopt': + "nosort" - do not sort completion results + "preinsert" - highlight to be inserted values - add |dist#vim9#Launch()| and |dist#vim9#Open()| to the |vim-script-library| and decouple it from |netrw| - 'termguicolors' is automatically enabled if the terminal supports the RGB diff --git a/src/edit.c b/src/edit.c index f4c5edfc43..72a82f8fc6 100644 --- a/src/edit.c +++ b/src/edit.c @@ -690,8 +690,11 @@ edit( && stop_arrow() == OK) { ins_compl_delete(); - ins_compl_insert(FALSE); + ins_compl_insert(FALSE, FALSE); } + // Delete preinserted text when typing special chars + else if (IS_WHITE_NL_OR_NUL(c) && ins_compl_preinsert_effect()) + ins_compl_delete(); } } diff --git a/src/insexpand.c b/src/insexpand.c index bf8f215e93..3b9494bb21 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -1946,6 +1946,28 @@ ins_compl_len(void) return compl_length; } +/* + * Return TRUE when preinsert is set otherwise FALSE. + */ + static int +ins_compl_has_preinsert(void) +{ + return (get_cot_flags() & (COT_PREINSERT | COT_FUZZY)) == COT_PREINSERT; +} + +/* + * Returns TRUE if the pre-insert effect is valid and the cursor is within + * the `compl_ins_end_col` range. + */ + int +ins_compl_preinsert_effect(void) +{ + if (!ins_compl_has_preinsert()) + return FALSE; + + return curwin->w_cursor.col < compl_ins_end_col; +} + /* * Delete one character before the cursor and show the subset of the matches * that match the word that is now before the cursor. @@ -1958,6 +1980,9 @@ ins_compl_bs(void) char_u *line; char_u *p; + if (ins_compl_preinsert_effect()) + ins_compl_delete(); + line = ml_get_curline(); p = line + curwin->w_cursor.col; MB_PTR_BACK(line, p); @@ -2054,6 +2079,8 @@ ins_compl_new_leader(void) // Don't let Enter select the original text when there is no popup menu. if (compl_match_array == NULL) compl_enter_selects = FALSE; + else if (ins_compl_has_preinsert() && compl_leader.length > 0) + ins_compl_insert(FALSE, TRUE); } /* @@ -2079,6 +2106,9 @@ ins_compl_addleader(int c) { int cc; + if (ins_compl_preinsert_effect()) + ins_compl_delete(); + if (stop_arrow() == FAIL) return; if (has_mbyte && (cc = (*mb_char2len)(c)) > 1) @@ -3092,7 +3122,8 @@ set_completion(colnr_T startcol, list_T *list) compl_col = startcol; compl_length = (int)curwin->w_cursor.col - (int)startcol; // compl_pattern doesn't need to be set - compl_orig_text.string = vim_strnsave(ml_get_curline() + compl_col, (size_t)compl_length); + compl_orig_text.string = vim_strnsave(ml_get_curline() + compl_col, + (size_t)compl_length); if (p_ic) flags |= CP_ICASE; if (compl_orig_text.string == NULL) @@ -4305,11 +4336,16 @@ ins_compl_update_shown_match(void) void ins_compl_delete(void) { - int col; - // In insert mode: Delete the typed part. // In replace mode: Put the old characters back, if any. - col = compl_col + (compl_status_adding() ? compl_length : 0); + int col = compl_col + (compl_status_adding() ? compl_length : 0); + int has_preinsert = ins_compl_preinsert_effect(); + if (has_preinsert) + { + col = compl_col + ins_compl_leader_len() - compl_length; + curwin->w_cursor.col = compl_ins_end_col; + } + if ((int)curwin->w_cursor.col > col) { if (stop_arrow() == FAIL) @@ -4330,17 +4366,26 @@ ins_compl_delete(void) /* * Insert the new text being completed. * "in_compl_func" is TRUE when called from complete_check(). + * "move_cursor" is used when 'completeopt' includes "preinsert" and when TRUE + * cursor needs to move back from the inserted text to the compl_leader. */ void -ins_compl_insert(int in_compl_func) +ins_compl_insert(int in_compl_func, int move_cursor) { - int compl_len = get_compl_len(); + int compl_len = get_compl_len(); + int preinsert = ins_compl_has_preinsert(); + char_u *str = compl_shown_match->cp_str.string; + int leader_len = ins_compl_leader_len(); // Make sure we don't go over the end of the string, this can happen with // illegal bytes. if (compl_len < (int)compl_shown_match->cp_str.length) - ins_compl_insert_bytes(compl_shown_match->cp_str.string + compl_len, -1); - if (match_at_original_text(compl_shown_match)) + { + ins_compl_insert_bytes(str + compl_len, -1); + if (preinsert && move_cursor) + curwin->w_cursor.col -= ((size_t)STRLEN(str) - leader_len); + } + if (match_at_original_text(compl_shown_match) || preinsert) compl_used_match = FALSE; else compl_used_match = TRUE; @@ -4572,6 +4617,7 @@ ins_compl_next( unsigned int cur_cot_flags = get_cot_flags(); int compl_no_insert = (cur_cot_flags & COT_NOINSERT) != 0; int compl_fuzzy_match = (cur_cot_flags & COT_FUZZY) != 0; + int compl_preinsert = ins_compl_has_preinsert(); // When user complete function return -1 for findstart which is next // time of 'always', compl_shown_match become NULL. @@ -4614,7 +4660,7 @@ ins_compl_next( } // Insert the text of the new completion, or the compl_leader. - if (compl_no_insert && !started) + if (compl_no_insert && !started && !compl_preinsert) { ins_compl_insert_bytes(compl_orig_text.string + get_compl_len(), -1); compl_used_match = FALSE; @@ -4622,7 +4668,7 @@ ins_compl_next( else if (insert_match) { if (!compl_get_longest || compl_used_match) - ins_compl_insert(in_compl_func); + ins_compl_insert(in_compl_func, TRUE); else ins_compl_insert_bytes(compl_leader.string + get_compl_len(), -1); } diff --git a/src/option.h b/src/option.h index 11fefc3208..df5bf4dc78 100644 --- a/src/option.h +++ b/src/option.h @@ -531,6 +531,7 @@ EXTERN unsigned cot_flags; // flags from 'completeopt' #define COT_NOSELECT 0x080 // FALSE: select & insert, TRUE: noselect #define COT_FUZZY 0x100 // TRUE: fuzzy match enabled #define COT_NOSORT 0x200 // TRUE: fuzzy match without qsort score +#define COT_PREINSERT 0x400 // TRUE: preinsert #ifdef BACKSLASH_IN_FILENAME EXTERN char_u *p_csl; // 'completeslash' #endif diff --git a/src/optionstr.c b/src/optionstr.c index 8672665bcb..4dda7a0642 100644 --- a/src/optionstr.c +++ b/src/optionstr.c @@ -120,7 +120,7 @@ static char *(p_fdm_values[]) = {"manual", "expr", "marker", "indent", "syntax", NULL}; static char *(p_fcl_values[]) = {"all", NULL}; #endif -static char *(p_cot_values[]) = {"menu", "menuone", "longest", "preview", "popup", "popuphidden", "noinsert", "noselect", "fuzzy", "nosort", NULL}; +static char *(p_cot_values[]) = {"menu", "menuone", "longest", "preview", "popup", "popuphidden", "noinsert", "noselect", "fuzzy", "nosort", "preinsert", NULL}; #ifdef BACKSLASH_IN_FILENAME static char *(p_csl_values[]) = {"slash", "backslash", NULL}; #endif diff --git a/src/proto/insexpand.pro b/src/proto/insexpand.pro index 24c325fe6e..fe3fead253 100644 --- a/src/proto/insexpand.pro +++ b/src/proto/insexpand.pro @@ -58,9 +58,10 @@ void f_complete_add(typval_T *argvars, typval_T *rettv); void f_complete_check(typval_T *argvars, typval_T *rettv); void f_complete_info(typval_T *argvars, typval_T *rettv); void ins_compl_delete(void); -void ins_compl_insert(int in_compl_func); +void ins_compl_insert(int in_compl_func, int move_cursor); void ins_compl_check_keys(int frequency, int in_compl_func); int ins_complete(int c, int enable_pum); int ins_compl_col_range_attr(int col); void free_insexpand_stuff(void); +int ins_compl_preinsert_effect(void); /* vim: set ft=c : */ diff --git a/src/testdir/gen_opt_test.vim b/src/testdir/gen_opt_test.vim index 7a4ffa8d63..30de28607e 100644 --- a/src/testdir/gen_opt_test.vim +++ b/src/testdir/gen_opt_test.vim @@ -155,7 +155,7 @@ let test_values = { \ ['xxx']], \ 'concealcursor': [['', 'n', 'v', 'i', 'c', 'nvic'], ['xxx']], \ 'completeopt': [['', 'menu', 'menuone', 'longest', 'preview', 'popup', - \ 'popuphidden', 'noinsert', 'noselect', 'fuzzy', 'menu,longest'], + \ 'popuphidden', 'noinsert', 'noselect', 'fuzzy', "preinsert", 'menu,longest'], \ ['xxx', 'menu,,,longest,']], \ 'completeitemalign': [['abbr,kind,menu', 'menu,abbr,kind'], \ ['', 'xxx', 'abbr', 'abbr,menu', 'abbr,menu,kind,abbr', diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index 466e358cf8..e4bc98ac9d 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -3025,4 +3025,121 @@ func Test_complete_info_completed() set cot& endfunc +function Test_completeopt_preinsert() + func Omni_test(findstart, base) + if a:findstart + return col(".") + endif + return [#{word: "fobar"}, #{word: "foobar"}, #{word: "你的"}, #{word: "你好世界"}] + endfunc + set omnifunc=Omni_test + set completeopt=menu,menuone,preinsert + + new + call feedkeys("S\\f", 'tx') + call assert_equal("fobar", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("S\\foo", 'tx') + call assert_equal("foobar", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("S\\foo\\\", 'tx') + call assert_equal("", getline('.')) + call feedkeys("\\", 'tx') + + " delete a character and input new leader + call feedkeys("S\\foo\b", 'tx') + call assert_equal("fobar", getline('.')) + call feedkeys("\\", 'tx') + + " delete preinsert when prepare completion + call feedkeys("S\\f\", 'tx') + call assert_equal("f ", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("S\\你", 'tx') + call assert_equal("你的", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("S\\你好", 'tx') + call assert_equal("你好世界", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("Shello wo\\\\\f", 'tx') + call assert_equal("hello fobar wo", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("Shello wo\\\\\f\", 'tx') + call assert_equal("hello wo", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("Shello wo\\\\\foo", 'tx') + call assert_equal("hello foobar wo", getline('.')) + call feedkeys("\\", 'tx') + + call feedkeys("Shello wo\\\\\foo\b", 'tx') + call assert_equal("hello fobar wo", getline('.')) + call feedkeys("\\", 'tx') + + " confrim + call feedkeys("S\\f\", 'tx') + call assert_equal("fobar", getline('.')) + call assert_equal(5, col('.')) + + " cancel + call feedkeys("S\\fo\", 'tx') + call assert_equal("fo", getline('.')) + call assert_equal(2, col('.')) + + call feedkeys("S hello hero\h\\", 'tx') + call assert_equal("hello", getline('.')) + call assert_equal(1, col('.')) + + call feedkeys("Sh\\\", 'tx') + call assert_equal("hello", getline('.')) + call assert_equal(5, col('.')) + + " delete preinsert part + call feedkeys("S\\fo ", 'tx') + call assert_equal("fo ", getline('.')) + call assert_equal(3, col('.')) + + " whole line + call feedkeys("Shello hero\\\", 'tx') + call assert_equal("hello hero", getline('.')) + call assert_equal(1, col('.')) + + call feedkeys("Shello hero\he\\", 'tx') + call assert_equal("hello hero", getline('.')) + call assert_equal(2, col('.')) + + " can not work with fuzzy + set cot+=fuzzy + call feedkeys("S\\", 'tx') + call assert_equal("fobar", getline('.')) + call assert_equal(5, col('.')) + + " test for fuzzy and noinsert + set cot+=noinsert + call feedkeys("S\\fb", 'tx') + call assert_equal("fb", getline('.')) + call assert_equal(2, col('.')) + + call feedkeys("S\\你", 'tx') + call assert_equal("你", getline('.')) + call assert_equal(1, col('.')) + + call feedkeys("S\\fb\", 'tx') + call assert_equal("fobar", getline('.')) + call assert_equal(5, col('.')) + + bw! + bw! + set cot& + set omnifunc& + delfunc Omni_test + autocmd! CompleteChanged +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable diff --git a/src/version.c b/src/version.c index 450bcdfaab..836f0a8446 100644 --- a/src/version.c +++ b/src/version.c @@ -704,6 +704,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 1056, /**/ 1055, /**/ -- 2.39.5