patch 9.1.1526: completion: search completion match may differ in case

Problem:  completion: search completion match may differ in case
          (techntools)
Solution: add "exacttext" to 'wildoptions' value (Girish Palya)

This flag does the following:

exacttext
      When this flag is present, search pattern completion
      (e.g., in |/|, |?|, |:s|, |:g|, |:v|, and |:vim|)
      shows exact buffer text as menu items, without
      preserving regex artifacts like position
      anchors (e.g., |/\<|). This provides more intuitive
      menu items that match the actual buffer text. However,
      searches may be less accurate since the pattern is not
      preserved exactly.
      By default, Vim preserves the typed pattern (with
      anchors) and appends the matched word. This preserves
      search correctness, especially when using regular
      expressions or with 'smartcase' enabled. However, the
      case of the appended matched word may not exactly
      match the case of the word in the buffer.

fixes: #17654
closes: #17667

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/src/cmdexpand.c b/src/cmdexpand.c
index d5730ab..75efe1c 100644
--- a/src/cmdexpand.c
+++ b/src/cmdexpand.c
@@ -4631,6 +4631,7 @@
     int		segment_len;
     linenr_T	lnum;
     garray_T	ga;
+    int		exacttext = vim_strchr(p_wop, WOP_EXACTTEXT) != NULL;
 
     if (start->lnum > end->lnum
 	    || (start->lnum == end->lnum && start->col >= end->col))
@@ -4646,12 +4647,17 @@
 
     segment_len = is_single_line ? (end->col - start->col)
 				: (int)STRLEN(start_ptr);
-    if (ga_grow(&ga, segment_len + 1) != OK)
+    if (ga_grow(&ga, segment_len + 2) != OK)
 	return FAIL;
 
     ga_concat_len(&ga, start_ptr, segment_len);
     if (!is_single_line)
-	ga_append(&ga, '\n');
+    {
+	if (exacttext)
+	    ga_concat_len(&ga, (char_u *)"\\n", 2);
+	else
+	    ga_append(&ga, '\n');
+    }
 
     // Append full lines between start and end
     if (!is_single_line)
@@ -4659,10 +4665,13 @@
 	for (lnum = start->lnum + 1; lnum < end->lnum; lnum++)
 	{
 	    line = ml_get(lnum);
-	    if (ga_grow(&ga, ml_get_len(lnum) + 1) != OK)
+	    if (ga_grow(&ga, ml_get_len(lnum) + 2) != OK)
 		return FAIL;
 	    ga_concat(&ga, line);
-	    ga_append(&ga, '\n');
+	    if (exacttext)
+		ga_concat_len(&ga, (char_u *)"\\n", 2);
+	    else
+		ga_append(&ga, '\n');
 	}
     }
 
@@ -4783,6 +4792,7 @@
     int		compl_started = FALSE;
     int		search_flags;
     char_u	*match, *full_match;
+    int		exacttext = vim_strchr(p_wop, WOP_EXACTTEXT) != NULL;
 
 #ifdef FEAT_SEARCH_EXTRA
     has_range = search_first_line != 0;
@@ -4871,26 +4881,31 @@
 		    &word_end_pos))
 	    break;
 
-	// Construct a new match from completed word appended to pattern itself
-	match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos,
-		FALSE);
-
-	// The regex pattern may include '\C' or '\c'. First, try matching the
-	// buffer word as-is. If it doesn't match, try again with the lowercase
-	// version of the word to handle smartcase behavior.
-	if (match == NULL || !is_regex_match(match, full_match))
+	if (exacttext)
+	    match = full_match;
+	else
 	{
-	    vim_free(match);
-	    match = concat_pattern_with_buffer_match(pat, pat_len,
-		    &end_match_pos, TRUE);
+	    // Construct a new match from completed word appended to pattern itself
+	    match = concat_pattern_with_buffer_match(pat, pat_len, &end_match_pos,
+		    FALSE);
+
+	    // The regex pattern may include '\C' or '\c'. First, try matching the
+	    // buffer word as-is. If it doesn't match, try again with the lowercase
+	    // version of the word to handle smartcase behavior.
 	    if (match == NULL || !is_regex_match(match, full_match))
 	    {
 		vim_free(match);
-		vim_free(full_match);
-		continue;
+		match = concat_pattern_with_buffer_match(pat, pat_len,
+			&end_match_pos, TRUE);
+		if (match == NULL || !is_regex_match(match, full_match))
+		{
+		    vim_free(match);
+		    vim_free(full_match);
+		    continue;
+		}
 	    }
+	    vim_free(full_match);
 	}
-	vim_free(full_match);
 
 	// Include this match if it is not a duplicate
 	for (int i = 0; i < ga.ga_len; ++i)
diff --git a/src/option.h b/src/option.h
index 5590e56..91810c5 100644
--- a/src/option.h
+++ b/src/option.h
@@ -375,8 +375,9 @@
 // flags for the 'wildoptions' option
 // each defined char should be unique over all values.
 #define WOP_FUZZY	'z'
-#define WOP_TAGFILE	't'
+#define WOP_TAGFILE	'g'
 #define WOP_PUM		'p'
+#define WOP_EXACTTEXT	'x'
 
 // arguments for can_bs()
 // each defined char should be unique over all values
diff --git a/src/optionstr.c b/src/optionstr.c
index dcbb30b..eb0b9d3 100644
--- a/src/optionstr.c
+++ b/src/optionstr.c
@@ -103,7 +103,7 @@
 static char *(p_ve_values[]) = {"block", "insert", "all", "onemore", "none", "NONE", NULL};
 // Note: Keep this in sync with check_opt_wim()
 static char *(p_wim_values[]) = {"full", "longest", "list", "lastused", "noselect", NULL};
-static char *(p_wop_values[]) = {"fuzzy", "tagfile", "pum", NULL};
+static char *(p_wop_values[]) = {"fuzzy", "tagfile", "pum", "exacttext", NULL};
 #ifdef FEAT_WAK
 static char *(p_wak_values[]) = {"yes", "menu", "no", NULL};
 #endif
diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim
index a68d3e1..47adb2b 100644
--- a/src/testdir/test_cmdline.vim
+++ b/src/testdir/test_cmdline.vim
@@ -4472,6 +4472,7 @@
   " Match case correctly
   %d
   call setline(1, ["foobar", "Foobar", "fooBAr", "FooBARR"])
+
   call feedkeys("gg/f\<tab>\<f9>", 'tx')
   call assert_equal(['fooBAr', 'foobar'], g:compl_info.matches)
   call feedkeys("gg/Fo\<tab>\<f9>", 'tx')
@@ -4480,6 +4481,7 @@
   call assert_equal({},  g:compl_info)
   call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
   call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
+
   set ignorecase
   call feedkeys("gg/f\<tab>\<f9>", 'tx')
   call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches)
@@ -4489,6 +4491,7 @@
   call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches)
   call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
   call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
+
   set smartcase
   call feedkeys("gg/f\<tab>\<f9>", 'tx')
   call assert_equal(['foobar', 'fooBAr', 'foobarr'], g:compl_info.matches)
@@ -4496,16 +4499,42 @@
   call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
   call feedkeys("gg/FO\<tab>\<f9>", 'tx')
   call assert_equal({},  g:compl_info)
+  call feedkeys("gg/foob\<tab>\<f9>", 'tx')
+  call assert_equal(['foobar', 'foobarr'], g:compl_info.matches)
   call feedkeys("gg/\\Cfo\<tab>\<f9>", 'tx')
   call assert_equal(['\CfooBAr', '\Cfoobar'], g:compl_info.matches)
   call feedkeys("gg/\\cFo\<tab>\<f9>", 'tx')
   call assert_equal(['\cFoobar', '\cFooBAr', '\cFooBARR'], g:compl_info.matches)
 
+  set wildoptions+=exacttext ignorecase& smartcase&
+  call feedkeys("gg/F\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
+  call feedkeys("gg/foob\<tab>\<f9>", 'tx')
+  call assert_equal([], g:compl_info.matches)
+  call feedkeys("gg/r\\n.\<tab>\<f9>", 'tx')
+  call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches)
+
+  set ignorecase
+  call feedkeys("gg/F\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
+  call feedkeys("gg/R\\n.\<tab>\<f9>", 'tx')
+  call assert_equal(['r\nFoobar', 'r\nfooBAr', 'r\nFooBARR'], g:compl_info.matches)
+
+  set smartcase
+  call feedkeys("gg/f\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
+  call feedkeys("gg/foob\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'fooBAr', 'FooBARR', 'foobar'], g:compl_info.matches)
+  call feedkeys("gg/R\\n.\<tab>\<f9>", 'tx')
+  call assert_equal({}, g:compl_info)
+  call feedkeys("gg/r\\n.*\\n\<tab>\<f9>", 'tx')
+  call assert_equal(['r\nFoobar\nfooBAr', 'r\nfooBAr\nFooBARR'], g:compl_info.matches)
+
   bw!
   call test_override("char_avail", 0)
   delfunc GetComplInfo
   unlet! g:compl_info
-  set wildcharm=0 incsearch& ignorecase& smartcase&
+  set wildcharm=0 incsearch& ignorecase& smartcase& wildoptions&
 endfunc
 
 func Test_search_wildmenu_screendump()
diff --git a/src/version.c b/src/version.c
index bf9424f..5a7225b 100644
--- a/src/version.c
+++ b/src/version.c
@@ -720,6 +720,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1526,
+/**/
     1525,
 /**/
     1524,