commit 15d31fe7a6c875eae82a2721941080586d89e876
parent 6b955af8755b416067733d862d1d341ef6bb9cb4
Author: zeertzjq <zeertzjq@outlook.com>
Date: Fri, 9 May 2025 06:30:59 +0800
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
https://github.com/vim/vim/commit/dc314053e121b0a995bdfbcdd2f03ce228e14eb3
Co-authored-by: Girish Palya <girishji@gmail.com>
Diffstat:
8 files changed, 111 insertions(+), 10 deletions(-)
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
@@ -1297,6 +1297,7 @@ use all space available.
The 'pumwidth' option can be used to set a minimum width. The default is 15
characters.
+ *compl-states*
There are three states:
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
diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt
@@ -211,6 +211,7 @@ CHANGED FEATURES *news-changed*
These existing features changed their behavior.
+• 'smartcase' applies to completion filtering.
• 'spellfile' location defaults to `stdpath("data").."/site/spell/"` instead of
the first writable directory in 'runtimepath'.
• |vim.version.range()| doesn't exclude `to` if it is equal to `from`.
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
@@ -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
case characters. Only used when the search pattern is typed and
'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
- ":g" and ":s". 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>.
+ ":g" and ":s" and when filtering matches for the completion menu
+ |compl-states|.
+ 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' boolean (default off)
diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua
@@ -6084,9 +6084,11 @@ vim.wo.scl = vim.wo.signcolumn
--- Override the 'ignorecase' option if the search pattern contains upper
--- case characters. Only used when the search pattern is typed and
--- 'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
---- ":g" and ":s". 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>.
+--- ":g" and ":s" and when filtering matches for the completion menu
+--- `compl-states`.
+--- 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
vim.o.smartcase = false
diff --git a/src/nvim/insexpand.c b/src/nvim/insexpand.c
@@ -1391,6 +1391,12 @@ static int ins_compl_build_pum(void)
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)
&& (compl_leader.data == NULL
|| ins_compl_equal(comp, compl_leader.data, compl_leader.size)
diff --git a/src/nvim/options.lua b/src/nvim/options.lua
@@ -8095,9 +8095,11 @@ local options = {
Override the 'ignorecase' option if the search pattern contains upper
case characters. Only used when the search pattern is typed and
'ignorecase' option is on. Used for the commands "/", "?", "n", "N",
- ":g" and ":s". 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>.
+ ":g" and ":s" and when filtering matches for the completion menu
+ |compl-states|.
+ 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',
scope = { 'global' },
diff --git a/src/nvim/search.c b/src/nvim/search.c
@@ -391,7 +391,7 @@ int ignorecase(char *pat)
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 ic = ic_in;
diff --git a/test/old/testdir/test_ins_complete.vim b/test/old/testdir/test_ins_complete.vim
@@ -3484,6 +3484,93 @@ func Test_complete_append_selected_match_default()
delfunc PrintMenuWords
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'
func Test_nearest_cpt_option()