【问题标题】:Fast word count function in VimVim 中的快速字数统计功能
【发布时间】:2021-05-27 21:33:31
【问题描述】:

我正在尝试在 vim 状态行中显示实时字数。为此,我在 .vimrc 中设置状态行并在其中插入一个函数。这个函数的想法是返回当前缓冲区中的单词数。这个数字随后会显示在状态行上。这应该可以很好地工作,因为几乎每个可能的机会都会更新状态行,因此计数将始终保持“实时”。

问题是我当前定义的函数很慢,所以当 vim 用于除最小文件之外的所有文件时,它显然很慢;因为这个函数执行的太频繁了。

总之,有没有人有一个聪明的技巧来生成一个计算当前缓冲区中单词数并返回结果的函数非常快?

【问题讨论】:

  • 你目前的功能是什么?
  • 对于其他来这里进行一般字数统计的人,请使用g Ctrl-g
  • 是我自己还是真的没有以下解决方案会以任何方式影响状态栏?

标签: vim


【解决方案1】:

我真的很喜欢上面 Michael Dunn 的回答,但我发现在编辑时它导致我无法访问最后一列。所以我对函数做了一个小改动:

function! WordCount()
   let s:old_status = v:statusmsg
   let position = getpos(".")
   exe ":silent normal g\<c-g>"
   let stat = v:statusmsg
   let s:word_count = 0
   if stat != '--No lines in buffer--'
     let s:word_count = str2nr(split(v:statusmsg)[11])
     let v:statusmsg = s:old_status
   end
   call setpos('.', position)
   return s:word_count 
endfunction

我已将它包含在我的状态行中,没有任何问题:

:set statusline=wc:%{WordCount()}

【讨论】:

  • 这里有一个小错字。第一个粗体下面的行应该改为exe ":silent normal g\&lt;c-g&gt;"
  • @JackFranklin 这不是错字。 OP 在他的 Markdown 中使用了手工编码的 HTML(在一段代码中生成粗体),并且 SO 的渲染引擎反应很糟糕。我已删除 HTML 以解决问题。感谢您指出代码似乎错误。
  • 迟到了,但在 Neovim 中,每当我进入终端窗口时,都会出现一些令人讨厌的错误消息。将此添加到函数顶部修复它:if &amp;buftype == 'terminal' | return 0 | endif
【解决方案2】:

这是 Rodrigo Queiro 想法的可用版本。它不会改变状态栏,它会恢复 statusmsg 变量。

function WordCount()
  let s:old_status = v:statusmsg
  exe "silent normal g\<c-g>"
  let s:word_count = str2nr(split(v:statusmsg)[11])
  let v:statusmsg = s:old_status
  return s:word_count
endfunction

这似乎足够快,可以直接包含在状态行中,例如:

:set statusline=wc:%{WordCount()}

【讨论】:

  • s/s://g ;或者 s/s:/l:/g 如果你真的想要一个明确的范围。
  • FWIW,这不适用于 Ubuntu 12.04 上的 Vim 7.3。我不能追加到行尾!也就是说,“A”和“a”(后者在一行中的最后一个字符上)都不会将光标留在最后一个字符之外,而是直接在它的前面。我无法想象为什么。
【解决方案3】:

为当前行保留一个计数,为缓冲区的其余部分保留一个单独的计数。当您在当前行键入(或删除)单词时,仅更新该计数,但显示当前行计数和其余缓冲区计数的总和。

当你换行时,将当前行数加到缓冲区数中,计算当前行中的字数,然后 a) 设置当前行数 b) 从缓冲区数中减去。

定期重新计算缓冲区也是明智之举(请注意,您不必一次计算整个缓冲区,因为您知道在哪里进行编辑)。

【讨论】:

    【解决方案4】:

    这将在您停止输入一段时间后重新计算字数(具体而言,updatetime ms)。

    let g:word_count="<unknown>"
    fun! WordCount()
        return g:word_count
    endfun
    fun! UpdateWordCount()
        let s = system("wc -w ".expand("%p"))
        let parts = split(s, ' ')
        if len(parts) > 1
            let g:word_count = parts[0]
        endif
    endfun
    
    augroup WordCounter
        au! CursorHold * call UpdateWordCount()
        au! CursorHoldI * call UpdateWordCount()
    augroup END
    
    " how eager are you? (default is 4000 ms)
    set updatetime=500
    
    " modify as you please...
    set statusline=%{WordCount()}\ words
    

    享受吧!

    【讨论】:

      【解决方案5】:

      所以我写了:

      函数计数字() exe“正常g\” 让单词=替换(v:statusmsg,“^.*单词[^]* of”,“”,“”) 让单词 = 替代(单词,“;.*”,“”,“”) 回话 结束函数

      但它会将信息打印到状态栏,所以我认为它不适合您的用例。不过速度非常快!

      【讨论】:

        【解决方案6】:

        我为此使用了稍微不同的方法。我没有确保字数统计功能特别快,而是仅在光标停止移动时调用它。这些命令将执行此操作:

        :au CursorHold * exe "normal g\<c-g>"
        :au CursorHoldI * exe "normal g\<c-g>"
        

        也许不是提问者想要的,但比这里的一些答案要简单得多,并且对于我的用例来说已经足够了(在输入一两个句子后向下看字数)。

        updatetime 设置为较小的值也有帮助:

        set updatetime=300
        

        字数的轮询不会产生巨大的开销,因为CursorHoldCursorHoldI 仅在光标停止移动时触发一次,而不是每个updatetime 毫秒。

        【讨论】:

        • 我在此页面上尝试了许多功能,但发现这个 3 行答案是我在 LInux 上的 Gvim(版本 8.0.550)中唯一工作的答案。
        【解决方案7】:

        这是对 Abslom Daak 答案的改进,它也适用于视觉模式。

        function! WordCount()
          let s:old_status = v:statusmsg
          let position = getpos(".")
          exe ":silent normal g\<c-g>"
          let stat = v:statusmsg
          let s:word_count = 0
          if stat != '--No lines in buffer--'
            if stat =~ "^Selected"
              let s:word_count = str2nr(split(v:statusmsg)[5])
            else
              let s:word_count = str2nr(split(v:statusmsg)[11])
            end
            let v:statusmsg = s:old_status
          end
          call setpos('.', position)
          return s:word_count 
        endfunction
        

        和以前一样包含在状态行中。这是一个右对齐的状态行:

        set statusline=%=%{WordCount()}\ words\

        【讨论】:

          【解决方案8】:

          vim 7.4.1042 版开始

          vim 7.4.1042 版本开始,您可以简单地更改statusline,如下所示:

          set statusline+=%{wordcount().words}\ words
          set laststatus=2    " enables the statusline.
          

          vim-airline 中的字数

          在撰写本文时,vim-airline 为多种文件类型提供了标准字数: asciidoc, help, mail, markdown, org, rst, tex ,text

          如果vim-airline 中未显示字数,这通常是由于文件类型无法识别。例如,at least for nowvim-airline 无法识别复合文件类型 markdown.pandoc 的字数。这可以通过如下修改.vimrc 来轻松解决:

          let g:airline#extensions#wordcount#filetypes = '\vasciidoc|help|mail|markdown|markdown.pandoc|org|rst|tex|text'
          set laststatus=2    " enables vim-airline.
          

          \v 语句覆盖默认的g:airline#extensions#wordcount#filetypes 变量。最后一行确保启用了vim-airline

          如果有疑问,在发出以下命令时会返回打开文件的&amp;filetype

          :echo &filetype
          

          这是一个元示例:

          【讨论】:

            【解决方案9】:

            我从有关编写函数的 vim 帮助页面中获取了大部分内容。

            function! WordCount()
              let lnum = 1
              let n = 0
              while lnum <= line('$')
                let n = n + len(split(getline(lnum)))
                let lnum = lnum + 1
              endwhile
              return n
            endfunction
            

            当然,和其他人一样,您需要:

            :set statusline=wc:%{WordCount()}
            

            我确信有人可以清理它以使其更具活力(s:n 而不仅仅是 n?),但我相信基本功能已经存在。

            编辑:

            再看一遍,我真的很喜欢 Mikael Jansson 的解决方案。我不喜欢炮轰wc(不便携而且可能很慢)。如果我们用我上面的代码替换他的UpdateWordCount 函数(将我的函数重命名为UpdateWordCount),那么我认为我们有更好的解决方案。

            【讨论】:

            • 局部变量应该保持局部。 s:变量是脚本局部全局变量,就像 C 中的静态变量一样。如果需要,可以使用 +=。您也可以在 getline(1,'$') 上执行 :foreach,但我不知道哪种解决方案更快。
            【解决方案10】:

            我的建议:

            function! UpdateWordCount()
              let b:word_count = eval(join(map(getline("1", "$"), "len(split(v:val, '\\s\\+'))"), "+"))
            endfunction
            
            augroup UpdateWordCount
              au!
              autocmd BufRead,BufNewFile,BufEnter,CursorHold,CursorHoldI,InsertEnter,InsertLeave * call UpdateWordCount()
            augroup END
            
            let &statusline='wc:%{get(b:, "word_count", 0)}'
            

            我不确定这与其他一些解决方案的速度相比如何,但它肯定比大多数解决方案简单得多。

            【讨论】:

              【解决方案11】:

              我是 Vim 脚本的新手,但我可能会建议

              function WordCount()
                  redir => l:status
                  exe "silent normal g\<c-g>"
                  redir END
                  return str2nr(split(l:status)[11])
              endfunction
              

              因为它不会覆盖现有的状态行,所以更简洁。

              我发帖的原因是指出这个函数有一个令人费解的错误:即它破坏了追加命令。点击 A 应该会让您进入插入模式,光标位于该行最后一个字符的右侧。但是,启用此自定义状态栏后,它会将您置于最后一个字符的左侧。

              有人知道这是什么原因吗?

              【讨论】:

                【解决方案12】:

                这是对Michael Dunn's version 的改进,缓存字数,因此需要的处理更少。

                function! WC()
                    if &modified || !exists("b:wordcount") 
                            let l:old_status = v:statusmsg  
                            execute "silent normal g\<c-g>"
                            let b:wordcount = str2nr(split(v:statusmsg)[11])
                            let v:statusmsg = l:old_status  
                            return b:wordcount
                    else
                            return b:wordcount
                    endif
                endfunction 
                

                【讨论】:

                  【解决方案13】:

                  因为 vim 现在原生支持这个:

                  :echo wordcount().words
                  

                  【讨论】:

                    【解决方案14】:

                    使用史蒂夫莫耶提供的答案中的方法,我能够产生以下解决方案。恐怕这是一个相当不雅的hack,我觉得必须有一个更简洁的解决方案,但它有效,并且比每次更新状态行时简单地计算缓冲区中的所有单词要快得多。我还应该注意,这个解决方案是独立于平台的,并且不假设系统具有“wc”或类似的东西。

                    我的解决方案不会定期更新缓冲区,但 Mikael Jansson 提供的答案将能够提供此功能。到目前为止,我还没有发现我的解决方案不同步的情况。然而,我只是简单地测试了这一点,因为准确的实时字数对我的需求并不重要。我用于匹配单词的模式也很简单,适用于简单的文本文档。如果有人对模式或任何其他建议有更好的想法,请随时发布答案或编辑此帖子。

                    我的解决方案:

                    "returns the count of how many words are in the entire file excluding the current line 
                    "updates the buffer variable Global_Word_Count to reflect this
                    fu! OtherLineWordCount() 
                        let data = []
                        "get lines above and below current line unless current line is first or last
                        if line(".") > 1
                            let data = getline(1, line(".")-1)
                        endif   
                        if line(".") < line("$")
                            let data = data + getline(line(".")+1, "$") 
                        endif   
                        let count_words = 0
                        let pattern = "\\<\\(\\w\\|-\\|'\\)\\+\\>"
                        for str in data
                            let count_words = count_words + NumPatternsInString(str, pattern)
                        endfor  
                        let b:Global_Word_Count = count_words
                        return count_words
                    endf    
                    
                    "returns the word count for the current line
                    "updates the buffer variable Current_Line_Number 
                    "updates the buffer variable Current_Line_Word_Count 
                    fu! CurrentLineWordCount()
                        if b:Current_Line_Number != line(".") "if the line number has changed then add old count
                            let b:Global_Word_Count = b:Global_Word_Count + b:Current_Line_Word_Count
                        endif   
                        "calculate number of words on current line
                        let line = getline(".")
                        let pattern = "\\<\\(\\w\\|-\\|'\\)\\+\\>"
                        let count_words = NumPatternsInString(line, pattern)
                        let b:Current_Line_Word_Count = count_words "update buffer variable with current line count
                        if b:Current_Line_Number != line(".") "if the line number has changed then subtract current line count
                            let b:Global_Word_Count = b:Global_Word_Count - b:Current_Line_Word_Count
                        endif   
                        let b:Current_Line_Number = line(".") "update buffer variable with current line number
                        return count_words
                    endf    
                    
                    "returns the word count for the entire file using variables defined in other procedures
                    "this is the function that is called repeatedly and controls the other word
                    "count functions.
                    fu! WordCount()
                        if exists("b:Global_Word_Count") == 0 
                            let b:Global_Word_Count = 0
                            let b:Current_Line_Word_Count = 0
                            let b:Current_Line_Number = line(".")
                            call OtherLineWordCount()
                        endif   
                        call CurrentLineWordCount()
                        return b:Global_Word_Count + b:Current_Line_Word_Count
                    endf
                    
                    "returns the number of patterns found in a string 
                    fu! NumPatternsInString(str, pat)
                        let i = 0
                        let num = -1
                        while i != -1
                            let num = num + 1
                            let i = matchend(a:str, a:pat, i)
                        endwhile
                        return num
                    endf
                    

                    然后通过以下方式将其添加到状态行:

                    :set statusline=wc:%{WordCount()}
                    

                    我希望这可以帮助任何在 Vim 中寻找实时字数统计的人。尽管并不总是准确的。或者当然 g ctrl-g 会为你提供 Vim 的字数统计!

                    【讨论】:

                    【解决方案15】:

                    如果有人从 Google 来到这里,我修改了 Abslom Daak 的答案以使用 Airline。我将以下内容另存为

                    ~/.vim/bundle/vim-airline/autoload/airline/extensions/pandoc.vim

                    并添加

                    call airline#extensions#pandoc#init(s:ext)

                    extensions.vim

                    let s:spc = g:airline_symbols.space
                    
                    function! airline#extensions#pandoc#word_count()
                    if mode() == "s"
                        return 0
                    else
                        let s:old_status = v:statusmsg
                        let position = getpos(".")
                        let s:word_count = 0
                        exe ":silent normal g\<c-g>"
                        let stat = v:statusmsg
                        let s:word_count = 0
                        if stat != '--No lines in buffer--'
                            let s:word_count = str2nr(split(v:statusmsg)[11])
                            let v:statusmsg = s:old_status
                        end
                        call setpos('.', position)
                        return s:word_count 
                    end
                    endfunction
                    
                    function! airline#extensions#pandoc#apply(...)
                    if &ft == "pandoc"
                        let w:airline_section_x = "%{airline#extensions#pandoc#word_count()} Words"
                    endif
                    endfunction
                    
                    function! airline#extensions#pandoc#init(ext)
                    call a:ext.add_statusline_func('airline#extensions#pandoc#apply')
                    endfunction
                    

                    【讨论】:

                      【解决方案16】:

                      Guy Gur-Ari 的 refinement 的变体

                      • 仅在启用拼写检查时才计算字数,
                      • 在可视模式下统计所选单词的数量
                      • 在插入和正常模式之外保持静音,并且
                      • 希望对系统语言更加不可知(当与英语不同时)
                      function! StatuslineWordCount()
                        if !&l:spell
                          return ''
                        endif
                      
                        if empty(getline(line('$')))
                          return ''
                        endif
                        let mode = mode()
                        if !(mode ==# 'v' || mode ==# 'V' || mode ==# "\<c-v>" || mode =~# '[ni]')
                          return ''
                        endif
                      
                        let s:old_status = v:statusmsg
                        let position = getpos('.')
                        let stat = v:statusmsg
                        let s:word_count = 0
                        exe ":silent normal g\<c-g>"
                        try
                          if mode ==# 'v' || mode ==# 'V'
                            let s:word_count = split(split(v:statusmsg, ';')[1])[0]
                          elseif mode ==# "\<c-v>"
                            let s:word_count = split(split(v:statusmsg, ';')[2])[0]
                          elseif mode =~# '[ni]'
                            let s:word_count = split(split(v:statusmsg, ';')[2])[3]
                          end
                        " index out of range
                        catch /^Vim\%((\a\+)\)\=:E\%(684\|116\)/
                          return ''
                        endtry
                        let v:statusmsg = s:old_status
                        call setpos('.', position)
                      
                        return "\ \|\ " . s:word_count . 'w'
                      endfunction
                      

                      可以附加到状态行,比如说,

                          set statusline+=%.10{StatuslineWordCount()}     " wordcount
                      

                      【讨论】:

                        【解决方案17】:

                        基于https://stackoverflow.com/a/60310471/11001018,我的建议是:

                        "new in vim 7.4.1042
                        let g:word_count=wordcount().words
                        function WordCount()
                            if has_key(wordcount(),'visual_words')
                                let g:word_count=wordcount().visual_words."/".wordcount().words
                            else
                                let g:word_count=wordcount().cursor_words."/".wordcount().words
                            endif
                            return g:word_count
                        endfunction
                        

                        然后:

                        set statusline+=\ w:%{WordCount()},
                        

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 2010-12-17
                          • 2015-07-16
                          • 2012-07-07
                          • 1970-01-01
                          • 2022-01-07
                          • 1970-01-01
                          • 1970-01-01
                          相关资源
                          最近更新 更多