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>
This commit is contained in:
Girish Palya
2025-06-28 19:47:34 +02:00
committed by Christian Brabandt
parent 1fa3f0c215
commit 6b49fba8c8
16 changed files with 754 additions and 90 deletions

View File

@ -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 @@ CTRL-D List names that match the pattern in front of the cursor.
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 @@ CTRL-D List names that match the pattern in front of the cursor.
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 @@ CTRL-G When 'incsearch' is set, entering a search pattern for "/" or
"?" 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

View File

@ -9752,7 +9752,10 @@ A jump table for the options with a short description can be found at |Q_op|.
: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'*

View File

@ -1815,6 +1815,7 @@ $quote eval.txt /*$quote*
/\{- 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*

View File

@ -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 @@ Completion: ~
- 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

View File

@ -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 expand_shellcmd(char_u *filepat, char_u ***matches, int *numMatches,
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 @@ nextwild(
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 @@ nextwild(
}
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 @@ nextwild(
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 @@ cmdline_compl_is_fuzzy(void)
/*
* 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 @@ addstar(
* 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 @@ set_context_in_filetype_cmd(expand_T *xp, char_u *arg)
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 @@ set_context_by_cmdname(
int compl,
int forceit)
{
char_u *nextcmd;
switch (cmdidx)
{
case CMD_find:
@ -2307,10 +2361,18 @@ set_context_by_cmdname(
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 @@ ExpandFromContext(
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 @@ wildmenu_cleanup(cmdline_info_T *cclp UNUSED)
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 @@ f_cmdcomplete_info(typval_T *argvars UNUSED, typval_T *rettv)
}
}
#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;
}

View File

@ -204,67 +204,55 @@ set_search_match(pos_T *t)
}
/*
* 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 @@ do_incsearch_highlighting(
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 @@ cmdline_wildchar_complete(
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 @@ cmdline_wildchar_complete(
}
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 @@ getcmdline_int(
{
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 @@ getcmdline_int(
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 @@ getcmdline_int(
// 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 @@ cmdline_changed:
// 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 @@ cmdline_changed:
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

View File

@ -45,4 +45,5 @@ char *did_set_cedit(optset_T *args);
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 : */

View File

@ -655,6 +655,8 @@ typedef struct expand
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;
/*

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1651,8 +1651,10 @@ func Test_cmdline_complete_various()
" 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 @@ endfunc
" 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 @@ func Test_redrawtabpanel_error()
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

View File

@ -719,6 +719,8 @@ static char *(features[]) =
static int included_patches[] =
{ /* Add new patch number below this line */
/**/
1490,
/**/
1489,
/**/

View File

@ -859,6 +859,7 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring);
#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 @@ extern int (*dyn_libintl_wputenv)(const wchar_t *envstring);
#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