vim-patch:9.1.1374: completion: 'smartcase' not respected when filtering matches

Problem:  Currently, 'smartcase' is respected when completing keywords
          using <C-N>, <C-P>, <C-X><C-N>, and <C-X><C-P>. However, when
          a user continues typing and the completion menu is filtered
          using cached matches, 'smartcase' is not applied. This leads
          to poor-quality or irrelevant completion suggestions, as shown
          in the example below.
Solution: When filtering cached completion items after typing additional
          characters, apply case-sensitive comparison if 'smartcase' is
          enabled and the typed pattern includes uppercase characters.
          This ensures consistent and expected completion behavior.
          (Girish Palya)

closes: vim/vim#17271

dc314053e1

Co-authored-by: Girish Palya <girishji@gmail.com>
This commit is contained in:
zeertzjq
2025-05-09 06:30:59 +08:00
parent 6b955af875
commit 15d31fe7a6
8 changed files with 111 additions and 10 deletions

View File

@@ -1297,6 +1297,7 @@ use all space available.
The 'pumwidth' option can be used to set a minimum width. The default is 15 The 'pumwidth' option can be used to set a minimum width. The default is 15
characters. characters.
*compl-states*
There are three states: There are three states:
1. A complete match has been inserted, e.g., after using CTRL-N or CTRL-P. 1. A complete match has been inserted, e.g., after using CTRL-N or CTRL-P.
2. A cursor key has been used to select another match. The match was not 2. A cursor key has been used to select another match. The match was not

View File

@@ -211,6 +211,7 @@ CHANGED FEATURES *news-changed*
These existing features changed their behavior. These existing features changed their behavior.
• 'smartcase' applies to completion filtering.
• 'spellfile' location defaults to `stdpath("data").."/site/spell/"` instead of • 'spellfile' location defaults to `stdpath("data").."/site/spell/"` instead of
the first writable directory in 'runtimepath'. the first writable directory in 'runtimepath'.
• |vim.version.range()| doesn't exclude `to` if it is equal to `from`. • |vim.version.range()| doesn't exclude `to` if it is equal to `from`.

View File

@@ -5718,9 +5718,11 @@ A jump table for the options with a short description can be found at |Q_op|.
Override the 'ignorecase' option if the search pattern contains upper Override the 'ignorecase' option if the search pattern contains upper
case characters. Only used when the search pattern is typed and case characters. Only used when the search pattern is typed and
'ignorecase' option is on. Used for the commands "/", "?", "n", "N", 'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
":g" and ":s". Not used for "*", "#", "gd", tag search, etc. After ":g" and ":s" and when filtering matches for the completion menu
"*" and "#" you can make 'smartcase' used by doing a "/" command, |compl-states|.
recalling the search pattern from history and hitting <Enter>. Not used for "*", "#", "gd", tag search, etc. After "*" and "#" you
can make 'smartcase' used by doing a "/" command, recalling the search
pattern from history and hitting <Enter>.
*'smartindent'* *'si'* *'nosmartindent'* *'nosi'* *'smartindent'* *'si'* *'nosmartindent'* *'nosi'*
'smartindent' 'si' boolean (default off) 'smartindent' 'si' boolean (default off)

View File

@@ -6084,9 +6084,11 @@ vim.wo.scl = vim.wo.signcolumn
--- Override the 'ignorecase' option if the search pattern contains upper --- Override the 'ignorecase' option if the search pattern contains upper
--- case characters. Only used when the search pattern is typed and --- case characters. Only used when the search pattern is typed and
--- 'ignorecase' option is on. Used for the commands "/", "?", "n", "N", --- 'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
--- ":g" and ":s". Not used for "*", "#", "gd", tag search, etc. After --- ":g" and ":s" and when filtering matches for the completion menu
--- "*" and "#" you can make 'smartcase' used by doing a "/" command, --- `compl-states`.
--- recalling the search pattern from history and hitting <Enter>. --- Not used for "*", "#", "gd", tag search, etc. After "*" and "#" you
--- can make 'smartcase' used by doing a "/" command, recalling the search
--- pattern from history and hitting <Enter>.
--- ---
--- @type boolean --- @type boolean
vim.o.smartcase = false vim.o.smartcase = false

View File

@@ -1391,6 +1391,12 @@ static int ins_compl_build_pum(void)
comp->cp_score = fuzzy_match_str(comp->cp_str.data, compl_leader.data); comp->cp_score = fuzzy_match_str(comp->cp_str.data, compl_leader.data);
} }
// Apply 'smartcase' behavior during normal mode
if (ctrl_x_mode_normal() && !p_inf && compl_leader.data
&& !ignorecase(compl_leader.data) && !fuzzy_filter) {
comp->cp_flags &= ~CP_ICASE;
}
if (!match_at_original_text(comp) if (!match_at_original_text(comp)
&& (compl_leader.data == NULL && (compl_leader.data == NULL
|| ins_compl_equal(comp, compl_leader.data, compl_leader.size) || ins_compl_equal(comp, compl_leader.data, compl_leader.size)

View File

@@ -8095,9 +8095,11 @@ local options = {
Override the 'ignorecase' option if the search pattern contains upper Override the 'ignorecase' option if the search pattern contains upper
case characters. Only used when the search pattern is typed and case characters. Only used when the search pattern is typed and
'ignorecase' option is on. Used for the commands "/", "?", "n", "N", 'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
":g" and ":s". Not used for "*", "#", "gd", tag search, etc. After ":g" and ":s" and when filtering matches for the completion menu
"*" and "#" you can make 'smartcase' used by doing a "/" command, |compl-states|.
recalling the search pattern from history and hitting <Enter>. Not used for "*", "#", "gd", tag search, etc. After "*" and "#" you
can make 'smartcase' used by doing a "/" command, recalling the search
pattern from history and hitting <Enter>.
]=], ]=],
full_name = 'smartcase', full_name = 'smartcase',
scope = { 'global' }, scope = { 'global' },

View File

@@ -391,7 +391,7 @@ int ignorecase(char *pat)
return ignorecase_opt(pat, p_ic, p_scs); return ignorecase_opt(pat, p_ic, p_scs);
} }
/// As ignorecase() put pass the "ic" and "scs" flags. /// As ignorecase() but pass the "ic" and "scs" flags.
int ignorecase_opt(char *pat, int ic_in, int scs) int ignorecase_opt(char *pat, int ic_in, int scs)
{ {
int ic = ic_in; int ic = ic_in;

View File

@@ -3484,6 +3484,93 @@ func Test_complete_append_selected_match_default()
delfunc PrintMenuWords delfunc PrintMenuWords
endfunc endfunc
" Test normal mode (^N/^P/^X^N/^X^P) with smartcase when 1) matches are first
" found and 2) matches are filtered (when a character is typed).
func Test_smartcase_normal_mode()
func! PrintMenu()
let info = complete_info(["matches"])
call map(info.matches, {_, v -> v.word})
return info
endfunc
func! TestInner(key)
let pr = "\<c-r>=PrintMenu()\<cr>"
new
set completeopt=menuone,noselect ignorecase smartcase
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}{pr}"
call assert_equal('F{''matches'': [''Fast'', ''FAST'', ''False'',
\ ''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}a{pr}"
call assert_equal('Fa{''matches'': [''Fast'', ''False'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}a\<bs>{pr}"
call assert_equal('F{''matches'': [''Fast'', ''FAST'', ''False'',
\ ''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}ax{pr}"
call assert_equal('Fax{''matches'': []}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}ax\<bs>{pr}"
call assert_equal('Fa{''matches'': [''Fast'', ''False'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}A{pr}"
call assert_equal('FA{''matches'': [''FAST'', ''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}A\<bs>{pr}"
call assert_equal('F{''matches'': [''Fast'', ''FAST'', ''False'',
\ ''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}AL{pr}"
call assert_equal('FAL{''matches'': [''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}ALx{pr}"
call assert_equal('FALx{''matches'': []}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOF{a:key}ALx\<bs>{pr}"
call assert_equal('FAL{''matches'': [''FALSE'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOf{a:key}{pr}"
call assert_equal('f{''matches'': [''Fast'', ''FAST'', ''False'', ''FALSE'',
\ ''fast'', ''false'']}', getline(1))
%d
call setline(1, ["Fast", "FAST", "False", "FALSE", "fast", "false"])
exe $"normal! ggOf{a:key}a{pr}"
call assert_equal('fa{''matches'': [''Fast'', ''FAST'', ''False'', ''FALSE'',
\ ''fast'', ''false'']}', getline(1))
%d
exe $"normal! ggOf{a:key}{pr}"
call assert_equal('f{''matches'': []}', getline(1))
exe $"normal! ggOf{a:key}a\<bs>{pr}"
call assert_equal('f{''matches'': []}', getline(1))
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 PrintMenu
delfunc TestInner
endfunc
" Test 'nearest' flag of 'completeopt' " Test 'nearest' flag of 'completeopt'
func Test_nearest_cpt_option() func Test_nearest_cpt_option()