Vim Statusline: Word Search

I searched this for this and could not find what I needed.

In my status bar, I want to count the number of matches that occur in the current file. The vim command below returns what I want. I need the returned number to appear in my status bar.

:%s/^I^I//n

      

vim returns: 16 matches over 16 lines

FYI Explanation: I am working in a CSV file. I'm looking for two tabs (^ i ^ I) because that points to the lines I still need to work on. This way my desired status line will indicate how much work is left in the current file.

I don't know how to type vim command in the status bar, I know% {} 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 are clearly wrong and just ended up with an error.

:set statusline+= %{s/^I^I//n}

      

Help me wash one kenobi, you are my only hope!

+3


source to share


2 answers


The first thing to mention here is that for large files this feature will be completely impractical. The reason is that the status bar is redrawn after every cursor movement, after every command finishes, and probably after other events that I am not even aware of. Performing a regex search on the entire buffer, and in addition, not only the current buffer, but every visible window (since each window has its own status bar) will slow things down significantly. Do not misunderstand me; the idea behind this function is a good one, as it will give you an immediate and fully automatic indication of your remaining work, but computers are simply not infinitely efficient (unfortunately) and so this can easily become a problem. I have edited files with millions of lines of text,and one re-lookup can take many seconds on such buffers.

But assuming you keep your files fairly small, I figured out three possible solutions with which you can achieve this.

Solution # 1: exe: s and output redirection

You can use :exe

for a function :s

with a parameterized template and :redir

to redirect the output to a local variable.

Unfortunately, this has two unwanted side effects, which in the context of this function would be full unlocks, since they will occur every time the status bar is redrawn:

  • The cursor moves to the beginning of the current line. (Personal notes: I never really understood why vim does this, whether you do it :s

    from a status bar invocation or manually enter it on the vim command line.)
  • Visual choice, if any, is lost.

(And there might actually be more adverse effects that I am not aware of.)

The cursor problem can be fixed by saving and restoring the cursor position with getcurpos()

and setpos()

. Note that this should be getcurpos()

, not getpos()

because the latter does not return the field curswant

that is needed to store the column the cursor "wants" to be on, which may be different from the column the cursor "actually" is on (for example, if the cursor is moved to a shorter line). Unfortunately, this getcurpos()

is a fairly recent addition to vim, namely 7.4.313, and doesn't even work correctly based on my testing. Fortunately, there are older winsaveview()

and winrestview()

function that can perform the task perfectly and compatible. Therefore, we will use them for now.

Solution # 1a: Restore visual selection with gv

The visual selection problem I thought could be solved by running gv

in normal mode, but for some reason the visual selection is completely corrupted in doing so. I've tested this on Cygwin CLI and Windows gvim and I don't have a solution for this (in regards to visual selection restoration).

Anyway, here's the result of the above construct:

fun! MatchCount(pat,...)
    "" return the number of matches for pat in the active buffer, by executing an :s call and redirecting the output to a local variable
    "" saves and restores both the cursor position and the visual selection, which are clobbered by the :s call, although the latter restoration doesn't work very well for some reason as of vim-7.4.729
    "" supports global matching (/g flag) by taking an optional second argument appended to :s flags
    if (a:0 > 1)| throw 'too many arguments'| endif
    let flags = a:0 == 1 ? a:000[0] : ''
    let mode = mode()
    let pos = winsaveview()
    redir => output| sil exe '%s/'.a:pat.'//ne'.flags| redir END
    call winrestview(pos)
    if (mode == 'v' || mode == 'V' || mode == nr2char(22))
        exe 'norm!gv'
    endif
    if (match(output,'Pattern not found') != -1)
        return 0
    else
        return str2nr(substitute(output,'^[\s\n]*\(\d\+\).*','\1',''))
    endif
    return 
endfun

set statusline+=\ [%{MatchCount('\\t\\t')}]

      

A few random notes:

  • The use ^[\s\n]*

    in the pattern of extracting matches was necessary to barrel through a leading line break that is captured during a redirect (not sure why this is happening). An alternative would be to skip any character up to the first digit with a non-living multiplier on the dot atom, i.e. ^.\{-}

    ...
  • Doubling backslashes in the parameter value is statusline

    necessary because interpolation / removal of backslashes occurs during parsing of the parameter value itself. In general, single quoted strings do not cause backslash interpolation / stripping , and our string pat

    , after parsing, ends up being concatenated directly to the string :s

    passed to :exe

    , so there is no backslash interpolation / stripping (at least not before evaluating commands :s

    when the backslash occurs with the backslash, which is what we want). I find this a little confusing, since inside the construct %{}

    you would expect it to be a normal flawless VimScript expression, but that's how it works.
  • I added a flag /e

    for the command :s

    . This is necessary to handle the zero match case. It usually :s

    really throws an error if there are zero matches. To invoke the status bar is a big problem, because any error that occurs while trying to redraw the status bar causes vim to invalidate the parameter statusline

    as a protective measure to prevent repeated errors. I initially looked for solutions that included an error like :try

    and :catch

    , but nothing worked; after an error occurs in the vim source, the flag ( called_emsg

    ) is set, which we cannot undo and is therefore statusline

    doomed to that point. Luckily, I found a flag /e

    that prevents the error from being thrown altogether.

Solution # 1b: Dodge visual mode with local buffer cache

I was not satisfied with the visual selection problem, so I wrote an alternative solution. This solution effectively avoids starting the search altogether if visual mode is in effect, and instead pulls the last known seek counter from the buffer-local cache. I'm sure this will never cause the search counter to become obsolete because it is impossible to edit the buffer without giving up visual mode (I'm sure ...).



So now the function MatchCount()

doesn't work with visual mode:

fun! MatchCount(pat,...)
    if (a:0 > 1)| throw 'too many arguments'| endif
    let flags = a:0 == 1 ? a:000[0] : ''
    let pos = winsaveview()
    redir => output| sil exe '%s/'.a:pat.'//ne'.flags| redir END
    call winrestview(pos)
    if (match(output,'Pattern not found') != -1)
        return 0
    else
        return str2nr(substitute(output,'^[\s\n]*\(\d\+\).*','\1',''))
    endif
    return 
endfun

      

And now we need this "predicate" helper function that tells us when it is (not) safe to run a command :s

:

fun! IsVisualMode(mode)
    return a:mode == 'v' || a:mode == 'V' || a:mode == nr2char(22)
endfun

      

And now we need a caching layer that leads to the result of the predicate and runs the main function only if it is safe, otherwise it pulls from the buffer-local cache the last known return value that was captured from the last call to the primary function taking these exact arguments:

fun! BufferCallCache(buf,callName,callArgs,callElseCache)
    let callCache = getbufvar(a:buf,'callCache')
    if (type(callCache) != type({}))
        unlet callCache
        let callCache = {}
        call UnletBufVar(a:buf,'callCache')
        call setbufvar(a:buf,'callCache',callCache)
    endif
    if (a:callElseCache)
        let newValue = call(a:callName,a:callArgs)
        if (!has_key(callCache,a:callName.':Args') || !has_key(callCache,a:callName.':Value'))
            let callCache[a:callName.':Args'] = []
            let callCache[a:callName.':Value'] = []
        endif
        let i = len(callCache[a:callName.':Args'])-1
        while (i >= 0)
            let args = callCache[a:callName.':Args'][i]
            if (args == a:callArgs)
                let callCache[a:callName.':Value'][i] = newValue
                return newValue
            endif
            let i -= 1
        endwhile
        let callCache[a:callName.':Args'] += [a:callArgs]
        let callCache[a:callName.':Value'] += [newValue]
        return newValue
    else
        if (has_key(callCache,a:callName.':Args') && has_key(callCache,a:callName.':Value'))
            let i = len(callCache[a:callName.':Args'])-1
            while (i >= 0)
                let args = callCache[a:callName.':Args'][i]
                if (args == a:callArgs)
                    return callCache[a:callName.':Value'][i]
                endif
                let i -= 1
            endwhile
        endif
        return ''
    endif
endfun

      

What do we need this helper function for, which I found somewhere on the Internet years ago:

fun! UnletBufVar(bufExpr, varName )
    "" source: <http://vim.1045645.n5.nabble.com/unlet-ing-variables-in-buffers-td5714912.html>
    call filter(getbufvar(a:bufExpr,''), 'v:key != '''.a:varName.'''' )
endfun

      

Finally, we can install statusline

:

set statusline+=\ [%{BufferCallCache('','MatchCount',['\\t\\t'],!IsVisualMode(mode()))}]

      

Solution # 2: Call match()

on each line

I thought of another possible solution that is actually much simpler and seems to work great for non-huge files, although it involves more looping and processing at the VimScript level. To do this, you need to iterate over all the lines in the file and call match()

:

fun! MatchCount(pat)
    "" return the number of matches for pat in the active buffer, by iterating over all lines and calling match() on them
    "" does not support global matching (normally achieved with the /g flag on :s)
    let i = line('$')
    let c = 0
    while (i >= 1)
        let c += match(getline(i),a:pat) != -1
        let i -= 1
    endwhile
    return c
endfun

set statusline+=\ [%{MatchCount('\\t\\t')}]

      

Solution # 3: challenge search()

/ searchpos()

multiple times

I wrote some slightly complex functions to do global and linear matching, built on searchpos()

and search()

, respectively. I've also included support for optional start and end boundaries.

fun! GlobalMatchCount(pat,...)
    "" searches for pattern matches in the active buffer, with optional start and end [line,col] specifications
    "" useful command-line for testing against last-used pattern within last-used visual selection: echo GlobalMatchCount(@/,getpos("'<")[1:2],getpos("'>")[1:2])
    if (a:0 > 2)| echoerr 'too many arguments for function: GlobalMatchCount()'| return| endif
    let start = a:0 >= 1 ? a:000[0] : [1,1]
    let end = a:0 >= 2 ? a:000[1] : [line('$'),2147483647]
    "" validate args
    if (type(start) != type([]) || len(start) != 2 || type(start[0]) != type(0) || type(start[1]) != type(0))| echoerr 'invalid type of argument: start'| return| endif
    if (type(end) != type([]) || len(end) != 2 || type(end[0]) != type(0) || type(end[1]) != type(0))| echoerr 'invalid type of argument: end'| return| endif
    if (end[0] < start[0] || end[0] == start[0] && end[1] < start[1])| echoerr 'invalid arguments: end < start'| return| endif
    "" allow degenerate case of end == start; just return zero immediately
    if (end == start)| return [0,0]| endif
    "" save current cursor position
    let wsv = winsaveview()
    "" set cursor position to start (defaults to start-of-buffer)
    call setpos('.',[0,start[0],start[1],0])
    "" accumulate match count and line count in local vars
    let matchCount = 0
    let lineCount = 0
    "" also must keep track of the last line number in which we found a match for lineCount
    let lastMatchLine = 0
    "" add one if a match exists right at start; must treat this case specially because the main loop must avoid matching at the cursor position
    if (searchpos(a:pat,'cn',start[0])[1] == start[1])
        let matchCount += 1
        let lineCount += 1
        let lastMatchLine = 1
    endif
    "" keep searching until we hit end-of-buffer
    let ret = searchpos(a:pat,'W')
    while (ret[0] != 0)
        "" break if the cursor is now at or past end; must do this prior to incrementing for most recent match, because if the match start is at or past end, it not a valid match for the caller
        if (ret[0] > end[0] || ret[0] == end[0] && ret[1] >= end[1])
            break
        endif
        let matchCount += 1
        if (ret[0] != lastMatchLine)
            let lineCount += 1
            let lastMatchLine = ret[0]
        endif
        let ret = searchpos(a:pat,'W')
    endwhile
    "" restore original cursor position
    call winrestview(wsv)
    "" return result
    return [matchCount,lineCount]
endfun

fun! LineMatchCount(pat,...)
    "" searches for pattern matches in the active buffer, with optional start and end line number specifications
    "" useful command-line for testing against last-used pattern within last-used visual selection: echo LineMatchCount(@/,getpos("'<")[1],getpos("'>")[1])
    if (a:0 > 2)| echoerr 'too many arguments for function: LineMatchCount()'| return| endif
    let start = a:0 >= 1 ? a:000[0] : 1
    let end = a:0 >= 2 ? a:000[1] : line('$')
    "" validate args
    if (type(start) != type(0))| echoerr 'invalid type of argument: start'| return| endif
    if (type(end) != type(0))| echoerr 'invalid type of argument: end'| return| endif
    if (end < start)| echoerr 'invalid arguments: end < start'| return| endif
    "" save current cursor position
    let wsv = winsaveview()
    "" set cursor position to start (defaults to start-of-buffer)
    call setpos('.',[0,start,1,0])
    "" accumulate line count in local var
    let lineCount = 0
    "" keep searching until we hit end-of-buffer
    let ret = search(a:pat,'cW')
    while (ret != 0)
        "" break if the latest match was past end; must do this prior to incrementing lineCount for it, because if the match start is past end, it not a valid match for the caller
        if (ret > end)
            break
        endif
        let lineCount += 1
        "" always move the cursor to the start of the line following the latest match; also, break if we're already at end; otherwise next search would be unnecessary, and could get stuck in an infinite loop if end == line('$')
        if (ret == end)
            break
        endif
        call setpos('.',[0,ret+1,1,0])
        let ret = search(a:pat,'cW')
    endwhile
    "" restore original cursor position
    call winrestview(wsv)
    "" return result
    return lineCount
endfun

      

+3


source


Maybe not exactly what you are looking for, but if you put a function like this in your $ HOME / .vimrc file:

:set statusline+=%!SearchResults('^I^I')

      

$ HOME / .vimrc



function SearchResults(q)
  redir => matches
  silent! execute "%s/".a:q."//n"
  redir END
  return substitute(matches, "^.", "", "")
endfunction

      

If nothing else, maybe it will bring you closer.

0


source







All Articles