Vim 状态栏:词搜索

Vim Statusline: Word search

我为此苦苦搜索,但未能找到我想要的东西。

在我的状态行上,我想要计算当前文件中出现的匹配项数。 returns 下面的 vim 命令就是我想要的。我需要 返回的数字 显示在我的状态栏中。

:%s/^I^I//n

vim returns: 16 行匹配 16 次

FYI 说明:我正在处理 CSV 文件。我正在搜索两个制表符 ( ^I^I ),因为这表示我仍然需要处理的行。所以我想要的状态行会指示当前文件中还有多少工作要做。

我不知道如何在状态行上输入 vim 命令,我知道 %{} 可以用于 运行 函数,但我如何 运行 vim 搜索命令?我已经尝试了以下的变体,但它们显然不正确并且最终会出现错误。

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

帮帮我vim你是我唯一的希望!

也许不完全是你要找的东西,但如果你在你的 $HOME/.vimrc 文件中加入如下函数,你可以这样做:

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

$HOME/.vimrc

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

如果不出意外,也许这会让你们离得更近一些。

这里首先要提到的是,对于大文件,此功能是完全不切实际的。原因是状态行在每次光标移动后、在每个命令完成后重新绘制,并且可能在我什至不知道的其他事件之后重新绘制。对整个缓冲区执行正则表达式搜索,此外,不仅是当前缓冲区,还有每个可见的 window(因为每个 window 都有自己的状态行),会显着降低速度。不要误会我的意思;此功能背后的 idea 是一个很好的功能,因为它可以立即和全自动地指示您的剩余工作量,但计算机的性能并不是无限的(不幸的是),因此这很容易成为问题。我编辑过包含数百万行文本的文件,在此类缓冲区上进行一次正则表达式搜索可能需要很多秒。

但是如果您的文件将保持相当小,我已经想出了三种可能的解决方案来实现这一点。

解决方案 #1:exe :s 和重定向输出

您可以使用 :exe from a function to run the :s command with a parameterized pattern, and :redir 将输出重定向到局部变量。

不幸的是,这有两个不良副作用,在此功能的上下文中,它们将完全破坏交易,因为它们会在每次重绘状态行时发生:

  1. 光标移动到当前行的开头。 (个人说明:我一直不明白为什么 vim 这样做,无论您是 运行ning :s 来自状态行调用还是通过在 vim命令行。)
  2. 视觉选择(如果有)丢失。

(实际上可能还有更多我不知道的不利影响。)

光标问题可以通过getcurpos() and setpos(). Note that it must be getcurpos() and not getpos() because the latter does not return the curswant 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 older winsaveview() and winrestview()函数保存和恢复光标位置来解决,可以完美兼容地完成任务。所以现在,我们将使用它们。

解决方案 #1a:使用 gv

恢复视觉选择

视觉选择问题我认为可以通过运行宁gv在正常模式下解决,但由于某种原因视觉选择完全损坏这样做的时候。我已经在 Cygwin CLI 和 Windows gvim 上对此进行了测试,但我没有解决方案(关于恢复视觉选择)。

无论如何,以上设计的结果如下:

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\+\).*','',''))
    endif
    return 
endfun

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

一些随机笔记:

  • 在匹配计数提取模式中使用 ^[\s\n]* 是通过在重定向期间捕获的前导换行符所必需的(不确定为什么会发生这种情况)。另一种方法是跳过 任何 字符直到第一个数字,在点原子上使用非贪婪乘数,即 ^.\{-}.
  • statusline 选项值中的双反斜杠是必要的,因为反斜杠 interpolation/removal 发生在解析选项值本身的过程中。通常,单引号字符串不会导致反斜杠 interpolation/removal,而我们的 pat 字符串一旦被解析,最终会直接与传递给 :exe:s 字符串连接起来,因此在这些点上没有反斜杠 interpolation/removal(至少在 :s 命令的评估之前没有,当我们的反斜杠 的反斜杠插值 发生时,这就是我们想要)。我觉得这有点令人困惑,因为在 %{} 结构中你会期望它是一个普通的纯 VimScript 表达式,但它就是这样工作的。
  • 我为 :s 命令添加了 /e 标志。这对于处理具有零匹配的缓冲区的情况是必要的。通常,如果匹配项为零,:s 实际上会抛出错误。对于状态行调用,这是一个大问题,因为在尝试重绘状态行时抛出的任何错误都会导致 vim 使 statusline 选项无效,作为防止重复错误的防御措施。我最初寻找涉及捕获错误的解决方案,例如 :try and :catch,但没有任何效果;一旦抛出错误,就会在 vim 源 (called_emsg) 中设置一个我们无法取消设置的标志,因此 statusline 在这一点上注定失败。幸运的是,我发现了 /e 标志,它根本不会引发错误。

解决方案 #1b:使用缓冲区本地缓存躲避视觉模式

我对视觉选择问题不满意,所以我写了一个替代解决方案。如果视觉模式有效,该解决方案实际上完全避免了 运行 搜索,而是从缓冲区本地缓存中提取最后已知的搜索计数。我很确定这永远不会导致搜索计数过时,因为如果不放弃可视模式就不可能编辑缓冲区(我很确定...)。

所以现在 MatchCount() 函数不会与可视模式混淆:

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\+\).*','',''))
    endif
    return 
endfun

现在我们需要这个辅助 "predicate" 函数,它告诉我们什么时候 运行 :s 命令是(不)安全的:

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

现在我们需要一个缓存层,它在谓词结果上分支,并且只有 运行 在安全的情况下才是主要函数,否则它会从缓冲区本地缓存中提取最后已知的 return从采用这些确切参数的主函数的最近一次调用中捕获的值:

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

为此我们需要我 somewhere on the Internet 年前发现的辅助函数:

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

最后,我们可以这样设置 statusline:

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

解决方案 #2:在每一行调用 match()

我想到了另一种可能的解决方案,它实际上更简单,并且似乎对非大文件执行得很好,尽管它涉及更多的 VimScript 级别的循环和处理。这是遍历文件中的每一行并在其上调用 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')}]

解决方案 #3:重复调用 search()/searchpos()

我编写了一些稍微复杂的函数来执行全局匹配和逐行匹配,分别围绕 searchpos() and search() 构建。我也支持可选的开始和结束范围。

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's 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's 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