I've searched hard for this and haven't been able to find what I'm after.
On my statusline I want a count of the number of matches that occur in the current file. The vim command below returns what I want. I need the returned number to be displayed in my statusline.
:%s/^I^I//n
vim returns: 16 matches on 16 lines
FYI Explanation: I'm working in a CSV file. I'm searching for two tab characters ( ^I^I ) because that indicates lines I still need to do work on. So my desired statusline would indicate how much work remains in the current file.
I don't know how to enter a vim command on the statusline, I know that %{} can be used to run a function but how do I run the vim search command? I've tried variations of the following, but they clearly aren't right and just end up with an error.
:set statusline+= %{s/^I^I//n}
Help me vimy one kenobi, you're my only hope!
The first thing to mention here is that for large files this feature would be completely impractical. The reason is that the status line is redrawn after every cursor movement, after the completion of every command, and probably following other events that I am not even aware of. Performing a regex search on the entire buffer, and furthermore, not just the current buffer, but every visible window (since every window has its own status line), would slow things down significantly. Don't get me wrong; the idea behind this feature is a good one, as it would give you an immediate and fully automated indication of your remaining work, but computers are simply not infinitely performant (unfortunately), and so this could easily become a problem. I've edited files with millions of lines of text, and a single regex search can take many seconds on such buffers.
But provided your files will remain fairly small, I've figured out three possible solutions by which you can achieve this.
Solution #1: exe :s and redirect output
You can use
:exe
from a function to run the:s
command with a parameterized pattern, and:redir
to redirect the output into a local variable.Unfortunately, this has two undesirable side effects, which, in the context of this feature, would be complete deal-breakers, since they would occur every time the status line is redrawn:
:s
from a status line call or by manually typing it out on the vim command-line.)(And there actually might be more adverse effects that I'm not aware of.)
The cursor issue can be fixed by saving and restoring the cursor position via
getcurpos()
andsetpos()
. Note that it must begetcurpos()
and notgetpos()
because the latter does not return thecurswant
field, which is necessary for preserving the column that the cursor "wants" to reside at, which may be different from the column the cursor is "actually" at (e.g. if the cursor was moved into a shorter line). Unfortunately,getcurpos()
is a fairly recent addition to vim, namely 7.4.313, and based on my testing doesn't even seem to work correctly. Fortunately, there are the olderwinsaveview()
andwinrestview()
functions which can accomplish the task perfectly and compatibly. So for now, we'll use those.Solution #1a: Restore visual selection with gv
The visual selection issue I thought could be solved by running
gv
in normal mode, but for some reason the visual selection gets completely corrupted when doing this. I've tested this on Cygwin CLI and Windows gvim, and I don't have a solution for this (with respect to restoring the visual selection).In any case, here's the result of the above design:
A few random notes:
^[\s\n]*
in the match-count extraction pattern was necessary to barrel through the leading line break that gets captured during the redirection (not sure why that happens). An alternative would be to skip over any character up to the first digit with a non-greedy multiplier on the dot atom, i.e.^.\{-}
.statusline
option value is necessary because backslash interpolation/removal occurs during parsing of the option value itself. In general, single-quoted strings do not cause backslash interpolation/removal, and ourpat
string, once parsed, eventually gets concatenated directly with the:s
string passed to:exe
, thus there's no backslash interpolation/removal at those points (at least not prior to the evaluation of the:s
command, when backslash interpolation of our backslashes does occur, which is what we want). I find this to be slightly confusing, since inside the%{}
construct you'd expect it to be a normal unadulterated VimScript expression, but that's the way it works./e
flag for the:s
command. This is necessary to handle the case of a buffer with zero matches. Normally,:s
actually throws an error if there are zero matches. For a status line call, this is a big problem, because any error thrown while attempting to redraw the status line causes vim to nullify thestatusline
option as a defensive measure to prevent repeated errors. I originally looked for solutions that involved catching the error, such as:try
and:catch
, but nothing worked; once an error is thrown, a flag is set in the vim source (called_emsg
) that we can't unset, and so thestatusline
is doomed at that point. Fortunately, I discovered the/e
flag, which prevents an error from being thrown at all.Solution #1b: Dodge visual mode with a buffer-local cache
I wasn't satisfied with the visual selection issue, so I wrote an alternative solution. This solution actually avoids running the search at all if visual mode is in effect, and instead pulls the last-known search count from a buffer-local cache. I'm pretty sure this will never cause the search count to become out-of-date, because it is impossible to edit the buffer without abandoning visual mode (I'm pretty sure...).
So now the
MatchCount()
function does not mess with visual mode:And now we need this helper "predicate" function which tells us when it's (not) safe to run the
:s
command:And now we need a caching layer that branches on the predicate result and only runs the primary function if safe, otherwise it pulls from the buffer-local cache the last-known return value that was captured from the most recent call of the primary function taking those exact arguments:
For which we need this helper function which I found somewhere on the Internet years ago:
And finally this is how we can set the
statusline
:Solution #2: Call
match()
on every lineI've thought of another possible solution which is actually much simpler, and seems to perform just fine for non-huge files, even though it involves more looping and processing at the VimScript level. This is to loop over every line in the file and call
match()
on it:Solution #3: Call
search()
/searchpos()
repeatedlyI've written some slightly intricate functions to perform global and linewise matching, built around
searchpos()
andsearch()
, respectively. I've included support for optional start and end bounds as well.