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