aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--runtime/doc/eval.txt121
-rw-r--r--src/nvim/eval.lua1
-rw-r--r--src/nvim/macros.h1
-rw-r--r--src/nvim/search.c376
-rw-r--r--src/nvim/search.h16
-rw-r--r--src/nvim/testdir/test_search_stat.vim81
6 files changed, 489 insertions, 107 deletions
diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt
index bb352022f9..b19583ed61 100644
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2394,6 +2394,7 @@ screenpos({winid}, {lnum}, {col}) Dict screen row and col of a text character
screenrow() Number current cursor row
search({pattern} [, {flags} [, {stopline} [, {timeout}]]])
Number search for {pattern}
+searchcount([{options}]) Dict Get or update the last search count
searchdecl({name} [, {global} [, {thisblock}]])
Number search for variable declaration
searchpair({start}, {middle}, {end} [, {flags} [, {skip} [...]]])
@@ -7293,6 +7294,126 @@ search({pattern} [, {flags} [, {stopline} [, {timeout}]]]) *search()*
The 'n' flag tells the function not to move the cursor.
+searchcount([{options}]) *searchcount()*
+ Get or update the last search count, like what is displayed
+ without the "S" flag in 'shortmess'. This works even if
+ 'shortmess' does contain the "S" flag.
+
+ This returns a Dictionary. The dictionary is empty if the
+ previous pattern was not set and "pattern" was not specified.
+
+ key type meaning ~
+ current |Number| current position of match;
+ 0 if the cursor position is
+ before the first match
+ exact_match |Boolean| 1 if "current" is matched on
+ "pos", otherwise 0
+ total |Number| total count of matches found
+ incomplete |Number| 0: search was fully completed
+ 1: recomputing was timed out
+ 2: max count exceeded
+
+ For {options} see further down.
+
+ To get the last search count when |n| or |N| was pressed, call
+ this function with `recompute: 0` . This sometimes returns
+ wrong information because |n| and |N|'s maximum count is 99.
+ If it exceeded 99 the result must be max count + 1 (100). If
+ you want to get correct information, specify `recompute: 1`: >
+
+ " result == maxcount + 1 (100) when many matches
+ let result = searchcount(#{recompute: 0})
+
+ " Below returns correct result (recompute defaults
+ " to 1)
+ let result = searchcount()
+<
+ The function is useful to add the count to |statusline|: >
+ function! LastSearchCount() abort
+ let result = searchcount(#{recompute: 0})
+ if empty(result)
+ return ''
+ endif
+ if result.incomplete ==# 1 " timed out
+ return printf(' /%s [?/??]', @/)
+ elseif result.incomplete ==# 2 " max count exceeded
+ if result.total > result.maxcount &&
+ \ result.current > result.maxcount
+ return printf(' /%s [>%d/>%d]', @/,
+ \ result.current, result.total)
+ elseif result.total > result.maxcount
+ return printf(' /%s [%d/>%d]', @/,
+ \ result.current, result.total)
+ endif
+ endif
+ return printf(' /%s [%d/%d]', @/,
+ \ result.current, result.total)
+ endfunction
+ let &statusline .= '%{LastSearchCount()}'
+
+ " Or if you want to show the count only when
+ " 'hlsearch' was on
+ " let &statusline .=
+ " \ '%{v:hlsearch ? LastSearchCount() : ""}'
+<
+ You can also update the search count, which can be useful in a
+ |CursorMoved| or |CursorMovedI| autocommand: >
+
+ autocmd CursorMoved,CursorMovedI *
+ \ let s:searchcount_timer = timer_start(
+ \ 200, function('s:update_searchcount'))
+ function! s:update_searchcount(timer) abort
+ if a:timer ==# s:searchcount_timer
+ call searchcount(#{
+ \ recompute: 1, maxcount: 0, timeout: 100})
+ redrawstatus
+ endif
+ endfunction
+<
+ This can also be used to count matched texts with specified
+ pattern in the current buffer using "pattern": >
+
+ " Count '\<foo\>' in this buffer
+ " (Note that it also updates search count)
+ let result = searchcount(#{pattern: '\<foo\>'})
+
+ " To restore old search count by old pattern,
+ " search again
+ call searchcount()
+<
+ {options} must be a Dictionary. It can contain:
+ key type meaning ~
+ recompute |Boolean| if |TRUE|, recompute the count
+ like |n| or |N| was executed.
+ otherwise returns the last
+ result by |n|, |N|, or this
+ function is returned.
+ (default: |TRUE|)
+ pattern |String| recompute if this was given
+ and different with |@/|.
+ this works as same as the
+ below command is executed
+ before calling this function >
+ let @/ = pattern
+< (default: |@/|)
+ timeout |Number| 0 or negative number is no
+ timeout. timeout milliseconds
+ for recomputing the result
+ (default: 0)
+ maxcount |Number| 0 or negative number is no
+ limit. max count of matched
+ text while recomputing the
+ result. if search exceeded
+ total count, "total" value
+ becomes `maxcount + 1`
+ (default: 0)
+ pos |List| `[lnum, col, off]` value
+ when recomputing the result.
+ this changes "current" result
+ value. see |cursor()|, |getpos()
+ (default: cursor's position)
+
+
searchdecl({name} [, {global} [, {thisblock}]]) *searchdecl()*
Search for the declaration of {name}.
diff --git a/src/nvim/eval.lua b/src/nvim/eval.lua
index 5ad7781a41..33c6fae5cf 100644
--- a/src/nvim/eval.lua
+++ b/src/nvim/eval.lua
@@ -286,6 +286,7 @@ return {
screenpos={args=3},
screenrow={},
search={args={1, 4}},
+ searchcount={args={0,1}},
searchdecl={args={1, 3}},
searchpair={args={3, 7}},
searchpairpos={args={3, 7}},
diff --git a/src/nvim/macros.h b/src/nvim/macros.h
index 303722b4a8..eb9357d027 100644
--- a/src/nvim/macros.h
+++ b/src/nvim/macros.h
@@ -238,5 +238,6 @@
# define PRAGMA_DIAG_POP
#endif
+#define EMPTY_POS(a) ((a).lnum == 0 && (a).col == 0 && (a).coladd == 0)
#endif // NVIM_MACROS_H
diff --git a/src/nvim/search.c b/src/nvim/search.c
index abe05bbd12..82fc0f9d8e 100644
--- a/src/nvim/search.c
+++ b/src/nvim/search.c
@@ -46,38 +46,36 @@
#include "nvim/window.h"
#include "nvim/os/time.h"
-
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "search.c.generated.h"
#endif
-/*
- * 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 a 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.
- */
+// 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 a 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.
-/*
- * 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 struct spat spats[2] =
{
// Last used search pattern
@@ -1002,7 +1000,7 @@ static int first_submatch(regmmatch_T *rp)
/*
* 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 '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
@@ -1042,7 +1040,6 @@ int do_search(
char_u *msgbuf = NULL;
size_t len;
bool has_offset = false;
-#define SEARCH_STAT_BUF_LEN 12
/*
* A line offset is not remembered, this is vi compatible.
@@ -1383,11 +1380,14 @@ int do_search(
&& c != FAIL
&& !shortmess(SHM_SEARCHCOUNT)
&& msgbuf != NULL) {
- search_stat(dirc, &pos, show_top_bot_msg, msgbuf,
- (count != 1
- || has_offset
- || (!(fdo_flags & FDO_SEARCH)
- && hasFolding(curwin->w_cursor.lnum, NULL, NULL))));
+ cmdline_search_stat(dirc, &pos, &curwin->w_cursor,
+ show_top_bot_msg, msgbuf,
+ (count != 1 || has_offset
+ || (!(fdo_flags & FDO_SEARCH)
+ && hasFolding(curwin->w_cursor.lnum, NULL,
+ NULL))),
+ SEARCH_STAT_DEF_MAX_COUNT,
+ SEARCH_STAT_DEF_TIMEOUT);
}
// The search command can be followed by a ';' to do another search.
@@ -1715,10 +1715,10 @@ static void find_mps_values(int *initc, int *findc, bool *backwards,
* '#' 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!)
+ * 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
@@ -3003,7 +3003,7 @@ current_word(
/*
* If the start is on white space, and white space should be included
- * (" word"), or start is not on white space, and white space should
+ * (" word"), or start is not on white space, and white space should
* not be included ("word"), find end of word.
*/
if ((cls() == 0) == include) {
@@ -3012,8 +3012,8 @@ current_word(
} else {
/*
* If the start is not on white space, and white space should be
- * included ("word "), or start is on white space and white
- * space should not be included (" "), find start of word.
+ * included ("word "), or start is on white space and white
+ * space should not be included (" "), find start of word.
* If we end up in the first column of the next line (single char
* word) back up to end of the line.
*/
@@ -4347,121 +4347,287 @@ int linewhite(linenr_T lnum)
}
// Add the search count "[3/19]" to "msgbuf".
-// When "recompute" is true Always recompute the numbers.
-static void search_stat(int dirc, pos_T *pos,
- bool show_top_bot_msg, char_u *msgbuf, bool recompute)
+// See update_search_stat() for other arguments.
+static void cmdline_search_stat(int dirc, pos_T *pos, pos_T *cursor_pos,
+ bool show_top_bot_msg, char_u *msgbuf,
+ bool 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];
+
+ 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 {
+ 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);
+ }
+ }
+
+ size_t len = strlen(t);
+ if (show_top_bot_msg && len + 2 < SEARCH_STAT_BUF_LEN) {
+ memmove(t + 2, t, len);
+ t[0] = 'W';
+ t[1] = ' ';
+ len += 2;
+ }
+
+ 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;
+ msg_ext_set_kind("search_count");
+ 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, bool recompute, int maxcount,
+ long timeout)
{
- int save_ws = p_ws;
- int wraparound = false;
- pos_T p = (*pos);
- static pos_T lastpos = { 0, 0, 0 };
+ int save_ws = p_ws;
+ bool wraparound = false;
+ pos_T p = (*pos);
+ static pos_T lastpos = { 0, 0, 0 };
static int cur = 0;
static int cnt = 0;
+ static bool 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;
- proftime_T start;
-#define OUT_OF_TIME 999
+ proftime_T start;
+ 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(lastpos, p))
|| (dirc == '/' && lt(p, lastpos)));
// If anything relevant changed the count has to be recomputed.
// STRNICMP ignores case, but we should not ignore case.
// Unfortunately, there is no STRNICMP function.
+ // XXX: above comment should be "no MB_STRCMP function" ?
if (!(chgtick == buf_get_changedtick(curbuf)
&& lastpat != NULL // supress clang/NULL passed as nonnull parameter
&& STRNICMP(lastpat, spats[last_idx].pat, STRLEN(lastpat)) == 0
&& STRLEN(lastpat) == STRLEN(spats[last_idx].pat)
- && equalpos(lastpos, curwin->w_cursor)
+ && equalpos(lastpos, *cursor_pos)
&& lbuf == curbuf)
- || wraparound || cur < 0 || cur > 99 || recompute) {
+ || wraparound || cur < 0 || (maxcount > 0 && cur > maxcount)
+ || recompute) {
cur = 0;
cnt = 0;
+ exact_match = false;
+ incomplete = 0;
clearpos(&lastpos);
lbuf = curbuf;
}
- if (equalpos(lastpos, curwin->w_cursor) && !wraparound
- && (dirc == '/' ? cur < cnt : cur > 0)) {
- cur += dirc == '/' ? 1 : -1;
+ if (equalpos(lastpos, *cursor_pos) && !wraparound
+ && (dirc == 0 || dirc == '/' ? cur < cnt : cur > 0)) {
+ cur += dirc == 0 ? 0 : dirc == '/' ? 1 : -1;
} else {
+ bool done_search = false;
+ pos_T endpos = { 0, 0, 0 };
p_ws = false;
- start = profile_setlimit(20L);
- while (!got_int && searchit(curwin, curbuf, &lastpos, NULL,
+ if (timeout > 0) {
+ start = profile_setlimit(timeout);
+ }
+ while (!got_int && searchit(curwin, curbuf, &lastpos, &endpos,
FORWARD, NULL, 1, SEARCH_KEEP, RE_LAST,
NULL) != FAIL) {
+ done_search = true;
// Stop after passing the time limit.
- if (profile_passed_limit(start)) {
- cnt = OUT_OF_TIME;
- cur = OUT_OF_TIME;
+ if (timeout > 0 && profile_passed_limit(start)) {
+ incomplete = 1;
break;
}
cnt++;
if (ltoreq(lastpos, p)) {
- cur++;
+ cur = cnt;
+ if (lt(p, endpos)) {
+ exact_match = true;
+ }
}
fast_breakcheck();
- if (cnt > 99) {
+ if (maxcount > 0 && cnt > maxcount) {
+ incomplete = 2; // max count exceeded
break;
}
}
if (got_int) {
cur = -1; // abort
}
+ if (done_search) {
+ xfree(lastpat);
+ lastpat = vim_strsave(spats[last_idx].pat);
+ chgtick = buf_get_changedtick(curbuf);
+ lbuf = curbuf;
+ lastpos = p;
+ }
}
- if (cur > 0) {
- char t[SEARCH_STAT_BUF_LEN] = "";
- int len;
+ stat->cur = cur;
+ stat->cnt = cnt;
+ stat->exact_match = exact_match;
+ stat->incomplete = incomplete;
+ stat->last_maxcount = last_maxcount;
+ p_ws = save_ws;
+}
- if (curwin->w_p_rl && *curwin->w_p_rlc == 's') {
- if (cur == OUT_OF_TIME) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[?\?/?]");
- } else if (cnt > 99 && cur > 99) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>99/>99]");
- } else if (cnt > 99) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>99/%d]", cur);
- } else {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]", cnt, cur);
+// "searchcount()" function
+void f_searchcount(typval_T *argvars, typval_T *rettv, FunPtr fptr)
+{
+ pos_T pos = curwin->w_cursor;
+ char_u *pattern = NULL;
+ int maxcount = SEARCH_STAT_DEF_MAX_COUNT;
+ long timeout = SEARCH_STAT_DEF_TIMEOUT;
+ bool recompute = true;
+ searchstat_T stat;
+
+ tv_dict_alloc_ret(rettv);
+
+ 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;
+ bool 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 = tv_dict_find(dict, (const char *)"timeout", -1);
+ if (di != NULL) {
+ timeout = (long)tv_get_number_chk(&di->di_tv, &error);
+ if (error) {
+ return;
}
- } else {
- if (cur == OUT_OF_TIME) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[?/??]");
- } else if (cnt > 99 && cur > 99) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[>99/>99]");
- } else if (cnt > 99) {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/>99]", cur);
- } else {
- vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]", cur, cnt);
+ }
+ di = tv_dict_find(dict, (const char *)"maxcount", -1);
+ if (di != NULL) {
+ maxcount = (int)tv_get_number_chk(&di->di_tv, &error);
+ if (error) {
+ return;
}
}
-
- len = STRLEN(t);
- if (show_top_bot_msg && len + 2 < SEARCH_STAT_BUF_LEN) {
- memmove(t + 2, t, len);
- t[0] = 'W';
- t[1] = ' ';
- len += 2;
+ di = tv_dict_find(dict, (const char *)"recompute", -1);
+ if (di != NULL) {
+ recompute = tv_get_number_chk(&di->di_tv, &error);
+ if (error) {
+ return;
+ }
}
+ di = tv_dict_find(dict, (const char *)"pattern", -1);
+ if (di != NULL) {
+ pattern = (char_u *)tv_get_string_chk(&di->di_tv);
+ if (pattern == NULL) {
+ return;
+ }
+ }
+ di = tv_dict_find(dict, (const char *)"pos", -1);
+ if (di != NULL) {
+ if (di->di_tv.v_type != VAR_LIST) {
+ EMSG2(_(e_invarg2), "pos");
+ return;
+ }
+ if (tv_list_len(di->di_tv.vval.v_list) != 3) {
+ EMSG2(_(e_invarg2), "List format should be [lnum, col, off]");
+ return;
+ }
+ li = tv_list_find(di->di_tv.vval.v_list, 0L);
+ if (li != NULL) {
+ pos.lnum = tv_get_number_chk(TV_LIST_ITEM_TV(li), &error);
+ if (error) {
+ return;
+ }
+ }
+ li = tv_list_find(di->di_tv.vval.v_list, 1L);
+ if (li != NULL) {
+ pos.col = tv_get_number_chk(TV_LIST_ITEM_TV(li), &error) - 1;
+ if (error) {
+ return;
+ }
+ }
+ li = tv_list_find(di->di_tv.vval.v_list, 2L);
+ if (li != NULL) {
+ pos.coladd = tv_get_number_chk(TV_LIST_ITEM_TV(li), &error);
+ if (error) {
+ return;
+ }
+ }
+ }
+ }
- memmove(msgbuf + STRLEN(msgbuf) - len, t, len);
- if (dirc == '?' && cur == 100) {
- cur = -1;
+ save_last_search_pattern();
+ if (pattern != NULL) {
+ if (*pattern == NUL) {
+ goto the_end;
}
+ xfree(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
+ }
- xfree(lastpat);
- lastpat = vim_strsave(spats[last_idx].pat);
- chgtick = buf_get_changedtick(curbuf);
- lbuf = curbuf;
- lastpos = p;
+ update_search_stat(0, &pos, &pos, &stat, recompute, maxcount, timeout);
- // keep the message even after redraw, but don't put in history
- msg_hist_off = true;
- msg_ext_set_kind("search_count");
- give_warning(msgbuf, false);
- msg_hist_off = false;
- }
- p_ws = save_ws;
+ tv_dict_add_nr(rettv->vval.v_dict, S_LEN("current"), stat.cur);
+ tv_dict_add_nr(rettv->vval.v_dict, S_LEN("total"), stat.cnt);
+ tv_dict_add_nr(rettv->vval.v_dict, S_LEN("exact_match"), stat.exact_match);
+ tv_dict_add_nr(rettv->vval.v_dict, S_LEN("incomplete"), stat.incomplete);
+ tv_dict_add_nr(rettv->vval.v_dict, S_LEN("maxcount"), stat.last_maxcount);
+
+the_end:
+ restore_last_search_pattern();
}
/*
diff --git a/src/nvim/search.h b/src/nvim/search.h
index 0366aee8a1..98ddaa5eeb 100644
--- a/src/nvim/search.h
+++ b/src/nvim/search.h
@@ -6,6 +6,7 @@
#include "nvim/vim.h"
#include "nvim/buffer_defs.h"
+#include "nvim/eval/funcs.h"
#include "nvim/eval/typval.h"
#include "nvim/normal.h"
#include "nvim/os/time.h"
@@ -49,6 +50,11 @@
#define RE_BOTH 2 /* save pat in both patterns */
#define RE_LAST 2 /* use last used pattern if "pat" is NULL */
+// Values for searchcount()
+#define SEARCH_STAT_DEF_TIMEOUT 40L
+#define SEARCH_STAT_DEF_MAX_COUNT 99
+#define SEARCH_STAT_BUF_LEN 12
+
/// Structure containing offset definition for the last search pattern
///
/// @note Only offset for the last search pattern is used, not for the last
@@ -78,6 +84,16 @@ typedef struct {
int sa_wrapped; ///< search wrapped around
} searchit_arg_T;
+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;
#ifdef INCLUDE_GENERATED_DECLARATIONS
# include "search.h.generated.h"
diff --git a/src/nvim/testdir/test_search_stat.vim b/src/nvim/testdir/test_search_stat.vim
index 11c6489ca2..335a51268d 100644
--- a/src/nvim/testdir/test_search_stat.vim
+++ b/src/nvim/testdir/test_search_stat.vim
@@ -8,6 +8,41 @@ func Test_search_stat()
set shortmess-=S
" Append 50 lines with text to search for, "foobar" appears 20 times
call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 10))
+ call nvim_win_set_cursor(0, [1, 0])
+
+ " searchcount() returns an empty dictionary when previous pattern was not set
+ call assert_equal({}, searchcount(#{pattern: ''}))
+ " but setting @/ should also work (even 'n' nor 'N' was executed)
+ " recompute the count when the last position is different.
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 40, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'foo'}))
+ call assert_equal(
+ \ #{current: 0, exact_match: 0, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar'}))
+ call assert_equal(
+ \ #{current: 0, exact_match: 0, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar', pos: [2, 1, 0]}))
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar', pos: [3, 1, 0]}))
+ " on last char of match
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar', pos: [3, 9, 0]}))
+ " on char after match
+ call assert_equal(
+ \ #{current: 1, exact_match: 0, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar', pos: [3, 10, 0]}))
+ call assert_equal(
+ \ #{current: 1, exact_match: 0, total: 10, incomplete: 0, maxcount: 99},
+ \ searchcount(#{pattern: 'fooooobar', pos: [4, 1, 0]}))
+ call assert_equal(
+ \ #{current: 1, exact_match: 0, total: 2, incomplete: 2, maxcount: 1},
+ \ searchcount(#{pattern: 'fooooobar', pos: [4, 1, 0], maxcount: 1}))
+ call assert_equal(
+ \ #{current: 0, exact_match: 0, total: 2, incomplete: 2, maxcount: 1},
+ \ searchcount(#{pattern: 'fooooobar', maxcount: 1}))
" match at second line
call cursor(1, 1)
@@ -17,6 +52,9 @@ func Test_search_stat()
let stat = '\[2/50\]'
let pat = escape(@/, '()*?'). '\s\+'
call assert_match(pat .. stat, g:a)
+ call assert_equal(
+ \ #{current: 2, exact_match: 1, total: 50, incomplete: 0, maxcount: 99},
+ \ searchcount(#{recompute: 0}))
" didn't get added to message history
call assert_equal(messages_before, execute('messages'))
@@ -25,6 +63,9 @@ func Test_search_stat()
let g:a = execute(':unsilent :norm! n')
let stat = '\[50/50\]'
call assert_match(pat .. stat, g:a)
+ call assert_equal(
+ \ #{current: 50, exact_match: 1, total: 50, incomplete: 0, maxcount: 99},
+ \ searchcount(#{recompute: 0}))
" No search stat
set shortmess+=S
@@ -32,6 +73,14 @@ func Test_search_stat()
let stat = '\[2/50\]'
let g:a = execute(':unsilent :norm! n')
call assert_notmatch(pat .. stat, g:a)
+ call writefile(getline(1, '$'), 'sample.txt')
+ " n does not update search stat
+ call assert_equal(
+ \ #{current: 50, exact_match: 1, total: 50, incomplete: 0, maxcount: 99},
+ \ searchcount(#{recompute: 0}))
+ call assert_equal(
+ \ #{current: 2, exact_match: 1, total: 50, incomplete: 0, maxcount: 99},
+ \ searchcount(#{recompute: v:true}))
set shortmess-=S
" Many matches
@@ -41,10 +90,28 @@ func Test_search_stat()
let g:a = execute(':unsilent :norm! n')
let stat = '\[>99/>99\]'
call assert_match(pat .. stat, g:a)
+ call assert_equal(
+ \ #{current: 100, exact_match: 0, total: 100, incomplete: 2, maxcount: 99},
+ \ searchcount(#{recompute: 0}))
+ call assert_equal(
+ \ #{current: 272, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+ \ searchcount(#{recompute: v:true, maxcount: 0, timeout: 200}))
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+ \ searchcount(#{recompute: 1, maxcount: 0, pos: [1, 1, 0], timeout: 200}))
call cursor(line('$'), 1)
let g:a = execute(':unsilent :norm! n')
let stat = 'W \[1/>99\]'
call assert_match(pat .. stat, g:a)
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 100, incomplete: 2, maxcount: 99},
+ \ searchcount(#{recompute: 0}))
+ call assert_equal(
+ \ #{current: 1, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+ \ searchcount(#{recompute: 1, maxcount: 0, timeout: 200}))
+ call assert_equal(
+ \ #{current: 271, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+ \ searchcount(#{recompute: 1, maxcount: 0, pos: [line('$')-2, 1, 0], timeout: 200}))
" Many matches
call cursor(1, 1)
@@ -180,12 +247,22 @@ func Test_search_stat()
call assert_match('^\s\+' .. stat, g:b)
unmap n
+ " Time out
+ %delete _
+ call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 100000))
+ call cursor(1, 1)
+ call assert_equal(1, searchcount(#{pattern: 'foo', maxcount: 0, timeout: 1}).incomplete)
+
" Clean up
set shortmess+=S
" close the window
bwipe!
endfunc
+func Test_searchcount_fails()
+ call assert_fails('echo searchcount("boo!")', 'E715:')
+endfunc
+
func Test_search_stat_foldopen()
CheckScreendump
@@ -252,9 +329,9 @@ func Test_searchcount_in_statusline()
function TestSearchCount() abort
let search_count = searchcount()
if !empty(search_count)
- return '[' . search_count.current . '/' . search_count.total . ']'
+ return '[' . search_count.current . '/' . search_count.total . ']'
else
- return ''
+ return ''
endif
endfunction
set hlsearch