diff --git a/runtime/doc/cmdline.txt b/runtime/doc/cmdline.txt index 9ab803c364..d0c7aa475f 100644 --- a/runtime/doc/cmdline.txt +++ b/runtime/doc/cmdline.txt @@ -388,7 +388,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_* + *c_CTRL-I* *c_wildchar* *c_* */_* '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 @@ -398,6 +398,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 or "\t" to search for a + literal instead of triggering completion. + The behavior can be changed with the 'wildmode' option. *c_* Like 'wildchar' or , but begin with the last match and @@ -430,7 +434,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 diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index d606e616e9..df62f5a5d3 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -161,6 +161,8 @@ EDITOR • 'shada' now correctly respects "/0" and "f0". • |prompt-buffer| supports multiline input/paste, undo/redo, and o/O normal commands. +• 'wildchar' now enables completion in search contexts using |/|, |?|, |:g|, |:v| + and |:vimgrep| commands. EVENTS diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index f72b558dab..74253532e3 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -7166,7 +7166,9 @@ A jump table for the options with a short description can be found at |Q_op|. :set wc=X :set wc=^I set wc= -< +< 'wildchar' also enables completion in search pattern contexts such as + |/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal + instead of triggering completion, type or "\t". *'wildcharm'* *'wcm'* 'wildcharm' 'wcm' number (default 0) diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index 3efbd04741..922f687db1 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -7810,7 +7810,9 @@ vim.go.ww = vim.go.whichwrap --- :set wc=^I --- set wc= --- ``` ---- +--- 'wildchar' also enables completion in search pattern contexts such as +--- `/`, `?`, `:s`, `:g`, `:v`, and `:vim`. To insert a literal +--- instead of triggering completion, type or "\t". --- --- @type integer vim.o.wildchar = 9 diff --git a/src/nvim/cmdexpand.c b/src/nvim/cmdexpand.c index 4a1e5a79bd..64ac0f4b64 100644 --- a/src/nvim/cmdexpand.c +++ b/src/nvim/cmdexpand.c @@ -41,12 +41,14 @@ #include "nvim/highlight.h" #include "nvim/highlight_defs.h" #include "nvim/highlight_group.h" +#include "nvim/insexpand.h" #include "nvim/keycodes.h" #include "nvim/lua/executor.h" #include "nvim/macros_defs.h" #include "nvim/mapping.h" #include "nvim/mbyte.h" #include "nvim/mbyte_defs.h" +#include "nvim/memline.h" #include "nvim/memory.h" #include "nvim/menu.h" #include "nvim/message.h" @@ -83,6 +85,8 @@ typedef void *(*user_expand_func_T)(const char *, int, typval_T *); #endif static bool cmd_showtail; ///< Only show path tail in lists ? +static bool may_expand_pattern = false; +static pos_T pre_incsearch_pos; ///< Cursor position when incsearch started /// "compl_match_array" points the currently displayed list of entries in the /// popup menu. It is NULL when there is no popup menu. @@ -245,6 +249,8 @@ int nextwild(expand_T *xp, int type, int options, bool escape) char *p; if (xp->xp_numfiles == -1) { + may_expand_pattern = options & WILD_MAY_EXPAND_PATTERN; + pre_incsearch_pos = xp->xp_pre_incsearch_pos; if (ccline->input_fn && ccline->xp_context == EXPAND_COMMANDS) { // Expand commands typed in input() function set_cmd_context(xp, ccline->cmdbuff, ccline->cmdlen, ccline->cmdpos, false); @@ -284,8 +290,9 @@ int nextwild(expand_T *xp, int type, int options, bool escape) p = ExpandOne(xp, NULL, NULL, 0, type); } else { char *tmp; - 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 tmp = xstrnsave(xp->xp_pattern, xp->xp_pattern_len); } else { tmp = addstar(xp->xp_pattern, xp->xp_pattern_len, xp->xp_context); @@ -430,10 +437,12 @@ bool cmdline_compl_is_fuzzy(void) } /// Return the number of characters that should be skipped in the wildmenu -/// 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_wildmenu_char(expand_T *xp, char *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) || ((xp->xp_context == EXPAND_MENUS || xp->xp_context == EXPAND_MENUNAMES) && (s[0] == '\t' || (s[0] == '\\' && s[1] != NUL)))) { #ifndef BACKSLASH_IN_FILENAME @@ -1398,17 +1407,31 @@ char *addstar(char *fname, size_t len, int context) /// 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) { CmdlineInfo *const 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 = (size_t)ccline->cmdpos; + search_first_line = 0; // Search entire buffer + return; + } + + // Only handle ':', '>', or '=' command-lines, or expression input if (ccline->cmdfirstc != ':' && ccline->cmdfirstc != '>' && ccline->cmdfirstc != '=' && !ccline->input_fn) { xp->xp_context = EXPAND_NOTHING; return; } + + // Fallback to command-line expansion set_cmd_context(xp, ccline->cmdbuff, ccline->cmdlen, ccline->cmdpos, true); } @@ -1891,6 +1914,29 @@ static const char *set_context_in_filetype_cmd(expand_T *xp, const char *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) +{ + CmdlineInfo *ccline = get_cmdline_info(); + + emsg_off++; + int skiplen = 0; + int dummy, patlen; + int 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; + } + + xp->xp_pattern = ccline->cmdbuff + skiplen; + xp->xp_pattern_len = (size_t)(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". /// The argument to the command is "arg" and the argument flags is "argt". /// For user-defined commands and for environment variables, "context" has the @@ -1900,6 +1946,8 @@ static const char *set_context_in_filetype_cmd(expand_T *xp, const char *arg) static const char *set_context_by_cmdname(const char *cmd, cmdidx_T cmdidx, expand_T *xp, const char *arg, uint32_t argt, int context, bool forceit) { + const char *nextcmd; + switch (cmdidx) { case CMD_find: case CMD_sfind: @@ -1978,10 +2026,20 @@ static const char *set_context_by_cmdname(const char *cmd, cmdidx_T cmdidx, expa 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: @@ -2913,6 +2971,9 @@ static int ExpandFromContext(expand_T *xp, char *pat, char ***matches, int *numM 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 nr_ // prefix. @@ -3630,6 +3691,9 @@ void wildmenu_cleanup(CmdlineInfo *cclp) RedrawingDisabled = 0; } + // Clear highlighting applied during wildmenu activity + set_no_hlsearch(true); + if (wild_menu_showing == WM_SCROLLED) { // Entered command line, move it up cmdline_row--; @@ -3800,3 +3864,219 @@ void f_cmdcomplete_info(typval_T *argvars, typval_T *rettv, EvalFuncData fptr) } } } + +/// 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 **match, pos_T *match_end) +{ + if (start->lnum > end->lnum + || (start->lnum == end->lnum && start->col >= end->col)) { + return FAIL; // invalid range + } + // Get line pointers + char *start_line = ml_get(start->lnum); + char *end_line = ml_get(end->lnum); + + // Use a growable string (ga) + garray_T ga; + ga_init(&ga, 1, 128); + + // Append start line from start->col to end + char *start_ptr = start_line + start->col; + bool is_single_line = start->lnum == end->lnum; + + int segment_len = is_single_line ? (int)(end->col - start->col) + : (int)strlen(start_ptr); + ga_grow(&ga, segment_len + 1); + ga_concat_len(&ga, start_ptr, (size_t)segment_len); + if (!is_single_line) { + ga_append(&ga, '\n'); + } + + // Append full lines between start and end + if (!is_single_line) { + for (linenr_T lnum = start->lnum + 1; lnum < end->lnum; lnum++) { + char *line = ml_get(lnum); + ga_grow(&ga, ml_get_len(lnum) + 1); + ga_concat(&ga, line); + ga_append(&ga, '\n'); + } + } + + // Append partial end line (up to word end) + char *word_end = find_word_end(end_line + end->col); + segment_len = (int)(word_end - end_line); + ga_grow(&ga, segment_len); + ga_concat_len(&ga, end_line + (is_single_line ? end->col : 0), + (size_t)(segment_len - (is_single_line ? end->col : 0))); + + // Null-terminate + ga_grow(&ga, 1); + ga_append(&ga, NUL); + + *match = (char *)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. +/// +/// @param pat pattern to match +/// @param dir FORWARD or BACKWARD +/// @param[out] matches array with matched string +/// @param[out] numMatches number of matches +static int expand_pattern_in_buf(char *pat, Direction dir, char ***matches, int *numMatches) +{ + bool has_range = search_first_line != 0; + + *matches = NULL; + *numMatches = 0; + + if (pat == NULL || *pat == NUL) { + return FAIL; + } + + int pat_len = (int)strlen(pat); + pos_T cur_match_pos = { 0 }, prev_match_pos = { 0 }; + if (has_range) { + cur_match_pos.lnum = search_first_line; + } else { + cur_match_pos = pre_incsearch_pos; + } + + int search_flags = SEARCH_OPT | SEARCH_NOOF | SEARCH_PEEK | SEARCH_NFMSG + | (has_range ? SEARCH_START : 0); + + regmatch_T regmatch; + regmatch.regprog = vim_regcomp(pat, RE_MAGIC + RE_STRING); + if (regmatch.regprog == NULL) { + return FAIL; + } + regmatch.rm_ic = p_ic; + + garray_T ga; + ga_init(&ga, sizeof(char *), 10); // Use growable array of char * + + pos_T end_match_pos, word_end_pos; + bool looped_around = false; + bool compl_started = false; + char *match; + + while (true) { + emsg_off++; + msg_silent++; + int found_new_match = searchit(NULL, curbuf, &cur_match_pos, + &end_match_pos, dir, pat, (size_t)pat_len, 1L, + search_flags, RE_LAST, NULL); + msg_silent--; + emsg_off--; + + if (found_new_match == FAIL) { + break; + } + + // 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; + } + + 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 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(®match, match, (colnr_T)0)) { + xfree(match); + continue; + } + xfree(match); + + // Construct a new match from completed word appended to pattern itself + char *line = ml_get(end_match_pos.lnum); + char *word_end = find_word_end(line + end_match_pos.col); // col starts from 0 + int match_len = (int)(word_end - (line + end_match_pos.col)); + match = xmalloc((size_t)match_len + (size_t)pat_len + 1); // +1 for NUL + memmove(match, pat, (size_t)pat_len); + if (match_len > 0) { + memmove(match + (size_t)pat_len, line + end_match_pos.col, (size_t)match_len); + } + match[pat_len + match_len] = NUL; + + // Include this match if it is not a duplicate + for (int i = 0; i < ga.ga_len; i++) { + if (strcmp(match, ((char **)ga.ga_data)[i]) == 0) { + XFREE_CLEAR(match); + break; + } + } + if (match != NULL) { + ga_grow(&ga, 1); + ((char **)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 **)ga.ga_data; + *numMatches = ga.ga_len; + return OK; + +cleanup: + vim_regfree(regmatch.regprog); + ga_clear_strings(&ga); + return FAIL; +} diff --git a/src/nvim/cmdexpand.h b/src/nvim/cmdexpand.h index 59c734e9a3..9f6825f4d6 100644 --- a/src/nvim/cmdexpand.h +++ b/src/nvim/cmdexpand.h @@ -41,6 +41,7 @@ enum { WILD_BUFLASTUSED = 0x1000, BUF_DIFF_FILTER = 0x2000, WILD_KEEP_SOLE_ITEM = 0x4000, + WILD_MAY_EXPAND_PATTERN = 0x8000, }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/cmdexpand_defs.h b/src/nvim/cmdexpand_defs.h index 29ad9ff251..e6a7429ff1 100644 --- a/src/nvim/cmdexpand_defs.h +++ b/src/nvim/cmdexpand_defs.h @@ -4,6 +4,8 @@ #include #include "nvim/eval/typval_defs.h" +#include "nvim/pos_defs.h" +#include "nvim/vim_defs.h" typedef enum { XP_PREFIX_NONE, ///< prefix not used @@ -35,6 +37,8 @@ typedef struct { char **xp_files; ///< list of files char *xp_line; ///< text being completed char xp_buf[EXPAND_BUF_LEN]; ///< buffer for returned match + Direction xp_search_dir; ///< Direction of search + pos_T xp_pre_incsearch_pos; ///< Cursor position before incsearch } expand_T; /// values for xp_backslash @@ -109,6 +113,7 @@ enum { EXPAND_SHELLCMDLINE, EXPAND_FINDFUNC, EXPAND_FILETYPECMD, + EXPAND_PATTERN_IN_BUF, EXPAND_CHECKHEALTH, EXPAND_LUA, }; diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index bec58f12b6..c67253e142 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -282,38 +282,25 @@ static void 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. -static bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_state_T *s, - int *skiplen, int *patlen) +/// 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. +bool parse_pattern_and_range(pos_T *incsearch_start, int *search_delim, int *skiplen, int *patlen) FUNC_ATTR_NONNULL_ALL { char *p; bool delim_optional = false; const char *dummy; - bool retval = false; magic_T magic = 0; *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++; exarg_T ea = { .line1 = 1, .line2 = 1, @@ -322,17 +309,19 @@ static bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_s }; cmdmod_T dummy_cmdmod; + // Skip over command modifiers parse_command_modifiers(&ea, &dummy, &dummy_cmdmod, true); + // Skip over the range to find the command. char *cmd = skip_range(ea.cmd, NULL); if (vim_strchr("sgvl", (uint8_t)(*cmd)) == NULL) { - goto theend; + return false; } - // Skip over "substitute" to find the pattern separator. + // 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", (size_t)(p - cmd)) == 0 @@ -354,70 +343,105 @@ static bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_s p++; } if (*p == NUL) { - goto theend; + return false; } } else if (strncmp(cmd, "vimgrep", (size_t)MAX(p - cmd, 3)) == 0 || strncmp(cmd, "vimgrepadd", (size_t)MAX(p - cmd, 8)) == 0 || strncmp(cmd, "lvimgrep", (size_t)MAX(p - cmd, 2)) == 0 || strncmp(cmd, "lvimgrepadd", (size_t)MAX(p - cmd, 9)) == 0 || strncmp(cmd, "global", (size_t)(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); int delim = (delim_optional && vim_isIDc((uint8_t)(*p))) ? ' ' : *p++; *search_delim = delim; - char *end = skip_regexp_ex(p, delim, magic_isset(), NULL, NULL, &magic); + char *end = skip_regexp_ex(p, delim, magic_isset(), NULL, NULL, &magic); bool 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; *end = NUL; bool 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 pos_T save_cursor = curwin->w_cursor; - curwin->w_cursor = s->search_start; + curwin->w_cursor = *incsearch_start; + parse_cmd_address(&ea, &dummy, true); + if (ea.addr_count > 0) { // Allow for reverse match. search_first_line = MIN(ea.line2, ea.line1); search_last_line = MAX(ea.line2, ea.line1); } 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 bool do_incsearch_highlighting(int firstc, int *search_delim, incsearch_state_T *is_state, + int *skiplen, int *patlen) +{ + bool 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; } @@ -1114,6 +1138,10 @@ static int command_line_wildchar_complete(CommandLineState *s) res = OK; // don't insert 'wildchar' now } } else { // typed p_wc first time + if (s->c == p_wc || s->c == p_wcm) { + options |= WILD_MAY_EXPAND_PATTERN; + s->xpc.xp_pre_incsearch_pos = s->is_state.search_start; + } s->wim_index = 0; int j = ccline.cmdpos; @@ -1320,6 +1348,9 @@ static int command_line_execute(VimState *state, int key) || s->c == Ctrl_C) { trigger_cmd_autocmd(s->cmdline_type, EVENT_CMDLINELEAVEPRE); s->event_cmdlineleavepre_triggered = true; + if ((s->c == ESC || s->c == Ctrl_C) && (wim_flags[0] & kOptWimFlagList)) { + set_no_hlsearch(true); + } } // The wildmenu is cleared if the pressed key is not used for @@ -1435,6 +1466,15 @@ static int command_line_execute(VimState *state, int key) // If already used to cancel/accept wildmenu, don't process the key further. if (wild_type == WILD_CANCEL || wild_type == WILD_APPLY) { return command_line_not_changed(s); + // Apply search highlighting + if (wild_type == WILD_APPLY) { + if (s->is_state.winid != curwin->handle) { + init_incsearch_state(&s->is_state); + } + if (KeyTyped || vpeekc() == NUL) { + may_do_incsearch_highlighting(s->firstc, s->count, &s->is_state); + } + } } return command_line_handle_key(s); diff --git a/src/nvim/options.lua b/src/nvim/options.lua index 2562c56972..5d7341fa8c 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -10101,7 +10101,9 @@ local options = { :set wc=X :set wc=^I set wc= - < + < 'wildchar' also enables completion in search pattern contexts such as + |/|, |?|, |:s|, |:g|, |:v|, and |:vim|. To insert a literal + instead of triggering completion, type or "\t". ]=], full_name = 'wildchar', scope = { 'global' }, diff --git a/test/functional/legacy/cmdline_spec.lua b/test/functional/legacy/cmdline_spec.lua index 94ef85591d..18b0d58136 100644 --- a/test/functional/legacy/cmdline_spec.lua +++ b/test/functional/legacy/cmdline_spec.lua @@ -357,6 +357,96 @@ describe('cmdline', function() 10,20 30% | ]]) end) + + -- oldtest: Test_search_wildmenu_screendump() + it('wildmenu for search completion', function() + local screen = Screen.new(60, 10) + screen:add_extra_attr_ids({ + [100] = { background = Screen.colors.Yellow, foreground = Screen.colors.Black }, + }) + exec([[ + set wildmenu wildcharm= wildoptions-=pum + call setline(1, ['the', 'these', 'the', 'foobar', 'thethe', 'thethere']) + ]]) + + -- Pattern has newline at EOF + feed('gg2j/e\\n') + screen:expect([[ + the | + these | + the | + foobar | + thethe | + thethere | + {1:~ }|*2 + {100:e\nfoobar}{3: e\nthethere e\nthese e\nthe }| + /e\nfoobar^ | + ]]) + + -- longest:full + feed('') + command('set wim=longest,full') + feed('gg/t') + screen:expect([[ + the | + these | + the | + foobar | + thethe | + thethere | + {1:~ }|*3 + /the^ | + ]]) + + -- list:full + feed('') + command('set wim=list,full') + feed('gg/t') + screen:expect([[ + {10:t}he | + {10:t}hese | + {10:t}he | + foobar | + {10:t}he{10:t}he | + {10:t}he{10:t}here | + {3: }| + /t | + these the thethe thethere there | + /t^ | + ]]) + + -- noselect:full + feed('') + command('set wim=noselect,full') + feed('gg/t') + screen:expect([[ + the | + these | + the | + foobar | + thethe | + thethere | + {1:~ }|*2 + {3:these the thethe thethere there }| + /t^ | + ]]) + + -- Multiline + feed('gg/t.*\\n.*\\n.') + screen:expect([[ + the | + these | + the | + foobar | + thethe | + thethere | + {1:~ }|*2 + {3:t.*\n.*\n.oobar t.*\n.*\n.hethe t.*\n.*\n.he }| + /t.*\n.*\n.^ | + ]]) + + feed('') + end) end) describe('cmdwin', function() diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 08139071de..567f1313cd 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -1435,8 +1435,10 @@ func Test_cmdline_complete_various() " completion after a :global command call feedkeys(":g/a/chist\t\\"\", 'xt') call assert_equal('"g/a/chistory', @:) + set wildchar=0 call feedkeys(":g/a\\/chist\t\\"\", 'xt') call assert_equal("\"g/a\\/chist\t", @:) + set wildchar& " use as the 'wildchar' for completion set wildchar= @@ -3119,12 +3121,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\\"\", 'xt') call assert_equal("\"s | \t", @:) call feedkeys(":s/ | \t\\"\", 'xt') call assert_equal("\"s/ | \t", @:) call feedkeys(":s/one | \t\\"\", 'xt') call assert_equal("\"s/one | \t", @:) + set wildchar& call feedkeys(":s/one/ | \t\\"\", 'xt') call assert_equal("\"s/one/ | \t", @:) call feedkeys(":s/one/two | \t\\"\", 'xt') @@ -4360,4 +4364,240 @@ func Test_cmdcomplete_info() set wildoptions& endfunc +" Test wildcharm completion for '/' and '?' search +func Test_search_complete() + CheckOption incsearch + set wildcharm= + + " Disable char_avail so that expansion of commandline works + call Ntest_override("char_avail", 1) + + func GetComplInfo() + let g:compl_info = cmdcomplete_info() + return '' + endfunc + + new + cnoremap GetComplInfo() + + " Pressing 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 ["\", "\"] + " Test menu first item and order + call feedkeys($"gg2j/t{trig}\", 'tx') + call assert_equal(['the', 'thethere', 'there', 'these', 'thethe'], g:compl_info.matches) + call feedkeys($"gg2j?t{trig}\", 'tx') + call assert_equal(['these', 'the', 'there', 'thethere', 'thethe'], g:compl_info.matches) + + " and cycle through menu items + call feedkeys($"gg/the{trig}\", 'tx') + call assert_equal('these', getline('.')) + call feedkeys($"gg/the{trig}\\", 'tx') + call assert_equal('thethe', getline('.')) + call feedkeys($"gg/the{trig}".repeat("\", 5)."\", 'tx') + call assert_equal('these', getline('.')) + call feedkeys($"G?the{trig}\", 'tx') + call assert_equal('thethere', getline('.')) + call feedkeys($"G?the{trig}".repeat("\", 5)."\", 'tx') + call assert_equal('thethere', getline('.')) + + " Beginning of word pattern (<) retains '<' + call feedkeys($"gg2j/\\", 'tx') + call assert_equal(['\", 'tx') + call assert_equal(['\", 'tx') + call assert_equal(['\v", 'tx') + call assert_equal(['\v\\\", 'tx') + call assert_equal('thethe', getline('.')) + " second match + call feedkeys("gg/the\\\\\", '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\".repeat("\", 2)."\\", 'tx') + call assert_equal('thethere', getline('.')) + " second match + call feedkeys("G?the\".repeat("\", 2).repeat("\", 2)."\", 'tx') + call assert_equal('thethe', getline('.')) + + " wild menu is cleared properly + call feedkeys("/the\\/\", 'tx') + call assert_equal({}, g:compl_info) + call feedkeys("/the\\\", '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 . "\\", 'tx') + call assert_equal({}, g:compl_info) + endfor + for pat in ["?", "?2", "?-3", "\\\\?"] + call feedkeys("?the" . pat . "\\", 'tx') + call assert_equal({}, g:compl_info) + endfor + + " Last letter of match is multibyte + call setline('$', ['theΩ']) + call feedkeys("gg/th\\", '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\\", 'tx') + call assert_equal(['foo', 'foobar'], g:compl_info.matches) + + " Exact match + call feedkeys("/foo\\", '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\\", 'tx') + call assert_equal(['fooBAr', 'foobar'], g:compl_info.matches) + call feedkeys("gg/Fo\\", 'tx') + call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) + call feedkeys("gg/FO\\", 'tx') + call assert_equal({}, g:compl_info) + set ignorecase + call feedkeys("gg/f\\", 'tx') + call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches) + call feedkeys("gg/Fo\\", 'tx') + call assert_equal(['Foobar', 'FooBAr', 'FooBARR'], g:compl_info.matches) + call feedkeys("gg/FO\\", 'tx') + call assert_equal(['FOobar', 'FOoBAr', 'FOoBARR'], g:compl_info.matches) + set smartcase + call feedkeys("gg/f\\", 'tx') + call assert_equal(['foobar', 'fooBAr', 'fooBARR'], g:compl_info.matches) + call feedkeys("gg/Fo\\", 'tx') + call assert_equal(['Foobar', 'FooBARR'], g:compl_info.matches) + call feedkeys("gg/FO\\", 'tx') + call assert_equal({}, g:compl_info) + + bw! + call Ntest_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= + 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\") + call VerifyScreenDump(buf, 'Test_search_wildmenu_1', {}) + + " longest:full + call term_sendkeys(buf, "\:set wim=longest,full\") + call term_sendkeys(buf, "gg/t\") + call VerifyScreenDump(buf, 'Test_search_wildmenu_2', {}) + + " list:full + call term_sendkeys(buf, "\:set wim=list,full\") + call term_sendkeys(buf, "gg/t\") + call VerifyScreenDump(buf, 'Test_search_wildmenu_3', {}) + + " noselect:full + call term_sendkeys(buf, "\:set wim=noselect,full\") + call term_sendkeys(buf, "gg/t\") + call VerifyScreenDump(buf, 'Test_search_wildmenu_4', {}) + + " Multiline + call term_sendkeys(buf, "\gg/t.*\\n.*\\n.\") + call VerifyScreenDump(buf, 'Test_search_wildmenu_5', {}) + + call term_sendkeys(buf, "\") + call StopVimInTerminal(buf) +endfunc + +" Test wildcharm completion for :s and :g with range +func Test_range_complete() + set wildcharm= + + " Disable char_avail so that expansion of commandline works + call Ntest_override("char_avail", 1) + + func GetComplInfo() + let g:compl_info = cmdcomplete_info() + return '' + endfunc + new + cnoremap GetComplInfo() + + call setline(1, ['ab', 'ba', 'ca', 'af']) + + for trig in ["\", "\"] + call feedkeys($":%s/a{trig}\", 'xt') + call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) + " call feedkeys($":vim9cmd :%s/a{trig}\", 'xt') + call feedkeys($":verbose :%s/a{trig}\", 'xt') + call assert_equal(['ab', 'a', 'af'], g:compl_info.matches) + endfor + + call feedkeys(":%s/\\", 'xt') + call assert_equal({}, g:compl_info) + + for cmd in ['s', 'g'] + call feedkeys(":1,2" . cmd . "/a\\", 'xt') + call assert_equal(['ab', 'a'], g:compl_info.matches) + endfor + + 1 + call feedkeys(":.,+2s/a\\", 'xt') + call assert_equal(['ab', 'a'], g:compl_info.matches) + + /f + call feedkeys(":1,s/b\\", 'xt') + call assert_equal(['b', 'ba'], g:compl_info.matches) + + /c + call feedkeys(":\\?,4s/a\\", 'xt') + call assert_equal(['a', 'af'], g:compl_info.matches) + + %s/c/c/ + call feedkeys(":1,\\&s/a\\", 'xt') + call assert_equal(['ab', 'a'], g:compl_info.matches) + + 3 + normal! ma + call feedkeys(":'a,$s/a\\", 'xt') + call assert_equal(['a', 'af'], g:compl_info.matches) + + " Line number followed by a search pattern ([start]/pattern/[command]) + call feedkeys("3/a\\", 'xt') + call assert_equal(['a', 'af', 'ab'], g:compl_info.matches) + + bw! + call Ntest_override("char_avail", 0) + delfunc GetComplInfo + unlet! g:compl_info + set wildcharm=0 +endfunc + " vim: shiftwidth=2 sts=2 expandtab