Live Word Count in VIM!

A live word count (one updated as you type) is a feature I love when writing non-technical documents and non-programs.

For instance, newer versions of WordPress feature this at the bottom of your new post text box (though it waits until you’ve hit a rest period in your typing before it updates the count, although this is a very short rest period indeed). It’s also a great feature of surprisingly few author-orientated writing programs (Scrivener is one).

However, for items that are neither WordPress blog posts nor fiction, I prefer to use vim, because you can’t take the programmer out of the writer sometimes. Heck, even for blog posts or fiction I often prefer vim (especially in the area of syntax highlighting). But sadly, for a long time, vim did not come with a live word count.

Until now.

Well, sort of. There’s a version that’s been cooked up by one of the commenters on Stack Overflow.

I use this just in macvim, not terminal vim, because macvim is for more extended projects/programming, and terminal vim is for quick and dirty scripts. Thus I only really care about wordcount in the Mac equivalent of gvim.

But no worries; if you use .gvimrc in addition to .vimrc, then .gvimrc acts as overrides for things in .vimrc. Excellent, of course.

So this went into my .gvimrc (and really, it should go into a separate file plugin or something in my .vim directory, but I’m too tired to care at the moment):


"-------------- word count ---------------
" from http://stackoverflow.com/questions/114431/fast-word-count-function-in-vim/120386#120386

"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 = "\"
    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 = "\"
    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

"example of using the function for statusline: 
"set statusline=wc:%{WordCount()}

"-------------------------------------------

set statusline=%<%f %y%m%r wc:%{WordCount()}%=%l,%c%V  %L lines:%P  

The last piece of configuration is my normal statusline with word count inserted, so it shows up like this:

gvimrc status line with word count

You can get more help with the statusline codes in vim via :help statusline, or visit the vim online documentation project, at ‘statusline’. (You can search the rest of the online documentation as well.)

This has been my geekout for the day.

6 thoughts on “Live Word Count in VIM!

  1. I tried this and get the error message:
    E117: Unknown function: OtherLineWordCount
    It seems like part of it is missing.

  2. Hi, thanks for the script. It works well when I create a new buffer and type something in it. However, when I create a new buffer and paste something in it, the number is always zero; and, after pasting if I start to type words, it counts only what I type but not the pasted texts. BTW, saving the file won’t update the number. How to figure out this?

  3. Hi xell,

    I’ve seen this happen, but I don’t know how to debug/fix it, not having written the actual guts of the script; maybe that’s something to ask at Stack Overflow.

    It’s a little frustrating, but I usually just closed and reload the file when that happens.

  4. Hi,

    I checked out the script and find out an unelegant solution, however it’s better than “closing and reloading the file”. After pasting just simply call OtherLineWordCount() (and CurrentLineWordCount() if need) manually. The count number will update correctly.

    It seems it’s the problem of the IF statement in the WordCount() function. In a nutshell, it can’t detect the paste/put action, so the count number will not be updated. Unluckily I don’t know how to do that either… (A more ugly trick is re-mapping the normal command “p” in vim to automatically call the above two functions as soon as a paste/put action is performed.)

  5. Thanks, xell!

    I think I’ll map the calling of the function(s) to something else rather than remapping “p”. Annoying.

Comments are closed.