]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
patch 9.2.0651: completion: 'smartcase' doesn't work with 'longest' v9.2.0651
authorglepnir <glephunter@gmail.com>
Mon, 15 Jun 2026 18:49:47 +0000 (18:49 +0000)
committerChristian Brabandt <cb@256bit.org>
Mon, 15 Jun 2026 18:49:47 +0000 (18:49 +0000)
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 <glephunter@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
src/insexpand.c
src/testdir/test_ins_complete.vim
src/version.c

index c63b49c7f40636446e2c7044b40d686f3af3dc58..a355e29492984cb742098c42819ef27967e244ec 100644 (file)
@@ -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.
index c741fd1e73866601285a308c2f72bd487fb1bf47..aafc4a84d3b3a25326619df061eaadda8f503d7f 100644 (file)
@@ -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 = "\<c-r>=string(GetMatches())\<cr>"
+    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("\<c-n>")
+  call TestInner("\<c-p>")
+  call TestInner("\<c-x>\<c-n>")
+  call TestInner("\<c-x>\<c-p>")
+  delfunc GetMatches
+  delfunc TestInner
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab nofoldenable
index ff26c4350d4f1ce9f6b05582f2384d33e4b63067..04b04e6b569efabf29d8059bc9c86ca1eba5e938 100644 (file)
@@ -759,6 +759,8 @@ static char *(features[]) =
 
 static int included_patches[] =
 {   /* Add new patch number below this line */
+/**/
+    651,
 /**/
     650,
 /**/