【问题标题】:Vim Statusline: Word searchVim 状态栏:单词搜索
【发布时间】:2015-08-26 11:43:59
【问题描述】:

我一直在努力寻找这个,但找不到我想要的。

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

:%s/^I^I//n

vim 返回:16 行 16 个匹配项

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

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

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

帮我维密一个克诺比,你是我唯一的希望!

【问题讨论】:

    标签: search vim statusline


    【解决方案1】:

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

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

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

    您可以使用函数中的:exe 以参数化模式运行:s 命令,并使用:redir 将输出重定向到局部变量。

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

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

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

    可以通过getcurpos()setpos() 保存和恢复光标位置来解决光标问题。请注意,它必须是 getcurpos() 而不是 getpos(),因为后者不返回 curswant 字段,这是保留光标“想要”驻留的列所必需的,这可能与光标“实际上”位于(例如,如果光标移动到较短的行中)。不幸的是,getcurpos() 是最近添加到 vim 的,即 7.4.313,根据我的测试,它甚至似乎不能正常工作。幸运的是,有较旧的winsaveview()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\+\).*','\1',''))
        endif
        return 
    endfun
    
    set statusline+=\ [%{MatchCount('\\t\\t')}]
    

    一些随机笔记:

    • 必须在匹配计数提取模式中使用^[\s\n]* 才能通过重定向期间捕获的引导换行符(不确定为什么会发生这种情况)。另一种方法是跳过 any 字符直到第一个数字,在点原子上使用非贪婪乘数,即^.\{-}
    • statusline 选项值中的反斜杠加倍是必要的,因为在解析选项值本身的过程中会发生反斜杠插值/删除。通常,单引号字符串不会导致反斜杠插值/删除,而我们的pat 字符串一旦解析,最终会直接与传递给:exe:s 字符串连接,因此在那些处没有反斜杠插值/删除点(至少不是在评估:s 命令之前,当我们的反斜杠确实发生反斜杠插值时,这是我们想要的)。我觉得这有点令人困惑,因为在 %{} 构造中,您会认为它是一个普通的纯 VimScript 表达式,但它就是这样工作的。
    • 我为:s 命令添加了/e 标志。这对于处理具有零匹配的缓冲区的情况是必要的。通常,如果匹配项为零,:s 实际上会引发错误。对于状态行调用,这是一个大问题,因为尝试重绘状态行时抛出的任何错误都会导致 vim 取消 statusline 选项作为防止重复错误的防御措施。我最初寻找涉及捕获错误的解决方案,例如:try: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\+\).*','\1',''))
        endif
        return 
    endfun
    

    现在我们需要这个辅助“谓词”函数,它告诉我们何时(不)运行:s 命令是安全的:

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

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

    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()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
    

    【讨论】:

    • 除了解决我的问题之外,这是我记得阅读过的最好、最详细和最具教育意义的回复。除了带有注释代码的合法工作解决方案之外,感谢您提供选项、清晰的解释、利弊。我衷心感谢您,先生。 世界论坛海报,观看学习!
    【解决方案2】:

    也许不是你想要的,但如果你在你的 $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
    

    如果不出意外,也许这会让你更接近一点。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-09-26
      相关资源
      最近更新 更多