neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

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:
Mruntime/doc/insert.txt | 1+
Mruntime/doc/news.txt | 1+
Mruntime/doc/options.txt | 8+++++---
Mruntime/lua/vim/_meta/options.lua | 8+++++---
Msrc/nvim/insexpand.c | 6++++++
Msrc/nvim/options.lua | 8+++++---
Msrc/nvim/search.c | 2+-
Mtest/old/testdir/test_ins_complete.vim | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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()