From 62cb36927d2ec5211d0999ad8856c6a2194c7008 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Tue, 16 Jun 2026 08:32:30 +0800 Subject: [PATCH 1/2] vim-patch:9.2.0651: completion: 'smartcase' doesn't work with 'longest' Problem: With 'longest', 'smartcase' is ignored when filtering matches: "inp" offers only "InputEvent", and an uppercase pattern gives different results for CTRL-N and CTRL-P. Solution: 'longest' rewrites the leader with the common prefix, picking up uppercase the user never typed. Judge case from the typed text instead, and match the auto-inserted part of the leader case-insensitively so CTRL-N and CTRL-P give the same result. (glepnir) related: neovim/neovim#40259 closes: vim/vim#20533 https://github.com/vim/vim/commit/50fe45aca72af15952131241067c8e3a5bf71d23 Co-authored-by: glepnir --- src/nvim/insexpand.c | 44 ++++++++++++++++++++++---- test/old/testdir/test_ins_complete.vim | 44 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 6 deletions(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 546f618a30..4d163d7e5b 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -274,6 +274,7 @@ static linenr_T compl_lnum = 0; ///< lnum where the completion start static colnr_T compl_col = 0; ///< column where the text starts ///< that is being completed static colnr_T compl_ins_end_col = 0; +static colnr_T compl_longest_end_col = 0; ///< end of 'longest' inserted text static String compl_orig_text = STRING_INIT; ///< text as it was before ///< completion started /// Undo information to restore extmarks for original text. @@ -1087,6 +1088,33 @@ static bool ins_compl_equal(compl_T *match, char *str, size_t len) return strncmp(match->cp_str.data, str, len) == 0; } +/// Like ins_compl_equal(), but ignore case in the 'longest'-inserted part of +/// the leader, so CTRL-N and CTRL-P filter the same way. +static bool ins_compl_equal_sc(compl_T *match, char *str, size_t len) + FUNC_ATTR_NONNULL_ALL +{ + int typed = compl_length; + int longest_end = (compl_get_longest && compl_longest_end_col > compl_col) + ? (int)(compl_longest_end_col - compl_col) : typed; + + if ((match->cp_flags & (CP_EQUAL | CP_ICASE)) || longest_end <= typed) { + return ins_compl_equal(match, str, len); + } + + if (match->cp_str.size < len) { + return false; + } + + for (int i = 0; (size_t)i < len; i++) { + if (i >= typed && i < longest_end + ? mb_tolower((uint8_t)match->cp_str.data[i]) != mb_tolower((uint8_t)str[i]) + : match->cp_str.data[i] != str[i]) { + return false; + } + } + return true; +} + /// when len is -1 mean use whole length of p otherwise part of p static void ins_compl_insert_bytes(char *p, int len) FUNC_ATTR_NONNULL_ALL @@ -1581,15 +1609,16 @@ static int ins_compl_build_pum(void) String *leader = get_leader_for_startcol(comp, true); - // Apply 'smartcase' behavior during normal mode - if (ctrl_x_mode_normal() && !p_inf && leader->data - && !ignorecase(leader->data) && !cot_fuzzy()) { + // Apply 'smartcase': judge case from compl_orig_text, not the leader + // which 'longest' may fill with uppercase the user never typed. + if (ctrl_x_mode_normal() && !p_inf && compl_orig_text.data + && !ignorecase(compl_orig_text.data) && !cot_fuzzy()) { comp->cp_flags &= ~CP_ICASE; } if (!match_at_original_text(comp) && (leader->data == NULL - || ins_compl_equal(comp, leader->data, leader->size) + || ins_compl_equal_sc(comp, leader->data, leader->size) || (cot_fuzzy() && comp->cp_score != FUZZY_SCORE_NONE))) { // Limit number of items from each source if max_items is set. bool match_limit_exceeded = false; @@ -2096,6 +2125,7 @@ void ins_compl_clear(void) compl_matches = 0; compl_selected_item = -1; compl_ins_end_col = 0; + compl_longest_end_col = 0; compl_curr_win = NULL; compl_curr_buf = NULL; API_CLEAR_STRING(compl_pattern); @@ -3956,6 +3986,7 @@ static void ins_compl_longest_insert(char *prefix) { ins_compl_delete(false); ins_compl_insert_bytes(prefix + get_compl_len(), -1); + compl_longest_end_col = curwin->w_cursor.col; ins_redraw(false); } @@ -5058,13 +5089,14 @@ static char *find_common_prefix(size_t *prefix_len, bool curbuf_only) String *leader = get_leader_for_startcol(compl, true); // Apply 'smartcase' behavior during normal mode - if (ctrl_x_mode_normal() && !p_inf && leader->data && !ignorecase(leader->data)) { + if (ctrl_x_mode_normal() && !p_inf && compl_orig_text.data + && !ignorecase(compl_orig_text.data)) { compl->cp_flags &= ~CP_ICASE; } if (!match_at_original_text(compl) && (leader->data == NULL - || ins_compl_equal(compl, leader->data, leader->size))) { + || ins_compl_equal_sc(compl, leader->data, leader->size))) { // Limit number of items from each source if max_items is set. bool match_limit_exceeded = false; int cur_source = compl->cp_cpt_source_idx; diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim index f7f3c9fa7b..d0a789aa06 100644 --- a/test/old/testdir/test_ins_complete.vim +++ b/test/old/testdir/test_ins_complete.vim @@ -6468,4 +6468,48 @@ func Test_mapped_ctrl_n_during_complete_function() bwipe! endfunc +func Test_smartcase_longest() + func! GetMatches() + let info = complete_info(["matches"]) + return map(copy(info.matches), {_, v -> v.word}) + endfunc + + func! TestInner(key) + let pr = "\=string(GetMatches())\" + let words = ["InputEvent", "inputmap", "INPUT_MAP"] + + new + set completeopt=menuone,noselect,longest ignorecase smartcase + + " Lowercase 'inp' all three (case-insensitive). + call setline(1, words) + exe $"normal! ggOinp{a:key}{pr}" + let line = getline(1) + call assert_match('\c^input', line, 'inp prefix, key=' .. strtrans(a:key)) + call assert_equal("['InputEvent', 'inputmap', 'INPUT_MAP']", + \ substitute(line, '\c^input', '', ''), + \ 'inp matches, key=' .. strtrans(a:key)) + + " Uppercase 'I' excludes lowercase 'inputmap' + %d + call setline(1, words) + exe $"normal! ggOI{a:key}{pr}" + let line = getline(1) + call assert_match('\c^input', line, 'I prefix, key=' .. strtrans(a:key)) + call assert_equal("['InputEvent', 'INPUT_MAP']", + \ substitute(line, '\c^input', '', ''), + \ 'I matches, key=' .. strtrans(a:key)) + + set ignorecase& smartcase& completeopt& + bw! + endfunc + + call TestInner("\") + call TestInner("\") + call TestInner("\\") + call TestInner("\\") + delfunc GetMatches + delfunc TestInner +endfunc + " vim: shiftwidth=2 sts=2 expandtab nofoldenable From 1abb41f38e530c7d059fedc5ff8a08a38670b72f Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Wed, 17 Jun 2026 08:57:08 +0800 Subject: [PATCH 2/2] vim-patch:9.2.0656: completion: using wrong tolower() in smartcase filtering Problem: ins_compl_equal_sc() uses MB_TOLOWER() on single bytes, but it indexes raw bytes, not decoded characters (after v9.1.0651). Solution: Use TOLOWER_LOC(), matching what STRNICMP()/ins_compl_equal() does (glephunter). closes: vim/vim#20535 https://github.com/vim/vim/commit/9f5d32cf5caf2476efe4ad1da2d4455920bcc28c Co-authored-by: glepnir --- src/nvim/insexpand.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c index 4d163d7e5b..fd3a0309f7 100644 --- a/src/nvim/insexpand.c +++ b/src/nvim/insexpand.c @@ -1107,7 +1107,7 @@ static bool ins_compl_equal_sc(compl_T *match, char *str, size_t len) for (int i = 0; (size_t)i < len; i++) { if (i >= typed && i < longest_end - ? mb_tolower((uint8_t)match->cp_str.data[i]) != mb_tolower((uint8_t)str[i]) + ? TOLOWER_LOC((uint8_t)match->cp_str.data[i]) != TOLOWER_LOC((uint8_t)str[i]) : match->cp_str.data[i] != str[i]) { return false; }