patch 9.1.1490: 'wildchar' does not work in search contexts

Problem:  'wildchar' does not work in search contexts
Solution: implement search completion when 'wildchar' is typed
          (Girish Palya).

This change enhances Vim's command-line completion by extending
'wildmode' behavior to search pattern contexts, including:

- '/' and '?' search commands
- ':s', ':g', ':v', and ':vim' commands

Completions preserve the exact regex pattern typed by the user,
appending the completed word directly to the original input. This
ensures that all regex elements — such as '<', '^', grouping brackets
'()', wildcards '\*', '.', and other special characters — remain intact
and in their original positions.

---

**Use Case**

While searching (using `/` or `?`) for lines containing a pattern like
`"foobar"`, you can now type a partial pattern (e.g., `/f`) followed by
a trigger key (`wildchar`) to open a **popup completion menu** showing
all matching words.

This offers two key benefits:

1. **Precision**: Select the exact word you're looking for without
typing it fully.
2. **Memory aid**: When you can’t recall a full function or variable
name, typing a few letters helps you visually identify and complete the
correct symbol.

---

**What’s New**

Completion is now supported in the following contexts:

- `/` and `?` search commands
- `:s`, `:g`, `:v`, and `:vimgrep` ex-commands

---

**Design Notes**

- While `'wildchar'` (usually `<Tab>`) triggers completion, you'll have
to use `<CTRL-V><Tab>` or "\t" to search for a literal tab.
- **Responsiveness**: Search remains responsive because it checks for
user input frequently.

---

**Try It Out**

Basic setup using the default `<Tab>` as the completion trigger:

```vim
set wim=noselect,full wop=pum wmnu
```

Now type:

```
/foo<Tab>
```

This opens a completion popup for matches containing "foo".
For matches beginning with "foo" type `/\<foo<Tab>`.

---

**Optional: Autocompletion**

For automatic popup menu completion as you type in search or `:`
commands, include this in your `.vimrc`:

```vim
vim9script
set wim=noselect:lastused,full wop=pum wcm=<C-@> wmnu

autocmd CmdlineChanged [:/?] CmdComplete()

def CmdComplete()
  var [cmdline, curpos, cmdmode] = [getcmdline(), getcmdpos(),
expand('<afile>') == ':']
  var trigger_char = '\%(\w\|[*/:.-]\)$'
  var not_trigger_char = '^\%(\d\|,\|+\|-\)\+$'  # Exclude numeric range
  if getchar(1, {number: true}) == 0  # Typehead is empty, no more
pasted input
      && !wildmenumode() && curpos == cmdline->len() + 1
      && (!cmdmode || (cmdline =~ trigger_char && cmdline !~
not_trigger_char))
    SkipCmdlineChanged()
    feedkeys("\<C-@>", "t")
    timer_start(0, (_) => getcmdline()->substitute('\%x00', '',
'ge')->setcmdline())  # Remove <C-@>
  endif
enddef

def SkipCmdlineChanged(key = ''): string
  set ei+=CmdlineChanged
  timer_start(0, (_) => execute('set ei-=CmdlineChanged'))
  return key == '' ? '' : ((wildmenumode() ? "\<C-E>" : '') .. key)
enddef

**Optional: Preserve history recall behavior**
cnoremap <expr> <Up> SkipCmdlineChanged("\<Up>")
cnoremap <expr> <Down> SkipCmdlineChanged("\<Down>")

**Optional: Customize popup height**
autocmd CmdlineEnter : set bo+=error | exec $'set ph={max([10,
winheight(0) - 4])}'
autocmd CmdlineEnter [/?] set bo+=error | set ph=8
autocmd CmdlineLeave [:/?] set bo-=error ph&
```

closes: #17570

Signed-off-by: Girish Palya <girishji@gmail.com>
Signed-off-by: Christian Brabandt <cb@256bit.org>
diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt
index 93431f8..a15debe 100644
--- a/runtime/doc/cmdline.txt
+++ b/runtime/doc/cmdline.txt
@@ -1,4 +1,4 @@
-*cmdline.txt*   For Vim version 9.1.  Last change: 2025 Mar 08
+*cmdline.txt*   For Vim version 9.1.  Last change: 2025 Jun 28
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -415,7 +415,7 @@
 		to the end.
 		The 'wildoptions' option can be set to "tagfile" to list the
 		file of matching tags.
-					*c_CTRL-I* *c_wildchar* *c_<Tab>*
+				*c_CTRL-I* *c_wildchar* *c_<Tab>* */_<Tab>*
 'wildchar' option
 		A match is done on the pattern in front of the cursor.  The
 		match (if there are several, the first match) is inserted
@@ -425,6 +425,10 @@
 		again and there were multiple matches, the next
 		match is inserted.  After the last match, the first is used
 		again (wrap around).
+
+		In search context use <CTRL-V><Tab> or "\t" to search for a
+		literal <Tab> instead of triggering completion.
+
 		The behavior can be changed with the 'wildmode' option.
 							*c_<S-Tab>*
 <S-Tab>		Like 'wildchar' or <Tab>, but begin with the last match and
@@ -458,7 +462,7 @@
 		"?" and the current match is displayed then CTRL-G will move
 		to the next match (does not take |search-offset| into account)
 		Use CTRL-T to move to the previous match.  Hint: on a regular
-		keyboard T is above G.
+		keyboard G is below T.
 	                                            *c_CTRL-T* */_CTRL-T*
 CTRL-T		When 'incsearch' is set, entering a search pattern for "/" or
 		"?" and the current match is displayed then CTRL-T will move
diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index df4842f..a7dd6a9 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -9752,7 +9752,10 @@
 		:set wc=X
 		:set wc=^I
 		:set wc=<Tab>
-<	NOTE: This option is set to the Vi default value when 'compatible' is
+<	'wildchar' also enables completion in search pattern contexts such as
+	|/|, |?|, |:s|, |:g|, |:v|, and |:vim|.  To insert a literal <Tab>
+	instead of triggering completion, type <C-V><Tab> or "\t".
+	NOTE: This option is set to the Vi default value when 'compatible' is
 	set and to the Vim default value when 'compatible' is reset.
 
 						*'wildcharm'* *'wcm'*
diff --git a/runtime/doc/tags b/runtime/doc/tags
index c96a8b6..30eb991 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -1815,6 +1815,7 @@
 /\{-	pattern.txt	/*\/\\{-*
 /\~	pattern.txt	/*\/\\~*
 /^	pattern.txt	/*\/^*
+/_<Tab>	cmdline.txt	/*\/_<Tab>*
 /_CTRL-G	cmdline.txt	/*\/_CTRL-G*
 /_CTRL-L	cmdline.txt	/*\/_CTRL-L*
 /_CTRL-T	cmdline.txt	/*\/_CTRL-T*
diff --git a/runtime/doc/version9.txt b/runtime/doc/version9.txt
index f2618e5..f8ddb7f 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 Jun 27
+*version9.txt*  For Vim version 9.1.  Last change: 2025 Jun 28
 
 
 		  VIM REFERENCE MANUAL    by Bram Moolenaar
@@ -41632,6 +41632,8 @@
 - add ":filetype" command completion
 - add "filetypecmd" completion type for |getcompletion()|
 - 'smartcase' applies to completion filtering
+- 'wildchar' enables completion in search contexts using |/|, |?|, |:g|, |:v|
+  and |:vimgrep| commands
 
 Options: ~
 - the default for 'commentstring' contains whitespace padding to have
diff --git a/src/cmdexpand.c b/src/cmdexpand.c
index 4077fb3..2a23607 100644
--- a/src/cmdexpand.c
+++ b/src/cmdexpand.c
@@ -14,6 +14,8 @@
 #include "vim.h"
 
 static int	cmd_showtail;	// Only show path tail in lists ?
+static int	may_expand_pattern = FALSE;
+static pos_T	pre_incsearch_pos; // Cursor position when incsearch started
 
 static void	set_context_for_wildcard_arg(exarg_T *eap, char_u *arg, int usefilter, expand_T *xp, int *complp);
 static int	ExpandFromContext(expand_T *xp, char_u *, char_u ***, int *, int);
@@ -24,6 +26,7 @@
 static int	ExpandUserDefined(char_u *pat, expand_T *xp, regmatch_T *regmatch, char_u ***matches, int *numMatches);
 static int	ExpandUserList(expand_T *xp, char_u ***matches, int *numMatches);
 #endif
+static int	expand_pattern_in_buf(char_u *pat, int dir, char_u ***matches, int *numMatches);
 
 // "compl_match_array" points the currently displayed list of entries in the
 // popup menu.  It is NULL when there is no popup menu.
@@ -233,6 +236,8 @@
 
     if (xp->xp_numfiles == -1)
     {
+	may_expand_pattern = options & WILD_MAY_EXPAND_PATTERN;
+	pre_incsearch_pos = xp->xp_pre_incsearch_pos;
 #ifdef FEAT_EVAL
 	if (ccline->input_fn && ccline->xp_context == EXPAND_COMMANDS)
 	{
@@ -277,8 +282,9 @@
     }
     else
     {
-	if (cmdline_fuzzy_completion_supported(xp))
-	    // If fuzzy matching, don't modify the search string
+	if (cmdline_fuzzy_completion_supported(xp)
+		|| xp->xp_context == EXPAND_PATTERN_IN_BUF)
+	    // Don't modify the search string
 	    p1 = vim_strnsave(xp->xp_pattern, xp->xp_pattern_len);
 	else
 	    p1 = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context);
@@ -292,12 +298,11 @@
 		    WILD_HOME_REPLACE|WILD_ADD_SLASH|WILD_SILENT;
 	    if (use_options & WILD_KEEP_SOLE_ITEM)
 		use_options &= ~WILD_KEEP_SOLE_ITEM;
-
 	    if (escape)
 		use_options |= WILD_ESCAPE;
-
 	    if (p_wic)
 		use_options += WILD_ICASE;
+
 	    p2 = ExpandOne(xp, p1,
 			 vim_strnsave(&ccline->cmdbuff[i], xp->xp_pattern_len),
 							   use_options, type);
@@ -495,12 +500,14 @@
 
 /*
  * Return the number of characters that should be skipped in a status match.
- * These are backslashes used for escaping.  Do show backslashes in help tags.
+ * These are backslashes used for escaping.  Do show backslashes in help tags
+ * and in search pattern completion matches.
  */
     static int
 skip_status_match_char(expand_T *xp, char_u *s)
 {
-    if ((rem_backslash(s) && xp->xp_context != EXPAND_HELP)
+    if ((rem_backslash(s) && xp->xp_context != EXPAND_HELP
+		&& xp->xp_context != EXPAND_PATTERN_IN_BUF)
 #ifdef FEAT_MENU
 	    || ((xp->xp_context == EXPAND_MENUS
 		    || xp->xp_context == EXPAND_MENUNAMES)
@@ -1598,23 +1605,40 @@
  *			    names in expressions, eg :while s^I
  *  EXPAND_ENV_VARS	    Complete environment variable names
  *  EXPAND_USER		    Complete user names
+ *  EXPAND_PATTERN_IN_BUF   Complete pattern in '/', '?', ':s', ':g', etc.
  */
     void
 set_expand_context(expand_T *xp)
 {
-    cmdline_info_T	*ccline = get_cmdline_info();
+    cmdline_info_T  *ccline = get_cmdline_info();
 
-    // only expansion for ':', '>' and '=' command-lines
+    // Handle search commands: '/' or '?'
+    if ((ccline->cmdfirstc == '/' || ccline->cmdfirstc == '?')
+	    && may_expand_pattern)
+    {
+	xp->xp_context = EXPAND_PATTERN_IN_BUF;
+	xp->xp_search_dir = (ccline->cmdfirstc == '/') ? FORWARD : BACKWARD;
+	xp->xp_pattern = ccline->cmdbuff;
+	xp->xp_pattern_len = ccline->cmdpos;
+#ifdef FEAT_SEARCH_EXTRA
+	search_first_line = 0; // Search entire buffer
+#endif
+	return;
+    }
+
+    // Only handle ':', '>', or '=' command-lines, or expression input
     if (ccline->cmdfirstc != ':'
 #ifdef FEAT_EVAL
 	    && ccline->cmdfirstc != '>' && ccline->cmdfirstc != '='
 	    && !ccline->input_fn
 #endif
-	    )
+       )
     {
 	xp->xp_context = EXPAND_NOTHING;
 	return;
     }
+
+    // Fallback to command-line expansion
     set_cmd_context(xp, ccline->cmdbuff, ccline->cmdlen, ccline->cmdpos, TRUE);
 }
 
@@ -2206,6 +2230,34 @@
     return NULL;
 }
 
+/*
+ * Sets the completion context for commands that involve a search pattern
+ * and a line range (e.g., :s, :g, :v).
+ */
+    static void
+set_context_with_pattern(expand_T *xp)
+{
+    int		    skiplen = 0;
+    cmdline_info_T  *ccline = get_cmdline_info();
+#ifdef FEAT_SEARCH_EXTRA
+    int		    dummy, patlen, retval;
+
+    ++emsg_off;
+    retval = parse_pattern_and_range(&pre_incsearch_pos, &dummy, &skiplen,
+	    &patlen);
+    --emsg_off;
+
+    // Check if cursor is within search pattern
+    if (!retval || ccline->cmdpos <= skiplen
+	    || ccline->cmdpos > skiplen + patlen)
+	return;
+#endif
+
+    xp->xp_pattern = ccline->cmdbuff + skiplen;
+    xp->xp_pattern_len = ccline->cmdpos - skiplen;
+    xp->xp_context = EXPAND_PATTERN_IN_BUF;
+    xp->xp_search_dir = FORWARD;
+}
 
 /*
  * Set the completion context in 'xp' for command 'cmd' with index 'cmdidx'.
@@ -2225,6 +2277,8 @@
 	int		compl,
 	int		forceit)
 {
+    char_u  *nextcmd;
+
     switch (cmdidx)
     {
 	case CMD_find:
@@ -2307,10 +2361,18 @@
 
 	case CMD_global:
 	case CMD_vglobal:
-	    return find_cmd_after_global_cmd(arg);
+	    nextcmd = find_cmd_after_global_cmd(arg);
+	    if (!nextcmd && may_expand_pattern)
+		set_context_with_pattern(xp);
+	    return nextcmd;
+
 	case CMD_and:
 	case CMD_substitute:
-	    return find_cmd_after_substitute_cmd(arg);
+	    nextcmd = find_cmd_after_substitute_cmd(arg);
+	    if (!nextcmd && may_expand_pattern)
+		set_context_with_pattern(xp);
+	    return nextcmd;
+
 	case CMD_isearch:
 	case CMD_dsearch:
 	case CMD_ilist:
@@ -3318,6 +3380,9 @@
 	return ExpandPackAddDir(pat, numMatches, matches);
     if (xp->xp_context == EXPAND_RUNTIME)
 	return expand_runtime_cmd(pat, numMatches, matches);
+    if (xp->xp_context == EXPAND_PATTERN_IN_BUF)
+	return expand_pattern_in_buf(pat, xp->xp_search_dir,
+		matches, numMatches);
 
     // When expanding a function name starting with s:, match the <SNR>nr_
     // prefix.
@@ -4241,6 +4306,11 @@
 	RedrawingDisabled = 0;
 #endif
 
+#if defined(FEAT_SEARCH_EXTRA) || defined(PROTO)
+    // Clear highlighting applied during wildmenu activity
+    set_no_hlsearch(TRUE);
+#endif
+
     if (wild_menu_showing == WM_SCROLLED)
     {
 	// Entered command line, move it up
@@ -4438,3 +4508,247 @@
     }
 }
 #endif // FEAT_EVAL
+
+/*
+ * Copy a substring from the current buffer (curbuf), spanning from the given
+ * 'start' position to the word boundary after 'end' position.
+ * The copied string is stored in '*match', and the actual end position of the
+ * matched text is returned in '*match_end'.
+ */
+    static int
+copy_substring_from_pos(pos_T *start, pos_T *end, char_u **match,
+	pos_T *match_end)
+{
+    char_u	*word_end;
+    char_u	*line, *start_line, *end_line;
+    int		segment_len;
+    linenr_T	lnum;
+    garray_T	ga;
+
+    if (start->lnum > end->lnum
+	    || (start->lnum == end->lnum && start->col >= end->col))
+	return FAIL; // invalid range
+
+    // Get line pointers
+    start_line = ml_get(start->lnum);
+    end_line = ml_get(end->lnum);
+
+    // Use a growable string (ga)
+    ga_init2(&ga, 1, 128);
+
+    // Append start line from start->col to end
+    char_u  *start_ptr = start_line + start->col;
+    int	    is_single_line = start->lnum == end->lnum;
+
+    segment_len = is_single_line ? (end->col - start->col)
+	: (int)STRLEN(start_ptr);
+    if (ga_grow(&ga, segment_len + 1) != OK)
+	return FAIL;
+    ga_concat_len(&ga, start_ptr, segment_len);
+    if (!is_single_line)
+	ga_append(&ga, '\n');
+
+    // Append full lines between start and end
+    if (!is_single_line)
+	for (lnum = start->lnum + 1; lnum < end->lnum; lnum++)
+	{
+	    line = ml_get(lnum);
+	    if (ga_grow(&ga, ml_get_len(lnum) + 1) != OK)
+		return FAIL;
+	    ga_concat(&ga, line);
+	    ga_append(&ga, '\n');
+	}
+
+    // Append partial end line (up to word end)
+    word_end = find_word_end(end_line + end->col);
+    segment_len = (int)(word_end - end_line);
+    if (ga_grow(&ga, segment_len) != OK)
+	return FAIL;
+    ga_concat_len(&ga, end_line + (is_single_line ? end->col : 0),
+	    segment_len - (is_single_line ? end->col : 0));
+
+    // Null-terminate
+    if (ga_grow(&ga, 1) != OK)
+	return FAIL;
+    ga_append(&ga, NUL);
+
+    *match = (char_u *)ga.ga_data;
+    match_end->lnum = end->lnum;
+    match_end->col = segment_len;
+
+    return OK;
+}
+
+/*
+ * Search for strings matching "pat" in the specified range and return them.
+ * Returns OK on success, FAIL otherwise.
+ */
+    static int
+expand_pattern_in_buf(
+    char_u	*pat,		    // pattern to match
+    int		dir,		    // direction: FORWARD or BACKWARD
+    char_u	***matches,	    // return: array with matched strings
+    int		*numMatches)	    // return: number of matches
+{
+    pos_T	cur_match_pos, prev_match_pos, end_match_pos, word_end_pos;
+    garray_T	ga;
+    int		found_new_match;
+    int		looped_around = FALSE;
+    int		pat_len, match_len;
+    int		has_range = FALSE;
+    int		compl_started = FALSE;
+    int		search_flags, i;
+    char_u	*match, *line, *word_end;
+    regmatch_T	regmatch;
+
+#ifdef FEAT_SEARCH_EXTRA
+    has_range = search_first_line != 0;
+#endif
+
+    *matches = NULL;
+    *numMatches = 0;
+
+    if (pat == NULL || *pat == NUL)
+	return FAIL;
+
+    pat_len = (int)STRLEN(pat);
+    CLEAR_FIELD(cur_match_pos);
+    CLEAR_FIELD(prev_match_pos);
+#ifdef FEAT_SEARCH_EXTRA
+    if (has_range)
+	cur_match_pos.lnum = search_first_line;
+    else
+#endif
+	cur_match_pos = pre_incsearch_pos;
+
+    search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG
+	| (has_range ? SEARCH_START : 0);
+
+    regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING);
+    if (regmatch.regprog == NULL)
+	return FAIL;
+    regmatch.rm_ic = p_ic;
+
+    ga_init2(&ga, sizeof(char_u *), 10); // Use growable array of char_u*
+
+    for (;;)
+    {
+	++emsg_off;
+	++msg_silent;
+	found_new_match = searchit(NULL, curbuf, &cur_match_pos,
+		&end_match_pos, dir, pat, pat_len, 1L,
+		search_flags, RE_LAST, NULL);
+	--msg_silent;
+	--emsg_off;
+
+	if (found_new_match == FAIL)
+	    break;
+
+#ifdef FEAT_SEARCH_EXTRA
+	// If in range mode, check if match is within the range
+	if (has_range && (cur_match_pos.lnum < search_first_line
+		    || cur_match_pos.lnum > search_last_line))
+		break;
+#endif
+
+	if (compl_started)
+	{
+	    // If we've looped back to an earlier match, stop
+	    if ((dir == FORWARD
+			&& (cur_match_pos.lnum < prev_match_pos.lnum
+			    || (cur_match_pos.lnum == prev_match_pos.lnum
+				&& cur_match_pos.col <= prev_match_pos.col)))
+		    || (dir == BACKWARD
+			&& (cur_match_pos.lnum > prev_match_pos.lnum
+			    || (cur_match_pos.lnum == prev_match_pos.lnum
+				&& cur_match_pos.col >= prev_match_pos.col))))
+	    {
+		if (looped_around)
+		    break;
+		else
+		    looped_around = TRUE;
+	    }
+	}
+
+	compl_started = TRUE;
+	prev_match_pos = cur_match_pos;
+
+	// Abort if user typed a character or interrupted
+	if (char_avail() || got_int)
+	{
+	    if (got_int)
+	    {
+		(void)vpeekc();  // Remove <C-C> from input stream
+		got_int = FALSE; // Don't abandon the command line
+	    }
+	    goto cleanup;
+	}
+
+	// searchit() can return line number +1 past the last line when
+	// searching for "foo\n" if "foo" is at end of buffer.
+	if (end_match_pos.lnum > curbuf->b_ml.ml_line_count)
+	{
+	    cur_match_pos.lnum = 1;
+	    cur_match_pos.col = 0;
+	    cur_match_pos.coladd = 0;
+	    continue;
+	}
+
+	// Extract the matching text prepended to completed word
+	if (!copy_substring_from_pos(&cur_match_pos, &end_match_pos, &match,
+		    &word_end_pos))
+	    break;
+
+	// Verify that the constructed match actually matches the pattern with
+	// correct case sensitivity
+	if (!vim_regexec_nl(&regmatch, match, (colnr_T)0))
+	{
+	    vim_free(match);
+	    continue;
+	}
+	vim_free(match);
+
+	// Construct a new match from completed word appended to pattern itself
+	line = ml_get(end_match_pos.lnum);
+	word_end = find_word_end(line + end_match_pos.col);  // col starts from 0
+	match_len = (int)(word_end - (line + end_match_pos.col));
+	match = alloc(match_len + pat_len + 1);  // +1 for NUL
+	if (match == NULL)
+	    goto cleanup;
+	mch_memmove(match, pat, pat_len);
+	if (match_len > 0)
+	    mch_memmove(match + pat_len, line + end_match_pos.col, match_len);
+	match[pat_len + match_len] = NUL;
+
+	// Include this match if it is not a duplicate
+	for (i = 0; i < ga.ga_len; ++i)
+	{
+	    if (STRCMP(match, ((char_u **)ga.ga_data)[i]) == 0)
+	    {
+		VIM_CLEAR(match);
+		break;
+	    }
+	}
+	if (match != NULL)
+	{
+	    if (ga_grow(&ga, 1) == FAIL)
+		goto cleanup;
+	    ((char_u **)ga.ga_data)[ga.ga_len++] = match;
+	    if (ga.ga_len > TAG_MANY)
+		break;
+	}
+	if (has_range)
+	    cur_match_pos = word_end_pos;
+    }
+
+    vim_regfree(regmatch.regprog);
+
+    *matches = (char_u **)ga.ga_data;
+    *numMatches = ga.ga_len;
+    return OK;
+
+cleanup:
+    vim_regfree(regmatch.regprog);
+    ga_clear_strings(&ga);
+    return FAIL;
+}
diff --git a/src/ex_getln.c b/src/ex_getln.c
index 36775ba..324f492 100644
--- a/src/ex_getln.c
+++ b/src/ex_getln.c
@@ -204,67 +204,55 @@
 }
 
 /*
- * Return TRUE when 'incsearch' highlighting is to be done.
- * Sets search_first_line and search_last_line to the address range.
- * May change the last search pattern.
+ * Parses the :[range]s/foo like commands and returns details needed for
+ * incsearch and wildmenu completion.
+ * Returns TRUE if pattern is valid.
+ * Sets skiplen, patlen, search_first_line, and search_last_line.
  */
-    static int
-do_incsearch_highlighting(
-	int		    firstc,
-	int		    *search_delim,
-	incsearch_state_T   *is_state,
-	int		    *skiplen,
-	int		    *patlen)
+    int
+parse_pattern_and_range(
+	pos_T	*incsearch_start,
+	int	*search_delim,
+	int	*skiplen,
+	int	*patlen)
 {
-    char_u	*cmd;
+    char_u	*cmd, *p, *end;
     cmdmod_T	dummy_cmdmod;
-    char_u	*p;
-    int		delim_optional = FALSE;
-    int		delim;
-    char_u	*end;
-    char	*dummy;
     exarg_T	ea;
     pos_T	save_cursor;
+    int		delim_optional = FALSE;
+    int		delim;
     int		use_last_pat;
-    int		retval = FALSE;
     magic_T     magic = 0;
+    char	*dummy;
 
     *skiplen = 0;
     *patlen = ccline.cmdlen;
 
-    if (!p_is || cmd_silent)
-	return FALSE;
-
-    // by default search all lines
+    // Default range
     search_first_line = 0;
     search_last_line = MAXLNUM;
 
-    if (firstc == '/' || firstc == '?')
-    {
-	*search_delim = firstc;
-	return TRUE;
-    }
-    if (firstc != ':')
-	return FALSE;
-
-    ++emsg_off;
     CLEAR_FIELD(ea);
     ea.line1 = 1;
     ea.line2 = 1;
     ea.cmd = ccline.cmdbuff;
     ea.addr_type = ADDR_LINES;
 
+    // Skip over command modifiers
     parse_command_modifiers(&ea, &dummy, &dummy_cmdmod, TRUE);
 
+    // Skip over the range to find the command.
     cmd = skip_range(ea.cmd, TRUE, NULL);
-    if (vim_strchr((char_u *)"sgvl", *cmd) == NULL)
-	goto theend;
 
-    // Skip over "substitute" to find the pattern separator.
+    if (vim_strchr((char_u *)"sgvl", *cmd) == NULL)
+	return FALSE;
+
+    // Skip over command name to find pattern separator
     for (p = cmd; ASCII_ISALPHA(*p); ++p)
 	;
     if (*skipwhite(p) == NUL)
-	goto theend;
+	return FALSE;
 
     if (STRNCMP(cmd, "substitute", p - cmd) == 0
 	    || STRNCMP(cmd, "smagic", p - cmd) == 0
@@ -285,83 +273,113 @@
 	while (ASCII_ISALPHA(*(p = skipwhite(p))))
 	    ++p;
 	if (*p == NUL)
-	    goto theend;
+	    return FALSE;
     }
     else if (STRNCMP(cmd, "vimgrep", MAX(p - cmd, 3)) == 0
-	|| STRNCMP(cmd, "vimgrepadd", MAX(p - cmd, 8)) == 0
-	|| STRNCMP(cmd, "lvimgrep", MAX(p - cmd, 2)) == 0
-	|| STRNCMP(cmd, "lvimgrepadd", MAX(p - cmd, 9)) == 0
-	|| STRNCMP(cmd, "global", p - cmd) == 0)
+	    || STRNCMP(cmd, "vimgrepadd", MAX(p - cmd, 8)) == 0
+	    || STRNCMP(cmd, "lvimgrep", MAX(p - cmd, 2)) == 0
+	    || STRNCMP(cmd, "lvimgrepadd", MAX(p - cmd, 9)) == 0
+	    || STRNCMP(cmd, "global", p - cmd) == 0)
     {
-	// skip over "!"
+	// skip optional "!"
 	if (*p == '!')
 	{
 	    p++;
 	    if (*skipwhite(p) == NUL)
-		goto theend;
+		return FALSE;
 	}
 	if (*cmd != 'g')
 	    delim_optional = TRUE;
     }
     else
-	goto theend;
+	return FALSE;
 
     p = skipwhite(p);
     delim = (delim_optional && vim_isIDc(*p)) ? ' ' : *p++;
     *search_delim = delim;
-    end = skip_regexp_ex(p, delim, magic_isset(), NULL, NULL, &magic);
 
+    end = skip_regexp_ex(p, delim, magic_isset(), NULL, NULL, &magic);
     use_last_pat = end == p && *end == delim;
 
     if (end == p && !use_last_pat)
-	goto theend;
+	return FALSE;
 
-    // Don't do 'hlsearch' highlighting if the pattern matches everything.
+    // Skip if the pattern matches everything (e.g., for 'hlsearch')
     if (!use_last_pat)
     {
 	char c = *end;
-	int  empty;
+	int empty;
 
 	*end = NUL;
 	empty = empty_pattern_magic(p, (size_t)(end - p), magic);
 	*end = c;
 	if (empty)
-	    goto theend;
+	    return FALSE;
     }
 
-    // found a non-empty pattern or //
+    // Found a non-empty pattern or //
     *skiplen = (int)(p - ccline.cmdbuff);
     *patlen = (int)(end - p);
 
-    // parse the address range
+    // Parse the address range
     save_cursor = curwin->w_cursor;
-    curwin->w_cursor = is_state->search_start;
+    curwin->w_cursor = *incsearch_start;
+
     parse_cmd_address(&ea, &dummy, TRUE);
+
     if (ea.addr_count > 0)
     {
-	// Allow for reverse match.
-	if (ea.line2 < ea.line1)
-	{
-	    search_first_line = ea.line2;
-	    search_last_line = ea.line1;
-	}
-	else
-	{
-	    search_first_line = ea.line1;
-	    search_last_line = ea.line2;
-	}
+	int reverse_match = ea.line2 < ea.line1;
+	search_first_line = reverse_match ? ea.line2 : ea.line1;
+	search_last_line = reverse_match ? ea.line1 : ea.line2;
     }
     else if (cmd[0] == 's' && cmd[1] != 'o')
-    {
 	// :s defaults to the current line
-	search_first_line = curwin->w_cursor.lnum;
-	search_last_line = curwin->w_cursor.lnum;
-    }
+	search_first_line = search_last_line = curwin->w_cursor.lnum;
 
     curwin->w_cursor = save_cursor;
-    retval = TRUE;
-theend:
+    return TRUE;
+}
+
+/*
+ * Return TRUE when 'incsearch' highlighting is to be done.
+ * Sets search_first_line and search_last_line to the address range.
+ * May change the last search pattern.
+ */
+    static int
+do_incsearch_highlighting(
+	int		    firstc,
+	int		    *search_delim,
+	incsearch_state_T   *is_state,
+	int		    *skiplen,
+	int		    *patlen)
+{
+    int retval = FALSE;
+
+    *skiplen = 0;
+    *patlen = ccline.cmdlen;
+
+    if (!p_is || cmd_silent)
+	return FALSE;
+
+    // By default search all lines
+    search_first_line = 0;
+    search_last_line = MAXLNUM;
+
+    if (firstc == '/' || firstc == '?')
+    {
+	*search_delim = firstc;
+	return TRUE;
+    }
+
+    if (firstc != ':')
+	return FALSE;
+
+    ++emsg_off;
+    retval = parse_pattern_and_range(&is_state->search_start, search_delim,
+	    skiplen, patlen);
     --emsg_off;
+
     return retval;
 }
 
@@ -905,7 +923,8 @@
 	int		*did_wild_list,
 	int		*wim_index_p,
 	expand_T	*xp,
-	int		*gotesc)
+	int		*gotesc,
+	pos_T		*pre_incsearch_pos)
 {
     int		wim_index = *wim_index_p;
     int		res;
@@ -938,6 +957,14 @@
     }
     else		    // typed p_wc first time
     {
+	if (c == p_wc || c == p_wcm)
+	{
+	    options |= WILD_MAY_EXPAND_PATTERN;
+	    if (pre_incsearch_pos)
+		xp->xp_pre_incsearch_pos = *pre_incsearch_pos;
+	    else
+		xp->xp_pre_incsearch_pos = curwin->w_cursor;
+	}
 	wim_index = 0;
 	j = ccline.cmdpos;
 	// if 'wildmode' first contains "longest", get longest
@@ -1927,6 +1954,10 @@
 	{
 	    trigger_cmd_autocmd(cmdline_type, EVENT_CMDLINELEAVEPRE);
 	    event_cmdlineleavepre_triggered = TRUE;
+#if defined(FEAT_SEARCH_EXTRA) || defined(PROTO)
+	    if ((c == ESC || c == Ctrl_C) && (wim_flags[0] & WIM_LIST))
+		set_no_hlsearch(TRUE);
+#endif
 	}
 
 	// The wildmenu is cleared if the pressed key is not used for
@@ -2021,7 +2052,13 @@
 	if ((c == p_wc && !gotesc && KeyTyped) || c == p_wcm)
 	{
 	    res = cmdline_wildchar_complete(c, firstc != '@', &did_wild_list,
-		    &wim_index, &xpc, &gotesc);
+		    &wim_index, &xpc, &gotesc,
+#ifdef FEAT_SEARCH_EXTRA
+		    &is_state.search_start
+#else
+		    NULL
+#endif
+		    );
 	    if (res == CMDLINE_CHANGED)
 		goto cmdline_changed;
 	}
@@ -2056,6 +2093,16 @@
 	// further.
 	if (wild_type == WILD_CANCEL || wild_type == WILD_APPLY)
 	{
+#ifdef FEAT_SEARCH_EXTRA
+	    // Apply search highlighting
+	    if (wild_type == WILD_APPLY)
+	    {
+		if (is_state.winid != curwin->w_id)
+		    init_incsearch_state(&is_state);
+		if (KeyTyped || vpeekc() == NUL)
+		    may_do_incsearch_highlighting(firstc, count, &is_state);
+	    }
+#endif
 	    wild_type = 0;
 	    goto cmdline_not_changed;
 	}
@@ -2527,6 +2574,8 @@
 	// If the window changed incremental search state is not valid.
 	if (is_state.winid != curwin->w_id)
 	    init_incsearch_state(&is_state);
+	if (xpc.xp_context == EXPAND_NOTHING && (KeyTyped || vpeekc() == NUL))
+	    may_do_incsearch_highlighting(firstc, count, &is_state);
 #endif
 	// Trigger CmdlineChanged autocommands.
 	if (trigger_cmdlinechanged)
@@ -2539,11 +2588,6 @@
 	    prev_cmdpos = ccline.cmdpos;
 	}
 
-#ifdef FEAT_SEARCH_EXTRA
-	if (xpc.xp_context == EXPAND_NOTHING && (KeyTyped || vpeekc() == NUL))
-	    may_do_incsearch_highlighting(firstc, count, &is_state);
-#endif
-
 #ifdef FEAT_RIGHTLEFT
 	if (cmdmsg_rl
 # ifdef FEAT_ARABIC
diff --git a/src/proto/ex_getln.pro b/src/proto/ex_getln.pro
index cc8723a..6c93ac7 100644
--- a/src/proto/ex_getln.pro
+++ b/src/proto/ex_getln.pro
@@ -45,4 +45,5 @@
 int is_in_cmdwin(void);
 char_u *script_get(exarg_T *eap, char_u *cmd);
 void get_user_input(typval_T *argvars, typval_T *rettv, int inputdialog, int secret);
+int parse_pattern_and_range(pos_T *is_start, int *search_delim, int *skiplen, int *patlen);
 /* vim: set ft=c : */
diff --git a/src/structs.h b/src/structs.h
index 423757a..2108c21 100644
--- a/src/structs.h
+++ b/src/structs.h
@@ -655,6 +655,8 @@
     char_u	*xp_line;		// text being completed
 #define EXPAND_BUF_LEN 256
     char_u	xp_buf[EXPAND_BUF_LEN];	// buffer for returned match
+    int		xp_search_dir;		// Direction of search
+    pos_T	xp_pre_incsearch_pos;	// Cursor position before incsearch
 } expand_T;
 
 /*
diff --git a/src/testdir/dumps/Test_search_wildmenu_1.dump b/src/testdir/dumps/Test_search_wildmenu_1.dump
new file mode 100644
index 0000000..46ab9a5
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_1.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|e+0#0000001#ffff4012|\|n|f|o@1|b|a|r| +3#0000000#ffffff0@1|e|\|n|t|h|e|t|h|e|r|e| @1|e|\|n|t|h|e|s|e| @1|e|\|n|t|h|e| @34
+|/+0&&|e|\|n|f|o@1|b|a|r> @64
diff --git a/src/testdir/dumps/Test_search_wildmenu_2.dump b/src/testdir/dumps/Test_search_wildmenu_2.dump
new file mode 100644
index 0000000..52889cc
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_2.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|/+0#0000000&|t|h|e> @70
diff --git a/src/testdir/dumps/Test_search_wildmenu_3.dump b/src/testdir/dumps/Test_search_wildmenu_3.dump
new file mode 100644
index 0000000..69e38f8
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_3.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|~| @73
+|/+0#0000000&|t| @72
+|t|h|e|s|e| @4|t|h|e| @6|t|h|e|t|h|e| @3|t|h|e|t|h|e|r|e| @1|t|h|e|r|e| @29
+|/|t> @72
diff --git a/src/testdir/dumps/Test_search_wildmenu_4.dump b/src/testdir/dumps/Test_search_wildmenu_4.dump
new file mode 100644
index 0000000..512f87e
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_4.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|t+3#0000000&|h|e|s|e| @1|t|h|e| @1|t|h|e|t|h|e| @1|t|h|e|t|h|e|r|e| @1|t|h|e|r|e| @39
+|/+0&&|t> @72
diff --git a/src/testdir/dumps/Test_search_wildmenu_5.dump b/src/testdir/dumps/Test_search_wildmenu_5.dump
new file mode 100644
index 0000000..533644f
--- /dev/null
+++ b/src/testdir/dumps/Test_search_wildmenu_5.dump
@@ -0,0 +1,10 @@
+|t+0&#ffffff0|h|e| @71
+|t|h|e|s|e| @69
+|t|h|e| @71
+|f|o@1|b|a|r| @68
+|t|h|e|t|h|e| @68
+|t|h|e|t|h|e|r|e| @66
+|~+0#4040ff13&| @73
+|~| @73
+|t+3#0000000&|.|*|\|n|.|*|\|n|.|o@1|b|a|r| @1|t|.|*|\|n|.|*|\|n|.|h|e|t|h|e| @1|t|.|*|\|n|.|*|\|n|.|h|e| @28
+|/+0&&|t|.|*|\|n|.|*|\|n|.> @63
diff --git a/src/testdir/test_cmdline.vim b/src/testdir/test_cmdline.vim
index 9a3fe20..59c25db 100644
--- a/src/testdir/test_cmdline.vim
+++ b/src/testdir/test_cmdline.vim
@@ -1651,8 +1651,10 @@
   " completion after a :global command
   call feedkeys(":g/a/chist\t\<C-B>\"\<CR>", 'xt')
   call assert_equal('"g/a/chistory', @:)
+  set wildchar=0
   call feedkeys(":g/a\\/chist\t\<C-B>\"\<CR>", 'xt')
   call assert_equal("\"g/a\\/chist\t", @:)
+  set wildchar&
 
   " use <Esc> as the 'wildchar' for completion
   set wildchar=<Esc>
@@ -3094,12 +3096,14 @@
 " Test for completion after a :substitute command followed by a pipe (|)
 " character
 func Test_cmdline_complete_substitute()
+  set wildchar=0
   call feedkeys(":s | \t\<C-B>\"\<CR>", 'xt')
   call assert_equal("\"s | \t", @:)
   call feedkeys(":s/ | \t\<C-B>\"\<CR>", 'xt')
   call assert_equal("\"s/ | \t", @:)
   call feedkeys(":s/one | \t\<C-B>\"\<CR>", 'xt')
   call assert_equal("\"s/one | \t", @:)
+  set wildchar&
   call feedkeys(":s/one/ | \t\<C-B>\"\<CR>", 'xt')
   call assert_equal("\"s/one/ | \t", @:)
   call feedkeys(":s/one/two | \t\<C-B>\"\<CR>", 'xt')
@@ -4350,4 +4354,239 @@
   call assert_fails(':redrawtabpanel', 'E1547:')
 endfunc
 
+" Test wildcharm completion for '/' and '?' search
+func Test_search_complete()
+  CheckOption incsearch
+  set wildcharm=<c-z>
+
+  " Disable char_avail so that expansion of commandline works
+  call test_override("char_avail", 1)
+
+  func GetComplInfo()
+    let g:compl_info = cmdcomplete_info()
+    return ''
+  endfunc
+
+  new
+  cnoremap <buffer><expr> <F9> GetComplInfo()
+
+  " Pressing <Tab> inserts tab character
+  set wildchar=0
+  call setline(1, "x\t")
+  call feedkeys("/x\t\r", "tx")
+  call assert_equal("x\t", @/)
+  set wildchar&
+
+  call setline(1, ['the', 'these', 'thethe', 'thethere', 'foobar'])
+
+  for trig in ["\<tab>", "\<c-z>"]
+    " Test menu first item and order
+    call feedkeys($"gg2j/t{trig}\<f9>", 'tx')
+    call assert_equal(['the', 'thethere', 'there', 'these', 'thethe'], g:compl_info.matches)
+    call feedkeys($"gg2j?t{trig}\<f9>", 'tx')
+    call assert_equal(['these', 'the', 'there', 'thethere', 'thethe'], g:compl_info.matches)
+
+    " <c-n> and <c-p> cycle through menu items
+    call feedkeys($"gg/the{trig}\<cr>", 'tx')
+    call assert_equal('these', getline('.'))
+    call feedkeys($"gg/the{trig}\<c-n>\<cr>", 'tx')
+    call assert_equal('thethe', getline('.'))
+    call feedkeys($"gg/the{trig}".repeat("\<c-n>", 5)."\<cr>", 'tx')
+    call assert_equal('these', getline('.'))
+    call feedkeys($"G?the{trig}\<cr>", 'tx')
+    call assert_equal('thethere', getline('.'))
+    call feedkeys($"G?the{trig}".repeat("\<c-p>", 5)."\<cr>", 'tx')
+    call assert_equal('thethere', getline('.'))
+
+    " Beginning of word pattern (<) retains '<'
+    call feedkeys($"gg2j/\\<t{trig}\<f9>", 'tx')
+    call assert_equal(['\<thethere', '\<the', '\<these', '\<thethe'], g:compl_info.matches)
+    call feedkeys($"gg2j?\\<t{trig}\<f9>", 'tx')
+    call assert_equal(['\<these', '\<the', '\<thethere', '\<thethe'], g:compl_info.matches)
+    call feedkeys($"gg2j/\\v<t{trig}\<f9>", 'tx')
+    call assert_equal(['\v<thethere', '\v<the', '\v<these', '\v<thethe'], g:compl_info.matches)
+    call feedkeys($"gg2j?\\v<th{trig}\<f9>", 'tx')
+    call assert_equal(['\v<these', '\v<the', '\v<thethere', '\v<thethe'], g:compl_info.matches)
+  endfor
+
+  " Ctrl-G goes from one match to the next, after menu is opened
+  set incsearch
+  " first match
+  call feedkeys("gg/the\<c-z>\<c-n>\<c-g>\<cr>", 'tx')
+  call assert_equal('thethe', getline('.'))
+  " second match
+  call feedkeys("gg/the\<c-z>\<c-n>\<c-g>\<c-g>\<cr>", 'tx')
+  call assert_equal('thethere', getline('.'))
+  call assert_equal([0, 0, 0, 0], getpos('"'))
+
+  " CTRL-T goes to the previous match
+  " first match
+  call feedkeys("G?the\<c-z>".repeat("\<c-n>", 2)."\<c-t>\<cr>", 'tx')
+  call assert_equal('thethere', getline('.'))
+  " second match
+  call feedkeys("G?the\<c-z>".repeat("\<c-n>", 2).repeat("\<c-t>", 2)."\<cr>", 'tx')
+  call assert_equal('thethe', getline('.'))
+
+  " wild menu is cleared properly
+  call feedkeys("/the\<c-z>\<esc>/\<f9>", 'tx')
+  call assert_equal({}, g:compl_info)
+  call feedkeys("/the\<c-z>\<c-e>\<f9>", 'tx')
+  call assert_equal([], g:compl_info.matches)
+
+  " Do not expand if offset is present (/pattern/offset and ?pattern?offset)
+  for pat in ["/", "/2", "/-3", "\\/"]
+    call feedkeys("/the" . pat . "\<c-z>\<f9>", 'tx')
+    call assert_equal({}, g:compl_info)
+  endfor
+  for pat in ["?", "?2", "?-3", "\\\\?"]
+    call feedkeys("?the" . pat . "\<c-z>\<f9>", 'tx')
+    call assert_equal({}, g:compl_info)
+  endfor
+
+  " Last letter of match is multibyte
+  call setline('$', ['theΩ'])
+  call feedkeys("gg/th\<c-z>\<f9>", 'tx')
+  call assert_equal(['these', 'thethe', 'the', 'thethere', 'there', 'theΩ'],
+        \ g:compl_info.matches)
+
+  " Identical words
+  call setline(1, ["foo", "foo", "foo", "foobar"])
+  call feedkeys("gg/f\<c-z>\<f9>", 'tx')
+  call assert_equal(['foo', 'foobar'], g:compl_info.matches)
+
+  " Exact match
+  call feedkeys("/foo\<c-z>\<f9>", 'tx')
+  call assert_equal(['foo', 'foobar'], g:compl_info.matches)
+
+  " 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')
+  call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
+  call feedkeys("gg/FO\<tab>\<f9>", 'tx')
+  call assert_equal({},  g:compl_info)
+  set ignorecase
+  call feedkeys("gg/f\<tab>\<f9>", 'tx')
+  call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches)
+  call feedkeys("gg/Fo\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'FooBAr', 'FooBARR'], g:compl_info.matches)
+  call feedkeys("gg/FO\<tab>\<f9>", 'tx')
+  call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches)
+  set smartcase
+  call feedkeys("gg/f\<tab>\<f9>", 'tx')
+  call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches)
+  call feedkeys("gg/Fo\<tab>\<f9>", 'tx')
+  call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches)
+  call feedkeys("gg/FO\<tab>\<f9>", 'tx')
+  call assert_equal({},  g:compl_info)
+
+  bw!
+  call test_override("char_avail", 0)
+  delfunc GetComplInfo
+  unlet! g:compl_info
+  set wildcharm=0 incsearch& ignorecase& smartcase&
+endfunc
+
+func Test_search_wildmenu_screendump()
+  CheckScreendump
+
+  let lines =<< trim [SCRIPT]
+    set wildmenu wildcharm=<f5>
+    call setline(1, ['the', 'these', 'the', 'foobar', 'thethe', 'thethere'])
+  [SCRIPT]
+  call writefile(lines, 'XTest_search_wildmenu', 'D')
+  let buf = RunVimInTerminal('-S XTest_search_wildmenu', {'rows': 10})
+
+  " Pattern has newline at EOF
+  call term_sendkeys(buf, "gg2j/e\\n\<f5>")
+  call VerifyScreenDump(buf, 'Test_search_wildmenu_1', {})
+
+  " longest:full
+  call term_sendkeys(buf, "\<esc>:set wim=longest,full\<cr>")
+  call term_sendkeys(buf, "gg/t\<f5>")
+  call VerifyScreenDump(buf, 'Test_search_wildmenu_2', {})
+
+  " list:full
+  call term_sendkeys(buf, "\<esc>:set wim=list,full\<cr>")
+  call term_sendkeys(buf, "gg/t\<f5>")
+  call VerifyScreenDump(buf, 'Test_search_wildmenu_3', {})
+
+  " noselect:full
+  call term_sendkeys(buf, "\<esc>:set wim=noselect,full\<cr>")
+  call term_sendkeys(buf, "gg/t\<f5>")
+  call VerifyScreenDump(buf, 'Test_search_wildmenu_4', {})
+
+  " Multiline
+  call term_sendkeys(buf, "\<esc>gg/t.*\\n.*\\n.\<tab>")
+  call VerifyScreenDump(buf, 'Test_search_wildmenu_5', {})
+
+  call term_sendkeys(buf, "\<esc>")
+  call StopVimInTerminal(buf)
+endfunc
+
+" Test wildcharm completion for :s and :g with range
+func Test_range_complete()
+  set wildcharm=<c-z>
+
+  " Disable char_avail so that expansion of commandline works
+  call test_override("char_avail", 1)
+
+  func GetComplInfo()
+    let g:compl_info = cmdcomplete_info()
+    return ''
+  endfunc
+  new
+  cnoremap <buffer><expr> <F9> GetComplInfo()
+
+  call setline(1, ['ab', 'ba', 'ca', 'af'])
+
+  for trig in ["\<tab>", "\<c-z>"]
+    call feedkeys($":%s/a{trig}\<f9>", 'xt')
+    call assert_equal(['ab', 'a', 'af'],  g:compl_info.matches)
+    call feedkeys($":vim9cmd :%s/a{trig}\<f9>", 'xt')
+    call assert_equal(['ab', 'a', 'af'],  g:compl_info.matches)
+  endfor
+
+  call feedkeys(":%s/\<c-z>\<f9>", 'xt')
+  call assert_equal({},  g:compl_info)
+
+  for cmd in ['s', 'g']
+    call feedkeys(":1,2" . cmd . "/a\<c-z>\<f9>", 'xt')
+    call assert_equal(['ab', 'a'],  g:compl_info.matches)
+  endfor
+
+  1
+  call feedkeys(":.,+2s/a\<c-z>\<f9>", 'xt')
+  call assert_equal(['ab', 'a'],  g:compl_info.matches)
+
+  /f
+  call feedkeys(":1,s/b\<c-z>\<f9>", 'xt')
+  call assert_equal(['b', 'ba'],  g:compl_info.matches)
+
+  /c
+  call feedkeys(":\\?,4s/a\<c-z>\<f9>", 'xt')
+  call assert_equal(['a', 'af'],  g:compl_info.matches)
+
+  %s/c/c/
+  call feedkeys(":1,\\&s/a\<c-z>\<f9>", 'xt')
+  call assert_equal(['ab', 'a'],  g:compl_info.matches)
+
+  3
+  normal! ma
+  call feedkeys(":'a,$s/a\<c-z>\<f9>", 'xt')
+  call assert_equal(['a', 'af'],  g:compl_info.matches)
+
+  " Line number followed by a search pattern ([start]/pattern/[command])
+  call feedkeys("3/a\<c-z>\<f9>", 'xt')
+  call assert_equal(['a', 'af', 'ab'],  g:compl_info.matches)
+
+  bw!
+  call test_override("char_avail", 0)
+  delfunc GetComplInfo
+  unlet! g:compl_info
+  set wildcharm=0
+endfunc
+
 " vim: shiftwidth=2 sts=2 expandtab
diff --git a/src/version.c b/src/version.c
index e912070..ff3c348 100644
--- a/src/version.c
+++ b/src/version.c
@@ -720,6 +720,8 @@
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    1490,
+/**/
     1489,
 /**/
     1488,
diff --git a/src/vim.h b/src/vim.h
index 7f79e68..b8569d8 100644
--- a/src/vim.h
+++ b/src/vim.h
@@ -859,6 +859,7 @@
 #define EXPAND_FINDFUNC		61
 #define EXPAND_HIGHLIGHT_GROUP  62
 #define EXPAND_FILETYPECMD	63
+#define EXPAND_PATTERN_IN_BUF	64
 
 
 // Values for exmode_active (0 is no exmode)
@@ -894,6 +895,7 @@
 #define WILD_BUFLASTUSED	    0x1000
 #define BUF_DIFF_FILTER		    0x2000
 #define WILD_KEEP_SOLE_ITEM	    0x4000
+#define WILD_MAY_EXPAND_PATTERN	    0x8000
 
 // Flags for expand_wildcards()
 #define EW_DIR		0x01	// include directory names