Reputation: 9279
I am trying to display a live word count in the vim statusline. I do this by setting my status line in my .vimrc and inserting a function into it. The idea of this function is to return the number of words in the current buffer. This number is then displayed on the status line. This should work nicely as the statusline is updated at just about every possible opportunity so the count will always remain 'live'.
The problem is that the function I have currently defined is slow and so vim is obviously sluggish when it is used for all but the smallest files; due to this function being executed so frequently.
In summary, does anyone have a clever trick for producing a function that is blazingly fast at calculating the number of words in the current buffer and returning the result?
Upvotes: 41
Views: 17214
Reputation: 523
Building upon https://stackoverflow.com/a/60310471/11001018, my suggestion is:
"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
And then:
set statusline+=\ w:%{WordCount()},
Upvotes: 0
Reputation: 31568
vim
version 7.4.1042Since vim
version 7.4.1042, one can simply alter the statusline
as follows:
set statusline+=%{wordcount().words}\ words
set laststatus=2 " enables the statusline.
vim-airline
Word count is provided standard by vim-airline
for a number of file types, being at the time of writing:
asciidoc, help, mail, markdown, org, rst, tex ,text
If word count is not shown in the vim-airline
, more often this is due to an unrecognised file type. For example, at least for now, the compound file type markdown.pandoc
is not being recognised by vim-airline
for word count. This can easily be remedied by amending the .vimrc
as follows:
let g:airline#extensions#wordcount#filetypes = '\vasciidoc|help|mail|markdown|markdown.pandoc|org|rst|tex|text'
set laststatus=2 " enables vim-airline.
The \v
statement overrides the default g:airline#extensions#wordcount#filetypes
variable. The last line ensures vim-airline
is enabled.
In case of doubt, the &filetype
of an opened file is returned upon issuing the following command:
:echo &filetype
Here is a meta-example:
Upvotes: 4
Reputation: 4683
Since vim now supports this natively:
:echo wordcount().words
Upvotes: 3
Reputation: 258
A variation of Guy Gur-Ari's refinement that
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
that can be appended to the statusline by, say,
set statusline+=%.10{StatuslineWordCount()} " wordcount
Upvotes: 0
Reputation: 251
I really like Michael Dunn's answer above but I found that when I was editing it was causing me to be unable to access the last column. So I have a minor change for the function:
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
I've included it in my status line without any issues:
:set statusline=wc:%{WordCount()}
Upvotes: 25
Reputation: 91
In case someone else is coming here from Google, I modified Abslom Daak's answer to work with Airline. I saved the following as
~/.vim/bundle/vim-airline/autoload/airline/extensions/pandoc.vim
and added
call airline#extensions#pandoc#init(s:ext)
to 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
Upvotes: 0
Reputation: 379
Here is a refinement of Abslom Daak's answer that also works in visual mode.
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
Included in the status line as before. Here is a right-aligned status line:
set statusline=%=%{WordCount()}\ words\
Upvotes: 2
Reputation: 4447
I used a slightly different approach for this. Rather than make sure the word count function is especially fast, I only call it when the cursor stops moving. These commands will do it:
:au CursorHold * exe "normal g\<c-g>"
:au CursorHoldI * exe "normal g\<c-g>"
Perhaps not quite what the questioner wanted, but much simpler than some of the answers here, and good enough for my use-case (glance down to see word count after typing a sentence or two).
Setting updatetime
to a smaller value also helps here:
set updatetime=300
There isn't a huge overhead polling for the word count because CursorHold
and CursorHoldI
only fire once when the cursor stops moving, not every updatetime
ms.
Upvotes: 1
Reputation: 11
This is an improvement on Michael Dunn's version, caching the word count so even less processing is needed.
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
Upvotes: 1
Reputation: 1512
I'm new to Vim scripting, but I might suggest
function WordCount()
redir => l:status
exe "silent normal g\<c-g>"
redir END
return str2nr(split(l:status)[11])
endfunction
as being a bit cleaner since it does not overwrite the existing status line.
My reason for posting is to point out that this function has a puzzling bug: namely, it breaks the append command. Hitting A should drop you into insert mode with the cursor positioned to the right of the final character on the line. However, with this custom status bar enabled it will put you to the left of the final character.
Anyone have any idea what causes this?
Upvotes: 1
Reputation: 1549
My suggestion:
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)}'
I'm not sure how this compares in speed to some of the other solutions, but it's certainly a lot simpler than most.
Upvotes: 1
Reputation: 329
I took the bulk of this from the vim help pages on writing functions.
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
Of course, like the others, you'll need to:
:set statusline=wc:%{WordCount()}
I'm sure this can be cleaned up by somebody to make it more vimmy (s:n instead of just n?), but I believe the basic functionality is there.
Edit:
Looking at this again, I really like Mikael Jansson's solution. I don't like shelling out to wc
(not portable and perhaps slow). If we replace his UpdateWordCount
function with the code I have above (renaming my function to UpdateWordCount
), then I think we have a better solution.
Upvotes: 1
Reputation:
Here's a usable version of Rodrigo Queiro's idea. It doesn't change the status bar, and it restores the statusmsg variable.
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
This seems to be fast enough to include directly in the status line, e.g.:
:set statusline=wc:%{WordCount()}
Upvotes: 23
Reputation: 9279
Using the method in the answer provided by Steve Moyer I was able to produce the following solution. It is a rather inelegant hack I'm afraid and I feel that there must be a neater solution, but it works, and is much faster than simply counting all of the words in a buffer every time the status line is updated. I should note also that this solution is platform independent and does not assume a system has 'wc' or something similar.
My solution does not periodically update the buffer, but the answer provided by Mikael Jansson would be able to provide this functionality. I have not, as of yet, found an instance where my solution becomes out of sync. However I have only tested this briefly as an accurate live word count is not essential to my needs. The pattern I use for matching words is also simple and is intended for simple text documents. If anyone has a better idea for a pattern or any other suggestions please feel free to post an answer or edit this post.
My solution:
"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
This is then added to the status line by:
:set statusline=wc:%{WordCount()}
I hope this helps anyone looking for a live word count in Vim. Albeit one that isn't always exact. Alternatively of course g ctrl-g will provide you with Vim's word count!
Upvotes: 0
Reputation: 4841
This will recalculate the number of words whenever you stop typing for a while (specifically, 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
Enjoy!
Upvotes: 4
Reputation: 1336
So I've written:
func CountWords() exe "normal g\" let words = substitute(v:statusmsg, "^.*Word [^ ]* of ", "", "") let words = substitute(words, ";.*", "", "") return words endfunc
But it prints out info to the statusbar, so I don't think it will be suitable for your use-case. It's very fast, though!
Upvotes: 3
Reputation: 5733
Keep a count for the current line and a separate count for the rest of the buffer. As you type (or delete) words on the current line, update only that count, but display the sum of the current line count and the rest of the buffer count.
When you change lines, add the current line count to the buffer count, count the words in the current line and a) set the current line count and b) subtract it from the buffer count.
It would also be wise to recount the buffer periodically (note that you don't have to count the whole buffer at once, since you know where editing is occurring).
Upvotes: 9