/* 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 static int check_linecomment(char_u *line); #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]; # ifdef FEAT_SEARCH_EXTRA static int saved_spats_last_idx = 0; static int saved_spats_no_hlsearch = 0; # endif static char_u *mr_pattern = NULL; // pattern used by search_regcomp() #ifdef FEAT_RIGHTLEFT static int mr_pattern_alloced = FALSE; // mr_pattern was allocated #endif #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, 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_nopresub)); else emsg(_(e_noprevre)); 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); #ifdef FEAT_RIGHTLEFT if (mr_pattern_alloced) { vim_free(mr_pattern); mr_pattern_alloced = FALSE; } if (curwin->w_p_rl && *curwin->w_p_rlc == 's') { char_u *rev_pattern; rev_pattern = reverse_text(pat); if (rev_pattern == NULL) mr_pattern = pat; // out of memory, keep normal pattern. else { mr_pattern = rev_pattern; mr_pattern_alloced = TRUE; } } else #endif mr_pattern = 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) { 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) { 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(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) { 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); #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) { 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]; #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); # ifdef FEAT_RIGHTLEFT if (mr_pattern_alloced) { vim_free(mr_pattern); mr_pattern_alloced = FALSE; mr_pattern = NULL; } # endif } #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; /* * 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); } 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; 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 == '\\') { 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 (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(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 *)"", 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 #ifdef FEAT_RELTIME proftime_T *tm = NULL; // timeout limit or NULL int *timed_out = NULL; // set when timed out or NULL #endif if (extra_arg != NULL) { stop_lnum = extra_arg->sa_stop_lnum; #ifdef FEAT_RELTIME tm = extra_arg->sa_tm; timed_out = &extra_arg->sa_timed_out; #endif } if (search_regcomp(pat, RE_SEARCH, pat_use, (options & (SEARCH_HIS + SEARCH_KEEP)), ®match) == FAIL) { if ((options & SEARCH_MSG) && !rc_did_emsg) semsg(_("E383: Invalid search string: %s"), mr_pattern); return FAIL; } /* * 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; #ifdef FEAT_RELTIME // Stop after passing the "tm" time limit. if (tm != NULL && profile_passed_limit(tm)) break; #endif /* * Look for a match somewhere in line "lnum". */ col = at_first_line && (options & SEARCH_COL) ? pos->col : (colnr_T)0; nmatched = vim_regexec_multi(®match, win, buf, lnum, col, #ifdef FEAT_RELTIME tm, timed_out #else NULL, NULL #endif ); // 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 #ifdef FEAT_RELTIME || (timed_out != NULL && *timed_out) #endif ) 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(®match); #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 { matchcol = matchpos.col; 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(®match, win, buf, lnum + matchpos.lnum, matchcol, #ifdef FEAT_RELTIME tm, timed_out #else NULL, NULL #endif )) == 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(®match); # 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(®match); # 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(®match, win, buf, lnum + matchpos.lnum, matchcol, #ifdef FEAT_RELTIME tm, timed_out #else NULL, NULL #endif )) == 0) { #ifdef FEAT_RELTIME // 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 != NULL && *timed_out) match_ok = FALSE; #endif 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 #ifdef FEAT_RELTIME || (timed_out != NULL && *timed_out) #endif #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 #ifdef FEAT_RELTIME || (timed_out != NULL && *timed_out) #endif #ifdef FEAT_SEARCH_EXTRA || break_loop #endif ) break; } while (--count > 0 && found); // stop after count matches or no match vim_regfree(regmatch.regprog); if (!found) // did not find it { if (got_int) emsg(_(e_interr)); else if ((options & SEARCH_MSG) == SEARCH_MSG) { if (p_ws) semsg(_(e_patnotf2), mr_pattern); else if (lnum == 0) semsg(_("E384: search hit TOP without match for: %s"), mr_pattern); else semsg(_("E385: search hit BOTTOM without match for: %s"), 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; } #ifdef FEAT_EVAL 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(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_noprevre)); 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; } } } 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(_("E386: Expected '?' or '/' after ';'")); 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_cont_status & CONT_ADDING) && !(compl_cont_status & CONT_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, compl_length) : STRNCMP(p, pat, compl_length)) == 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 #ifdef FEAT_LISP int lispcomm = FALSE; // inside of Lisp-style comment int lisp = curbuf->b_p_lisp; // engage Lisp-specific hacks ;) #endif 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 (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) #ifdef FEAT_LISP || lisp #endif ) comment_col = check_linecomment(linep); #ifdef FEAT_LISP if (lisp && comment_col != MAXCOL && pos.col > (colnr_T)comment_col) lispcomm = TRUE; // find match inside this comment #endif 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) { #ifdef FEAT_LISP // char to match is inside of comment, don't search outside if (lispcomm && pos.col < (colnr_T)comment_col) break; #endif 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 #ifdef FEAT_LISP || lisp #endif ) comment_col = check_linecomment(linep); #ifdef FEAT_LISP // skip comment if (lisp && comment_col != MAXCOL) pos.col = comment_col; #endif } 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 #ifdef FEAT_LISP // don't search for match in comment || (lisp && comment_col != MAXCOL && pos.col == (colnr_T)comment_col) #endif ) { if (pos.lnum == curbuf->b_ml.ml_line_count // end of file #ifdef FEAT_LISP // line is exhausted and comment with it, // don't search for match in code || lispcomm #endif ) break; ++pos.lnum; if (maxtravel && traveled++ > maxtravel) break; linep = ml_get(pos.lnum); pos.col = 0; do_quotes = -1; line_breakcheck(); #ifdef FEAT_LISP if (lisp) // find comment pos in new line comment_col = check_linecomment(linep); #endif } 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: #ifdef FEAT_LISP /* * 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; #endif // 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. * TODO: skip strings. */ static int check_linecomment(char_u *line) { char_u *p; p = line; #ifdef FEAT_LISP // 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) != '#'))) break; // found! ++p; } } else p = NULL; } else #endif 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 if (p[1] == '/' && (p == line || p[-1] != '*' || p[2] != '*')) 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 ((lpos = findmatch(NULL, NUL)) == NULL) // no match, so beep vim_beep(BO_MATCH); else if (lpos->lnum >= curwin->w_topline && lpos->lnum < curwin->w_botline) { if (!curwin->w_p_wrap) getvcol(curwin, lpos, NULL, &vcol, NULL); if (curwin->w_p_wrap || (vcol >= curwin->w_leftcol && vcol < curwin->w_leftcol + curwin->w_width)) { 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(VALID); // show the new char first save_dollar_vcol = dollar_vcol; #ifdef CURSOR_SHAPE save_state = State; State = 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, RE_SEARCH, RE_SEARCH, SEARCH_KEEP, ®match) == 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(®match, curwin, curbuf, pos.lnum, regmatch.startpos[0].col, NULL, 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(INVERTED); showmode(); return OK; } #if defined(FEAT_LISP) || defined(FEAT_CINDENT) || defined(FEAT_TEXTOBJ) \ || defined(PROTO) /* * 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); } #endif /* * 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) { 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; } mch_memmove(msgbuf + STRLEN(msgbuf) - 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) /* * Find identifiers or defines in included files. * If p_ic && (compl_cont_status & CONT_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_cont_status & CONT_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 = ml_get(lnum); 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_cont_status & CONT_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(®match, 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_cont_status & CONT_ADDING) { p += compl_length; if (vim_iswordp(p)) goto exit_matched; p = find_word_start(p); } p = find_word_end(p); i = (int)(p - aux); if ((compl_cont_status & CONT_ADDING) && i == compl_length) { // 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 = ml_get(++lnum); } 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 == compl_length) 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(_("E387: 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 (!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(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_cont_status & CONT_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 = ml_get(lnum); } 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_interr)); else if (type == FIND_DEFINE) emsg(_("E388: Couldn't find definition")); else emsg(_("E389: Couldn't 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 #ifdef FEAT_EVAL /* * "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 (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 (argvars[0].v_type != VAR_DICT || argvars[0].vval.v_dict == NULL) { emsg(_(e_dictreq)); 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, (char_u *)"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_invarg2), "pos"); return; } if (list_len(di->di_tv.vval.v_list) != 3) { semsg(_(e_invarg2), "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(); 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(); } /* * 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 // Maximum number of characters that can be fuzzy matched #define MAXMATCHES 256 typedef int_u matchidx_T; /* * 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, matchidx_T *matches, int numMatches) { int score; int penalty; int unmatched; int i; char_u *p = str; matchidx_T 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) { matchidx_T currIdx = matches[i]; if (i > 0) { matchidx_T 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, matchidx_T strIdx, int *outScore, char_u *strBegin, int strLen, matchidx_T *srcMatches, matchidx_T *matches, int maxMatches, int nextMatch, int *recursionCount) { // Recursion params int recursiveMatch = FALSE; matchidx_T bestRecursiveMatches[MAXMATCHES]; 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 == '\0' || *str == '\0') 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)) { matchidx_T recursiveMatches[MAXMATCHES]; 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, sizeof(recursiveMatches)/sizeof(recursiveMatches[0]), nextMatch, recursionCount)) { // Pick best recursive score if (!recursiveMatch || recursiveScore > bestRecursiveScore) { memcpy(bestRecursiveMatches, recursiveMatches, MAXMATCHES * 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 MAXMATCHES * characters. * * Returns TRUE if 'pat_arg' matches 'str'. Also returns the match score in * 'outScore' and the matching character positions in 'matches'. */ static int fuzzy_match( char_u *str, char_u *pat_arg, int matchseq, int *outScore, matchidx_T *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; } /* * 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 *items, char_u *str, int matchseq, char_u *key, callback_T *item_cb, int retmatchpos, list_T *fmatchlist) { long len; fuzzyItem_T *ptrs; listitem_T *li; long i = 0; int found_match = FALSE; matchidx_T matches[MAXMATCHES]; len = list_len(items); if (len == 0) return; ptrs = ALLOC_CLEAR_MULT(fuzzyItem_T, len); if (ptrs == NULL) return; // For all the string items in items, get the fuzzy matching score FOR_ALL_LIST_ITEMS(items, li) { int score; char_u *itemstr; typval_T rettv; ptrs[i].idx = i; ptrs[i].item = li; ptrs[i].score = SCORE_NONE; 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, 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, sizeof(matches) / sizeof(matches[0]))) { // Copy the list of matching positions in itemstr to a list, if // 'retmatchpos' is set. if (retmatchpos) { int j = 0; char_u *p; ptrs[i].lmatchpos = list_alloc(); if (ptrs[i].lmatchpos == NULL) goto done; p = str; while (*p != NUL) { if (!VIM_ISWHITE(PTR2CHAR(p))) { if (list_append_number(ptrs[i].lmatchpos, matches[j]) == FAIL) goto done; j++; } if (has_mbyte) MB_PTR_ADV(p); else ++p; } } ptrs[i].score = score; found_match = TRUE; } ++i; clear_tv(&rettv); } if (found_match) { list_T *l; // Sort the list by the descending order of the match score qsort((void *)ptrs, (size_t)len, 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; l = li->li_tv.vval.v_list; } else l = fmatchlist; // Copy the matching strings with a valid score to the return list for (i = 0; i < len; i++) { if (ptrs[i].score == SCORE_NONE) break; list_append_tv(l, &ptrs[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; l = li->li_tv.vval.v_list; for (i = 0; i < len; i++) { if (ptrs[i].score == SCORE_NONE) break; if (ptrs[i].lmatchpos != NULL && list_append_list(l, ptrs[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; l = li->li_tv.vval.v_list; for (i = 0; i < len; i++) { if (ptrs[i].score == SCORE_NONE) break; if (list_append_number(l, ptrs[i].score) == FAIL) goto done; } } } done: vim_free(ptrs); } /* * 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; CLEAR_POINTER(&cb); // validate and get the arguments if (argvars[0].v_type != VAR_LIST || argvars[0].vval.v_list == NULL) { semsg(_(e_listarg), retmatchpos ? "matchfuzzypos()" : "matchfuzzy()"); return; } if (argvars[1].v_type != VAR_STRING || argvars[1].vval.v_string == NULL) { semsg(_(e_invarg2), tv_get_string(&argvars[1])); return; } if (argvars[2].v_type != VAR_UNKNOWN) { dict_T *d; dictitem_T *di; if (argvars[2].v_type != VAR_DICT || argvars[2].vval.v_dict == NULL) { emsg(_(e_dictreq)); 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_invarg2), 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_invargval), "text_cb"); return; } } if ((di = dict_find(d, (char_u *)"matchseq", -1)) != NULL) matchseq = TRUE; } // get the fuzzy matches ret = rettv_list_alloc(rettv); if (ret != OK) 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) goto done; l = list_alloc(); if (l == NULL) goto done; if (list_append_list(rettv->vval.v_list, l) == FAIL) goto done; l = list_alloc(); if (l == NULL) goto done; if (list_append_list(rettv->vval.v_list, l) == FAIL) goto done; } fuzzy_match_in_list(argvars[0].vval.v_list, tv_get_string(&argvars[1]), matchseq, key, &cb, retmatchpos, rettv->vval.v_list); 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