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: #17271
Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/insert.txt b/runtime/doc/insert.txt
index f52cf40..f3d92b2 100644
--- a/runtime/doc/insert.txt
+++ b/runtime/doc/insert.txt
@@ -1,4 +1,4 @@
-*insert.txt* For Vim version 9.1. Last change: 2025 Apr 14
+*insert.txt* For Vim version 9.1. Last change: 2025 May 08
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -1347,6 +1347,7 @@
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/options.txt b/runtime/doc/options.txt
index bed14ab..f3824cd 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1,4 +1,4 @@
-*options.txt* For Vim version 9.1. Last change: 2025 May 07
+*options.txt* For Vim version 9.1. Last change: 2025 May 08
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -7742,9 +7742,11 @@
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>.
NOTE: This option is reset when 'compatible' is set.
*'smartindent'* *'si'* *'nosmartindent'* *'nosi'*
diff --git a/runtime/doc/tags b/runtime/doc/tags
index bb0558d..ae80f0e 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6645,6 +6645,7 @@
compl-omni insert.txt /*compl-omni*
compl-omni-filetypes insert.txt /*compl-omni-filetypes*
compl-spelling insert.txt /*compl-spelling*
+compl-states insert.txt /*compl-states*
compl-stop insert.txt /*compl-stop*
compl-tag insert.txt /*compl-tag*
compl-thesaurus insert.txt /*compl-thesaurus*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index afa800d..371e4b3 100644
--- a/runtime/doc/version9.txt
+++ b/runtime/doc/version9.txt
@@ -1,4 +1,4 @@
-*version9.txt* For Vim version 9.1. Last change: 2025 May 07
+*version9.txt* For Vim version 9.1. Last change: 2025 May 08
VIM REFERENCE MANUAL by Bram Moolenaar
@@ -41626,6 +41626,7 @@
"{flag}^<limit>" notation
- add ":filetype" command completion
- add "filetypecmd" completion type for |getcompletion()|
+- 'smartcase' applies to completion filtering
Options: ~
- the default for 'commentstring' contains whitespace padding to have
diff --git a/src/insexpand.c b/src/insexpand.c
index 3839586..7bbff4e 100644
--- a/src/insexpand.c
+++ b/src/insexpand.c
@@ -1507,7 +1507,7 @@
match_count = 1;
max_matches_found = FALSE;
}
- else if (cpt_sources_array && !max_matches_found)
+ else if (cpt_sources_array != NULL && !max_matches_found)
{
int max_matches = cpt_sources_array[cur_source].max_matches;
if (max_matches > 0 && match_count > max_matches)
@@ -1515,10 +1515,16 @@
}
}
+ // Apply 'smartcase' behavior during normal mode
+ if (ctrl_x_mode_normal() && !p_inf && compl_leader.string
+ && !ignorecase(compl_leader.string) && !fuzzy_filter)
+ compl->cp_flags &= ~CP_ICASE;
+
if (!match_at_original_text(compl)
&& !max_matches_found
&& (compl_leader.string == NULL
- || ins_compl_equal(compl, compl_leader.string, (int)compl_leader.length)
+ || ins_compl_equal(compl, compl_leader.string,
+ (int)compl_leader.length)
|| (fuzzy_filter && compl->cp_score > 0)))
{
++compl_match_arraysize;
diff --git a/src/search.c b/src/search.c
index 5222a43..ea7e654 100644
--- a/src/search.c
+++ b/src/search.c
@@ -439,7 +439,7 @@
}
/*
- * As ignorecase() put pass the "ic" and "scs" flags.
+ * As ignorecase() but pass the "ic" and "scs" flags.
*/
int
ignorecase_opt(char_u *pat, int ic_in, int scs)
diff --git a/src/testdir/test_ins_complete.vim b/src/testdir/test_ins_complete.vim
index 73565a5..6f342ae 100644
--- a/src/testdir/test_ins_complete.vim
+++ b/src/testdir/test_ins_complete.vim
@@ -4213,6 +4213,93 @@
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()
diff --git a/src/version.c b/src/version.c
index 5b85a2b..236306e 100644
--- a/src/version.c
+++ b/src/version.c
@@ -705,6 +705,8 @@
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
+ 1374,
+/**/
1373,
/**/
1372,