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/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,