From: glepnir Date: Mon, 15 Jun 2026 18:49:47 +0000 (+0000) Subject: patch 9.2.0651: completion: 'smartcase' doesn't work with 'longest' X-Git-Tag: v9.2.0651^0 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=50fe45aca72af15952131241067c8e3a5bf71d23;p=thirdparty%2Fvim.git 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: #20533 Signed-off-by: glepnir Signed-off-by: Christian Brabandt --- diff --git a/src/insexpand.c b/src/insexpand.c index c63b49c7f4..a355e29492 100644 --- a/src/insexpand.c +++ b/src/insexpand.c @@ -187,6 +187,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_T compl_orig_text = {NULL, 0}; // text as it was before // completion started static int compl_cont_mode = 0; @@ -1051,6 +1052,33 @@ ins_compl_equal(compl_T *match, char_u *str, int len) return STRNCMP(match->cp_str.string, str, (size_t)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 int +ins_compl_equal_sc(compl_T *match, char_u *str, int len) +{ + 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 ((int)match->cp_str.length < len) + return FALSE; + + for (int i = 0; i < len; ++i) + { + if (i >= typed && i < longest_end + ? MB_TOLOWER(match->cp_str.string[i]) != MB_TOLOWER(str[i]) + : match->cp_str.string[i] != str[i]) + return FALSE; + } + return TRUE; +} + /* * when len is -1 mean use whole length of p otherwise part of p */ @@ -1687,14 +1715,15 @@ ins_compl_build_pum(void) leader = get_leader_for_startcol(compl, TRUE); - // Apply 'smartcase' behavior during normal mode - if (ctrl_x_mode_normal() && !p_inf && leader->string - && !ignorecase(leader->string) && !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.string + && !ignorecase(compl_orig_text.string) && !cot_fuzzy()) compl->cp_flags &= ~CP_ICASE; if (!match_at_original_text(compl) && (leader->string == NULL - || ins_compl_equal(compl, leader->string, + || ins_compl_equal_sc(compl, leader->string, (int)leader->length) || (cot_fuzzy() && compl->cp_score != FUZZY_SCORE_NONE))) { @@ -2285,6 +2314,7 @@ 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; VIM_CLEAR_STRING(compl_pattern); @@ -4517,6 +4547,7 @@ ins_compl_longest_insert(char_u *prefix) { ins_compl_delete(); ins_compl_insert_bytes(prefix + get_compl_len(), -1); + compl_longest_end_col = curwin->w_cursor.col; ins_redraw(FALSE); } @@ -5805,13 +5836,13 @@ find_common_prefix(size_t *prefix_len, int curbuf_only) string_T *leader = get_leader_for_startcol(compl, TRUE); // Apply 'smartcase' behavior during normal mode - if (ctrl_x_mode_normal() && !p_inf && leader->string - && !ignorecase(leader->string)) + if (ctrl_x_mode_normal() && !p_inf && compl_orig_text.string + && !ignorecase(compl_orig_text.string)) compl->cp_flags &= ~CP_ICASE; if (!match_at_original_text(compl) && (leader->string == NULL - || ins_compl_equal(compl, leader->string, + || ins_compl_equal_sc(compl, leader->string, (int)leader->length))) { // Limit number of items from each source if max_items is set. diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim index c741fd1e73..aafc4a84d3 100644 --- a/src/testdir/test_ins_complete.vim +++ b/src/testdir/test_ins_complete.vim @@ -6357,4 +6357,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 diff --git a/src/version.c b/src/version.c index ff26c4350d..04b04e6b56 100644 --- a/src/version.c +++ b/src/version.c @@ -759,6 +759,8 @@ static char *(features[]) = static int included_patches[] = { /* Add new patch number below this line */ +/**/ + 651, /**/ 650, /**/