changeset 20647:8a2b86a39ef4 v8.2.0877

patch 8.2.0877: cannot get the search statistics Commit: https://github.com/vim/vim/commit/e8f5ec0d30b629d7166f0ad03434065d8bc822df Author: Bram Moolenaar <Bram@vim.org> Date: Mon Jun 1 17:28:35 2020 +0200 patch 8.2.0877: cannot get the search statistics Problem: Cannot get the search statistics. Solution: Add the searchcount() function. (Fujiwara Takuya, closes https://github.com/vim/vim/issues/4446)
author Bram Moolenaar <Bram@vim.org>
date Mon, 01 Jun 2020 17:30:04 +0200
parents ad14bd3f8c4f
children 78db823f74d0
files runtime/doc/eval.txt src/evalfunc.c src/macros.h src/proto/search.pro src/search.c src/testdir/test_search_stat.vim src/version.c
diffstat 7 files changed, 496 insertions(+), 100 deletions(-) [+]
line wrap: on
line diff
--- a/runtime/doc/eval.txt
+++ b/runtime/doc/eval.txt
@@ -2714,6 +2714,7 @@ screenrow()			Number	current cursor row
 screenstring({row}, {col})	String	characters at screen position
 search({pattern} [, {flags} [, {stopline} [, {timeout}]]])
 				Number	search for {pattern}
+searchcount([{options}])	Dict	get or update search stats
 searchdecl({name} [, {global} [, {thisblock}]])
 				Number	search for variable declaration
 searchpair({start}, {middle}, {end} [, {flags} [, {skip} [...]]])
@@ -8429,6 +8430,126 @@ search({pattern} [, {flags} [, {stopline
 		Can also be used as a |method|: >
 			GetPattern()->search()
 
+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}.
 
--- a/src/evalfunc.c
+++ b/src/evalfunc.c
@@ -801,6 +801,7 @@ static funcentry_T global_functions[] =
     {"screenrow",	0, 0, 0,	  ret_number,	f_screenrow},
     {"screenstring",	2, 2, FEARG_1,	  ret_string,	f_screenstring},
     {"search",		1, 4, FEARG_1,	  ret_number,	f_search},
+    {"searchcount",	0, 1, FEARG_1,	  ret_dict_any,	f_searchcount},
     {"searchdecl",	1, 3, FEARG_1,	  ret_number,	f_searchdecl},
     {"searchpair",	3, 7, 0,	  ret_number,	f_searchpair},
     {"searchpairpos",	3, 7, 0,	  ret_list_number, f_searchpairpos},
--- a/src/macros.h
+++ b/src/macros.h
@@ -33,6 +33,7 @@
 		       : (a)->coladd < (b)->coladd)
 #define EQUAL_POS(a, b) (((a).lnum == (b).lnum) && ((a).col == (b).col) && ((a).coladd == (b).coladd))
 #define CLEAR_POS(a) do {(a)->lnum = 0; (a)->col = 0; (a)->coladd = 0;} while (0)
+#define EMPTY_POS(a) ((a).lnum == 0 && (a).col == 0 && (a).coladd == 0)
 
 #define LTOREQ_POS(a, b) (LT_POS(a, b) || EQUAL_POS(a, b))
 
--- a/src/proto/search.pro
+++ b/src/proto/search.pro
@@ -35,4 +35,5 @@ int linewhite(linenr_T lnum);
 void find_pattern_in_path(char_u *ptr, int dir, int len, int whole, int skip_comments, int type, long count, int action, linenr_T start_lnum, linenr_T end_lnum);
 spat_T *get_spat(int idx);
 int get_spat_last_idx(void);
+void f_searchcount(typval_T *argvars, typval_T *rettv);
 /* vim: set ft=c : */
--- a/src/search.c
+++ b/src/search.c
@@ -21,7 +21,24 @@ static int check_linecomment(char_u *lin
 static void show_pat_in_path(char_u *, int,
 					 int, int, FILE *, linenr_T *, long);
 #endif
-static void search_stat(int dirc, pos_T *pos, int show_top_bot_msg, char_u *msgbuf, int recompute);
+
+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 20L
+#define SEARCH_STAT_DEF_MAX_COUNT 99
+#define SEARCH_STAT_BUF_LEN 12
 
 /*
  * This file contains various searching-related routines. These fall into
@@ -1203,7 +1220,6 @@ do_search(
     char_u	    *msgbuf = NULL;
     size_t	    len;
     int		    has_offset = FALSE;
-#define SEARCH_STAT_BUF_LEN 12
 
     /*
      * A line offset is not remembered, this is vi compatible.
@@ -1591,13 +1607,17 @@ do_search(
 		&& c != FAIL
 		&& !shortmess(SHM_SEARCHCOUNT)
 		&& msgbuf != NULL)
-	    search_stat(dirc, &pos, show_top_bot_msg, msgbuf,
-			(count != 1 || has_offset
+	     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))
+				 || (!(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.
@@ -3061,107 +3081,57 @@ linewhite(linenr_T lnum)
 
 /*
  * Add the search count "[3/19]" to "msgbuf".
- * When "recompute" is TRUE always recompute the numbers.
+ * See update_search_stat() for other arguments.
  */
     static void
-search_stat(
-    int	    dirc,
-    pos_T   *pos,
-    int	    show_top_bot_msg,
-    char_u  *msgbuf,
-    int	    recompute)
+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)
 {
-    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	    chgtick = 0;
-    static char_u   *lastpat = NULL;
-    static buf_T    *lbuf = NULL;
-#ifdef FEAT_RELTIME
-    proftime_T  start;
-#endif
-#define OUT_OF_TIME 999
-
-    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.
-    if (!(chgtick == CHANGEDTICK(curbuf)
-	&& MB_STRNICMP(lastpat, spats[last_idx].pat, STRLEN(lastpat)) == 0
-	&& STRLEN(lastpat) == STRLEN(spats[last_idx].pat)
-	&& EQUAL_POS(lastpos, curwin->w_cursor)
-	&& lbuf == curbuf) || wraparound || cur < 0 || cur > 99 || recompute)
+    searchstat_T stat;
+
+    update_search_stat(dirc, pos, cursor_pos, &stat, recompute, maxcount,
+								      timeout);
+    if (stat.cur > 0)
     {
-	cur = 0;
-	cnt = 0;
-	CLEAR_POS(&lastpos);
-	lbuf = curbuf;
-    }
-
-    if (EQUAL_POS(lastpos, curwin->w_cursor) && !wraparound
-					&& (dirc == '/' ? cur < cnt : cur > 0))
-	cur += dirc == '/' ? 1 : -1;
-    else
-    {
-	p_ws = FALSE;
-#ifdef FEAT_RELTIME
-	profile_setlimit(20L, &start);
-#endif
-	while (!got_int && searchit(curwin, curbuf, &lastpos, NULL,
-			 FORWARD, NULL, 1, SEARCH_KEEP, RE_LAST, NULL) != FAIL)
-	{
-#ifdef FEAT_RELTIME
-	    // Stop after passing the time limit.
-	    if (profile_passed_limit(&start))
-	    {
-		cnt = OUT_OF_TIME;
-		cur = OUT_OF_TIME;
-		break;
-	    }
-#endif
-	    cnt++;
-	    if (LTOREQ_POS(lastpos, p))
-		cur++;
-	    fast_breakcheck();
-	    if (cnt > 99)
-		break;
-	}
-	if (got_int)
-	    cur = -1; // abort
-    }
-    if (cur > 0)
-    {
-	char	t[SEARCH_STAT_BUF_LEN] = "";
+	char	t[SEARCH_STAT_BUF_LEN];
 	size_t	len;
 
 #ifdef FEAT_RIGHTLEFT
 	if (curwin->w_p_rl && *curwin->w_p_rlc == 's')
 	{
-	    if (cur == OUT_OF_TIME)
+	    if (stat.incomplete == 1)
 		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 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]", cnt, cur);
+		vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]",
+							   stat.cnt, stat.cur);
 	}
 	else
 #endif
 	{
-	    if (cur == OUT_OF_TIME)
+	    if (stat.incomplete == 1)
 		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 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]", cur, cnt);
+		vim_snprintf(t, SEARCH_STAT_BUF_LEN, "[%d/%d]",
+							   stat.cur, stat.cnt);
 	}
 
 	len = STRLEN(t);
@@ -3174,20 +3144,140 @@ search_stat(
 	}
 
 	mch_memmove(msgbuf + STRLEN(msgbuf) - len, t, len);
-	if (dirc == '?' && cur == 100)
-	    cur = -1;
-
-	vim_free(lastpat);
-	lastpat = vim_strsave(spats[last_idx].pat);
-	chgtick = CHANGEDTICK(curbuf);
-	lbuf    = curbuf;
-	lastpos = p;
+	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)
+{
+    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 (LTOREQ_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;
 }
 
@@ -3959,3 +4049,118 @@ 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 = argvars[0].vval.v_dict;
+	dictitem_T	*di;
+	listitem_T	*li;
+	int		error = FALSE;
+
+	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;
+	}
+	di = dict_find(dict, (char_u *)"recompute", -1);
+	if (di != NULL)
+	{
+	    recompute = tv_get_number_chk(&di->di_tv, &error);
+	    if (error)
+		return;
+	}
+	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;
+	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();
+}
+#endif
--- a/src/testdir/test_search_stat.vim
+++ b/src/testdir/test_search_stat.vim
@@ -9,14 +9,44 @@ func Test_search_stat()
   " Append 50 lines with text to search for, "foobar" appears 20 times
   call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 10))
 
+  call cursor(1, 1)
+
+  " 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]}))
+  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)
   let messages_before = execute('messages')
   let @/ = 'fo*\(bar\?\)\?'
   let g:a = execute(':unsilent :norm! n')
   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 +55,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 +65,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 +82,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}))
+  call assert_equal(
+    \ #{current: 1, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+    \ searchcount(#{recompute: 1, maxcount: 0, pos: [1, 1, 0]}))
   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}))
+  call assert_equal(
+    \ #{current: 271, exact_match: 1, total: 280, incomplete: 0, maxcount: 0},
+    \ searchcount(#{recompute: 1, maxcount: 0, pos: [line('$')-2, 1, 0]}))
 
   " Many matches
   call cursor(1, 1)
@@ -180,6 +239,12 @@ 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
--- a/src/version.c
+++ b/src/version.c
@@ -747,6 +747,8 @@ static char *(features[]) =
 static int included_patches[] =
 {   /* Add new patch number below this line */
 /**/
+    877,
+/**/
     876,
 /**/
     875,