summaryrefslogtreecommitdiffstats
path: root/src/search.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 08:50:31 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 08:50:31 +0000
commitaed8ce9da277f5ecffe968b324f242c41c3b752a (patch)
treed2e538394cb7a8a7c42a4aac6ccf1a8e3256999b /src/search.c
parentInitial commit. (diff)
downloadvim-aed8ce9da277f5ecffe968b324f242c41c3b752a.tar.xz
vim-aed8ce9da277f5ecffe968b324f242c41c3b752a.zip
Adding upstream version 2:9.0.1378.upstream/2%9.0.1378upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/search.c')
-rw-r--r--src/search.c5063
1 files changed, 5063 insertions, 0 deletions
diff --git a/src/search.c b/src/search.c
new file mode 100644
index 0000000..1e4464b
--- /dev/null
+++ b/src/search.c
@@ -0,0 +1,5063 @@
+/* vi:set ts=8 sts=4 sw=4 noet:
+ *
+ * VIM - Vi IMproved by Bram Moolenaar
+ *
+ * Do ":help uganda" in Vim to read copying and usage conditions.
+ * Do ":help credits" in Vim to see a list of people who contributed.
+ * See README.txt for an overview of the Vim source code.
+ */
+/*
+ * search.c: code for normal mode searching commands
+ */
+
+#include "vim.h"
+
+#ifdef FEAT_EVAL
+static void set_vv_searchforward(void);
+static int first_submatch(regmmatch_T *rp);
+#endif
+#ifdef FEAT_FIND_ID
+static void show_pat_in_path(char_u *, int,
+ int, int, FILE *, linenr_T *, long);
+#endif
+
+typedef struct searchstat
+{
+ int cur; // current position of found words
+ int cnt; // total count of found words
+ int exact_match; // TRUE if matched exactly on specified position
+ int incomplete; // 0: search was fully completed
+ // 1: recomputing was timed out
+ // 2: max count exceeded
+ int last_maxcount; // the max count of the last search
+} searchstat_T;
+
+static void cmdline_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos, int show_top_bot_msg, char_u *msgbuf, int recompute, int maxcount, long timeout);
+static void update_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos, searchstat_T *stat, int recompute, int maxcount, long timeout);
+
+#define SEARCH_STAT_DEF_TIMEOUT 40L
+#define SEARCH_STAT_DEF_MAX_COUNT 99
+#define SEARCH_STAT_BUF_LEN 12
+
+/*
+ * This file contains various searching-related routines. These fall into
+ * three groups:
+ * 1. string searches (for /, ?, n, and N)
+ * 2. character searches within a single line (for f, F, t, T, etc)
+ * 3. "other" kinds of searches like the '%' command, and 'word' searches.
+ */
+
+/*
+ * String searches
+ *
+ * The string search functions are divided into two levels:
+ * lowest: searchit(); uses an pos_T for starting position and found match.
+ * Highest: do_search(); uses curwin->w_cursor; calls searchit().
+ *
+ * The last search pattern is remembered for repeating the same search.
+ * This pattern is shared between the :g, :s, ? and / commands.
+ * This is in search_regcomp().
+ *
+ * The actual string matching is done using a heavily modified version of
+ * Henry Spencer's regular expression library. See regexp.c.
+ */
+
+/*
+ * Two search patterns are remembered: One for the :substitute command and
+ * one for other searches. last_idx points to the one that was used the last
+ * time.
+ */
+static spat_T spats[2] =
+{
+ {NULL, TRUE, FALSE, {'/', 0, 0, 0L}}, // last used search pat
+ {NULL, TRUE, FALSE, {'/', 0, 0, 0L}} // last used substitute pat
+};
+
+static int last_idx = 0; // index in spats[] for RE_LAST
+
+static char_u lastc[2] = {NUL, NUL}; // last character searched for
+static int lastcdir = FORWARD; // last direction of character search
+static int last_t_cmd = TRUE; // last search t_cmd
+static char_u lastc_bytes[MB_MAXBYTES + 1];
+static int lastc_bytelen = 1; // >1 for multi-byte char
+
+// copy of spats[], for keeping the search patterns while executing autocmds
+static spat_T saved_spats[2];
+static char_u *saved_mr_pattern = NULL;
+# ifdef FEAT_SEARCH_EXTRA
+static int saved_spats_last_idx = 0;
+static int saved_spats_no_hlsearch = 0;
+# endif
+
+// allocated copy of pattern used by search_regcomp()
+static char_u *mr_pattern = NULL;
+
+#ifdef FEAT_FIND_ID
+/*
+ * Type used by find_pattern_in_path() to remember which included files have
+ * been searched already.
+ */
+typedef struct SearchedFile
+{
+ FILE *fp; // File pointer
+ char_u *name; // Full name of file
+ linenr_T lnum; // Line we were up to in file
+ int matched; // Found a match in this file
+} SearchedFile;
+#endif
+
+/*
+ * translate search pattern for vim_regcomp()
+ *
+ * pat_save == RE_SEARCH: save pat in spats[RE_SEARCH].pat (normal search cmd)
+ * pat_save == RE_SUBST: save pat in spats[RE_SUBST].pat (:substitute command)
+ * pat_save == RE_BOTH: save pat in both patterns (:global command)
+ * pat_use == RE_SEARCH: use previous search pattern if "pat" is NULL
+ * pat_use == RE_SUBST: use previous substitute pattern if "pat" is NULL
+ * pat_use == RE_LAST: use last used pattern if "pat" is NULL
+ * options & SEARCH_HIS: put search string in history
+ * options & SEARCH_KEEP: keep previous search pattern
+ *
+ * returns FAIL if failed, OK otherwise.
+ */
+ int
+search_regcomp(
+ char_u *pat,
+ char_u **used_pat,
+ int pat_save,
+ int pat_use,
+ int options,
+ regmmatch_T *regmatch) // return: pattern and ignore-case flag
+{
+ int magic;
+ int i;
+
+ rc_did_emsg = FALSE;
+ magic = magic_isset();
+
+ /*
+ * If no pattern given, use a previously defined pattern.
+ */
+ if (pat == NULL || *pat == NUL)
+ {
+ if (pat_use == RE_LAST)
+ i = last_idx;
+ else
+ i = pat_use;
+ if (spats[i].pat == NULL) // pattern was never defined
+ {
+ if (pat_use == RE_SUBST)
+ emsg(_(e_no_previous_substitute_regular_expression));
+ else
+ emsg(_(e_no_previous_regular_expression));
+ rc_did_emsg = TRUE;
+ return FAIL;
+ }
+ pat = spats[i].pat;
+ magic = spats[i].magic;
+ no_smartcase = spats[i].no_scs;
+ }
+ else if (options & SEARCH_HIS) // put new pattern in history
+ add_to_history(HIST_SEARCH, pat, TRUE, NUL);
+
+ if (used_pat)
+ *used_pat = pat;
+
+ vim_free(mr_pattern);
+#ifdef FEAT_RIGHTLEFT
+ if (curwin->w_p_rl && *curwin->w_p_rlc == 's')
+ mr_pattern = reverse_text(pat);
+ else
+#endif
+ mr_pattern = vim_strsave(pat);
+
+ /*
+ * Save the currently used pattern in the appropriate place,
+ * unless the pattern should not be remembered.
+ */
+ if (!(options & SEARCH_KEEP)
+ && (cmdmod.cmod_flags & CMOD_KEEPPATTERNS) == 0)
+ {
+ // search or global command
+ if (pat_save == RE_SEARCH || pat_save == RE_BOTH)
+ save_re_pat(RE_SEARCH, pat, magic);
+ // substitute or global command
+ if (pat_save == RE_SUBST || pat_save == RE_BOTH)
+ save_re_pat(RE_SUBST, pat, magic);
+ }
+
+ regmatch->rmm_ic = ignorecase(pat);
+ regmatch->rmm_maxcol = 0;
+ regmatch->regprog = vim_regcomp(pat, magic ? RE_MAGIC : 0);
+ if (regmatch->regprog == NULL)
+ return FAIL;
+ return OK;
+}
+
+/*
+ * Get search pattern used by search_regcomp().
+ */
+ char_u *
+get_search_pat(void)
+{
+ return mr_pattern;
+}
+
+#if defined(FEAT_RIGHTLEFT) || defined(PROTO)
+/*
+ * Reverse text into allocated memory.
+ * Returns the allocated string, NULL when out of memory.
+ */
+ char_u *
+reverse_text(char_u *s)
+{
+ unsigned len;
+ unsigned s_i, rev_i;
+ char_u *rev;
+
+ /*
+ * Reverse the pattern.
+ */
+ len = (unsigned)STRLEN(s);
+ rev = alloc(len + 1);
+ if (rev == NULL)
+ return NULL;
+
+ rev_i = len;
+ for (s_i = 0; s_i < len; ++s_i)
+ {
+ if (has_mbyte)
+ {
+ int mb_len;
+
+ mb_len = (*mb_ptr2len)(s + s_i);
+ rev_i -= mb_len;
+ mch_memmove(rev + rev_i, s + s_i, mb_len);
+ s_i += mb_len - 1;
+ }
+ else
+ rev[--rev_i] = s[s_i];
+
+ }
+ rev[len] = NUL;
+ return rev;
+}
+#endif
+
+ void
+save_re_pat(int idx, char_u *pat, int magic)
+{
+ if (spats[idx].pat == pat)
+ return;
+
+ vim_free(spats[idx].pat);
+ spats[idx].pat = vim_strsave(pat);
+ spats[idx].magic = magic;
+ spats[idx].no_scs = no_smartcase;
+ last_idx = idx;
+#ifdef FEAT_SEARCH_EXTRA
+ // If 'hlsearch' set and search pat changed: need redraw.
+ if (p_hls)
+ redraw_all_later(UPD_SOME_VALID);
+ set_no_hlsearch(FALSE);
+#endif
+}
+
+/*
+ * Save the search patterns, so they can be restored later.
+ * Used before/after executing autocommands and user functions.
+ */
+static int save_level = 0;
+
+ void
+save_search_patterns(void)
+{
+ if (save_level++ != 0)
+ return;
+
+ saved_spats[0] = spats[0];
+ if (spats[0].pat != NULL)
+ saved_spats[0].pat = vim_strsave(spats[0].pat);
+ saved_spats[1] = spats[1];
+ if (spats[1].pat != NULL)
+ saved_spats[1].pat = vim_strsave(spats[1].pat);
+ if (mr_pattern == NULL)
+ saved_mr_pattern = NULL;
+ else
+ saved_mr_pattern = vim_strsave(mr_pattern);
+#ifdef FEAT_SEARCH_EXTRA
+ saved_spats_last_idx = last_idx;
+ saved_spats_no_hlsearch = no_hlsearch;
+#endif
+}
+
+ void
+restore_search_patterns(void)
+{
+ if (--save_level != 0)
+ return;
+
+ vim_free(spats[0].pat);
+ spats[0] = saved_spats[0];
+#if defined(FEAT_EVAL)
+ set_vv_searchforward();
+#endif
+ vim_free(spats[1].pat);
+ spats[1] = saved_spats[1];
+ vim_free(mr_pattern);
+ mr_pattern = saved_mr_pattern;
+#ifdef FEAT_SEARCH_EXTRA
+ last_idx = saved_spats_last_idx;
+ set_no_hlsearch(saved_spats_no_hlsearch);
+#endif
+}
+
+#if defined(EXITFREE) || defined(PROTO)
+ void
+free_search_patterns(void)
+{
+ vim_free(spats[0].pat);
+ vim_free(spats[1].pat);
+ VIM_CLEAR(mr_pattern);
+}
+#endif
+
+#ifdef FEAT_SEARCH_EXTRA
+// copy of spats[RE_SEARCH], for keeping the search patterns while incremental
+// searching
+static spat_T saved_last_search_spat;
+static int did_save_last_search_spat = 0;
+static int saved_last_idx = 0;
+static int saved_no_hlsearch = 0;
+static int saved_search_match_endcol;
+static int saved_search_match_lines;
+
+/*
+ * Save and restore the search pattern for incremental highlight search
+ * feature.
+ *
+ * It's similar to but different from save_search_patterns() and
+ * restore_search_patterns(), because the search pattern must be restored when
+ * canceling incremental searching even if it's called inside user functions.
+ */
+ void
+save_last_search_pattern(void)
+{
+ if (++did_save_last_search_spat != 1)
+ // nested call, nothing to do
+ return;
+
+ saved_last_search_spat = spats[RE_SEARCH];
+ if (spats[RE_SEARCH].pat != NULL)
+ saved_last_search_spat.pat = vim_strsave(spats[RE_SEARCH].pat);
+ saved_last_idx = last_idx;
+ saved_no_hlsearch = no_hlsearch;
+}
+
+ void
+restore_last_search_pattern(void)
+{
+ if (--did_save_last_search_spat > 0)
+ // nested call, nothing to do
+ return;
+ if (did_save_last_search_spat != 0)
+ {
+ iemsg("restore_last_search_pattern() called more often than save_last_search_pattern()");
+ return;
+ }
+
+ vim_free(spats[RE_SEARCH].pat);
+ spats[RE_SEARCH] = saved_last_search_spat;
+ saved_last_search_spat.pat = NULL;
+# if defined(FEAT_EVAL)
+ set_vv_searchforward();
+# endif
+ last_idx = saved_last_idx;
+ set_no_hlsearch(saved_no_hlsearch);
+}
+
+/*
+ * Save and restore the incsearch highlighting variables.
+ * This is required so that calling searchcount() at does not invalidate the
+ * incsearch highlighting.
+ */
+ static void
+save_incsearch_state(void)
+{
+ saved_search_match_endcol = search_match_endcol;
+ saved_search_match_lines = search_match_lines;
+}
+
+ static void
+restore_incsearch_state(void)
+{
+ search_match_endcol = saved_search_match_endcol;
+ search_match_lines = saved_search_match_lines;
+}
+
+ char_u *
+last_search_pattern(void)
+{
+ return spats[RE_SEARCH].pat;
+}
+#endif
+
+/*
+ * Return TRUE when case should be ignored for search pattern "pat".
+ * Uses the 'ignorecase' and 'smartcase' options.
+ */
+ int
+ignorecase(char_u *pat)
+{
+ return ignorecase_opt(pat, p_ic, p_scs);
+}
+
+/*
+ * As ignorecase() put pass the "ic" and "scs" flags.
+ */
+ int
+ignorecase_opt(char_u *pat, int ic_in, int scs)
+{
+ int ic = ic_in;
+
+ if (ic && !no_smartcase && scs
+ && !(ctrl_x_mode_not_default() && curbuf->b_p_inf))
+ ic = !pat_has_uppercase(pat);
+ no_smartcase = FALSE;
+
+ return ic;
+}
+
+/*
+ * Return TRUE if pattern "pat" has an uppercase character.
+ */
+ int
+pat_has_uppercase(char_u *pat)
+{
+ char_u *p = pat;
+ magic_T magic_val = MAGIC_ON;
+
+ // get the magicness of the pattern
+ (void)skip_regexp_ex(pat, NUL, magic_isset(), NULL, NULL, &magic_val);
+
+ while (*p != NUL)
+ {
+ int l;
+
+ if (has_mbyte && (l = (*mb_ptr2len)(p)) > 1)
+ {
+ if (enc_utf8 && utf_isupper(utf_ptr2char(p)))
+ return TRUE;
+ p += l;
+ }
+ else if (*p == '\\' && magic_val <= MAGIC_ON)
+ {
+ if (p[1] == '_' && p[2] != NUL) // skip "\_X"
+ p += 3;
+ else if (p[1] == '%' && p[2] != NUL) // skip "\%X"
+ p += 3;
+ else if (p[1] != NUL) // skip "\X"
+ p += 2;
+ else
+ p += 1;
+ }
+ else if ((*p == '%' || *p == '_') && magic_val == MAGIC_ALL)
+ {
+ if (p[1] != NUL) // skip "_X" and %X
+ p += 2;
+ else
+ p++;
+ }
+ else if (MB_ISUPPER(*p))
+ return TRUE;
+ else
+ ++p;
+ }
+ return FALSE;
+}
+
+#if defined(FEAT_EVAL) || defined(PROTO)
+ char_u *
+last_csearch(void)
+{
+ return lastc_bytes;
+}
+
+ int
+last_csearch_forward(void)
+{
+ return lastcdir == FORWARD;
+}
+
+ int
+last_csearch_until(void)
+{
+ return last_t_cmd == TRUE;
+}
+
+ void
+set_last_csearch(int c, char_u *s UNUSED, int len UNUSED)
+{
+ *lastc = c;
+ lastc_bytelen = len;
+ if (len)
+ memcpy(lastc_bytes, s, len);
+ else
+ CLEAR_FIELD(lastc_bytes);
+}
+#endif
+
+ void
+set_csearch_direction(int cdir)
+{
+ lastcdir = cdir;
+}
+
+ void
+set_csearch_until(int t_cmd)
+{
+ last_t_cmd = t_cmd;
+}
+
+ char_u *
+last_search_pat(void)
+{
+ return spats[last_idx].pat;
+}
+
+/*
+ * Reset search direction to forward. For "gd" and "gD" commands.
+ */
+ void
+reset_search_dir(void)
+{
+ spats[0].off.dir = '/';
+#if defined(FEAT_EVAL)
+ set_vv_searchforward();
+#endif
+}
+
+#if defined(FEAT_EVAL) || defined(FEAT_VIMINFO)
+/*
+ * Set the last search pattern. For ":let @/ =" and viminfo.
+ * Also set the saved search pattern, so that this works in an autocommand.
+ */
+ void
+set_last_search_pat(
+ char_u *s,
+ int idx,
+ int magic,
+ int setlast)
+{
+ vim_free(spats[idx].pat);
+ // An empty string means that nothing should be matched.
+ if (*s == NUL)
+ spats[idx].pat = NULL;
+ else
+ spats[idx].pat = vim_strsave(s);
+ spats[idx].magic = magic;
+ spats[idx].no_scs = FALSE;
+ spats[idx].off.dir = '/';
+#if defined(FEAT_EVAL)
+ set_vv_searchforward();
+#endif
+ spats[idx].off.line = FALSE;
+ spats[idx].off.end = FALSE;
+ spats[idx].off.off = 0;
+ if (setlast)
+ last_idx = idx;
+ if (save_level)
+ {
+ vim_free(saved_spats[idx].pat);
+ saved_spats[idx] = spats[0];
+ if (spats[idx].pat == NULL)
+ saved_spats[idx].pat = NULL;
+ else
+ saved_spats[idx].pat = vim_strsave(spats[idx].pat);
+# ifdef FEAT_SEARCH_EXTRA
+ saved_spats_last_idx = last_idx;
+# endif
+ }
+# ifdef FEAT_SEARCH_EXTRA
+ // If 'hlsearch' set and search pat changed: need redraw.
+ if (p_hls && idx == last_idx && !no_hlsearch)
+ redraw_all_later(UPD_SOME_VALID);
+# endif
+}
+#endif
+
+#ifdef FEAT_SEARCH_EXTRA
+/*
+ * Get a regexp program for the last used search pattern.
+ * This is used for highlighting all matches in a window.
+ * Values returned in regmatch->regprog and regmatch->rmm_ic.
+ */
+ void
+last_pat_prog(regmmatch_T *regmatch)
+{
+ if (spats[last_idx].pat == NULL)
+ {
+ regmatch->regprog = NULL;
+ return;
+ }
+ ++emsg_off; // So it doesn't beep if bad expr
+ (void)search_regcomp((char_u *)"", NULL, 0, last_idx, SEARCH_KEEP, regmatch);
+ --emsg_off;
+}
+#endif
+
+/*
+ * Lowest level search function.
+ * Search for 'count'th occurrence of pattern "pat" in direction "dir".
+ * Start at position "pos" and return the found position in "pos".
+ *
+ * if (options & SEARCH_MSG) == 0 don't give any messages
+ * if (options & SEARCH_MSG) == SEARCH_NFMSG don't give 'notfound' messages
+ * if (options & SEARCH_MSG) == SEARCH_MSG give all messages
+ * if (options & SEARCH_HIS) put search pattern in history
+ * if (options & SEARCH_END) return position at end of match
+ * if (options & SEARCH_START) accept match at pos itself
+ * if (options & SEARCH_KEEP) keep previous search pattern
+ * if (options & SEARCH_FOLD) match only once in a closed fold
+ * if (options & SEARCH_PEEK) check for typed char, cancel search
+ * if (options & SEARCH_COL) start at pos->col instead of zero
+ *
+ * Return FAIL (zero) for failure, non-zero for success.
+ * When FEAT_EVAL is defined, returns the index of the first matching
+ * subpattern plus one; one if there was none.
+ */
+ int
+searchit(
+ win_T *win, // window to search in; can be NULL for a
+ // buffer without a window!
+ buf_T *buf,
+ pos_T *pos,
+ pos_T *end_pos, // set to end of the match, unless NULL
+ int dir,
+ char_u *pat,
+ long count,
+ int options,
+ int pat_use, // which pattern to use when "pat" is empty
+ searchit_arg_T *extra_arg) // optional extra arguments, can be NULL
+{
+ int found;
+ linenr_T lnum; // no init to shut up Apollo cc
+ colnr_T col;
+ regmmatch_T regmatch;
+ char_u *ptr;
+ colnr_T matchcol;
+ lpos_T endpos;
+ lpos_T matchpos;
+ int loop;
+ pos_T start_pos;
+ int at_first_line;
+ int extra_col;
+ int start_char_len;
+ int match_ok;
+ long nmatched;
+ int submatch = 0;
+ int first_match = TRUE;
+ int called_emsg_before = called_emsg;
+#ifdef FEAT_SEARCH_EXTRA
+ int break_loop = FALSE;
+#endif
+ linenr_T stop_lnum = 0; // stop after this line number when != 0
+ int unused_timeout_flag = FALSE;
+ int *timed_out = &unused_timeout_flag; // set when timed out.
+
+ if (search_regcomp(pat, NULL, RE_SEARCH, pat_use,
+ (options & (SEARCH_HIS + SEARCH_KEEP)), &regmatch) == FAIL)
+ {
+ if ((options & SEARCH_MSG) && !rc_did_emsg)
+ semsg(_(e_invalid_search_string_str), mr_pattern);
+ return FAIL;
+ }
+
+ if (extra_arg != NULL)
+ {
+ stop_lnum = extra_arg->sa_stop_lnum;
+#ifdef FEAT_RELTIME
+ if (extra_arg->sa_tm > 0)
+ init_regexp_timeout(extra_arg->sa_tm);
+ // Also set the pointer when sa_tm is zero, the caller may have set the
+ // timeout.
+ timed_out = &extra_arg->sa_timed_out;
+#endif
+ }
+
+ /*
+ * find the string
+ */
+ do // loop for count
+ {
+ // When not accepting a match at the start position set "extra_col" to
+ // a non-zero value. Don't do that when starting at MAXCOL, since
+ // MAXCOL + 1 is zero.
+ if (pos->col == MAXCOL)
+ start_char_len = 0;
+ // Watch out for the "col" being MAXCOL - 2, used in a closed fold.
+ else if (has_mbyte
+ && pos->lnum >= 1 && pos->lnum <= buf->b_ml.ml_line_count
+ && pos->col < MAXCOL - 2)
+ {
+ ptr = ml_get_buf(buf, pos->lnum, FALSE);
+ if ((int)STRLEN(ptr) <= pos->col)
+ start_char_len = 1;
+ else
+ start_char_len = (*mb_ptr2len)(ptr + pos->col);
+ }
+ else
+ start_char_len = 1;
+ if (dir == FORWARD)
+ {
+ if (options & SEARCH_START)
+ extra_col = 0;
+ else
+ extra_col = start_char_len;
+ }
+ else
+ {
+ if (options & SEARCH_START)
+ extra_col = start_char_len;
+ else
+ extra_col = 0;
+ }
+
+ start_pos = *pos; // remember start pos for detecting no match
+ found = 0; // default: not found
+ at_first_line = TRUE; // default: start in first line
+ if (pos->lnum == 0) // correct lnum for when starting in line 0
+ {
+ pos->lnum = 1;
+ pos->col = 0;
+ at_first_line = FALSE; // not in first line now
+ }
+
+ /*
+ * Start searching in current line, unless searching backwards and
+ * we're in column 0.
+ * If we are searching backwards, in column 0, and not including the
+ * current position, gain some efficiency by skipping back a line.
+ * Otherwise begin the search in the current line.
+ */
+ if (dir == BACKWARD && start_pos.col == 0
+ && (options & SEARCH_START) == 0)
+ {
+ lnum = pos->lnum - 1;
+ at_first_line = FALSE;
+ }
+ else
+ lnum = pos->lnum;
+
+ for (loop = 0; loop <= 1; ++loop) // loop twice if 'wrapscan' set
+ {
+ for ( ; lnum > 0 && lnum <= buf->b_ml.ml_line_count;
+ lnum += dir, at_first_line = FALSE)
+ {
+ // Stop after checking "stop_lnum", if it's set.
+ if (stop_lnum != 0 && (dir == FORWARD
+ ? lnum > stop_lnum : lnum < stop_lnum))
+ break;
+ // Stop after passing the time limit.
+ if (*timed_out)
+ break;
+
+ /*
+ * Look for a match somewhere in line "lnum".
+ */
+ col = at_first_line && (options & SEARCH_COL) ? pos->col
+ : (colnr_T)0;
+ nmatched = vim_regexec_multi(&regmatch, win, buf,
+ lnum, col, timed_out);
+ // vim_regexec_multi() may clear "regprog"
+ if (regmatch.regprog == NULL)
+ break;
+ // Abort searching on an error (e.g., out of stack).
+ if (called_emsg > called_emsg_before || *timed_out)
+ break;
+ if (nmatched > 0)
+ {
+ // match may actually be in another line when using \zs
+ matchpos = regmatch.startpos[0];
+ endpos = regmatch.endpos[0];
+#ifdef FEAT_EVAL
+ submatch = first_submatch(&regmatch);
+#endif
+ // "lnum" may be past end of buffer for "\n\zs".
+ if (lnum + matchpos.lnum > buf->b_ml.ml_line_count)
+ ptr = (char_u *)"";
+ else
+ ptr = ml_get_buf(buf, lnum + matchpos.lnum, FALSE);
+
+ /*
+ * Forward search in the first line: match should be after
+ * the start position. If not, continue at the end of the
+ * match (this is vi compatible) or on the next char.
+ */
+ if (dir == FORWARD && at_first_line)
+ {
+ match_ok = TRUE;
+
+ /*
+ * When the match starts in a next line it's certainly
+ * past the start position.
+ * When match lands on a NUL the cursor will be put
+ * one back afterwards, compare with that position,
+ * otherwise "/$" will get stuck on end of line.
+ */
+ while (matchpos.lnum == 0
+ && ((options & SEARCH_END) && first_match
+ ? (nmatched == 1
+ && (int)endpos.col - 1
+ < (int)start_pos.col + extra_col)
+ : ((int)matchpos.col
+ - (ptr[matchpos.col] == NUL)
+ < (int)start_pos.col + extra_col)))
+ {
+ /*
+ * If vi-compatible searching, continue at the end
+ * of the match, otherwise continue one position
+ * forward.
+ */
+ if (vim_strchr(p_cpo, CPO_SEARCH) != NULL)
+ {
+ if (nmatched > 1)
+ {
+ // end is in next line, thus no match in
+ // this line
+ match_ok = FALSE;
+ break;
+ }
+ matchcol = endpos.col;
+ // for empty match: advance one char
+ if (matchcol == matchpos.col
+ && ptr[matchcol] != NUL)
+ {
+ if (has_mbyte)
+ matchcol +=
+ (*mb_ptr2len)(ptr + matchcol);
+ else
+ ++matchcol;
+ }
+ }
+ else
+ {
+ // Advance "matchcol" to the next character.
+ // This uses rmm_matchcol, the actual start of
+ // the match, ignoring "\zs".
+ matchcol = regmatch.rmm_matchcol;
+ if (ptr[matchcol] != NUL)
+ {
+ if (has_mbyte)
+ matchcol += (*mb_ptr2len)(ptr
+ + matchcol);
+ else
+ ++matchcol;
+ }
+ }
+ if (matchcol == 0 && (options & SEARCH_START))
+ break;
+ if (ptr[matchcol] == NUL
+ || (nmatched = vim_regexec_multi(&regmatch,
+ win, buf, lnum + matchpos.lnum,
+ matchcol, timed_out)) == 0)
+ {
+ match_ok = FALSE;
+ break;
+ }
+ // vim_regexec_multi() may clear "regprog"
+ if (regmatch.regprog == NULL)
+ break;
+ matchpos = regmatch.startpos[0];
+ endpos = regmatch.endpos[0];
+# ifdef FEAT_EVAL
+ submatch = first_submatch(&regmatch);
+# endif
+
+ // Need to get the line pointer again, a
+ // multi-line search may have made it invalid.
+ ptr = ml_get_buf(buf, lnum + matchpos.lnum, FALSE);
+ }
+ if (!match_ok)
+ continue;
+ }
+ if (dir == BACKWARD)
+ {
+ /*
+ * Now, if there are multiple matches on this line,
+ * we have to get the last one. Or the last one before
+ * the cursor, if we're on that line.
+ * When putting the new cursor at the end, compare
+ * relative to the end of the match.
+ */
+ match_ok = FALSE;
+ for (;;)
+ {
+ // Remember a position that is before the start
+ // position, we use it if it's the last match in
+ // the line. Always accept a position after
+ // wrapping around.
+ if (loop
+ || ((options & SEARCH_END)
+ ? (lnum + regmatch.endpos[0].lnum
+ < start_pos.lnum
+ || (lnum + regmatch.endpos[0].lnum
+ == start_pos.lnum
+ && (int)regmatch.endpos[0].col - 1
+ < (int)start_pos.col
+ + extra_col))
+ : (lnum + regmatch.startpos[0].lnum
+ < start_pos.lnum
+ || (lnum + regmatch.startpos[0].lnum
+ == start_pos.lnum
+ && (int)regmatch.startpos[0].col
+ < (int)start_pos.col
+ + extra_col))))
+ {
+ match_ok = TRUE;
+ matchpos = regmatch.startpos[0];
+ endpos = regmatch.endpos[0];
+# ifdef FEAT_EVAL
+ submatch = first_submatch(&regmatch);
+# endif
+ }
+ else
+ break;
+
+ /*
+ * We found a valid match, now check if there is
+ * another one after it.
+ * If vi-compatible searching, continue at the end
+ * of the match, otherwise continue one position
+ * forward.
+ */
+ if (vim_strchr(p_cpo, CPO_SEARCH) != NULL)
+ {
+ if (nmatched > 1)
+ break;
+ matchcol = endpos.col;
+ // for empty match: advance one char
+ if (matchcol == matchpos.col
+ && ptr[matchcol] != NUL)
+ {
+ if (has_mbyte)
+ matchcol +=
+ (*mb_ptr2len)(ptr + matchcol);
+ else
+ ++matchcol;
+ }
+ }
+ else
+ {
+ // Stop when the match is in a next line.
+ if (matchpos.lnum > 0)
+ break;
+ matchcol = matchpos.col;
+ if (ptr[matchcol] != NUL)
+ {
+ if (has_mbyte)
+ matchcol +=
+ (*mb_ptr2len)(ptr + matchcol);
+ else
+ ++matchcol;
+ }
+ }
+ if (ptr[matchcol] == NUL
+ || (nmatched = vim_regexec_multi(&regmatch,
+ win, buf, lnum + matchpos.lnum,
+ matchcol, timed_out)) == 0)
+ {
+ // If the search timed out, we did find a match
+ // but it might be the wrong one, so that's not
+ // OK.
+ if (*timed_out)
+ match_ok = FALSE;
+ break;
+ }
+ // vim_regexec_multi() may clear "regprog"
+ if (regmatch.regprog == NULL)
+ break;
+
+ // Need to get the line pointer again, a
+ // multi-line search may have made it invalid.
+ ptr = ml_get_buf(buf, lnum + matchpos.lnum, FALSE);
+ }
+
+ /*
+ * If there is only a match after the cursor, skip
+ * this match.
+ */
+ if (!match_ok)
+ continue;
+ }
+
+ // With the SEARCH_END option move to the last character
+ // of the match. Don't do it for an empty match, end
+ // should be same as start then.
+ if ((options & SEARCH_END) && !(options & SEARCH_NOOF)
+ && !(matchpos.lnum == endpos.lnum
+ && matchpos.col == endpos.col))
+ {
+ // For a match in the first column, set the position
+ // on the NUL in the previous line.
+ pos->lnum = lnum + endpos.lnum;
+ pos->col = endpos.col;
+ if (endpos.col == 0)
+ {
+ if (pos->lnum > 1) // just in case
+ {
+ --pos->lnum;
+ pos->col = (colnr_T)STRLEN(ml_get_buf(buf,
+ pos->lnum, FALSE));
+ }
+ }
+ else
+ {
+ --pos->col;
+ if (has_mbyte
+ && pos->lnum <= buf->b_ml.ml_line_count)
+ {
+ ptr = ml_get_buf(buf, pos->lnum, FALSE);
+ pos->col -= (*mb_head_off)(ptr, ptr + pos->col);
+ }
+ }
+ if (end_pos != NULL)
+ {
+ end_pos->lnum = lnum + matchpos.lnum;
+ end_pos->col = matchpos.col;
+ }
+ }
+ else
+ {
+ pos->lnum = lnum + matchpos.lnum;
+ pos->col = matchpos.col;
+ if (end_pos != NULL)
+ {
+ end_pos->lnum = lnum + endpos.lnum;
+ end_pos->col = endpos.col;
+ }
+ }
+ pos->coladd = 0;
+ if (end_pos != NULL)
+ end_pos->coladd = 0;
+ found = 1;
+ first_match = FALSE;
+
+ // Set variables used for 'incsearch' highlighting.
+ search_match_lines = endpos.lnum - matchpos.lnum;
+ search_match_endcol = endpos.col;
+ break;
+ }
+ line_breakcheck(); // stop if ctrl-C typed
+ if (got_int)
+ break;
+
+#ifdef FEAT_SEARCH_EXTRA
+ // Cancel searching if a character was typed. Used for
+ // 'incsearch'. Don't check too often, that would slowdown
+ // searching too much.
+ if ((options & SEARCH_PEEK)
+ && ((lnum - pos->lnum) & 0x3f) == 0
+ && char_avail())
+ {
+ break_loop = TRUE;
+ break;
+ }
+#endif
+
+ if (loop && lnum == start_pos.lnum)
+ break; // if second loop, stop where started
+ }
+ at_first_line = FALSE;
+
+ // vim_regexec_multi() may clear "regprog"
+ if (regmatch.regprog == NULL)
+ break;
+
+ /*
+ * Stop the search if wrapscan isn't set, "stop_lnum" is
+ * specified, after an interrupt, after a match and after looping
+ * twice.
+ */
+ if (!p_ws || stop_lnum != 0 || got_int
+ || called_emsg > called_emsg_before || *timed_out
+#ifdef FEAT_SEARCH_EXTRA
+ || break_loop
+#endif
+ || found || loop)
+ break;
+
+ /*
+ * If 'wrapscan' is set we continue at the other end of the file.
+ * If 'shortmess' does not contain 's', we give a message.
+ * This message is also remembered in keep_msg for when the screen
+ * is redrawn. The keep_msg is cleared whenever another message is
+ * written.
+ */
+ if (dir == BACKWARD) // start second loop at the other end
+ lnum = buf->b_ml.ml_line_count;
+ else
+ lnum = 1;
+ if (!shortmess(SHM_SEARCH) && (options & SEARCH_MSG))
+ give_warning((char_u *)_(dir == BACKWARD
+ ? top_bot_msg : bot_top_msg), TRUE);
+ if (extra_arg != NULL)
+ extra_arg->sa_wrapped = TRUE;
+ }
+ if (got_int || called_emsg > called_emsg_before || *timed_out
+#ifdef FEAT_SEARCH_EXTRA
+ || break_loop
+#endif
+ )
+ break;
+ }
+ while (--count > 0 && found); // stop after count matches or no match
+
+#ifdef FEAT_RELTIME
+ if (extra_arg != NULL && extra_arg->sa_tm > 0)
+ disable_regexp_timeout();
+#endif
+ vim_regfree(regmatch.regprog);
+
+ if (!found) // did not find it
+ {
+ if (got_int)
+ emsg(_(e_interrupted));
+ else if ((options & SEARCH_MSG) == SEARCH_MSG)
+ {
+ if (p_ws)
+ semsg(_(e_pattern_not_found_str), mr_pattern);
+ else if (lnum == 0)
+ semsg(_(e_search_hit_top_without_match_for_str), mr_pattern);
+ else
+ semsg(_(e_search_hit_bottom_without_match_for_str), mr_pattern);
+ }
+ return FAIL;
+ }
+
+ // A pattern like "\n\zs" may go past the last line.
+ if (pos->lnum > buf->b_ml.ml_line_count)
+ {
+ pos->lnum = buf->b_ml.ml_line_count;
+ pos->col = (int)STRLEN(ml_get_buf(buf, pos->lnum, FALSE));
+ if (pos->col > 0)
+ --pos->col;
+ }
+
+ return submatch + 1;
+}
+
+#if defined(FEAT_EVAL) || defined(FEAT_PROTO)
+ void
+set_search_direction(int cdir)
+{
+ spats[0].off.dir = cdir;
+}
+
+ static void
+set_vv_searchforward(void)
+{
+ set_vim_var_nr(VV_SEARCHFORWARD, (long)(spats[0].off.dir == '/'));
+}
+
+/*
+ * Return the number of the first subpat that matched.
+ * Return zero if none of them matched.
+ */
+ static int
+first_submatch(regmmatch_T *rp)
+{
+ int submatch;
+
+ for (submatch = 1; ; ++submatch)
+ {
+ if (rp->startpos[submatch].lnum >= 0)
+ break;
+ if (submatch == 9)
+ {
+ submatch = 0;
+ break;
+ }
+ }
+ return submatch;
+}
+#endif
+
+/*
+ * Highest level string search function.
+ * Search for the 'count'th occurrence of pattern 'pat' in direction 'dirc'
+ * If 'dirc' is 0: use previous dir.
+ * If 'pat' is NULL or empty : use previous string.
+ * If 'options & SEARCH_REV' : go in reverse of previous dir.
+ * If 'options & SEARCH_ECHO': echo the search command and handle options
+ * If 'options & SEARCH_MSG' : may give error message
+ * If 'options & SEARCH_OPT' : interpret optional flags
+ * If 'options & SEARCH_HIS' : put search pattern in history
+ * If 'options & SEARCH_NOOF': don't add offset to position
+ * If 'options & SEARCH_MARK': set previous context mark
+ * If 'options & SEARCH_KEEP': keep previous search pattern
+ * If 'options & SEARCH_START': accept match at curpos itself
+ * If 'options & SEARCH_PEEK': check for typed char, cancel search
+ *
+ * Careful: If spats[0].off.line == TRUE and spats[0].off.off == 0 this
+ * makes the movement linewise without moving the match position.
+ *
+ * Return 0 for failure, 1 for found, 2 for found and line offset added.
+ */
+ int
+do_search(
+ oparg_T *oap, // can be NULL
+ int dirc, // '/' or '?'
+ int search_delim, // the delimiter for the search, e.g. '%' in
+ // s%regex%replacement%
+ char_u *pat,
+ long count,
+ int options,
+ searchit_arg_T *sia) // optional arguments or NULL
+{
+ pos_T pos; // position of the last match
+ char_u *searchstr;
+ soffset_T old_off;
+ int retval; // Return value
+ char_u *p;
+ long c;
+ char_u *dircp;
+ char_u *strcopy = NULL;
+ char_u *ps;
+ char_u *msgbuf = NULL;
+ size_t len;
+ int has_offset = FALSE;
+
+ /*
+ * A line offset is not remembered, this is vi compatible.
+ */
+ if (spats[0].off.line && vim_strchr(p_cpo, CPO_LINEOFF) != NULL)
+ {
+ spats[0].off.line = FALSE;
+ spats[0].off.off = 0;
+ }
+
+ /*
+ * Save the values for when (options & SEARCH_KEEP) is used.
+ * (there is no "if ()" around this because gcc wants them initialized)
+ */
+ old_off = spats[0].off;
+
+ pos = curwin->w_cursor; // start searching at the cursor position
+
+ /*
+ * Find out the direction of the search.
+ */
+ if (dirc == 0)
+ dirc = spats[0].off.dir;
+ else
+ {
+ spats[0].off.dir = dirc;
+#if defined(FEAT_EVAL)
+ set_vv_searchforward();
+#endif
+ }
+ if (options & SEARCH_REV)
+ {
+#ifdef MSWIN
+ // There is a bug in the Visual C++ 2.2 compiler which means that
+ // dirc always ends up being '/'
+ dirc = (dirc == '/') ? '?' : '/';
+#else
+ if (dirc == '/')
+ dirc = '?';
+ else
+ dirc = '/';
+#endif
+ }
+
+#ifdef FEAT_FOLDING
+ // If the cursor is in a closed fold, don't find another match in the same
+ // fold.
+ if (dirc == '/')
+ {
+ if (hasFolding(pos.lnum, NULL, &pos.lnum))
+ pos.col = MAXCOL - 2; // avoid overflow when adding 1
+ }
+ else
+ {
+ if (hasFolding(pos.lnum, &pos.lnum, NULL))
+ pos.col = 0;
+ }
+#endif
+
+#ifdef FEAT_SEARCH_EXTRA
+ /*
+ * Turn 'hlsearch' highlighting back on.
+ */
+ if (no_hlsearch && !(options & SEARCH_KEEP))
+ {
+ redraw_all_later(UPD_SOME_VALID);
+ set_no_hlsearch(FALSE);
+ }
+#endif
+
+ /*
+ * Repeat the search when pattern followed by ';', e.g. "/foo/;?bar".
+ */
+ for (;;)
+ {
+ int show_top_bot_msg = FALSE;
+
+ searchstr = pat;
+ dircp = NULL;
+ // use previous pattern
+ if (pat == NULL || *pat == NUL || *pat == search_delim)
+ {
+ if (spats[RE_SEARCH].pat == NULL) // no previous pattern
+ {
+ searchstr = spats[RE_SUBST].pat;
+ if (searchstr == NULL)
+ {
+ emsg(_(e_no_previous_regular_expression));
+ retval = 0;
+ goto end_do_search;
+ }
+ }
+ else
+ {
+ // make search_regcomp() use spats[RE_SEARCH].pat
+ searchstr = (char_u *)"";
+ }
+ }
+
+ if (pat != NULL && *pat != NUL) // look for (new) offset
+ {
+ /*
+ * Find end of regular expression.
+ * If there is a matching '/' or '?', toss it.
+ */
+ ps = strcopy;
+ p = skip_regexp_ex(pat, search_delim, magic_isset(),
+ &strcopy, NULL, NULL);
+ if (strcopy != ps)
+ {
+ // made a copy of "pat" to change "\?" to "?"
+ searchcmdlen += (int)(STRLEN(pat) - STRLEN(strcopy));
+ pat = strcopy;
+ searchstr = strcopy;
+ }
+ if (*p == search_delim)
+ {
+ dircp = p; // remember where we put the NUL
+ *p++ = NUL;
+ }
+ spats[0].off.line = FALSE;
+ spats[0].off.end = FALSE;
+ spats[0].off.off = 0;
+ /*
+ * Check for a line offset or a character offset.
+ * For get_address (echo off) we don't check for a character
+ * offset, because it is meaningless and the 's' could be a
+ * substitute command.
+ */
+ if (*p == '+' || *p == '-' || VIM_ISDIGIT(*p))
+ spats[0].off.line = TRUE;
+ else if ((options & SEARCH_OPT)
+ && (*p == 'e' || *p == 's' || *p == 'b'))
+ {
+ if (*p == 'e') // end
+ spats[0].off.end = SEARCH_END;
+ ++p;
+ }
+ if (VIM_ISDIGIT(*p) || *p == '+' || *p == '-') // got an offset
+ {
+ // 'nr' or '+nr' or '-nr'
+ if (VIM_ISDIGIT(*p) || VIM_ISDIGIT(*(p + 1)))
+ spats[0].off.off = atol((char *)p);
+ else if (*p == '-') // single '-'
+ spats[0].off.off = -1;
+ else // single '+'
+ spats[0].off.off = 1;
+ ++p;
+ while (VIM_ISDIGIT(*p)) // skip number
+ ++p;
+ }
+
+ // compute length of search command for get_address()
+ searchcmdlen += (int)(p - pat);
+
+ pat = p; // put pat after search command
+ }
+
+ if ((options & SEARCH_ECHO) && messaging()
+ && !msg_silent
+ && (!cmd_silent || !shortmess(SHM_SEARCHCOUNT)))
+ {
+ char_u *trunc;
+ char_u off_buf[40];
+ size_t off_len = 0;
+
+ // Compute msg_row early.
+ msg_start();
+
+ // Get the offset, so we know how long it is.
+ if (!cmd_silent &&
+ (spats[0].off.line || spats[0].off.end || spats[0].off.off))
+ {
+ p = off_buf;
+ *p++ = dirc;
+ if (spats[0].off.end)
+ *p++ = 'e';
+ else if (!spats[0].off.line)
+ *p++ = 's';
+ if (spats[0].off.off > 0 || spats[0].off.line)
+ *p++ = '+';
+ *p = NUL;
+ if (spats[0].off.off != 0 || spats[0].off.line)
+ sprintf((char *)p, "%ld", spats[0].off.off);
+ off_len = STRLEN(off_buf);
+ }
+
+ if (*searchstr == NUL)
+ p = spats[0].pat;
+ else
+ p = searchstr;
+
+ if (!shortmess(SHM_SEARCHCOUNT) || cmd_silent)
+ {
+ // Reserve enough space for the search pattern + offset +
+ // search stat. Use all the space available, so that the
+ // search state is right aligned. If there is not enough space
+ // msg_strtrunc() will shorten in the middle.
+ if (msg_scrolled != 0 && !cmd_silent)
+ // Use all the columns.
+ len = (int)(Rows - msg_row) * Columns - 1;
+ else
+ // Use up to 'showcmd' column.
+ len = (int)(Rows - msg_row - 1) * Columns + sc_col - 1;
+ if (len < STRLEN(p) + off_len + SEARCH_STAT_BUF_LEN + 3)
+ len = STRLEN(p) + off_len + SEARCH_STAT_BUF_LEN + 3;
+ }
+ else
+ // Reserve enough space for the search pattern + offset.
+ len = STRLEN(p) + off_len + 3;
+
+ vim_free(msgbuf);
+ msgbuf = alloc(len);
+ if (msgbuf != NULL)
+ {
+ vim_memset(msgbuf, ' ', len);
+ msgbuf[len - 1] = NUL;
+ // do not fill the msgbuf buffer, if cmd_silent is set, leave it
+ // empty for the search_stat feature.
+ if (!cmd_silent)
+ {
+ msgbuf[0] = dirc;
+
+ if (enc_utf8 && utf_iscomposing(utf_ptr2char(p)))
+ {
+ // Use a space to draw the composing char on.
+ msgbuf[1] = ' ';
+ mch_memmove(msgbuf + 2, p, STRLEN(p));
+ }
+ else
+ mch_memmove(msgbuf + 1, p, STRLEN(p));
+ if (off_len > 0)
+ mch_memmove(msgbuf + STRLEN(p) + 1, off_buf, off_len);
+
+ trunc = msg_strtrunc(msgbuf, TRUE);
+ if (trunc != NULL)
+ {
+ vim_free(msgbuf);
+ msgbuf = trunc;
+ }
+
+#ifdef FEAT_RIGHTLEFT
+ // The search pattern could be shown on the right in
+ // rightleft mode, but the 'ruler' and 'showcmd' area use
+ // it too, thus it would be blanked out again very soon.
+ // Show it on the left, but do reverse the text.
+ if (curwin->w_p_rl && *curwin->w_p_rlc == 's')
+ {
+ char_u *r;
+ size_t pat_len;
+
+ r = reverse_text(msgbuf);
+ if (r != NULL)
+ {
+ vim_free(msgbuf);
+ msgbuf = r;
+ // move reversed text to beginning of buffer
+ while (*r != NUL && *r == ' ')
+ r++;
+ pat_len = msgbuf + STRLEN(msgbuf) - r;
+ mch_memmove(msgbuf, r, pat_len);
+ // overwrite old text
+ if ((size_t)(r - msgbuf) >= pat_len)
+ vim_memset(r, ' ', pat_len);
+ else
+ vim_memset(msgbuf + pat_len, ' ', r - msgbuf);
+ }
+ }
+#endif
+ msg_outtrans(msgbuf);
+ msg_clr_eos();
+ msg_check();
+
+ gotocmdline(FALSE);
+ out_flush();
+ msg_nowait = TRUE; // don't wait for this message
+ }
+ }
+ }
+
+ /*
+ * If there is a character offset, subtract it from the current
+ * position, so we don't get stuck at "?pat?e+2" or "/pat/s-2".
+ * Skip this if pos.col is near MAXCOL (closed fold).
+ * This is not done for a line offset, because then we would not be vi
+ * compatible.
+ */
+ if (!spats[0].off.line && spats[0].off.off && pos.col < MAXCOL - 2)
+ {
+ if (spats[0].off.off > 0)
+ {
+ for (c = spats[0].off.off; c; --c)
+ if (decl(&pos) == -1)
+ break;
+ if (c) // at start of buffer
+ {
+ pos.lnum = 0; // allow lnum == 0 here
+ pos.col = MAXCOL;
+ }
+ }
+ else
+ {
+ for (c = spats[0].off.off; c; ++c)
+ if (incl(&pos) == -1)
+ break;
+ if (c) // at end of buffer
+ {
+ pos.lnum = curbuf->b_ml.ml_line_count + 1;
+ pos.col = 0;
+ }
+ }
+ }
+
+ /*
+ * The actual search.
+ */
+ c = searchit(curwin, curbuf, &pos, NULL,
+ dirc == '/' ? FORWARD : BACKWARD,
+ searchstr, count, spats[0].off.end + (options &
+ (SEARCH_KEEP + SEARCH_PEEK + SEARCH_HIS
+ + SEARCH_MSG + SEARCH_START
+ + ((pat != NULL && *pat == ';') ? 0 : SEARCH_NOOF))),
+ RE_LAST, sia);
+
+ if (dircp != NULL)
+ *dircp = search_delim; // restore second '/' or '?' for normal_cmd()
+
+ if (!shortmess(SHM_SEARCH)
+ && ((dirc == '/' && LT_POS(pos, curwin->w_cursor))
+ || (dirc == '?' && LT_POS(curwin->w_cursor, pos))))
+ show_top_bot_msg = TRUE;
+
+ if (c == FAIL)
+ {
+ retval = 0;
+ goto end_do_search;
+ }
+ if (spats[0].off.end && oap != NULL)
+ oap->inclusive = TRUE; // 'e' includes last character
+
+ retval = 1; // pattern found
+
+ /*
+ * Add character and/or line offset
+ */
+ if (!(options & SEARCH_NOOF) || (pat != NULL && *pat == ';'))
+ {
+ pos_T org_pos = pos;
+
+ if (spats[0].off.line) // Add the offset to the line number.
+ {
+ c = pos.lnum + spats[0].off.off;
+ if (c < 1)
+ pos.lnum = 1;
+ else if (c > curbuf->b_ml.ml_line_count)
+ pos.lnum = curbuf->b_ml.ml_line_count;
+ else
+ pos.lnum = c;
+ pos.col = 0;
+
+ retval = 2; // pattern found, line offset added
+ }
+ else if (pos.col < MAXCOL - 2) // just in case
+ {
+ // to the right, check for end of file
+ c = spats[0].off.off;
+ if (c > 0)
+ {
+ while (c-- > 0)
+ if (incl(&pos) == -1)
+ break;
+ }
+ // to the left, check for start of file
+ else
+ {
+ while (c++ < 0)
+ if (decl(&pos) == -1)
+ break;
+ }
+ }
+ if (!EQUAL_POS(pos, org_pos))
+ has_offset = TRUE;
+ }
+
+ // Show [1/15] if 'S' is not in 'shortmess'.
+ if ((options & SEARCH_ECHO)
+ && messaging()
+ && !msg_silent
+ && c != FAIL
+ && !shortmess(SHM_SEARCHCOUNT)
+ && msgbuf != NULL)
+ cmdline_search_stat(dirc, &pos, &curwin->w_cursor,
+ show_top_bot_msg, msgbuf,
+ (count != 1 || has_offset
+#ifdef FEAT_FOLDING
+ || (!(fdo_flags & FDO_SEARCH)
+ && hasFolding(curwin->w_cursor.lnum,
+ NULL, NULL))
+#endif
+ ),
+ SEARCH_STAT_DEF_MAX_COUNT,
+ SEARCH_STAT_DEF_TIMEOUT);
+
+ /*
+ * The search command can be followed by a ';' to do another search.
+ * For example: "/pat/;/foo/+3;?bar"
+ * This is like doing another search command, except:
+ * - The remembered direction '/' or '?' is from the first search.
+ * - When an error happens the cursor isn't moved at all.
+ * Don't do this when called by get_address() (it handles ';' itself).
+ */
+ if (!(options & SEARCH_OPT) || pat == NULL || *pat != ';')
+ break;
+
+ dirc = *++pat;
+ search_delim = dirc;
+ if (dirc != '?' && dirc != '/')
+ {
+ retval = 0;
+ emsg(_(e_expected_question_or_slash_after_semicolon));
+ goto end_do_search;
+ }
+ ++pat;
+ }
+
+ if (options & SEARCH_MARK)
+ setpcmark();
+ curwin->w_cursor = pos;
+ curwin->w_set_curswant = TRUE;
+
+end_do_search:
+ if ((options & SEARCH_KEEP) || (cmdmod.cmod_flags & CMOD_KEEPPATTERNS))
+ spats[0].off = old_off;
+ vim_free(strcopy);
+ vim_free(msgbuf);
+
+ return retval;
+}
+
+/*
+ * search_for_exact_line(buf, pos, dir, pat)
+ *
+ * Search for a line starting with the given pattern (ignoring leading
+ * white-space), starting from pos and going in direction "dir". "pos" will
+ * contain the position of the match found. Blank lines match only if
+ * ADDING is set. If p_ic is set then the pattern must be in lowercase.
+ * Return OK for success, or FAIL if no line found.
+ */
+ int
+search_for_exact_line(
+ buf_T *buf,
+ pos_T *pos,
+ int dir,
+ char_u *pat)
+{
+ linenr_T start = 0;
+ char_u *ptr;
+ char_u *p;
+
+ if (buf->b_ml.ml_line_count == 0)
+ return FAIL;
+ for (;;)
+ {
+ pos->lnum += dir;
+ if (pos->lnum < 1)
+ {
+ if (p_ws)
+ {
+ pos->lnum = buf->b_ml.ml_line_count;
+ if (!shortmess(SHM_SEARCH))
+ give_warning((char_u *)_(top_bot_msg), TRUE);
+ }
+ else
+ {
+ pos->lnum = 1;
+ break;
+ }
+ }
+ else if (pos->lnum > buf->b_ml.ml_line_count)
+ {
+ if (p_ws)
+ {
+ pos->lnum = 1;
+ if (!shortmess(SHM_SEARCH))
+ give_warning((char_u *)_(bot_top_msg), TRUE);
+ }
+ else
+ {
+ pos->lnum = 1;
+ break;
+ }
+ }
+ if (pos->lnum == start)
+ break;
+ if (start == 0)
+ start = pos->lnum;
+ ptr = ml_get_buf(buf, pos->lnum, FALSE);
+ p = skipwhite(ptr);
+ pos->col = (colnr_T) (p - ptr);
+
+ // when adding lines the matching line may be empty but it is not
+ // ignored because we are interested in the next line -- Acevedo
+ if (compl_status_adding() && !compl_status_sol())
+ {
+ if ((p_ic ? MB_STRICMP(p, pat) : STRCMP(p, pat)) == 0)
+ return OK;
+ }
+ else if (*p != NUL) // ignore empty lines
+ { // expanding lines or words
+ if ((p_ic ? MB_STRNICMP(p, pat, ins_compl_len())
+ : STRNCMP(p, pat, ins_compl_len())) == 0)
+ return OK;
+ }
+ }
+ return FAIL;
+}
+
+/*
+ * Character Searches
+ */
+
+/*
+ * Search for a character in a line. If "t_cmd" is FALSE, move to the
+ * position of the character, otherwise move to just before the char.
+ * Do this "cap->count1" times.
+ * Return FAIL or OK.
+ */
+ int
+searchc(cmdarg_T *cap, int t_cmd)
+{
+ int c = cap->nchar; // char to search for
+ int dir = cap->arg; // TRUE for searching forward
+ long count = cap->count1; // repeat count
+ int col;
+ char_u *p;
+ int len;
+ int stop = TRUE;
+
+ if (c != NUL) // normal search: remember args for repeat
+ {
+ if (!KeyStuffed) // don't remember when redoing
+ {
+ *lastc = c;
+ set_csearch_direction(dir);
+ set_csearch_until(t_cmd);
+ lastc_bytelen = (*mb_char2bytes)(c, lastc_bytes);
+ if (cap->ncharC1 != 0)
+ {
+ lastc_bytelen += (*mb_char2bytes)(cap->ncharC1,
+ lastc_bytes + lastc_bytelen);
+ if (cap->ncharC2 != 0)
+ lastc_bytelen += (*mb_char2bytes)(cap->ncharC2,
+ lastc_bytes + lastc_bytelen);
+ }
+ }
+ }
+ else // repeat previous search
+ {
+ if (*lastc == NUL && lastc_bytelen == 1)
+ return FAIL;
+ if (dir) // repeat in opposite direction
+ dir = -lastcdir;
+ else
+ dir = lastcdir;
+ t_cmd = last_t_cmd;
+ c = *lastc;
+ // For multi-byte re-use last lastc_bytes[] and lastc_bytelen.
+
+ // Force a move of at least one char, so ";" and "," will move the
+ // cursor, even if the cursor is right in front of char we are looking
+ // at.
+ if (vim_strchr(p_cpo, CPO_SCOLON) == NULL && count == 1 && t_cmd)
+ stop = FALSE;
+ }
+
+ if (dir == BACKWARD)
+ cap->oap->inclusive = FALSE;
+ else
+ cap->oap->inclusive = TRUE;
+
+ p = ml_get_curline();
+ col = curwin->w_cursor.col;
+ len = (int)STRLEN(p);
+
+ while (count--)
+ {
+ if (has_mbyte)
+ {
+ for (;;)
+ {
+ if (dir > 0)
+ {
+ col += (*mb_ptr2len)(p + col);
+ if (col >= len)
+ return FAIL;
+ }
+ else
+ {
+ if (col == 0)
+ return FAIL;
+ col -= (*mb_head_off)(p, p + col - 1) + 1;
+ }
+ if (lastc_bytelen == 1)
+ {
+ if (p[col] == c && stop)
+ break;
+ }
+ else if (STRNCMP(p + col, lastc_bytes, lastc_bytelen) == 0
+ && stop)
+ break;
+ stop = TRUE;
+ }
+ }
+ else
+ {
+ for (;;)
+ {
+ if ((col += dir) < 0 || col >= len)
+ return FAIL;
+ if (p[col] == c && stop)
+ break;
+ stop = TRUE;
+ }
+ }
+ }
+
+ if (t_cmd)
+ {
+ // backup to before the character (possibly double-byte)
+ col -= dir;
+ if (has_mbyte)
+ {
+ if (dir < 0)
+ // Landed on the search char which is lastc_bytelen long
+ col += lastc_bytelen - 1;
+ else
+ // To previous char, which may be multi-byte.
+ col -= (*mb_head_off)(p, p + col);
+ }
+ }
+ curwin->w_cursor.col = col;
+
+ return OK;
+}
+
+/*
+ * "Other" Searches
+ */
+
+/*
+ * findmatch - find the matching paren or brace
+ *
+ * Improvement over vi: Braces inside quotes are ignored.
+ */
+ pos_T *
+findmatch(oparg_T *oap, int initc)
+{
+ return findmatchlimit(oap, initc, 0, 0);
+}
+
+/*
+ * Return TRUE if the character before "linep[col]" equals "ch".
+ * Return FALSE if "col" is zero.
+ * Update "*prevcol" to the column of the previous character, unless "prevcol"
+ * is NULL.
+ * Handles multibyte string correctly.
+ */
+ static int
+check_prevcol(
+ char_u *linep,
+ int col,
+ int ch,
+ int *prevcol)
+{
+ --col;
+ if (col > 0 && has_mbyte)
+ col -= (*mb_head_off)(linep, linep + col);
+ if (prevcol)
+ *prevcol = col;
+ return (col >= 0 && linep[col] == ch) ? TRUE : FALSE;
+}
+
+/*
+ * Raw string start is found at linep[startpos.col - 1].
+ * Return TRUE if the matching end can be found between startpos and endpos.
+ */
+ static int
+find_rawstring_end(char_u *linep, pos_T *startpos, pos_T *endpos)
+{
+ char_u *p;
+ char_u *delim_copy;
+ size_t delim_len;
+ linenr_T lnum;
+ int found = FALSE;
+
+ for (p = linep + startpos->col + 1; *p && *p != '('; ++p)
+ ;
+ delim_len = (p - linep) - startpos->col - 1;
+ delim_copy = vim_strnsave(linep + startpos->col + 1, delim_len);
+ if (delim_copy == NULL)
+ return FALSE;
+ for (lnum = startpos->lnum; lnum <= endpos->lnum; ++lnum)
+ {
+ char_u *line = ml_get(lnum);
+
+ for (p = line + (lnum == startpos->lnum
+ ? startpos->col + 1 : 0); *p; ++p)
+ {
+ if (lnum == endpos->lnum && (colnr_T)(p - line) >= endpos->col)
+ break;
+ if (*p == ')' && STRNCMP(delim_copy, p + 1, delim_len) == 0
+ && p[delim_len + 1] == '"')
+ {
+ found = TRUE;
+ break;
+ }
+ }
+ if (found)
+ break;
+ }
+ vim_free(delim_copy);
+ return found;
+}
+
+/*
+ * Check matchpairs option for "*initc".
+ * If there is a match set "*initc" to the matching character and "*findc" to
+ * the opposite character. Set "*backwards" to the direction.
+ * When "switchit" is TRUE swap the direction.
+ */
+ static void
+find_mps_values(
+ int *initc,
+ int *findc,
+ int *backwards,
+ int switchit)
+{
+ char_u *ptr;
+
+ ptr = curbuf->b_p_mps;
+ while (*ptr != NUL)
+ {
+ if (has_mbyte)
+ {
+ char_u *prev;
+
+ if (mb_ptr2char(ptr) == *initc)
+ {
+ if (switchit)
+ {
+ *findc = *initc;
+ *initc = mb_ptr2char(ptr + mb_ptr2len(ptr) + 1);
+ *backwards = TRUE;
+ }
+ else
+ {
+ *findc = mb_ptr2char(ptr + mb_ptr2len(ptr) + 1);
+ *backwards = FALSE;
+ }
+ return;
+ }
+ prev = ptr;
+ ptr += mb_ptr2len(ptr) + 1;
+ if (mb_ptr2char(ptr) == *initc)
+ {
+ if (switchit)
+ {
+ *findc = *initc;
+ *initc = mb_ptr2char(prev);
+ *backwards = FALSE;
+ }
+ else
+ {
+ *findc = mb_ptr2char(prev);
+ *backwards = TRUE;
+ }
+ return;
+ }
+ ptr += mb_ptr2len(ptr);
+ }
+ else
+ {
+ if (*ptr == *initc)
+ {
+ if (switchit)
+ {
+ *backwards = TRUE;
+ *findc = *initc;
+ *initc = ptr[2];
+ }
+ else
+ {
+ *backwards = FALSE;
+ *findc = ptr[2];
+ }
+ return;
+ }
+ ptr += 2;
+ if (*ptr == *initc)
+ {
+ if (switchit)
+ {
+ *backwards = FALSE;
+ *findc = *initc;
+ *initc = ptr[-2];
+ }
+ else
+ {
+ *backwards = TRUE;
+ *findc = ptr[-2];
+ }
+ return;
+ }
+ ++ptr;
+ }
+ if (*ptr == ',')
+ ++ptr;
+ }
+}
+
+/*
+ * findmatchlimit -- find the matching paren or brace, if it exists within
+ * maxtravel lines of the cursor. A maxtravel of 0 means search until falling
+ * off the edge of the file.
+ *
+ * "initc" is the character to find a match for. NUL means to find the
+ * character at or after the cursor. Special values:
+ * '*' look for C-style comment / *
+ * '/' look for C-style comment / *, ignoring comment-end
+ * '#' look for preprocessor directives
+ * 'R' look for raw string start: R"delim(text)delim" (only backwards)
+ *
+ * flags: FM_BACKWARD search backwards (when initc is '/', '*' or '#')
+ * FM_FORWARD search forwards (when initc is '/', '*' or '#')
+ * FM_BLOCKSTOP stop at start/end of block ({ or } in column 0)
+ * FM_SKIPCOMM skip comments (not implemented yet!)
+ *
+ * "oap" is only used to set oap->motion_type for a linewise motion, it can be
+ * NULL
+ */
+ pos_T *
+findmatchlimit(
+ oparg_T *oap,
+ int initc,
+ int flags,
+ int maxtravel)
+{
+ static pos_T pos; // current search position
+ int findc = 0; // matching brace
+ int c;
+ int count = 0; // cumulative number of braces
+ int backwards = FALSE; // init for gcc
+ int raw_string = FALSE; // search for raw string
+ int inquote = FALSE; // TRUE when inside quotes
+ char_u *linep; // pointer to current line
+ char_u *ptr;
+ int do_quotes; // check for quotes in current line
+ int at_start; // do_quotes value at start position
+ int hash_dir = 0; // Direction searched for # things
+ int comment_dir = 0; // Direction searched for comments
+ pos_T match_pos; // Where last slash-star was found
+ int start_in_quotes; // start position is in quotes
+ int traveled = 0; // how far we've searched so far
+ int ignore_cend = FALSE; // ignore comment end
+ int cpo_match; // vi compatible matching
+ int cpo_bsl; // don't recognize backslashes
+ int match_escaped = 0; // search for escaped match
+ int dir; // Direction to search
+ int comment_col = MAXCOL; // start of / / comment
+ int lispcomm = FALSE; // inside of Lisp-style comment
+ int lisp = curbuf->b_p_lisp; // engage Lisp-specific hacks ;)
+
+ pos = curwin->w_cursor;
+ pos.coladd = 0;
+ linep = ml_get(pos.lnum);
+
+ cpo_match = (vim_strchr(p_cpo, CPO_MATCH) != NULL);
+ cpo_bsl = (vim_strchr(p_cpo, CPO_MATCHBSL) != NULL);
+
+ // Direction to search when initc is '/', '*' or '#'
+ if (flags & FM_BACKWARD)
+ dir = BACKWARD;
+ else if (flags & FM_FORWARD)
+ dir = FORWARD;
+ else
+ dir = 0;
+
+ /*
+ * if initc given, look in the table for the matching character
+ * '/' and '*' are special cases: look for start or end of comment.
+ * When '/' is used, we ignore running backwards into an star-slash, for
+ * "[*" command, we just want to find any comment.
+ */
+ if (initc == '/' || initc == '*' || initc == 'R')
+ {
+ comment_dir = dir;
+ if (initc == '/')
+ ignore_cend = TRUE;
+ backwards = (dir == FORWARD) ? FALSE : TRUE;
+ raw_string = (initc == 'R');
+ initc = NUL;
+ }
+ else if (initc != '#' && initc != NUL)
+ {
+ find_mps_values(&initc, &findc, &backwards, TRUE);
+ if (dir)
+ backwards = (dir == FORWARD) ? FALSE : TRUE;
+ if (findc == NUL)
+ return NULL;
+ }
+ else
+ {
+ /*
+ * Either initc is '#', or no initc was given and we need to look
+ * under the cursor.
+ */
+ if (initc == '#')
+ {
+ hash_dir = dir;
+ }
+ else
+ {
+ /*
+ * initc was not given, must look for something to match under
+ * or near the cursor.
+ * Only check for special things when 'cpo' doesn't have '%'.
+ */
+ if (!cpo_match)
+ {
+ // Are we before or at #if, #else etc.?
+ ptr = skipwhite(linep);
+ if (*ptr == '#' && pos.col <= (colnr_T)(ptr - linep))
+ {
+ ptr = skipwhite(ptr + 1);
+ if ( STRNCMP(ptr, "if", 2) == 0
+ || STRNCMP(ptr, "endif", 5) == 0
+ || STRNCMP(ptr, "el", 2) == 0)
+ hash_dir = 1;
+ }
+
+ // Are we on a comment?
+ else if (linep[pos.col] == '/')
+ {
+ if (linep[pos.col + 1] == '*')
+ {
+ comment_dir = FORWARD;
+ backwards = FALSE;
+ pos.col++;
+ }
+ else if (pos.col > 0 && linep[pos.col - 1] == '*')
+ {
+ comment_dir = BACKWARD;
+ backwards = TRUE;
+ pos.col--;
+ }
+ }
+ else if (linep[pos.col] == '*')
+ {
+ if (linep[pos.col + 1] == '/')
+ {
+ comment_dir = BACKWARD;
+ backwards = TRUE;
+ }
+ else if (pos.col > 0 && linep[pos.col - 1] == '/')
+ {
+ comment_dir = FORWARD;
+ backwards = FALSE;
+ }
+ }
+ }
+
+ /*
+ * If we are not on a comment or the # at the start of a line, then
+ * look for brace anywhere on this line after the cursor.
+ */
+ if (!hash_dir && !comment_dir)
+ {
+ /*
+ * Find the brace under or after the cursor.
+ * If beyond the end of the line, use the last character in
+ * the line.
+ */
+ if (linep[pos.col] == NUL && pos.col)
+ --pos.col;
+ for (;;)
+ {
+ initc = PTR2CHAR(linep + pos.col);
+ if (initc == NUL)
+ break;
+
+ find_mps_values(&initc, &findc, &backwards, FALSE);
+ if (findc)
+ break;
+ pos.col += mb_ptr2len(linep + pos.col);
+ }
+ if (!findc)
+ {
+ // no brace in the line, maybe use " #if" then
+ if (!cpo_match && *skipwhite(linep) == '#')
+ hash_dir = 1;
+ else
+ return NULL;
+ }
+ else if (!cpo_bsl)
+ {
+ int col, bslcnt = 0;
+
+ // Set "match_escaped" if there are an odd number of
+ // backslashes.
+ for (col = pos.col; check_prevcol(linep, col, '\\', &col);)
+ bslcnt++;
+ match_escaped = (bslcnt & 1);
+ }
+ }
+ }
+ if (hash_dir)
+ {
+ /*
+ * Look for matching #if, #else, #elif, or #endif
+ */
+ if (oap != NULL)
+ oap->motion_type = MLINE; // Linewise for this case only
+ if (initc != '#')
+ {
+ ptr = skipwhite(skipwhite(linep) + 1);
+ if (STRNCMP(ptr, "if", 2) == 0 || STRNCMP(ptr, "el", 2) == 0)
+ hash_dir = 1;
+ else if (STRNCMP(ptr, "endif", 5) == 0)
+ hash_dir = -1;
+ else
+ return NULL;
+ }
+ pos.col = 0;
+ while (!got_int)
+ {
+ if (hash_dir > 0)
+ {
+ if (pos.lnum == curbuf->b_ml.ml_line_count)
+ break;
+ }
+ else if (pos.lnum == 1)
+ break;
+ pos.lnum += hash_dir;
+ linep = ml_get(pos.lnum);
+ line_breakcheck(); // check for CTRL-C typed
+ ptr = skipwhite(linep);
+ if (*ptr != '#')
+ continue;
+ pos.col = (colnr_T) (ptr - linep);
+ ptr = skipwhite(ptr + 1);
+ if (hash_dir > 0)
+ {
+ if (STRNCMP(ptr, "if", 2) == 0)
+ count++;
+ else if (STRNCMP(ptr, "el", 2) == 0)
+ {
+ if (count == 0)
+ return &pos;
+ }
+ else if (STRNCMP(ptr, "endif", 5) == 0)
+ {
+ if (count == 0)
+ return &pos;
+ count--;
+ }
+ }
+ else
+ {
+ if (STRNCMP(ptr, "if", 2) == 0)
+ {
+ if (count == 0)
+ return &pos;
+ count--;
+ }
+ else if (initc == '#' && STRNCMP(ptr, "el", 2) == 0)
+ {
+ if (count == 0)
+ return &pos;
+ }
+ else if (STRNCMP(ptr, "endif", 5) == 0)
+ count++;
+ }
+ }
+ return NULL;
+ }
+ }
+
+#ifdef FEAT_RIGHTLEFT
+ // This is just guessing: when 'rightleft' is set, search for a matching
+ // paren/brace in the other direction.
+ if (curwin->w_p_rl && vim_strchr((char_u *)"()[]{}<>", initc) != NULL)
+ backwards = !backwards;
+#endif
+
+ do_quotes = -1;
+ start_in_quotes = MAYBE;
+ CLEAR_POS(&match_pos);
+
+ // backward search: Check if this line contains a single-line comment
+ if ((backwards && comment_dir) || lisp)
+ comment_col = check_linecomment(linep);
+ if (lisp && comment_col != MAXCOL && pos.col > (colnr_T)comment_col)
+ lispcomm = TRUE; // find match inside this comment
+
+ while (!got_int)
+ {
+ /*
+ * Go to the next position, forward or backward. We could use
+ * inc() and dec() here, but that is much slower
+ */
+ if (backwards)
+ {
+ // char to match is inside of comment, don't search outside
+ if (lispcomm && pos.col < (colnr_T)comment_col)
+ break;
+ if (pos.col == 0) // at start of line, go to prev. one
+ {
+ if (pos.lnum == 1) // start of file
+ break;
+ --pos.lnum;
+
+ if (maxtravel > 0 && ++traveled > maxtravel)
+ break;
+
+ linep = ml_get(pos.lnum);
+ pos.col = (colnr_T)STRLEN(linep); // pos.col on trailing NUL
+ do_quotes = -1;
+ line_breakcheck();
+
+ // Check if this line contains a single-line comment
+ if (comment_dir || lisp)
+ comment_col = check_linecomment(linep);
+ // skip comment
+ if (lisp && comment_col != MAXCOL)
+ pos.col = comment_col;
+ }
+ else
+ {
+ --pos.col;
+ if (has_mbyte)
+ pos.col -= (*mb_head_off)(linep, linep + pos.col);
+ }
+ }
+ else // forward search
+ {
+ if (linep[pos.col] == NUL
+ // at end of line, go to next one
+ // For lisp don't search for match in comment
+ || (lisp && comment_col != MAXCOL
+ && pos.col == (colnr_T)comment_col))
+ {
+ if (pos.lnum == curbuf->b_ml.ml_line_count // end of file
+ // line is exhausted and comment with it,
+ // don't search for match in code
+ || lispcomm)
+ break;
+ ++pos.lnum;
+
+ if (maxtravel && traveled++ > maxtravel)
+ break;
+
+ linep = ml_get(pos.lnum);
+ pos.col = 0;
+ do_quotes = -1;
+ line_breakcheck();
+ if (lisp) // find comment pos in new line
+ comment_col = check_linecomment(linep);
+ }
+ else
+ {
+ if (has_mbyte)
+ pos.col += (*mb_ptr2len)(linep + pos.col);
+ else
+ ++pos.col;
+ }
+ }
+
+ /*
+ * If FM_BLOCKSTOP given, stop at a '{' or '}' in column 0.
+ */
+ if (pos.col == 0 && (flags & FM_BLOCKSTOP)
+ && (linep[0] == '{' || linep[0] == '}'))
+ {
+ if (linep[0] == findc && count == 0) // match!
+ return &pos;
+ break; // out of scope
+ }
+
+ if (comment_dir)
+ {
+ // Note: comments do not nest, and we ignore quotes in them
+ // TODO: ignore comment brackets inside strings
+ if (comment_dir == FORWARD)
+ {
+ if (linep[pos.col] == '*' && linep[pos.col + 1] == '/')
+ {
+ pos.col++;
+ return &pos;
+ }
+ }
+ else // Searching backwards
+ {
+ /*
+ * A comment may contain / * or / /, it may also start or end
+ * with / * /. Ignore a / * after / / and after *.
+ */
+ if (pos.col == 0)
+ continue;
+ else if (raw_string)
+ {
+ if (linep[pos.col - 1] == 'R'
+ && linep[pos.col] == '"'
+ && vim_strchr(linep + pos.col + 1, '(') != NULL)
+ {
+ // Possible start of raw string. Now that we have the
+ // delimiter we can check if it ends before where we
+ // started searching, or before the previously found
+ // raw string start.
+ if (!find_rawstring_end(linep, &pos,
+ count > 0 ? &match_pos : &curwin->w_cursor))
+ {
+ count++;
+ match_pos = pos;
+ match_pos.col--;
+ }
+ linep = ml_get(pos.lnum); // may have been released
+ }
+ }
+ else if ( linep[pos.col - 1] == '/'
+ && linep[pos.col] == '*'
+ && (pos.col == 1 || linep[pos.col - 2] != '*')
+ && (int)pos.col < comment_col)
+ {
+ count++;
+ match_pos = pos;
+ match_pos.col--;
+ }
+ else if (linep[pos.col - 1] == '*' && linep[pos.col] == '/')
+ {
+ if (count > 0)
+ pos = match_pos;
+ else if (pos.col > 1 && linep[pos.col - 2] == '/'
+ && (int)pos.col <= comment_col)
+ pos.col -= 2;
+ else if (ignore_cend)
+ continue;
+ else
+ return NULL;
+ return &pos;
+ }
+ }
+ continue;
+ }
+
+ /*
+ * If smart matching ('cpoptions' does not contain '%'), braces inside
+ * of quotes are ignored, but only if there is an even number of
+ * quotes in the line.
+ */
+ if (cpo_match)
+ do_quotes = 0;
+ else if (do_quotes == -1)
+ {
+ /*
+ * Count the number of quotes in the line, skipping \" and '"'.
+ * Watch out for "\\".
+ */
+ at_start = do_quotes;
+ for (ptr = linep; *ptr; ++ptr)
+ {
+ if (ptr == linep + pos.col + backwards)
+ at_start = (do_quotes & 1);
+ if (*ptr == '"'
+ && (ptr == linep || ptr[-1] != '\'' || ptr[1] != '\''))
+ ++do_quotes;
+ if (*ptr == '\\' && ptr[1] != NUL)
+ ++ptr;
+ }
+ do_quotes &= 1; // result is 1 with even number of quotes
+
+ /*
+ * If we find an uneven count, check current line and previous
+ * one for a '\' at the end.
+ */
+ if (!do_quotes)
+ {
+ inquote = FALSE;
+ if (ptr[-1] == '\\')
+ {
+ do_quotes = 1;
+ if (start_in_quotes == MAYBE)
+ {
+ // Do we need to use at_start here?
+ inquote = TRUE;
+ start_in_quotes = TRUE;
+ }
+ else if (backwards)
+ inquote = TRUE;
+ }
+ if (pos.lnum > 1)
+ {
+ ptr = ml_get(pos.lnum - 1);
+ if (*ptr && *(ptr + STRLEN(ptr) - 1) == '\\')
+ {
+ do_quotes = 1;
+ if (start_in_quotes == MAYBE)
+ {
+ inquote = at_start;
+ if (inquote)
+ start_in_quotes = TRUE;
+ }
+ else if (!backwards)
+ inquote = TRUE;
+ }
+
+ // ml_get() only keeps one line, need to get linep again
+ linep = ml_get(pos.lnum);
+ }
+ }
+ }
+ if (start_in_quotes == MAYBE)
+ start_in_quotes = FALSE;
+
+ /*
+ * If 'smartmatch' is set:
+ * Things inside quotes are ignored by setting 'inquote'. If we
+ * find a quote without a preceding '\' invert 'inquote'. At the
+ * end of a line not ending in '\' we reset 'inquote'.
+ *
+ * In lines with an uneven number of quotes (without preceding '\')
+ * we do not know which part to ignore. Therefore we only set
+ * inquote if the number of quotes in a line is even, unless this
+ * line or the previous one ends in a '\'. Complicated, isn't it?
+ */
+ c = PTR2CHAR(linep + pos.col);
+ switch (c)
+ {
+ case NUL:
+ // at end of line without trailing backslash, reset inquote
+ if (pos.col == 0 || linep[pos.col - 1] != '\\')
+ {
+ inquote = FALSE;
+ start_in_quotes = FALSE;
+ }
+ break;
+
+ case '"':
+ // a quote that is preceded with an odd number of backslashes is
+ // ignored
+ if (do_quotes)
+ {
+ int col;
+
+ for (col = pos.col - 1; col >= 0; --col)
+ if (linep[col] != '\\')
+ break;
+ if ((((int)pos.col - 1 - col) & 1) == 0)
+ {
+ inquote = !inquote;
+ start_in_quotes = FALSE;
+ }
+ }
+ break;
+
+ /*
+ * If smart matching ('cpoptions' does not contain '%'):
+ * Skip things in single quotes: 'x' or '\x'. Be careful for single
+ * single quotes, eg jon's. Things like '\233' or '\x3f' are not
+ * skipped, there is never a brace in them.
+ * Ignore this when finding matches for `'.
+ */
+ case '\'':
+ if (!cpo_match && initc != '\'' && findc != '\'')
+ {
+ if (backwards)
+ {
+ if (pos.col > 1)
+ {
+ if (linep[pos.col - 2] == '\'')
+ {
+ pos.col -= 2;
+ break;
+ }
+ else if (linep[pos.col - 2] == '\\'
+ && pos.col > 2 && linep[pos.col - 3] == '\'')
+ {
+ pos.col -= 3;
+ break;
+ }
+ }
+ }
+ else if (linep[pos.col + 1]) // forward search
+ {
+ if (linep[pos.col + 1] == '\\'
+ && linep[pos.col + 2] && linep[pos.col + 3] == '\'')
+ {
+ pos.col += 3;
+ break;
+ }
+ else if (linep[pos.col + 2] == '\'')
+ {
+ pos.col += 2;
+ break;
+ }
+ }
+ }
+ // FALLTHROUGH
+
+ default:
+ /*
+ * For Lisp skip over backslashed (), {} and [].
+ * (actually, we skip #\( et al)
+ */
+ if (curbuf->b_p_lisp
+ && vim_strchr((char_u *)"{}()[]", c) != NULL
+ && pos.col > 1
+ && check_prevcol(linep, pos.col, '\\', NULL)
+ && check_prevcol(linep, pos.col - 1, '#', NULL))
+ break;
+
+ // Check for match outside of quotes, and inside of
+ // quotes when the start is also inside of quotes.
+ if ((!inquote || start_in_quotes == TRUE)
+ && (c == initc || c == findc))
+ {
+ int col, bslcnt = 0;
+
+ if (!cpo_bsl)
+ {
+ for (col = pos.col; check_prevcol(linep, col, '\\', &col);)
+ bslcnt++;
+ }
+ // Only accept a match when 'M' is in 'cpo' or when escaping
+ // is what we expect.
+ if (cpo_bsl || (bslcnt & 1) == match_escaped)
+ {
+ if (c == initc)
+ count++;
+ else
+ {
+ if (count == 0)
+ return &pos;
+ count--;
+ }
+ }
+ }
+ }
+ }
+
+ if (comment_dir == BACKWARD && count > 0)
+ {
+ pos = match_pos;
+ return &pos;
+ }
+ return (pos_T *)NULL; // never found it
+}
+
+/*
+ * Check if line[] contains a / / comment.
+ * Return MAXCOL if not, otherwise return the column.
+ */
+ int
+check_linecomment(char_u *line)
+{
+ char_u *p;
+
+ p = line;
+ // skip Lispish one-line comments
+ if (curbuf->b_p_lisp)
+ {
+ if (vim_strchr(p, ';') != NULL) // there may be comments
+ {
+ int in_str = FALSE; // inside of string
+
+ p = line; // scan from start
+ while ((p = vim_strpbrk(p, (char_u *)"\";")) != NULL)
+ {
+ if (*p == '"')
+ {
+ if (in_str)
+ {
+ if (*(p - 1) != '\\') // skip escaped quote
+ in_str = FALSE;
+ }
+ else if (p == line || ((p - line) >= 2
+ // skip #\" form
+ && *(p - 1) != '\\' && *(p - 2) != '#'))
+ in_str = TRUE;
+ }
+ else if (!in_str && ((p - line) < 2
+ || (*(p - 1) != '\\' && *(p - 2) != '#'))
+ && !is_pos_in_string(line, (colnr_T)(p - line)))
+ break; // found!
+ ++p;
+ }
+ }
+ else
+ p = NULL;
+ }
+ else
+ while ((p = vim_strchr(p, '/')) != NULL)
+ {
+ // Accept a double /, unless it's preceded with * and followed by
+ // *, because * / / * is an end and start of a C comment. Only
+ // accept the position if it is not inside a string.
+ if (p[1] == '/' && (p == line || p[-1] != '*' || p[2] != '*')
+ && !is_pos_in_string(line, (colnr_T)(p - line)))
+ break;
+ ++p;
+ }
+
+ if (p == NULL)
+ return MAXCOL;
+ return (int)(p - line);
+}
+
+/*
+ * Move cursor briefly to character matching the one under the cursor.
+ * Used for Insert mode and "r" command.
+ * Show the match only if it is visible on the screen.
+ * If there isn't a match, then beep.
+ */
+ void
+showmatch(
+ int c) // char to show match for
+{
+ pos_T *lpos, save_cursor;
+ pos_T mpos;
+ colnr_T vcol;
+ long save_so;
+ long save_siso;
+#ifdef CURSOR_SHAPE
+ int save_state;
+#endif
+ colnr_T save_dollar_vcol;
+ char_u *p;
+ long *so = curwin->w_p_so >= 0 ? &curwin->w_p_so : &p_so;
+ long *siso = curwin->w_p_siso >= 0 ? &curwin->w_p_siso : &p_siso;
+
+ /*
+ * Only show match for chars in the 'matchpairs' option.
+ */
+ // 'matchpairs' is "x:y,x:y"
+ for (p = curbuf->b_p_mps; *p != NUL; ++p)
+ {
+#ifdef FEAT_RIGHTLEFT
+ if (PTR2CHAR(p) == c && (curwin->w_p_rl ^ p_ri))
+ break;
+#endif
+ p += mb_ptr2len(p) + 1;
+ if (PTR2CHAR(p) == c
+#ifdef FEAT_RIGHTLEFT
+ && !(curwin->w_p_rl ^ p_ri)
+#endif
+ )
+ break;
+ p += mb_ptr2len(p);
+ if (*p == NUL)
+ return;
+ }
+ if (*p == NUL)
+ return;
+
+ if ((lpos = findmatch(NULL, NUL)) == NULL) // no match, so beep
+ {
+ vim_beep(BO_MATCH);
+ return;
+ }
+
+ if (lpos->lnum < curwin->w_topline || lpos->lnum >= curwin->w_botline)
+ return;
+
+ if (!curwin->w_p_wrap)
+ getvcol(curwin, lpos, NULL, &vcol, NULL);
+
+ int col_visible = (curwin->w_p_wrap
+ || (vcol >= curwin->w_leftcol
+ && vcol < curwin->w_leftcol + curwin->w_width));
+ if (!col_visible)
+ return;
+
+ mpos = *lpos; // save the pos, update_screen() may change it
+ save_cursor = curwin->w_cursor;
+ save_so = *so;
+ save_siso = *siso;
+ // Handle "$" in 'cpo': If the ')' is typed on top of the "$",
+ // stop displaying the "$".
+ if (dollar_vcol >= 0 && dollar_vcol == curwin->w_virtcol)
+ dollar_vcol = -1;
+ ++curwin->w_virtcol; // do display ')' just before "$"
+ update_screen(UPD_VALID); // show the new char first
+
+ save_dollar_vcol = dollar_vcol;
+#ifdef CURSOR_SHAPE
+ save_state = State;
+ State = MODE_SHOWMATCH;
+ ui_cursor_shape(); // may show different cursor shape
+#endif
+ curwin->w_cursor = mpos; // move to matching char
+ *so = 0; // don't use 'scrolloff' here
+ *siso = 0; // don't use 'sidescrolloff' here
+ showruler(FALSE);
+ setcursor();
+ cursor_on(); // make sure that the cursor is shown
+ out_flush_cursor(TRUE, FALSE);
+
+ // Restore dollar_vcol(), because setcursor() may call curs_rows()
+ // which resets it if the matching position is in a previous line
+ // and has a higher column number.
+ dollar_vcol = save_dollar_vcol;
+
+ /*
+ * brief pause, unless 'm' is present in 'cpo' and a character is
+ * available.
+ */
+ if (vim_strchr(p_cpo, CPO_SHOWMATCH) != NULL)
+ ui_delay(p_mat * 100L + 8, TRUE);
+ else if (!char_avail())
+ ui_delay(p_mat * 100L + 9, FALSE);
+ curwin->w_cursor = save_cursor; // restore cursor position
+ *so = save_so;
+ *siso = save_siso;
+#ifdef CURSOR_SHAPE
+ State = save_state;
+ ui_cursor_shape(); // may show different cursor shape
+#endif
+}
+
+/*
+ * Check if the pattern is zero-width.
+ * If move is TRUE, check from the beginning of the buffer, else from position
+ * "cur".
+ * "direction" is FORWARD or BACKWARD.
+ * Returns TRUE, FALSE or -1 for failure.
+ */
+ static int
+is_zero_width(char_u *pattern, int move, pos_T *cur, int direction)
+{
+ regmmatch_T regmatch;
+ int nmatched = 0;
+ int result = -1;
+ pos_T pos;
+ int called_emsg_before = called_emsg;
+ int flag = 0;
+
+ if (pattern == NULL)
+ pattern = spats[last_idx].pat;
+
+ if (search_regcomp(pattern, NULL, RE_SEARCH, RE_SEARCH,
+ SEARCH_KEEP, &regmatch) == FAIL)
+ return -1;
+
+ // init startcol correctly
+ regmatch.startpos[0].col = -1;
+ // move to match
+ if (move)
+ {
+ CLEAR_POS(&pos);
+ }
+ else
+ {
+ pos = *cur;
+ // accept a match at the cursor position
+ flag = SEARCH_START;
+ }
+
+ if (searchit(curwin, curbuf, &pos, NULL, direction, pattern, 1,
+ SEARCH_KEEP + flag, RE_SEARCH, NULL) != FAIL)
+ {
+ // Zero-width pattern should match somewhere, then we can check if
+ // start and end are in the same position.
+ do
+ {
+ regmatch.startpos[0].col++;
+ nmatched = vim_regexec_multi(&regmatch, curwin, curbuf,
+ pos.lnum, regmatch.startpos[0].col, NULL);
+ if (nmatched != 0)
+ break;
+ } while (regmatch.regprog != NULL
+ && direction == FORWARD ? regmatch.startpos[0].col < pos.col
+ : regmatch.startpos[0].col > pos.col);
+
+ if (called_emsg == called_emsg_before)
+ {
+ result = (nmatched != 0
+ && regmatch.startpos[0].lnum == regmatch.endpos[0].lnum
+ && regmatch.startpos[0].col == regmatch.endpos[0].col);
+ }
+ }
+
+ vim_regfree(regmatch.regprog);
+ return result;
+}
+
+
+/*
+ * Find next search match under cursor, cursor at end.
+ * Used while an operator is pending, and in Visual mode.
+ */
+ int
+current_search(
+ long count,
+ int forward) // TRUE for forward, FALSE for backward
+{
+ pos_T start_pos; // start position of the pattern match
+ pos_T end_pos; // end position of the pattern match
+ pos_T orig_pos; // position of the cursor at beginning
+ pos_T pos; // position after the pattern
+ int i;
+ int dir;
+ int result; // result of various function calls
+ char_u old_p_ws = p_ws;
+ int flags = 0;
+ pos_T save_VIsual = VIsual;
+ int zero_width;
+ int skip_first_backward;
+
+ // Correct cursor when 'selection' is exclusive
+ if (VIsual_active && *p_sel == 'e' && LT_POS(VIsual, curwin->w_cursor))
+ dec_cursor();
+
+ // When searching forward and the cursor is at the start of the Visual
+ // area, skip the first search backward, otherwise it doesn't move.
+ skip_first_backward = forward && VIsual_active
+ && LT_POS(curwin->w_cursor, VIsual);
+
+ orig_pos = pos = curwin->w_cursor;
+ if (VIsual_active)
+ {
+ if (forward)
+ incl(&pos);
+ else
+ decl(&pos);
+ }
+
+ // Is the pattern is zero-width?, this time, don't care about the direction
+ zero_width = is_zero_width(spats[last_idx].pat, TRUE, &curwin->w_cursor,
+ FORWARD);
+ if (zero_width == -1)
+ return FAIL; // pattern not found
+
+ /*
+ * The trick is to first search backwards and then search forward again,
+ * so that a match at the current cursor position will be correctly
+ * captured. When "forward" is false do it the other way around.
+ */
+ for (i = 0; i < 2; i++)
+ {
+ if (forward)
+ {
+ if (i == 0 && skip_first_backward)
+ continue;
+ dir = i;
+ }
+ else
+ dir = !i;
+
+ flags = 0;
+ if (!dir && !zero_width)
+ flags = SEARCH_END;
+ end_pos = pos;
+
+ // wrapping should not occur in the first round
+ if (i == 0)
+ p_ws = FALSE;
+
+ result = searchit(curwin, curbuf, &pos, &end_pos,
+ (dir ? FORWARD : BACKWARD),
+ spats[last_idx].pat, (long) (i ? count : 1),
+ SEARCH_KEEP | flags, RE_SEARCH, NULL);
+
+ p_ws = old_p_ws;
+
+ // First search may fail, but then start searching from the
+ // beginning of the file (cursor might be on the search match)
+ // except when Visual mode is active, so that extending the visual
+ // selection works.
+ if (i == 1 && !result) // not found, abort
+ {
+ curwin->w_cursor = orig_pos;
+ if (VIsual_active)
+ VIsual = save_VIsual;
+ return FAIL;
+ }
+ else if (i == 0 && !result)
+ {
+ if (forward)
+ {
+ // try again from start of buffer
+ CLEAR_POS(&pos);
+ }
+ else
+ {
+ // try again from end of buffer
+ // searching backwards, so set pos to last line and col
+ pos.lnum = curwin->w_buffer->b_ml.ml_line_count;
+ pos.col = (colnr_T)STRLEN(
+ ml_get(curwin->w_buffer->b_ml.ml_line_count));
+ }
+ }
+ }
+
+ start_pos = pos;
+
+ if (!VIsual_active)
+ VIsual = start_pos;
+
+ // put the cursor after the match
+ curwin->w_cursor = end_pos;
+ if (LT_POS(VIsual, end_pos) && forward)
+ {
+ if (skip_first_backward)
+ // put the cursor on the start of the match
+ curwin->w_cursor = pos;
+ else
+ // put the cursor on last character of match
+ dec_cursor();
+ }
+ else if (VIsual_active && LT_POS(curwin->w_cursor, VIsual) && forward)
+ curwin->w_cursor = pos; // put the cursor on the start of the match
+ VIsual_active = TRUE;
+ VIsual_mode = 'v';
+
+ if (*p_sel == 'e')
+ {
+ // Correction for exclusive selection depends on the direction.
+ if (forward && LTOREQ_POS(VIsual, curwin->w_cursor))
+ inc_cursor();
+ else if (!forward && LTOREQ_POS(curwin->w_cursor, VIsual))
+ inc(&VIsual);
+ }
+
+#ifdef FEAT_FOLDING
+ if (fdo_flags & FDO_SEARCH && KeyTyped)
+ foldOpenCursor();
+#endif
+
+ may_start_select('c');
+ setmouse();
+#ifdef FEAT_CLIPBOARD
+ // Make sure the clipboard gets updated. Needed because start and
+ // end are still the same, and the selection needs to be owned
+ clip_star.vmode = NUL;
+#endif
+ redraw_curbuf_later(UPD_INVERTED);
+ showmode();
+
+ return OK;
+}
+
+/*
+ * return TRUE if line 'lnum' is empty or has white chars only.
+ */
+ int
+linewhite(linenr_T lnum)
+{
+ char_u *p;
+
+ p = skipwhite(ml_get(lnum));
+ return (*p == NUL);
+}
+
+/*
+ * Add the search count "[3/19]" to "msgbuf".
+ * See update_search_stat() for other arguments.
+ */
+ static void
+cmdline_search_stat(
+ int dirc,
+ pos_T *pos,
+ pos_T *cursor_pos,
+ int show_top_bot_msg,
+ char_u *msgbuf,
+ int recompute,
+ int maxcount,
+ long timeout)
+{
+ searchstat_T stat;
+
+ update_search_stat(dirc, pos, cursor_pos, &stat, recompute, maxcount,
+ timeout);
+ if (stat.cur <= 0)
+ return;
+
+ char t[SEARCH_STAT_BUF_LEN];
+ size_t len;
+
+#ifdef FEAT_RIGHTLEFT
+ if (curwin->w_p_rl && *curwin->w_p_rlc == 's')
+ {
+ if (stat.incomplete == 1)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[?/??]");
+ else if (stat.cnt > maxcount && stat.cur > maxcount)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>%d/>%d]",
+ maxcount, maxcount);
+ else if (stat.cnt > maxcount)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>%d/%d]",
+ maxcount, stat.cur);
+ else
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]",
+ stat.cnt, stat.cur);
+ }
+ else
+#endif
+ {
+ if (stat.incomplete == 1)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[?/??]");
+ else if (stat.cnt > maxcount && stat.cur > maxcount)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>%d/>%d]",
+ maxcount, maxcount);
+ else if (stat.cnt > maxcount)
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/>%d]",
+ stat.cur, maxcount);
+ else
+ vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]",
+ stat.cur, stat.cnt);
+ }
+
+ len = STRLEN(t);
+ if (show_top_bot_msg && len + 2 < SEARCH_STAT_BUF_LEN)
+ {
+ mch_memmove(t + 2, t, len);
+ t[0] = 'W';
+ t[1] = ' ';
+ len += 2;
+ }
+
+ size_t msgbuf_len = STRLEN(msgbuf);
+ if (len > msgbuf_len)
+ len = msgbuf_len;
+ mch_memmove(msgbuf + msgbuf_len - len, t, len);
+
+ if (dirc == '?' && stat.cur == maxcount + 1)
+ stat.cur = -1;
+
+ // keep the message even after redraw, but don't put in history
+ msg_hist_off = TRUE;
+ give_warning(msgbuf, FALSE);
+ msg_hist_off = FALSE;
+}
+
+/*
+ * Add the search count information to "stat".
+ * "stat" must not be NULL.
+ * When "recompute" is TRUE always recompute the numbers.
+ * dirc == 0: don't find the next/previous match (only set the result to "stat")
+ * dirc == '/': find the next match
+ * dirc == '?': find the previous match
+ */
+ static void
+update_search_stat(
+ int dirc,
+ pos_T *pos,
+ pos_T *cursor_pos,
+ searchstat_T *stat,
+ int recompute,
+ int maxcount,
+ long timeout UNUSED)
+{
+ int save_ws = p_ws;
+ int wraparound = FALSE;
+ pos_T p = (*pos);
+ static pos_T lastpos = {0, 0, 0};
+ static int cur = 0;
+ static int cnt = 0;
+ static int exact_match = FALSE;
+ static int incomplete = 0;
+ static int last_maxcount = SEARCH_STAT_DEF_MAX_COUNT;
+ static int chgtick = 0;
+ static char_u *lastpat = NULL;
+ static buf_T *lbuf = NULL;
+#ifdef FEAT_RELTIME
+ proftime_T start;
+#endif
+
+ vim_memset(stat, 0, sizeof(searchstat_T));
+
+ if (dirc == 0 && !recompute && !EMPTY_POS(lastpos))
+ {
+ stat->cur = cur;
+ stat->cnt = cnt;
+ stat->exact_match = exact_match;
+ stat->incomplete = incomplete;
+ stat->last_maxcount = last_maxcount;
+ return;
+ }
+ last_maxcount = maxcount;
+
+ wraparound = ((dirc == '?' && LT_POS(lastpos, p))
+ || (dirc == '/' && LT_POS(p, lastpos)));
+
+ // If anything relevant changed the count has to be recomputed.
+ // MB_STRNICMP ignores case, but we should not ignore case.
+ // Unfortunately, there is no MB_STRNICMP function.
+ // XXX: above comment should be "no MB_STRCMP function" ?
+ if (!(chgtick == CHANGEDTICK(curbuf)
+ && MB_STRNICMP(lastpat, spats[last_idx].pat, STRLEN(lastpat)) == 0
+ && STRLEN(lastpat) == STRLEN(spats[last_idx].pat)
+ && EQUAL_POS(lastpos, *cursor_pos)
+ && lbuf == curbuf) || wraparound || cur < 0
+ || (maxcount > 0 && cur > maxcount) || recompute)
+ {
+ cur = 0;
+ cnt = 0;
+ exact_match = FALSE;
+ incomplete = 0;
+ CLEAR_POS(&lastpos);
+ lbuf = curbuf;
+ }
+
+ if (EQUAL_POS(lastpos, *cursor_pos) && !wraparound
+ && (dirc == 0 || dirc == '/' ? cur < cnt : cur > 0))
+ cur += dirc == 0 ? 0 : dirc == '/' ? 1 : -1;
+ else
+ {
+ int done_search = FALSE;
+ pos_T endpos = {0, 0, 0};
+
+ p_ws = FALSE;
+#ifdef FEAT_RELTIME
+ if (timeout > 0)
+ profile_setlimit(timeout, &start);
+#endif
+ while (!got_int && searchit(curwin, curbuf, &lastpos, &endpos,
+ FORWARD, NULL, 1, SEARCH_KEEP, RE_LAST, NULL) != FAIL)
+ {
+ done_search = TRUE;
+#ifdef FEAT_RELTIME
+ // Stop after passing the time limit.
+ if (timeout > 0 && profile_passed_limit(&start))
+ {
+ incomplete = 1;
+ break;
+ }
+#endif
+ cnt++;
+ if (LTOREQ_POS(lastpos, p))
+ {
+ cur = cnt;
+ if (LT_POS(p, endpos))
+ exact_match = TRUE;
+ }
+ fast_breakcheck();
+ if (maxcount > 0 && cnt > maxcount)
+ {
+ incomplete = 2; // max count exceeded
+ break;
+ }
+ }
+ if (got_int)
+ cur = -1; // abort
+ if (done_search)
+ {
+ vim_free(lastpat);
+ lastpat = vim_strsave(spats[last_idx].pat);
+ chgtick = CHANGEDTICK(curbuf);
+ lbuf = curbuf;
+ lastpos = p;
+ }
+ }
+ stat->cur = cur;
+ stat->cnt = cnt;
+ stat->exact_match = exact_match;
+ stat->incomplete = incomplete;
+ stat->last_maxcount = last_maxcount;
+ p_ws = save_ws;
+}
+
+#if defined(FEAT_FIND_ID) || defined(PROTO)
+
+/*
+ * Get line "lnum" and copy it into "buf[LSIZE]".
+ * The copy is made because the regexp may make the line invalid when using a
+ * mark.
+ */
+ static char_u *
+get_line_and_copy(linenr_T lnum, char_u *buf)
+{
+ char_u *line = ml_get(lnum);
+
+ vim_strncpy(buf, line, LSIZE - 1);
+ return buf;
+}
+
+/*
+ * Find identifiers or defines in included files.
+ * If p_ic && compl_status_sol() then ptr must be in lowercase.
+ */
+ void
+find_pattern_in_path(
+ char_u *ptr, // pointer to search pattern
+ int dir UNUSED, // direction of expansion
+ int len, // length of search pattern
+ int whole, // match whole words only
+ int skip_comments, // don't match inside comments
+ int type, // Type of search; are we looking for a type?
+ // a macro?
+ long count,
+ int action, // What to do when we find it
+ linenr_T start_lnum, // first line to start searching
+ linenr_T end_lnum) // last line for searching
+{
+ SearchedFile *files; // Stack of included files
+ SearchedFile *bigger; // When we need more space
+ int max_path_depth = 50;
+ long match_count = 1;
+
+ char_u *pat;
+ char_u *new_fname;
+ char_u *curr_fname = curbuf->b_fname;
+ char_u *prev_fname = NULL;
+ linenr_T lnum;
+ int depth;
+ int depth_displayed; // For type==CHECK_PATH
+ int old_files;
+ int already_searched;
+ char_u *file_line;
+ char_u *line;
+ char_u *p;
+ char_u save_char;
+ int define_matched;
+ regmatch_T regmatch;
+ regmatch_T incl_regmatch;
+ regmatch_T def_regmatch;
+ int matched = FALSE;
+ int did_show = FALSE;
+ int found = FALSE;
+ int i;
+ char_u *already = NULL;
+ char_u *startp = NULL;
+ char_u *inc_opt = NULL;
+#if defined(FEAT_QUICKFIX)
+ win_T *curwin_save = NULL;
+#endif
+
+ regmatch.regprog = NULL;
+ incl_regmatch.regprog = NULL;
+ def_regmatch.regprog = NULL;
+
+ file_line = alloc(LSIZE);
+ if (file_line == NULL)
+ return;
+
+ if (type != CHECK_PATH && type != FIND_DEFINE
+ // when CONT_SOL is set compare "ptr" with the beginning of the
+ // line is faster than quote_meta/regcomp/regexec "ptr" -- Acevedo
+ && !compl_status_sol())
+ {
+ pat = alloc(len + 5);
+ if (pat == NULL)
+ goto fpip_end;
+ sprintf((char *)pat, whole ? "\\<%.*s\\>" : "%.*s", len, ptr);
+ // ignore case according to p_ic, p_scs and pat
+ regmatch.rm_ic = ignorecase(pat);
+ regmatch.regprog = vim_regcomp(pat, magic_isset() ? RE_MAGIC : 0);
+ vim_free(pat);
+ if (regmatch.regprog == NULL)
+ goto fpip_end;
+ }
+ inc_opt = (*curbuf->b_p_inc == NUL) ? p_inc : curbuf->b_p_inc;
+ if (*inc_opt != NUL)
+ {
+ incl_regmatch.regprog = vim_regcomp(inc_opt,
+ magic_isset() ? RE_MAGIC : 0);
+ if (incl_regmatch.regprog == NULL)
+ goto fpip_end;
+ incl_regmatch.rm_ic = FALSE; // don't ignore case in incl. pat.
+ }
+ if (type == FIND_DEFINE && (*curbuf->b_p_def != NUL || *p_def != NUL))
+ {
+ def_regmatch.regprog = vim_regcomp(*curbuf->b_p_def == NUL
+ ? p_def : curbuf->b_p_def,
+ magic_isset() ? RE_MAGIC : 0);
+ if (def_regmatch.regprog == NULL)
+ goto fpip_end;
+ def_regmatch.rm_ic = FALSE; // don't ignore case in define pat.
+ }
+ files = lalloc_clear(max_path_depth * sizeof(SearchedFile), TRUE);
+ if (files == NULL)
+ goto fpip_end;
+ old_files = max_path_depth;
+ depth = depth_displayed = -1;
+
+ lnum = start_lnum;
+ if (end_lnum > curbuf->b_ml.ml_line_count)
+ end_lnum = curbuf->b_ml.ml_line_count;
+ if (lnum > end_lnum) // do at least one line
+ lnum = end_lnum;
+ line = get_line_and_copy(lnum, file_line);
+
+ for (;;)
+ {
+ if (incl_regmatch.regprog != NULL
+ && vim_regexec(&incl_regmatch, line, (colnr_T)0))
+ {
+ char_u *p_fname = (curr_fname == curbuf->b_fname)
+ ? curbuf->b_ffname : curr_fname;
+
+ if (inc_opt != NULL && strstr((char *)inc_opt, "\\zs") != NULL)
+ // Use text from '\zs' to '\ze' (or end) of 'include'.
+ new_fname = find_file_name_in_path(incl_regmatch.startp[0],
+ (int)(incl_regmatch.endp[0] - incl_regmatch.startp[0]),
+ FNAME_EXP|FNAME_INCL|FNAME_REL, 1L, p_fname);
+ else
+ // Use text after match with 'include'.
+ new_fname = file_name_in_line(incl_regmatch.endp[0], 0,
+ FNAME_EXP|FNAME_INCL|FNAME_REL, 1L, p_fname, NULL);
+ already_searched = FALSE;
+ if (new_fname != NULL)
+ {
+ // Check whether we have already searched in this file
+ for (i = 0;; i++)
+ {
+ if (i == depth + 1)
+ i = old_files;
+ if (i == max_path_depth)
+ break;
+ if (fullpathcmp(new_fname, files[i].name, TRUE, TRUE)
+ & FPC_SAME)
+ {
+ if (type != CHECK_PATH
+ && action == ACTION_SHOW_ALL
+ && files[i].matched)
+ {
+ msg_putchar('\n'); // cursor below last one
+ if (!got_int) // don't display if 'q'
+ // typed at "--more--"
+ // message
+ {
+ msg_home_replace_hl(new_fname);
+ msg_puts(_(" (includes previously listed match)"));
+ prev_fname = NULL;
+ }
+ }
+ VIM_CLEAR(new_fname);
+ already_searched = TRUE;
+ break;
+ }
+ }
+ }
+
+ if (type == CHECK_PATH && (action == ACTION_SHOW_ALL
+ || (new_fname == NULL && !already_searched)))
+ {
+ if (did_show)
+ msg_putchar('\n'); // cursor below last one
+ else
+ {
+ gotocmdline(TRUE); // cursor at status line
+ msg_puts_title(_("--- Included files "));
+ if (action != ACTION_SHOW_ALL)
+ msg_puts_title(_("not found "));
+ msg_puts_title(_("in path ---\n"));
+ }
+ did_show = TRUE;
+ while (depth_displayed < depth && !got_int)
+ {
+ ++depth_displayed;
+ for (i = 0; i < depth_displayed; i++)
+ msg_puts(" ");
+ msg_home_replace(files[depth_displayed].name);
+ msg_puts(" -->\n");
+ }
+ if (!got_int) // don't display if 'q' typed
+ // for "--more--" message
+ {
+ for (i = 0; i <= depth_displayed; i++)
+ msg_puts(" ");
+ if (new_fname != NULL)
+ {
+ // using "new_fname" is more reliable, e.g., when
+ // 'includeexpr' is set.
+ msg_outtrans_attr(new_fname, HL_ATTR(HLF_D));
+ }
+ else
+ {
+ /*
+ * Isolate the file name.
+ * Include the surrounding "" or <> if present.
+ */
+ if (inc_opt != NULL
+ && strstr((char *)inc_opt, "\\zs") != NULL)
+ {
+ // pattern contains \zs, use the match
+ p = incl_regmatch.startp[0];
+ i = (int)(incl_regmatch.endp[0]
+ - incl_regmatch.startp[0]);
+ }
+ else
+ {
+ // find the file name after the end of the match
+ for (p = incl_regmatch.endp[0];
+ *p && !vim_isfilec(*p); p++)
+ ;
+ for (i = 0; vim_isfilec(p[i]); i++)
+ ;
+ }
+
+ if (i == 0)
+ {
+ // Nothing found, use the rest of the line.
+ p = incl_regmatch.endp[0];
+ i = (int)STRLEN(p);
+ }
+ // Avoid checking before the start of the line, can
+ // happen if \zs appears in the regexp.
+ else if (p > line)
+ {
+ if (p[-1] == '"' || p[-1] == '<')
+ {
+ --p;
+ ++i;
+ }
+ if (p[i] == '"' || p[i] == '>')
+ ++i;
+ }
+ save_char = p[i];
+ p[i] = NUL;
+ msg_outtrans_attr(p, HL_ATTR(HLF_D));
+ p[i] = save_char;
+ }
+
+ if (new_fname == NULL && action == ACTION_SHOW_ALL)
+ {
+ if (already_searched)
+ msg_puts(_(" (Already listed)"));
+ else
+ msg_puts(_(" NOT FOUND"));
+ }
+ }
+ out_flush(); // output each line directly
+ }
+
+ if (new_fname != NULL)
+ {
+ // Push the new file onto the file stack
+ if (depth + 1 == old_files)
+ {
+ bigger = ALLOC_MULT(SearchedFile, max_path_depth * 2);
+ if (bigger != NULL)
+ {
+ for (i = 0; i <= depth; i++)
+ bigger[i] = files[i];
+ for (i = depth + 1; i < old_files + max_path_depth; i++)
+ {
+ bigger[i].fp = NULL;
+ bigger[i].name = NULL;
+ bigger[i].lnum = 0;
+ bigger[i].matched = FALSE;
+ }
+ for (i = old_files; i < max_path_depth; i++)
+ bigger[i + max_path_depth] = files[i];
+ old_files += max_path_depth;
+ max_path_depth *= 2;
+ vim_free(files);
+ files = bigger;
+ }
+ }
+ if ((files[depth + 1].fp = mch_fopen((char *)new_fname, "r"))
+ == NULL)
+ vim_free(new_fname);
+ else
+ {
+ if (++depth == old_files)
+ {
+ /*
+ * lalloc() for 'bigger' must have failed above. We
+ * will forget one of our already visited files now.
+ */
+ vim_free(files[old_files].name);
+ ++old_files;
+ }
+ files[depth].name = curr_fname = new_fname;
+ files[depth].lnum = 0;
+ files[depth].matched = FALSE;
+ if (action == ACTION_EXPAND)
+ {
+ msg_hist_off = TRUE; // reset in msg_trunc_attr()
+ vim_snprintf((char*)IObuff, IOSIZE,
+ _("Scanning included file: %s"),
+ (char *)new_fname);
+ msg_trunc_attr((char *)IObuff, TRUE, HL_ATTR(HLF_R));
+ }
+ else if (p_verbose >= 5)
+ {
+ verbose_enter();
+ smsg(_("Searching included file %s"),
+ (char *)new_fname);
+ verbose_leave();
+ }
+
+ }
+ }
+ }
+ else
+ {
+ /*
+ * Check if the line is a define (type == FIND_DEFINE)
+ */
+ p = line;
+search_line:
+ define_matched = FALSE;
+ if (def_regmatch.regprog != NULL
+ && vim_regexec(&def_regmatch, line, (colnr_T)0))
+ {
+ /*
+ * Pattern must be first identifier after 'define', so skip
+ * to that position before checking for match of pattern. Also
+ * don't let it match beyond the end of this identifier.
+ */
+ p = def_regmatch.endp[0];
+ while (*p && !vim_iswordc(*p))
+ p++;
+ define_matched = TRUE;
+ }
+
+ /*
+ * Look for a match. Don't do this if we are looking for a
+ * define and this line didn't match define_prog above.
+ */
+ if (def_regmatch.regprog == NULL || define_matched)
+ {
+ if (define_matched || compl_status_sol())
+ {
+ // compare the first "len" chars from "ptr"
+ startp = skipwhite(p);
+ if (p_ic)
+ matched = !MB_STRNICMP(startp, ptr, len);
+ else
+ matched = !STRNCMP(startp, ptr, len);
+ if (matched && define_matched && whole
+ && vim_iswordc(startp[len]))
+ matched = FALSE;
+ }
+ else if (regmatch.regprog != NULL
+ && vim_regexec(&regmatch, line, (colnr_T)(p - line)))
+ {
+ matched = TRUE;
+ startp = regmatch.startp[0];
+ /*
+ * Check if the line is not a comment line (unless we are
+ * looking for a define). A line starting with "# define"
+ * is not considered to be a comment line.
+ */
+ if (!define_matched && skip_comments)
+ {
+ if ((*line != '#' ||
+ STRNCMP(skipwhite(line + 1), "define", 6) != 0)
+ && get_leader_len(line, NULL, FALSE, TRUE))
+ matched = FALSE;
+
+ /*
+ * Also check for a "/ *" or "/ /" before the match.
+ * Skips lines like "int backwards; / * normal index
+ * * /" when looking for "normal".
+ * Note: Doesn't skip "/ *" in comments.
+ */
+ p = skipwhite(line);
+ if (matched
+ || (p[0] == '/' && p[1] == '*') || p[0] == '*')
+ for (p = line; *p && p < startp; ++p)
+ {
+ if (matched
+ && p[0] == '/'
+ && (p[1] == '*' || p[1] == '/'))
+ {
+ matched = FALSE;
+ // After "//" all text is comment
+ if (p[1] == '/')
+ break;
+ ++p;
+ }
+ else if (!matched && p[0] == '*' && p[1] == '/')
+ {
+ // Can find match after "* /".
+ matched = TRUE;
+ ++p;
+ }
+ }
+ }
+ }
+ }
+ }
+ if (matched)
+ {
+ if (action == ACTION_EXPAND)
+ {
+ int cont_s_ipos = FALSE;
+ int add_r;
+ char_u *aux;
+
+ if (depth == -1 && lnum == curwin->w_cursor.lnum)
+ break;
+ found = TRUE;
+ aux = p = startp;
+ if (compl_status_adding())
+ {
+ p += ins_compl_len();
+ if (vim_iswordp(p))
+ goto exit_matched;
+ p = find_word_start(p);
+ }
+ p = find_word_end(p);
+ i = (int)(p - aux);
+
+ if (compl_status_adding() && i == ins_compl_len())
+ {
+ // IOSIZE > compl_length, so the STRNCPY works
+ STRNCPY(IObuff, aux, i);
+
+ // Get the next line: when "depth" < 0 from the current
+ // buffer, otherwise from the included file. Jump to
+ // exit_matched when past the last line.
+ if (depth < 0)
+ {
+ if (lnum >= end_lnum)
+ goto exit_matched;
+ line = get_line_and_copy(++lnum, file_line);
+ }
+ else if (vim_fgets(line = file_line,
+ LSIZE, files[depth].fp))
+ goto exit_matched;
+
+ // we read a line, set "already" to check this "line" later
+ // if depth >= 0 we'll increase files[depth].lnum far
+ // below -- Acevedo
+ already = aux = p = skipwhite(line);
+ p = find_word_start(p);
+ p = find_word_end(p);
+ if (p > aux)
+ {
+ if (*aux != ')' && IObuff[i-1] != TAB)
+ {
+ if (IObuff[i-1] != ' ')
+ IObuff[i++] = ' ';
+ // IObuf =~ "\(\k\|\i\).* ", thus i >= 2
+ if (p_js
+ && (IObuff[i-2] == '.'
+ || (vim_strchr(p_cpo, CPO_JOINSP) == NULL
+ && (IObuff[i-2] == '?'
+ || IObuff[i-2] == '!'))))
+ IObuff[i++] = ' ';
+ }
+ // copy as much as possible of the new word
+ if (p - aux >= IOSIZE - i)
+ p = aux + IOSIZE - i - 1;
+ STRNCPY(IObuff + i, aux, p - aux);
+ i += (int)(p - aux);
+ cont_s_ipos = TRUE;
+ }
+ IObuff[i] = NUL;
+ aux = IObuff;
+
+ if (i == ins_compl_len())
+ goto exit_matched;
+ }
+
+ add_r = ins_compl_add_infercase(aux, i, p_ic,
+ curr_fname == curbuf->b_fname ? NULL : curr_fname,
+ dir, cont_s_ipos);
+ if (add_r == OK)
+ // if dir was BACKWARD then honor it just once
+ dir = FORWARD;
+ else if (add_r == FAIL)
+ break;
+ }
+ else if (action == ACTION_SHOW_ALL)
+ {
+ found = TRUE;
+ if (!did_show)
+ gotocmdline(TRUE); // cursor at status line
+ if (curr_fname != prev_fname)
+ {
+ if (did_show)
+ msg_putchar('\n'); // cursor below last one
+ if (!got_int) // don't display if 'q' typed
+ // at "--more--" message
+ msg_home_replace_hl(curr_fname);
+ prev_fname = curr_fname;
+ }
+ did_show = TRUE;
+ if (!got_int)
+ show_pat_in_path(line, type, TRUE, action,
+ (depth == -1) ? NULL : files[depth].fp,
+ (depth == -1) ? &lnum : &files[depth].lnum,
+ match_count++);
+
+ // Set matched flag for this file and all the ones that
+ // include it
+ for (i = 0; i <= depth; ++i)
+ files[i].matched = TRUE;
+ }
+ else if (--count <= 0)
+ {
+ found = TRUE;
+ if (depth == -1 && lnum == curwin->w_cursor.lnum
+#if defined(FEAT_QUICKFIX)
+ && g_do_tagpreview == 0
+#endif
+ )
+ emsg(_(e_match_is_on_current_line));
+ else if (action == ACTION_SHOW)
+ {
+ show_pat_in_path(line, type, did_show, action,
+ (depth == -1) ? NULL : files[depth].fp,
+ (depth == -1) ? &lnum : &files[depth].lnum, 1L);
+ did_show = TRUE;
+ }
+ else
+ {
+#ifdef FEAT_GUI
+ need_mouse_correct = TRUE;
+#endif
+#if defined(FEAT_QUICKFIX)
+ // ":psearch" uses the preview window
+ if (g_do_tagpreview != 0)
+ {
+ curwin_save = curwin;
+ prepare_tagpreview(TRUE, TRUE, FALSE);
+ }
+#endif
+ if (action == ACTION_SPLIT)
+ {
+ if (win_split(0, 0) == FAIL)
+ break;
+ RESET_BINDING(curwin);
+ }
+ if (depth == -1)
+ {
+ // match in current file
+#if defined(FEAT_QUICKFIX)
+ if (g_do_tagpreview != 0)
+ {
+ if (!win_valid(curwin_save))
+ break;
+ if (!GETFILE_SUCCESS(getfile(
+ curwin_save->w_buffer->b_fnum, NULL,
+ NULL, TRUE, lnum, FALSE)))
+ break; // failed to jump to file
+ }
+ else
+#endif
+ setpcmark();
+ curwin->w_cursor.lnum = lnum;
+ check_cursor();
+ }
+ else
+ {
+ if (!GETFILE_SUCCESS(getfile(
+ 0, files[depth].name, NULL, TRUE,
+ files[depth].lnum, FALSE)))
+ break; // failed to jump to file
+ // autocommands may have changed the lnum, we don't
+ // want that here
+ curwin->w_cursor.lnum = files[depth].lnum;
+ }
+ }
+ if (action != ACTION_SHOW)
+ {
+ curwin->w_cursor.col = (colnr_T)(startp - line);
+ curwin->w_set_curswant = TRUE;
+ }
+
+#if defined(FEAT_QUICKFIX)
+ if (g_do_tagpreview != 0
+ && curwin != curwin_save && win_valid(curwin_save))
+ {
+ // Return cursor to where we were
+ validate_cursor();
+ redraw_later(UPD_VALID);
+ win_enter(curwin_save, TRUE);
+ }
+# ifdef FEAT_PROP_POPUP
+ else if (WIN_IS_POPUP(curwin))
+ // can't keep focus in popup window
+ win_enter(firstwin, TRUE);
+# endif
+#endif
+ break;
+ }
+exit_matched:
+ matched = FALSE;
+ // look for other matches in the rest of the line if we
+ // are not at the end of it already
+ if (def_regmatch.regprog == NULL
+ && action == ACTION_EXPAND
+ && !compl_status_sol()
+ && *startp != NUL
+ && *(p = startp + mb_ptr2len(startp)) != NUL)
+ goto search_line;
+ }
+ line_breakcheck();
+ if (action == ACTION_EXPAND)
+ ins_compl_check_keys(30, FALSE);
+ if (got_int || ins_compl_interrupted())
+ break;
+
+ /*
+ * Read the next line. When reading an included file and encountering
+ * end-of-file, close the file and continue in the file that included
+ * it.
+ */
+ while (depth >= 0 && !already
+ && vim_fgets(line = file_line, LSIZE, files[depth].fp))
+ {
+ fclose(files[depth].fp);
+ --old_files;
+ files[old_files].name = files[depth].name;
+ files[old_files].matched = files[depth].matched;
+ --depth;
+ curr_fname = (depth == -1) ? curbuf->b_fname
+ : files[depth].name;
+ if (depth < depth_displayed)
+ depth_displayed = depth;
+ }
+ if (depth >= 0) // we could read the line
+ {
+ files[depth].lnum++;
+ // Remove any CR and LF from the line.
+ i = (int)STRLEN(line);
+ if (i > 0 && line[i - 1] == '\n')
+ line[--i] = NUL;
+ if (i > 0 && line[i - 1] == '\r')
+ line[--i] = NUL;
+ }
+ else if (!already)
+ {
+ if (++lnum > end_lnum)
+ break;
+ line = get_line_and_copy(lnum, file_line);
+ }
+ already = NULL;
+ }
+ // End of big for (;;) loop.
+
+ // Close any files that are still open.
+ for (i = 0; i <= depth; i++)
+ {
+ fclose(files[i].fp);
+ vim_free(files[i].name);
+ }
+ for (i = old_files; i < max_path_depth; i++)
+ vim_free(files[i].name);
+ vim_free(files);
+
+ if (type == CHECK_PATH)
+ {
+ if (!did_show)
+ {
+ if (action != ACTION_SHOW_ALL)
+ msg(_("All included files were found"));
+ else
+ msg(_("No included files"));
+ }
+ }
+ else if (!found && action != ACTION_EXPAND)
+ {
+ if (got_int || ins_compl_interrupted())
+ emsg(_(e_interrupted));
+ else if (type == FIND_DEFINE)
+ emsg(_(e_couldnt_find_definition));
+ else
+ emsg(_(e_couldnt_find_pattern));
+ }
+ if (action == ACTION_SHOW || action == ACTION_SHOW_ALL)
+ msg_end();
+
+fpip_end:
+ vim_free(file_line);
+ vim_regfree(regmatch.regprog);
+ vim_regfree(incl_regmatch.regprog);
+ vim_regfree(def_regmatch.regprog);
+}
+
+ static void
+show_pat_in_path(
+ char_u *line,
+ int type,
+ int did_show,
+ int action,
+ FILE *fp,
+ linenr_T *lnum,
+ long count)
+{
+ char_u *p;
+
+ if (did_show)
+ msg_putchar('\n'); // cursor below last one
+ else if (!msg_silent)
+ gotocmdline(TRUE); // cursor at status line
+ if (got_int) // 'q' typed at "--more--" message
+ return;
+ for (;;)
+ {
+ p = line + STRLEN(line) - 1;
+ if (fp != NULL)
+ {
+ // We used fgets(), so get rid of newline at end
+ if (p >= line && *p == '\n')
+ --p;
+ if (p >= line && *p == '\r')
+ --p;
+ *(p + 1) = NUL;
+ }
+ if (action == ACTION_SHOW_ALL)
+ {
+ sprintf((char *)IObuff, "%3ld: ", count); // show match nr
+ msg_puts((char *)IObuff);
+ sprintf((char *)IObuff, "%4ld", *lnum); // show line nr
+ // Highlight line numbers
+ msg_puts_attr((char *)IObuff, HL_ATTR(HLF_N));
+ msg_puts(" ");
+ }
+ msg_prt_line(line, FALSE);
+ out_flush(); // show one line at a time
+
+ // Definition continues until line that doesn't end with '\'
+ if (got_int || type != FIND_DEFINE || p < line || *p != '\\')
+ break;
+
+ if (fp != NULL)
+ {
+ if (vim_fgets(line, LSIZE, fp)) // end of file
+ break;
+ ++*lnum;
+ }
+ else
+ {
+ if (++*lnum > curbuf->b_ml.ml_line_count)
+ break;
+ line = ml_get(*lnum);
+ }
+ msg_putchar('\n');
+ }
+}
+#endif
+
+#ifdef FEAT_VIMINFO
+/*
+ * Return the last used search pattern at "idx".
+ */
+ spat_T *
+get_spat(int idx)
+{
+ return &spats[idx];
+}
+
+/*
+ * Return the last used search pattern index.
+ */
+ int
+get_spat_last_idx(void)
+{
+ return last_idx;
+}
+#endif
+
+#if defined(FEAT_EVAL) || defined(FEAT_PROTO)
+/*
+ * "searchcount()" function
+ */
+ void
+f_searchcount(typval_T *argvars, typval_T *rettv)
+{
+ pos_T pos = curwin->w_cursor;
+ char_u *pattern = NULL;
+ int maxcount = SEARCH_STAT_DEF_MAX_COUNT;
+ long timeout = SEARCH_STAT_DEF_TIMEOUT;
+ int recompute = TRUE;
+ searchstat_T stat;
+
+ if (rettv_dict_alloc(rettv) == FAIL)
+ return;
+
+ if (in_vim9script() && check_for_opt_dict_arg(argvars, 0) == FAIL)
+ return;
+
+ if (shortmess(SHM_SEARCHCOUNT)) // 'shortmess' contains 'S' flag
+ recompute = TRUE;
+
+ if (argvars[0].v_type != VAR_UNKNOWN)
+ {
+ dict_T *dict;
+ dictitem_T *di;
+ listitem_T *li;
+ int error = FALSE;
+
+ if (check_for_nonnull_dict_arg(argvars, 0) == FAIL)
+ return;
+ dict = argvars[0].vval.v_dict;
+ di = dict_find(dict, (char_u *)"timeout", -1);
+ if (di != NULL)
+ {
+ timeout = (long)tv_get_number_chk(&di->di_tv, &error);
+ if (error)
+ return;
+ }
+ di = dict_find(dict, (char_u *)"maxcount", -1);
+ if (di != NULL)
+ {
+ maxcount = (int)tv_get_number_chk(&di->di_tv, &error);
+ if (error)
+ return;
+ }
+ recompute = dict_get_bool(dict, "recompute", recompute);
+ di = dict_find(dict, (char_u *)"pattern", -1);
+ if (di != NULL)
+ {
+ pattern = tv_get_string_chk(&di->di_tv);
+ if (pattern == NULL)
+ return;
+ }
+ di = dict_find(dict, (char_u *)"pos", -1);
+ if (di != NULL)
+ {
+ if (di->di_tv.v_type != VAR_LIST)
+ {
+ semsg(_(e_invalid_argument_str), "pos");
+ return;
+ }
+ if (list_len(di->di_tv.vval.v_list) != 3)
+ {
+ semsg(_(e_invalid_argument_str), "List format should be [lnum, col, off]");
+ return;
+ }
+ li = list_find(di->di_tv.vval.v_list, 0L);
+ if (li != NULL)
+ {
+ pos.lnum = tv_get_number_chk(&li->li_tv, &error);
+ if (error)
+ return;
+ }
+ li = list_find(di->di_tv.vval.v_list, 1L);
+ if (li != NULL)
+ {
+ pos.col = tv_get_number_chk(&li->li_tv, &error) - 1;
+ if (error)
+ return;
+ }
+ li = list_find(di->di_tv.vval.v_list, 2L);
+ if (li != NULL)
+ {
+ pos.coladd = tv_get_number_chk(&li->li_tv, &error);
+ if (error)
+ return;
+ }
+ }
+ }
+
+ save_last_search_pattern();
+#ifdef FEAT_SEARCH_EXTRA
+ save_incsearch_state();
+#endif
+ if (pattern != NULL)
+ {
+ if (*pattern == NUL)
+ goto the_end;
+ vim_free(spats[last_idx].pat);
+ spats[last_idx].pat = vim_strsave(pattern);
+ }
+ if (spats[last_idx].pat == NULL || *spats[last_idx].pat == NUL)
+ goto the_end; // the previous pattern was never defined
+
+ update_search_stat(0, &pos, &pos, &stat, recompute, maxcount, timeout);
+
+ dict_add_number(rettv->vval.v_dict, "current", stat.cur);
+ dict_add_number(rettv->vval.v_dict, "total", stat.cnt);
+ dict_add_number(rettv->vval.v_dict, "exact_match", stat.exact_match);
+ dict_add_number(rettv->vval.v_dict, "incomplete", stat.incomplete);
+ dict_add_number(rettv->vval.v_dict, "maxcount", stat.last_maxcount);
+
+the_end:
+ restore_last_search_pattern();
+#ifdef FEAT_SEARCH_EXTRA
+ restore_incsearch_state();
+#endif
+}
+#endif
+
+/*
+ * Fuzzy string matching
+ *
+ * Ported from the lib_fts library authored by Forrest Smith.
+ * https://github.com/forrestthewoods/lib_fts/tree/master/code
+ *
+ * The following blog describes the fuzzy matching algorithm:
+ * https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/
+ *
+ * Each matching string is assigned a score. The following factors are checked:
+ * - Matched letter
+ * - Unmatched letter
+ * - Consecutively matched letters
+ * - Proximity to start
+ * - Letter following a separator (space, underscore)
+ * - Uppercase letter following lowercase (aka CamelCase)
+ *
+ * Matched letters are good. Unmatched letters are bad. Matching near the start
+ * is good. Matching the first letter in the middle of a phrase is good.
+ * Matching the uppercase letters in camel case entries is good.
+ *
+ * The score assigned for each factor is explained below.
+ * File paths are different from file names. File extensions may be ignorable.
+ * Single words care about consecutive matches but not separators or camel
+ * case.
+ * Score starts at 100
+ * Matched letter: +0 points
+ * Unmatched letter: -1 point
+ * Consecutive match bonus: +15 points
+ * First letter bonus: +15 points
+ * Separator bonus: +30 points
+ * Camel case bonus: +30 points
+ * Unmatched leading letter: -5 points (max: -15)
+ *
+ * There is some nuance to this. Scores don’t have an intrinsic meaning. The
+ * score range isn’t 0 to 100. It’s roughly [50, 150]. Longer words have a
+ * lower minimum score due to unmatched letter penalty. Longer search patterns
+ * have a higher maximum score due to match bonuses.
+ *
+ * Separator and camel case bonus is worth a LOT. Consecutive matches are worth
+ * quite a bit.
+ *
+ * There is a penalty if you DON’T match the first three letters. Which
+ * effectively rewards matching near the start. However there’s no difference
+ * in matching between the middle and end.
+ *
+ * There is not an explicit bonus for an exact match. Unmatched letters receive
+ * a penalty. So shorter strings and closer matches are worth more.
+ */
+typedef struct
+{
+ int idx; // used for stable sort
+ listitem_T *item;
+ int score;
+ list_T *lmatchpos;
+} fuzzyItem_T;
+
+// bonus for adjacent matches; this is higher than SEPARATOR_BONUS so that
+// matching a whole word is preferred.
+#define SEQUENTIAL_BONUS 40
+// bonus if match occurs after a path separator
+#define PATH_SEPARATOR_BONUS 30
+// bonus if match occurs after a word separator
+#define WORD_SEPARATOR_BONUS 25
+// bonus if match is uppercase and prev is lower
+#define CAMEL_BONUS 30
+// bonus if the first letter is matched
+#define FIRST_LETTER_BONUS 15
+// penalty applied for every letter in str before the first match
+#define LEADING_LETTER_PENALTY (-5)
+// maximum penalty for leading letters
+#define MAX_LEADING_LETTER_PENALTY (-15)
+// penalty for every letter that doesn't match
+#define UNMATCHED_LETTER_PENALTY (-1)
+// penalty for gap in matching positions (-2 * k)
+#define GAP_PENALTY (-2)
+// Score for a string that doesn't fuzzy match the pattern
+#define SCORE_NONE (-9999)
+
+#define FUZZY_MATCH_RECURSION_LIMIT 10
+
+/*
+ * Compute a score for a fuzzy matched string. The matching character locations
+ * are in 'matches'.
+ */
+ static int
+fuzzy_match_compute_score(
+ char_u *str,
+ int strSz,
+ int_u *matches,
+ int numMatches)
+{
+ int score;
+ int penalty;
+ int unmatched;
+ int i;
+ char_u *p = str;
+ int_u sidx = 0;
+
+ // Initialize score
+ score = 100;
+
+ // Apply leading letter penalty
+ penalty = LEADING_LETTER_PENALTY * matches[0];
+ if (penalty < MAX_LEADING_LETTER_PENALTY)
+ penalty = MAX_LEADING_LETTER_PENALTY;
+ score += penalty;
+
+ // Apply unmatched penalty
+ unmatched = strSz - numMatches;
+ score += UNMATCHED_LETTER_PENALTY * unmatched;
+
+ // Apply ordering bonuses
+ for (i = 0; i < numMatches; ++i)
+ {
+ int_u currIdx = matches[i];
+
+ if (i > 0)
+ {
+ int_u prevIdx = matches[i - 1];
+
+ // Sequential
+ if (currIdx == (prevIdx + 1))
+ score += SEQUENTIAL_BONUS;
+ else
+ score += GAP_PENALTY * (currIdx - prevIdx);
+ }
+
+ // Check for bonuses based on neighbor character value
+ if (currIdx > 0)
+ {
+ // Camel case
+ int neighbor = ' ';
+ int curr;
+
+ if (has_mbyte)
+ {
+ while (sidx < currIdx)
+ {
+ neighbor = (*mb_ptr2char)(p);
+ MB_PTR_ADV(p);
+ sidx++;
+ }
+ curr = (*mb_ptr2char)(p);
+ }
+ else
+ {
+ neighbor = str[currIdx - 1];
+ curr = str[currIdx];
+ }
+
+ if (vim_islower(neighbor) && vim_isupper(curr))
+ score += CAMEL_BONUS;
+
+ // Bonus if the match follows a separator character
+ if (neighbor == '/' || neighbor == '\\')
+ score += PATH_SEPARATOR_BONUS;
+ else if (neighbor == ' ' || neighbor == '_')
+ score += WORD_SEPARATOR_BONUS;
+ }
+ else
+ {
+ // First letter
+ score += FIRST_LETTER_BONUS;
+ }
+ }
+ return score;
+}
+
+/*
+ * Perform a recursive search for fuzzy matching 'fuzpat' in 'str'.
+ * Return the number of matching characters.
+ */
+ static int
+fuzzy_match_recursive(
+ char_u *fuzpat,
+ char_u *str,
+ int_u strIdx,
+ int *outScore,
+ char_u *strBegin,
+ int strLen,
+ int_u *srcMatches,
+ int_u *matches,
+ int maxMatches,
+ int nextMatch,
+ int *recursionCount)
+{
+ // Recursion params
+ int recursiveMatch = FALSE;
+ int_u bestRecursiveMatches[MAX_FUZZY_MATCHES];
+ int bestRecursiveScore = 0;
+ int first_match;
+ int matched;
+
+ // Count recursions
+ ++*recursionCount;
+ if (*recursionCount >= FUZZY_MATCH_RECURSION_LIMIT)
+ return 0;
+
+ // Detect end of strings
+ if (*fuzpat == NUL || *str == NUL)
+ return 0;
+
+ // Loop through fuzpat and str looking for a match
+ first_match = TRUE;
+ while (*fuzpat != NUL && *str != NUL)
+ {
+ int c1;
+ int c2;
+
+ c1 = PTR2CHAR(fuzpat);
+ c2 = PTR2CHAR(str);
+
+ // Found match
+ if (vim_tolower(c1) == vim_tolower(c2))
+ {
+ int_u recursiveMatches[MAX_FUZZY_MATCHES];
+ int recursiveScore = 0;
+ char_u *next_char;
+
+ // Supplied matches buffer was too short
+ if (nextMatch >= maxMatches)
+ return 0;
+
+ // "Copy-on-Write" srcMatches into matches
+ if (first_match && srcMatches)
+ {
+ memcpy(matches, srcMatches, nextMatch * sizeof(srcMatches[0]));
+ first_match = FALSE;
+ }
+
+ // Recursive call that "skips" this match
+ if (has_mbyte)
+ next_char = str + (*mb_ptr2len)(str);
+ else
+ next_char = str + 1;
+ if (fuzzy_match_recursive(fuzpat, next_char, strIdx + 1,
+ &recursiveScore, strBegin, strLen, matches,
+ recursiveMatches,
+ ARRAY_LENGTH(recursiveMatches),
+ nextMatch, recursionCount))
+ {
+ // Pick best recursive score
+ if (!recursiveMatch || recursiveScore > bestRecursiveScore)
+ {
+ memcpy(bestRecursiveMatches, recursiveMatches,
+ MAX_FUZZY_MATCHES * sizeof(recursiveMatches[0]));
+ bestRecursiveScore = recursiveScore;
+ }
+ recursiveMatch = TRUE;
+ }
+
+ // Advance
+ matches[nextMatch++] = strIdx;
+ if (has_mbyte)
+ MB_PTR_ADV(fuzpat);
+ else
+ ++fuzpat;
+ }
+ if (has_mbyte)
+ MB_PTR_ADV(str);
+ else
+ ++str;
+ strIdx++;
+ }
+
+ // Determine if full fuzpat was matched
+ matched = *fuzpat == NUL ? TRUE : FALSE;
+
+ // Calculate score
+ if (matched)
+ *outScore = fuzzy_match_compute_score(strBegin, strLen, matches,
+ nextMatch);
+
+ // Return best result
+ if (recursiveMatch && (!matched || bestRecursiveScore > *outScore))
+ {
+ // Recursive score is better than "this"
+ memcpy(matches, bestRecursiveMatches, maxMatches * sizeof(matches[0]));
+ *outScore = bestRecursiveScore;
+ return nextMatch;
+ }
+ else if (matched)
+ return nextMatch; // "this" score is better than recursive
+
+ return 0; // no match
+}
+
+/*
+ * fuzzy_match()
+ *
+ * Performs exhaustive search via recursion to find all possible matches and
+ * match with highest score.
+ * Scores values have no intrinsic meaning. Possible score range is not
+ * normalized and varies with pattern.
+ * Recursion is limited internally (default=10) to prevent degenerate cases
+ * (pat_arg="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa").
+ * Uses char_u for match indices. Therefore patterns are limited to
+ * MAX_FUZZY_MATCHES characters.
+ *
+ * Returns TRUE if 'pat_arg' matches 'str'. Also returns the match score in
+ * 'outScore' and the matching character positions in 'matches'.
+ */
+ int
+fuzzy_match(
+ char_u *str,
+ char_u *pat_arg,
+ int matchseq,
+ int *outScore,
+ int_u *matches,
+ int maxMatches)
+{
+ int recursionCount = 0;
+ int len = MB_CHARLEN(str);
+ char_u *save_pat;
+ char_u *pat;
+ char_u *p;
+ int complete = FALSE;
+ int score = 0;
+ int numMatches = 0;
+ int matchCount;
+
+ *outScore = 0;
+
+ save_pat = vim_strsave(pat_arg);
+ if (save_pat == NULL)
+ return FALSE;
+ pat = save_pat;
+ p = pat;
+
+ // Try matching each word in 'pat_arg' in 'str'
+ while (TRUE)
+ {
+ if (matchseq)
+ complete = TRUE;
+ else
+ {
+ // Extract one word from the pattern (separated by space)
+ p = skipwhite(p);
+ if (*p == NUL)
+ break;
+ pat = p;
+ while (*p != NUL && !VIM_ISWHITE(PTR2CHAR(p)))
+ {
+ if (has_mbyte)
+ MB_PTR_ADV(p);
+ else
+ ++p;
+ }
+ if (*p == NUL) // processed all the words
+ complete = TRUE;
+ *p = NUL;
+ }
+
+ score = 0;
+ recursionCount = 0;
+ matchCount = fuzzy_match_recursive(pat, str, 0, &score, str, len, NULL,
+ matches + numMatches, maxMatches - numMatches,
+ 0, &recursionCount);
+ if (matchCount == 0)
+ {
+ numMatches = 0;
+ break;
+ }
+
+ // Accumulate the match score and the number of matches
+ *outScore += score;
+ numMatches += matchCount;
+
+ if (complete)
+ break;
+
+ // try matching the next word
+ ++p;
+ }
+
+ vim_free(save_pat);
+ return numMatches != 0;
+}
+
+#if defined(FEAT_EVAL) || defined(FEAT_PROTO)
+/*
+ * Sort the fuzzy matches in the descending order of the match score.
+ * For items with same score, retain the order using the index (stable sort)
+ */
+ static int
+fuzzy_match_item_compare(const void *s1, const void *s2)
+{
+ int v1 = ((fuzzyItem_T *)s1)->score;
+ int v2 = ((fuzzyItem_T *)s2)->score;
+ int idx1 = ((fuzzyItem_T *)s1)->idx;
+ int idx2 = ((fuzzyItem_T *)s2)->idx;
+
+ return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1;
+}
+
+/*
+ * Fuzzy search the string 'str' in a list of 'items' and return the matching
+ * strings in 'fmatchlist'.
+ * If 'matchseq' is TRUE, then for multi-word search strings, match all the
+ * words in sequence.
+ * If 'items' is a list of strings, then search for 'str' in the list.
+ * If 'items' is a list of dicts, then either use 'key' to lookup the string
+ * for each item or use 'item_cb' Funcref function to get the string.
+ * If 'retmatchpos' is TRUE, then return a list of positions where 'str'
+ * matches for each item.
+ */
+ static void
+fuzzy_match_in_list(
+ list_T *l,
+ char_u *str,
+ int matchseq,
+ char_u *key,
+ callback_T *item_cb,
+ int retmatchpos,
+ list_T *fmatchlist,
+ long max_matches)
+{
+ long len;
+ fuzzyItem_T *items;
+ listitem_T *li;
+ long i = 0;
+ long match_count = 0;
+ int_u matches[MAX_FUZZY_MATCHES];
+
+ len = list_len(l);
+ if (len == 0)
+ return;
+ if (max_matches > 0 && len > max_matches)
+ len = max_matches;
+
+ items = ALLOC_CLEAR_MULT(fuzzyItem_T, len);
+ if (items == NULL)
+ return;
+
+ // For all the string items in items, get the fuzzy matching score
+ FOR_ALL_LIST_ITEMS(l, li)
+ {
+ int score;
+ char_u *itemstr;
+ typval_T rettv;
+
+ if (max_matches > 0 && match_count >= max_matches)
+ break;
+
+ itemstr = NULL;
+ rettv.v_type = VAR_UNKNOWN;
+ if (li->li_tv.v_type == VAR_STRING) // list of strings
+ itemstr = li->li_tv.vval.v_string;
+ else if (li->li_tv.v_type == VAR_DICT
+ && (key != NULL || item_cb->cb_name != NULL))
+ {
+ // For a dict, either use the specified key to lookup the string or
+ // use the specified callback function to get the string.
+ if (key != NULL)
+ itemstr = dict_get_string(li->li_tv.vval.v_dict,
+ (char *)key, FALSE);
+ else
+ {
+ typval_T argv[2];
+
+ // Invoke the supplied callback (if any) to get the dict item
+ li->li_tv.vval.v_dict->dv_refcount++;
+ argv[0].v_type = VAR_DICT;
+ argv[0].vval.v_dict = li->li_tv.vval.v_dict;
+ argv[1].v_type = VAR_UNKNOWN;
+ if (call_callback(item_cb, -1, &rettv, 1, argv) != FAIL)
+ {
+ if (rettv.v_type == VAR_STRING)
+ itemstr = rettv.vval.v_string;
+ }
+ dict_unref(li->li_tv.vval.v_dict);
+ }
+ }
+
+ if (itemstr != NULL
+ && fuzzy_match(itemstr, str, matchseq, &score, matches,
+ MAX_FUZZY_MATCHES))
+ {
+ items[match_count].idx = match_count;
+ items[match_count].item = li;
+ items[match_count].score = score;
+
+ // Copy the list of matching positions in itemstr to a list, if
+ // 'retmatchpos' is set.
+ if (retmatchpos)
+ {
+ int j = 0;
+ char_u *p;
+
+ items[match_count].lmatchpos = list_alloc();
+ if (items[match_count].lmatchpos == NULL)
+ goto done;
+
+ p = str;
+ while (*p != NUL)
+ {
+ if (!VIM_ISWHITE(PTR2CHAR(p)) || matchseq)
+ {
+ if (list_append_number(items[match_count].lmatchpos,
+ matches[j]) == FAIL)
+ goto done;
+ j++;
+ }
+ if (has_mbyte)
+ MB_PTR_ADV(p);
+ else
+ ++p;
+ }
+ }
+ ++match_count;
+ }
+ clear_tv(&rettv);
+ }
+
+ if (match_count > 0)
+ {
+ list_T *retlist;
+
+ // Sort the list by the descending order of the match score
+ qsort((void *)items, (size_t)match_count, sizeof(fuzzyItem_T),
+ fuzzy_match_item_compare);
+
+ // For matchfuzzy(), return a list of matched strings.
+ // ['str1', 'str2', 'str3']
+ // For matchfuzzypos(), return a list with three items.
+ // The first item is a list of matched strings. The second item
+ // is a list of lists where each list item is a list of matched
+ // character positions. The third item is a list of matching scores.
+ // [['str1', 'str2', 'str3'], [[1, 3], [1, 3], [1, 3]]]
+ if (retmatchpos)
+ {
+ li = list_find(fmatchlist, 0);
+ if (li == NULL || li->li_tv.vval.v_list == NULL)
+ goto done;
+ retlist = li->li_tv.vval.v_list;
+ }
+ else
+ retlist = fmatchlist;
+
+ // Copy the matching strings with a valid score to the return list
+ for (i = 0; i < match_count; i++)
+ {
+ if (items[i].score == SCORE_NONE)
+ break;
+ list_append_tv(retlist, &items[i].item->li_tv);
+ }
+
+ // next copy the list of matching positions
+ if (retmatchpos)
+ {
+ li = list_find(fmatchlist, -2);
+ if (li == NULL || li->li_tv.vval.v_list == NULL)
+ goto done;
+ retlist = li->li_tv.vval.v_list;
+
+ for (i = 0; i < match_count; i++)
+ {
+ if (items[i].score == SCORE_NONE)
+ break;
+ if (items[i].lmatchpos != NULL
+ && list_append_list(retlist, items[i].lmatchpos) == FAIL)
+ goto done;
+ }
+
+ // copy the matching scores
+ li = list_find(fmatchlist, -1);
+ if (li == NULL || li->li_tv.vval.v_list == NULL)
+ goto done;
+ retlist = li->li_tv.vval.v_list;
+ for (i = 0; i < match_count; i++)
+ {
+ if (items[i].score == SCORE_NONE)
+ break;
+ if (list_append_number(retlist, items[i].score) == FAIL)
+ goto done;
+ }
+ }
+ }
+
+done:
+ vim_free(items);
+}
+
+/*
+ * Do fuzzy matching. Returns the list of matched strings in 'rettv'.
+ * If 'retmatchpos' is TRUE, also returns the matching character positions.
+ */
+ static void
+do_fuzzymatch(typval_T *argvars, typval_T *rettv, int retmatchpos)
+{
+ callback_T cb;
+ char_u *key = NULL;
+ int ret;
+ int matchseq = FALSE;
+ long max_matches = 0;
+
+ if (in_vim9script()
+ && (check_for_list_arg(argvars, 0) == FAIL
+ || check_for_string_arg(argvars, 1) == FAIL
+ || check_for_opt_dict_arg(argvars, 2) == FAIL))
+ return;
+
+ CLEAR_POINTER(&cb);
+
+ // validate and get the arguments
+ if (argvars[0].v_type != VAR_LIST || argvars[0].vval.v_list == NULL)
+ {
+ semsg(_(e_argument_of_str_must_be_list),
+ retmatchpos ? "matchfuzzypos()" : "matchfuzzy()");
+ return;
+ }
+ if (argvars[1].v_type != VAR_STRING
+ || argvars[1].vval.v_string == NULL)
+ {
+ semsg(_(e_invalid_argument_str), tv_get_string(&argvars[1]));
+ return;
+ }
+
+ if (argvars[2].v_type != VAR_UNKNOWN)
+ {
+ dict_T *d;
+ dictitem_T *di;
+
+ if (check_for_nonnull_dict_arg(argvars, 2) == FAIL)
+ return;
+
+ // To search a dict, either a callback function or a key can be
+ // specified.
+ d = argvars[2].vval.v_dict;
+ if ((di = dict_find(d, (char_u *)"key", -1)) != NULL)
+ {
+ if (di->di_tv.v_type != VAR_STRING
+ || di->di_tv.vval.v_string == NULL
+ || *di->di_tv.vval.v_string == NUL)
+ {
+ semsg(_(e_invalid_argument_str), tv_get_string(&di->di_tv));
+ return;
+ }
+ key = tv_get_string(&di->di_tv);
+ }
+ else if ((di = dict_find(d, (char_u *)"text_cb", -1)) != NULL)
+ {
+ cb = get_callback(&di->di_tv);
+ if (cb.cb_name == NULL)
+ {
+ semsg(_(e_invalid_value_for_argument_str), "text_cb");
+ return;
+ }
+ }
+
+ if ((di = dict_find(d, (char_u *)"limit", -1)) != NULL)
+ {
+ if (di->di_tv.v_type != VAR_NUMBER)
+ {
+ semsg(_(e_invalid_argument_str), tv_get_string(&di->di_tv));
+ return;
+ }
+ max_matches = (long)tv_get_number_chk(&di->di_tv, NULL);
+ }
+
+ if (dict_has_key(d, "matchseq"))
+ matchseq = TRUE;
+ }
+
+ // get the fuzzy matches
+ ret = rettv_list_alloc(rettv);
+ if (ret == FAIL)
+ goto done;
+ if (retmatchpos)
+ {
+ list_T *l;
+
+ // For matchfuzzypos(), a list with three items are returned. First
+ // item is a list of matching strings, the second item is a list of
+ // lists with matching positions within each string and the third item
+ // is the list of scores of the matches.
+ l = list_alloc();
+ if (l == NULL)
+ goto done;
+ if (list_append_list(rettv->vval.v_list, l) == FAIL)
+ {
+ vim_free(l);
+ goto done;
+ }
+ l = list_alloc();
+ if (l == NULL)
+ goto done;
+ if (list_append_list(rettv->vval.v_list, l) == FAIL)
+ {
+ vim_free(l);
+ goto done;
+ }
+ l = list_alloc();
+ if (l == NULL)
+ goto done;
+ if (list_append_list(rettv->vval.v_list, l) == FAIL)
+ {
+ vim_free(l);
+ goto done;
+ }
+ }
+
+ fuzzy_match_in_list(argvars[0].vval.v_list, tv_get_string(&argvars[1]),
+ matchseq, key, &cb, retmatchpos, rettv->vval.v_list, max_matches);
+
+done:
+ free_callback(&cb);
+}
+
+/*
+ * "matchfuzzy()" function
+ */
+ void
+f_matchfuzzy(typval_T *argvars, typval_T *rettv)
+{
+ do_fuzzymatch(argvars, rettv, FALSE);
+}
+
+/*
+ * "matchfuzzypos()" function
+ */
+ void
+f_matchfuzzypos(typval_T *argvars, typval_T *rettv)
+{
+ do_fuzzymatch(argvars, rettv, TRUE);
+}
+#endif
+
+/*
+ * Same as fuzzy_match_item_compare() except for use with a string match
+ */
+ static int
+fuzzy_match_str_compare(const void *s1, const void *s2)
+{
+ int v1 = ((fuzmatch_str_T *)s1)->score;
+ int v2 = ((fuzmatch_str_T *)s2)->score;
+ int idx1 = ((fuzmatch_str_T *)s1)->idx;
+ int idx2 = ((fuzmatch_str_T *)s2)->idx;
+
+ return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1;
+}
+
+/*
+ * Sort fuzzy matches by score
+ */
+ static void
+fuzzy_match_str_sort(fuzmatch_str_T *fm, int sz)
+{
+ // Sort the list by the descending order of the match score
+ qsort((void *)fm, (size_t)sz, sizeof(fuzmatch_str_T),
+ fuzzy_match_str_compare);
+}
+
+/*
+ * Same as fuzzy_match_item_compare() except for use with a function name
+ * string match. <SNR> functions should be sorted to the end.
+ */
+ static int
+fuzzy_match_func_compare(const void *s1, const void *s2)
+{
+ int v1 = ((fuzmatch_str_T *)s1)->score;
+ int v2 = ((fuzmatch_str_T *)s2)->score;
+ int idx1 = ((fuzmatch_str_T *)s1)->idx;
+ int idx2 = ((fuzmatch_str_T *)s2)->idx;
+ char_u *str1 = ((fuzmatch_str_T *)s1)->str;
+ char_u *str2 = ((fuzmatch_str_T *)s2)->str;
+
+ if (*str1 != '<' && *str2 == '<') return -1;
+ if (*str1 == '<' && *str2 != '<') return 1;
+ return v1 == v2 ? (idx1 - idx2) : v1 > v2 ? -1 : 1;
+}
+
+/*
+ * Sort fuzzy matches of function names by score.
+ * <SNR> functions should be sorted to the end.
+ */
+ static void
+fuzzy_match_func_sort(fuzmatch_str_T *fm, int sz)
+{
+ // Sort the list by the descending order of the match score
+ qsort((void *)fm, (size_t)sz, sizeof(fuzmatch_str_T),
+ fuzzy_match_func_compare);
+}
+
+/*
+ * Fuzzy match 'pat' in 'str'. Returns 0 if there is no match. Otherwise,
+ * returns the match score.
+ */
+ int
+fuzzy_match_str(char_u *str, char_u *pat)
+{
+ int score = 0;
+ int_u matchpos[MAX_FUZZY_MATCHES];
+
+ if (str == NULL || pat == NULL)
+ return 0;
+
+ fuzzy_match(str, pat, TRUE, &score, matchpos,
+ sizeof(matchpos) / sizeof(matchpos[0]));
+
+ return score;
+}
+
+/*
+ * Free an array of fuzzy string matches "fuzmatch[count]".
+ */
+ void
+fuzmatch_str_free(fuzmatch_str_T *fuzmatch, int count)
+{
+ int i;
+
+ if (fuzmatch == NULL)
+ return;
+ for (i = 0; i < count; ++i)
+ vim_free(fuzmatch[i].str);
+ vim_free(fuzmatch);
+}
+
+/*
+ * Copy a list of fuzzy matches into a string list after sorting the matches by
+ * the fuzzy score. Frees the memory allocated for 'fuzmatch'.
+ * Returns OK on success and FAIL on memory allocation failure.
+ */
+ int
+fuzzymatches_to_strmatches(
+ fuzmatch_str_T *fuzmatch,
+ char_u ***matches,
+ int count,
+ int funcsort)
+{
+ int i;
+
+ if (count <= 0)
+ return OK;
+
+ *matches = ALLOC_MULT(char_u *, count);
+ if (*matches == NULL)
+ {
+ fuzmatch_str_free(fuzmatch, count);
+ return FAIL;
+ }
+
+ // Sort the list by the descending order of the match score
+ if (funcsort)
+ fuzzy_match_func_sort((void *)fuzmatch, (size_t)count);
+ else
+ fuzzy_match_str_sort((void *)fuzmatch, (size_t)count);
+
+ for (i = 0; i < count; i++)
+ (*matches)[i] = fuzmatch[i].str;
+ vim_free(fuzmatch);
+
+ return OK;
+}