Attached patch implements search statistics, like wished at #453.

This means, when searching, a little buffer on the right  will be shown, 
that shows, on which match the cursor currently is and how many matches 
are in the current buffer, e.g. when searching you'll see something like 
this:
,----
| /foobar                                          [01/10]
`----

Currently, this is limited to at most 99 matches, more matches will be 
indicated by something like [>99/>99] if the cursor is beyond match 100.

To not delay searching too much, the search is limited by 20ms, if the 
search does not finish, it will show something like [?/??].

Current caveat: the counting does not take search offsets into account. 
To get this done correctly is not easy, since the offset will be added 
after the search is done and who knows how many matches have been 
skipped by then. So I have just left this documented.

This is made the default behaviour, if you don't want that,
":set shortmess+=S" gets rid of it.

The current behaviour is also tested by the new test_search_stat.vim, 
which catches the current match positions using execute(). Test seems to 
work, although running it interactively does make the test fail. I don't 
know why this happens, but since the test works, I guess this is okay.

Best,
Christian
-- 
Man sollte das Leben anderer nicht nur nach seinen eigenen
Bedürfnissen beurteilen.
                -- Michael Drury

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
For more options, visit https://groups.google.com/d/optout.
From bf78fe05c78896b8df0a8a43c487fbbb79b9d74a Mon Sep 17 00:00:00 2001
From: Christian Brabandt <[email protected]>
Date: Thu, 21 Jul 2016 17:12:21 +0200
Subject: [PATCH] Add search statistics

1) output search statistics like

When 'shortmess' does not include the "S" flag, Vim will automatically show an
index, on which the cursor is. This can look like this: >

    [01/05]	Cursor is on first of 5 matches
    [01/>99]      Cursor is on first of more than 99 matches
    [>99/>99]     Cursor is after 99 match of more than 99 matches
    [ ?/??]       Unknown how many matches exists, generating the
		statistics was aborted because of bad performance.

Note: the count does not take offset into account.

2) Updated documentation
3) Add tests

This implements issue #453.

---
 runtime/doc/options.txt          |   8 +-
 runtime/doc/pattern.txt          |  11 +++
 src/option.c                     |   4 +-
 src/option.h                     |   4 +-
 src/search.c                     | 207 +++++++++++++++++++++++++++++++++++----
 src/testdir/Make_all.mak         |   1 +
 src/testdir/test_search_stat.vim |  84 ++++++++++++++++
 7 files changed, 294 insertions(+), 25 deletions(-)
 create mode 100644 src/testdir/test_search_stat.vim

diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt
index ea247e2..5969728 100644
--- a/runtime/doc/options.txt
+++ b/runtime/doc/options.txt
@@ -1767,7 +1767,7 @@ A jump table for the options with a short description can be found at |Q_op|.
 	'scrolljump'	  1		no jump scroll
 	'scrolloff'	  0		no scroll offset
 	'shiftround'	  off		indent not rounded to shiftwidth
-	'shortmess'	+ ""		no shortening of messages
+	'shortmess'	+ "S"		no shortening of messages
 	'showcmd'	+ off		command characters not shown
 	'showmode'	+ off		current mode not shown
 	'smartcase'	  off		no automatic ignore case switch
@@ -6523,8 +6523,8 @@ A jump table for the options with a short description can be found at |Q_op|.
 	function to get the effective shiftwidth value.
 
 						*'shortmess'* *'shm'*
-'shortmess' 'shm'	string	(Vim default "filnxtToO", Vi default: "",
-							POSIX default: "A")
+'shortmess' 'shm'	string	(Vim default "filnxtToO", Vi default: "S",
+							POSIX default: "AS")
 			global
 			{not in Vi}
 	This option helps to avoid all the |hit-enter| prompts caused by file
@@ -6565,6 +6565,8 @@ A jump table for the options with a short description can be found at |Q_op|.
 	  q	use "recording" instead of "recording @a"
 	  F	don't give the file info when editing a file, like `:silent`
 		was used for the command
+	  S     don't show search count message when searching, e.g.
+	        "[01/05]"
 
 	This gives you the opportunity to avoid that a change between buffers
 	requires you to hit <Enter>, but still gives as useful a message as
diff --git a/runtime/doc/pattern.txt b/runtime/doc/pattern.txt
index d47fd92..cf2f224 100644
--- a/runtime/doc/pattern.txt
+++ b/runtime/doc/pattern.txt
@@ -152,6 +152,17 @@ use <Esc> to abandon the search.
 All matches for the last used search pattern will be highlighted if you set
 the 'hlsearch' option.  This can be suspended with the |:nohlsearch| command.
 
+When 'shortmess' does not include the "S" flag, Vim will automatically show an
+index, on which the cursor is. This can look like this: >
+
+  [01/05]	Cursor is on first of 5 matches
+  [01/>99]      Cursor is on first of more than 99 matches
+  [>99/>99]     Cursor is after 99 match of more than 99 matches
+  [ ?/??]       Unknown how many matches exists, generating the
+                statistics was aborted because of bad performance.
+
+Note: the count does not take offset into account.
+
 When no match is found you get the error: *E486* Pattern not found
 Note that for the |:global| command this behaves like a normal message, for Vi
 compatibility.  For the |:s| command the "e" flag can be used to avoid the
diff --git a/src/option.c b/src/option.c
index 288c01b..ac2e33c 100644
--- a/src/option.c
+++ b/src/option.c
@@ -2363,7 +2363,7 @@ static struct vimoption options[] =
 			    {(char_u *)8L, (char_u *)0L} SCRIPTID_INIT},
     {"shortmess",   "shm",  P_STRING|P_VIM|P_FLAGLIST,
 			    (char_u *)&p_shm, PV_NONE,
-			    {(char_u *)"", (char_u *)"filnxtToO"}
+			    {(char_u *)"S", (char_u *)"filnxtToO"}
 			    SCRIPTID_INIT},
     {"shortname",   "sn",   P_BOOL|P_VI_DEF,
 			    (char_u *)&p_sn, PV_SN,
@@ -3171,7 +3171,7 @@ set_init_1(void)
     if (mch_getenv((char_u *)"VIM_POSIX") != NULL)
     {
 	set_string_default("cpo", (char_u *)CPO_ALL);
-	set_string_default("shm", (char_u *)"A");
+	set_string_default("shm", (char_u *)SHM_POSIX);
     }
 
     /*
diff --git a/src/option.h b/src/option.h
index f05a466..3511247 100644
--- a/src/option.h
+++ b/src/option.h
@@ -211,7 +211,9 @@
 #define SHM_COMPLETIONMENU  'c'		/* completion menu messages */
 #define SHM_RECORDING	'q'		/* short recording message */
 #define SHM_FILEINFO	'F'		/* no file info messages */
-#define SHM_ALL		"rmfixlnwaWtToOsAIcqF" /* all possible flags for 'shm' */
+#define SHM_SEARCHCOUNT  'S'	        /* search stats: '[ 1/10 ]' when searching */
+#define SHM_POSIX       "AS"            /* POSIX value */
+#define SHM_ALL		"rmfixlnwaWtToOsAIcqFS" /* all possible flags for 'shm' */
 
 /* characters for p_go: */
 #define GO_ASEL		'a'		/* autoselect */
diff --git a/src/search.c b/src/search.c
index 5fc6820..93f0eef 100644
--- a/src/search.c
+++ b/src/search.c
@@ -33,6 +33,7 @@ static void show_pat_in_path(char_u *, int,
 #ifdef FEAT_VIMINFO
 static void wvsp_one(FILE *fp, int idx, char *s, int sc);
 #endif
+static void search_stat(int dirc, pos_T *pos, char_u  *msgbuf);
 
 /*
  * This file contains various searching-related routines. These fall into
@@ -1149,6 +1150,8 @@ do_search(
     char_u	    *dircp;
     char_u	    *strcopy = NULL;
     char_u	    *ps;
+    char_u	    *msgbuf = NULL;
+    size_t	    len;
 
     /*
      * A line offset is not remembered, this is vi compatible.
@@ -1308,30 +1311,52 @@ do_search(
 	if ((options & SEARCH_ECHO) && messaging()
 					    && !cmd_silent && msg_silent == 0)
 	{
-	    char_u	*msgbuf;
 	    char_u	*trunc;
 
 	    if (*searchstr == NUL)
 		p = spats[last_idx].pat;
 	    else
 		p = searchstr;
-	    msgbuf = alloc((unsigned)(STRLEN(p) + 40));
+
+	    if (!shortmess(SHM_SEARCHCOUNT))
+	    {
+		if (msg_scrolled != 0)
+		    /* 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) + 40 + 11)
+		    len = STRLEN(p) + 40 + 11;
+	    }
+	    else
+		len = STRLEN(p) + 40;
+
+	    /* allocate enough space for the search pattern + offset + search stat
+	     * but leave some space at the end to put the search stat there
+	     */
+	    msgbuf = alloc(len);
+
 	    if (msgbuf != NULL)
 	    {
+
+		vim_memset(msgbuf, ' ', len);
 		msgbuf[0] = dirc;
+		msgbuf[len-1] = NUL;
+
 #ifdef FEAT_MBYTE
 		if (enc_utf8 && utf_iscomposing(utf_ptr2char(p)))
 		{
 		    /* Use a space to draw the composing char on. */
 		    msgbuf[1] = ' ';
-		    STRCPY(msgbuf + 2, p);
+		    STRNCPY(msgbuf + 2, p, STRLEN(p));
 		}
 		else
 #endif
-		    STRCPY(msgbuf + 1, p);
+		    STRNCPY(msgbuf + 1, p, STRLEN(p));
 		if (spats[0].off.line || spats[0].off.end || spats[0].off.off)
 		{
-		    p = msgbuf + STRLEN(msgbuf);
+		    p = msgbuf + STRLEN(p) + 1;
 		    *p++ = dirc;
 		    if (spats[0].off.end)
 			*p++ = 'e';
@@ -1340,13 +1365,20 @@ do_search(
 		    if (spats[0].off.off > 0 || spats[0].off.line)
 			*p++ = '+';
 		    if (spats[0].off.off != 0 || spats[0].off.line)
-			sprintf((char *)p, "%ld", spats[0].off.off);
-		    else
-			*p = NUL;
+		    {
+			int l = 0;
+			l = sprintf((char *)p, "%ld", spats[0].off.off);
+			p[l] = ' '; /* remove NUL from sprintf */
+		    }
 		}
 
 		msg_start();
 		trunc = msg_strtrunc(msgbuf, FALSE);
+		if (trunc != NULL)
+		{
+		    vim_free(msgbuf);
+		    msgbuf = trunc;
+		}
 
 #ifdef FEAT_RIGHTLEFT
 		/* The search pattern could be shown on the right in rightleft
@@ -1357,24 +1389,23 @@ do_search(
 		{
 		    char_u *r;
 
-		    r = reverse_text(trunc != NULL ? trunc : msgbuf);
+		    r = reverse_text(msgbuf);
 		    if (r != NULL)
 		    {
-			vim_free(trunc);
-			trunc = r;
+			vim_free(msgbuf);
+			msgbuf = r;
+			/* move reversed text to beginning of buffer */
+			while (*r != NUL && *r == ' ')
+			    r++;
+			mch_memmove(msgbuf, r, msgbuf + STRLEN(msgbuf) - r);
+			/* overwrite old text */
+			vim_memset(r, ' ', msgbuf + STRLEN(msgbuf) - r);
 		    }
 		}
 #endif
-		if (trunc != NULL)
-		{
-		    msg_outtrans(trunc);
-		    vim_free(trunc);
-		}
-		else
-		    msg_outtrans(msgbuf);
+		msg_outtrans(msgbuf);
 		msg_clr_eos();
 		msg_check();
-		vim_free(msgbuf);
 
 		gotocmdline(FALSE);
 		out_flush();
@@ -1429,6 +1460,11 @@ do_search(
 
 	if (dircp != NULL)
 	    *dircp = dirc;	/* restore second '/' or '?' for normal_cmd() */
+
+	if (!shortmess(SHM_SEARCH) && (dirc == '/' && lt(pos, curwin->w_cursor)
+			|| (dirc == '?' && lt(curwin->w_cursor, pos))))
+		ui_delay(500L, FALSE);  /* leave some time for top_bot_msg */
+
 	if (c == FAIL)
 	{
 	    retval = 0;
@@ -1477,6 +1513,12 @@ do_search(
 	    }
 	}
 
+	if ((options & SEARCH_ECHO) && messaging()
+	    && !(cmd_silent + msg_silent) && c != FAIL &&
+	    !shortmess(SHM_SEARCHCOUNT) && msgbuf != NULL)
+	    search_stat(dirc, &pos, msgbuf);
+
+
 	/*
 	 * The search command can be followed by a ';' to do another search.
 	 * For example: "/pat/;/foo/+3;?bar"
@@ -1507,6 +1549,7 @@ end_do_search:
     if ((options & SEARCH_KEEP) || cmdmod.keeppatterns)
 	spats[0].off = old_off;
     vim_free(strcopy);
+    vim_free(msgbuf);
 
     return retval;
 }
@@ -4786,6 +4829,132 @@ linewhite(linenr_T lnum)
 }
 #endif
 
+    static void
+search_stat(
+    int	    dirc,
+    pos_T   *pos,
+    char_u  *msgbuf)
+{
+    int	    old_ws = p_ws;
+    int	    wraparound = FALSE;
+    pos_T   p = (*pos);
+    static  pos_T   lastpos = {0, 0
+#ifdef FEAT_VIRTUALEDIT
+	, 0
+#endif
+    };
+    static int	    cur = 0;
+    static int	    cnt = 0;
+    static int	    chgtick = 0;
+    static char_u   *lastpat = NULL;
+    static buf_T    *lbuf = NULL;
+
+    wraparound = ((dirc == '?' && lt(lastpos, p)) ||
+	    (dirc == '/' && lt(p, lastpos)));
+
+    /* MB_STRNICMP ignores case, but we should not ignore case.
+	* Unfortunately, there is not MB_STRNICMP function */
+    if (!(chgtick == curbuf->b_changedtick &&
+	    MB_STRNICMP(lastpat, spats[last_idx].pat, STRLEN(lastpat)) == 0 &&
+	    STRLEN(lastpat) == STRLEN(spats[last_idx].pat) &&
+	    equalpos(lastpos, curwin->w_cursor) &&
+	    lbuf == curbuf) || wraparound || cur < 0 || cur > 99)
+
+    {
+	cur = 0;
+	cnt = 0;
+	clearpos(&lastpos);
+	lbuf = curbuf;
+    }
+
+
+    if (equalpos(lastpos, curwin->w_cursor) && !wraparound &&
+	    (dirc == '/' ? cur < cnt : cur > 0))
+	cur += dirc == '/' ? 1 : -1;
+    else
+    {
+	p_ws = FALSE;
+#ifdef FEAT_RELTIME
+	proftime_T  start;
+
+	profile_setlimit(20L, &start);
+	    /* Stop after passing the "tm" time limit. */
+#else
+	time_T seconds = (time_t)0;
+#endif
+	while (!got_int && searchit(curwin, curbuf, &lastpos, FORWARD, NULL, 1,
+		SEARCH_PEEK + SEARCH_KEEP, RE_LAST, (linenr_T)0, NULL) != FAIL)
+	{
+	    if
+#ifdef FEAT_RELTIME
+	    (profile_passed_limit(&start))
+#else
+		(time(NULL) - start > (time_t)0)
+#endif
+	    {
+		cnt = 150;
+		cur = 150;
+		break;
+	    }
+	    cnt++;
+	    if (ltoreq(lastpos, p))
+		cur++;
+	    fast_breakcheck();
+	    if (cnt > 99)
+		break;
+	}
+	if (got_int)
+	    cur = -1; /* abort */
+    }
+    if (cur > 0)
+    {
+	char	t[10] = "";
+#ifdef FEAT_RIGHTLEFT
+	if (curwin->w_p_rl && *curwin->w_p_rlc == 's')
+	{
+	    if (cur == 150) /* time limit hit */
+		sprintf(t, "[ ?/??]");
+	    else if (cnt > 99 && cur > 99)
+		sprintf(t, "[>99/>99]");
+	    else if (cnt > 99)
+		sprintf(t, "[>99/%02d]", cur);
+	    else
+		sprintf(t, "[%02d/%02d]", cnt, cur);
+	}
+	else
+#endif
+	{
+	    if (cur == 150)
+		sprintf(t, "[ ?/??]");
+	    else if (cnt > 99 && cur > 99)
+		sprintf(t, "[>99/>99]");
+	    else if (cnt > 99)
+		sprintf(t, "[%02d/>99]", cur);
+	    else
+		sprintf(t, "[%02d/%02d]", cur, cnt);
+	}
+	STRNCPY(msgbuf + STRLEN(msgbuf) - STRLEN(t), t, STRLEN(t));
+	if (dirc == '?' && cur == 100)
+	    cur = -1;
+
+	vim_free(lastpat);
+	lastpat = vim_strsave(spats[last_idx].pat);
+	chgtick = curbuf->b_changedtick;
+	lbuf    = curbuf;
+	lastpos = p;
+
+	msg_start();
+	msg_outtrans(msgbuf);
+	msg_clr_eos();
+	msg_check();
+
+	gotocmdline(FALSE);
+	out_flush();
+	msg_nowait = TRUE;	    /* don't wait for this message */
+    }
+    p_ws = old_ws;
+}
+
 #if defined(FEAT_FIND_ID) || defined(PROTO)
 /*
  * Find identifiers or defines in included files.
diff --git a/src/testdir/Make_all.mak b/src/testdir/Make_all.mak
index 11cc934..79b05cc 100644
--- a/src/testdir/Make_all.mak
+++ b/src/testdir/Make_all.mak
@@ -185,6 +185,7 @@ NEW_TESTS = test_arglist.res \
 	    test_perl.res \
 	    test_quickfix.res \
 	    test_ruby.res \
+	    test_search_stat.res \
 	    test_stat.res \
 	    test_syntax.res \
 	    test_usercommands.res \
diff --git a/src/testdir/test_search_stat.vim b/src/testdir/test_search_stat.vim
new file mode 100644
index 0000000..614f8b5
--- /dev/null
+++ b/src/testdir/test_search_stat.vim
@@ -0,0 +1,84 @@
+" Tests for search_stats
+"
+" This test is fragile, it might not work interactively, but it works when run
+" as test!
+
+func! Test_search_stat()
+  new
+  call append(0, repeat(['foobar', 'foo', 'fooooobar', 'foba', 'foobar'], 10))
+  " 1) match at second line
+  call cursor(1, 1)
+  let @/='fo*\(bar\?\)\?'
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[02/50\]'
+  let pat = escape(@/, '()*?'). '\s\+'
+  call assert_match(pat.stat, g:a)
+
+  " 2) Match at last line
+  call cursor(line('$')-2, 1)
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[50/50\]'
+  call assert_match(pat.stat, g:a)
+
+  " 3) No search stat
+  set shortmess+=S
+  call cursor(1, 1)
+  let stat='\[02/50\]'
+  let g:a = execute(':unsilent :norm! n')
+  call assert_notmatch(pat.stat, g:a)
+  set shortmess-=S
+
+  " 4) Many matches
+  call cursor(line('$')-2, 1)
+  let @/='.'
+  let pat = escape(@/, '()*?'). '\s\+'
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[>99/>99\]'
+  call assert_match(pat.stat, g:a)
+
+  " 5) Many matches
+  call cursor(1, 1)
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[02/>99\]'
+  call assert_match(pat.stat, g:a)
+
+  " 6) right-left
+  set rl
+  call cursor(1,1)
+  let @/='foobar'
+  let pat = 'raboof/\s\+'
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[20/02\]'
+  call assert_match(pat.stat, g:a)
+  set norl
+
+  " 7) right-left bottom
+  set rl
+  call cursor('$',1)
+  let pat = 'raboof?\s\+'
+  let g:a = execute(':unsilent :norm! N')
+  let stat='\[20/20\]'
+  call assert_match(pat.stat, g:a)
+  set norl
+
+  " 8) right-left back at top
+  set rl
+  call cursor('$',1)
+  let pat = 'raboof/\s\+'
+  let g:a = execute(':unsilent :norm! n')
+  let stat='\[20/01\]'
+  call assert_match(pat.stat, g:a)
+  call assert_match('search hit BOTTOM, continuing at TOP', g:a)
+  set norl
+
+  " 9) normal, back at top
+  call cursor(1,1)
+  let pat = '?foobar\s\+'
+  let g:a = execute(':unsilent :norm! N')
+  let stat='\[20/20\]'
+  call assert_match(pat.stat, g:a)
+  call assert_match('search hit TOP, continuing at BOTTOM', g:a)
+
+  " close the window
+  bd!
+endfunc
-- 
2.1.4

Raspunde prin e-mail lui