Vim Statusline: Word search - search

I've searched hard for this and haven't been able to find what I'm after.
On my statusline I want a count of the number of matches that occur in the current file. The vim command below returns what I want. I need the returned number to be displayed in my statusline.
:%s/^I^I//n
vim returns: 16 matches on 16 lines
FYI Explanation: I'm working in a CSV file. I'm searching for two tab characters ( ^I^I ) because that indicates lines I still need to do work on. So my desired statusline would indicate how much work remains in the current file.
I don't know how to enter a vim command on the statusline, I know that %{} can be used to run a function but how do I run the vim search command? I've tried variations of the following, but they clearly aren't right and just end up with an error.
:set statusline+= %{s/^I^I//n}
Help me vimy one kenobi, you're my only hope!

The first thing to mention here is that for large files this feature would be completely impractical. The reason is that the status line is redrawn after every cursor movement, after the completion of every command, and probably following other events that I am not even aware of. Performing a regex search on the entire buffer, and furthermore, not just the current buffer, but every visible window (since every window has its own status line), would slow things down significantly. Don't get me wrong; the idea behind this feature is a good one, as it would give you an immediate and fully automated indication of your remaining work, but computers are simply not infinitely performant (unfortunately), and so this could easily become a problem. I've edited files with millions of lines of text, and a single regex search can take many seconds on such buffers.
But provided your files will remain fairly small, I've figured out three possible solutions by which you can achieve this.
Solution #1: exe :s and redirect output
You can use :exe from a function to run the :s command with a parameterized pattern, and :redir to redirect the output into a local variable.
Unfortunately, this has two undesirable side effects, which, in the context of this feature, would be complete deal-breakers, since they would occur every time the status line is redrawn:
The cursor is moved to the start of the current line. (Personal note: I've never understood why vim does this, whether you're running :s from a status line call or by manually typing it out on the vim command-line.)
The visual selection, if any, is lost.
(And there actually might be more adverse effects that I'm not aware of.)
The cursor issue can be fixed by saving and restoring the cursor position via 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() functions which can accomplish the task perfectly and compatibly. So for now, we'll use those.
Solution #1a: Restore visual selection with gv
The visual selection issue I thought could be solved by running gv in normal mode, but for some reason the visual selection gets completely corrupted when doing this. I've tested this on Cygwin CLI and Windows gvim, and I don't have a solution for this (with respect to restoring the visual selection).
In any case, here's the result of the above design:
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')}]
A few random notes:
The use of ^[\s\n]* in the match-count extraction pattern was necessary to barrel through the leading line break that gets captured during the redirection (not sure why that happens). An alternative would be to skip over any character up to the first digit with a non-greedy multiplier on the dot atom, i.e. ^.\{-}.
The doubling of the backslashes in the statusline option value is necessary because backslash interpolation/removal occurs during parsing of the option value itself. In general, single-quoted strings do not cause backslash interpolation/removal, and our pat string, once parsed, eventually gets concatenated directly with the :s string passed to :exe, thus there's no backslash interpolation/removal at those points (at least not prior to the evaluation of the :s command, when backslash interpolation of our backslashes does occur, which is what we want). I find this to be slightly confusing, since inside the %{} construct you'd expect it to be a normal unadulterated VimScript expression, but that's the way it works.
I added the /e flag for the :s command. This is necessary to handle the case of a buffer with zero matches. Normally, :s actually throws an error if there are zero matches. For a status line call, this is a big problem, because any error thrown while attempting to redraw the status line causes vim to nullify the statusline option as a defensive measure to prevent repeated errors. I originally looked for solutions that involved catching the error, such as :try and :catch, but nothing worked; once an error is thrown, a flag is set in the vim source (called_emsg) that we can't unset, and so the statusline is doomed at that point. Fortunately, I discovered the /e flag, which prevents an error from being thrown at all.
Solution #1b: Dodge visual mode with a buffer-local cache
I wasn't satisfied with the visual selection issue, so I wrote an alternative solution. This solution actually avoids running the search at all if visual mode is in effect, and instead pulls the last-known search count from a buffer-local cache. I'm pretty sure this will never cause the search count to become out-of-date, because it is impossible to edit the buffer without abandoning visual mode (I'm pretty sure...).
So now the MatchCount() function does not mess with visual mode:
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
And now we need this helper "predicate" function which tells us when it's (not) safe to run the :s command:
fun! IsVisualMode(mode)
return a:mode == 'v' || a:mode == 'V' || a:mode == nr2char(22)
endfun
And now we need a caching layer that branches on the predicate result and only runs the primary function if safe, otherwise it pulls from the buffer-local cache the last-known return value that was captured from the most recent call of the primary function taking those exact arguments:
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
For which we need this helper function which I found somewhere on the Internet years ago:
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
And finally this is how we can set the statusline:
set statusline+=\ [%{BufferCallCache('','MatchCount',['\\t\\t'],!IsVisualMode(mode()))}]
Solution #2: Call match() on every line
I've thought of another possible solution which is actually much simpler, and seems to perform just fine for non-huge files, even though it involves more looping and processing at the VimScript level. This is to loop over every line in the file and call match() on it:
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')}]
Solution #3: Call search()/searchpos() repeatedly
I've written some slightly intricate functions to perform global and linewise matching, built around searchpos() and search(), respectively. I've included support for optional start and end bounds as well.
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

Maybe not exactly what you are looking for, but if you put a function like the following in your $HOME/.vimrc file you could do:
:set statusline+=%!SearchResults('^I^I')
$HOME/.vimrc
function SearchResults(q)
redir => matches
silent! execute "%s/".a:q."//n"
redir END
return substitute(matches, "^.", "", "")
endfunction
If nothing else, maybe that will get you a little closer.

Related

In vim, how to split a word and flip the halves? FooBar => BarFoo

I sometimes write a multi-word identifier in one order, then decide the other order makes more sense. Sometimes there is a separator character, sometimes there is case boundary, and sometimes the separation is positional. For example:
$foobar becomes $barfoo
$FooBar becomes $BarFoo
$foo_bar becomes $bar_foo
How would I accomplish this in vim? I want to put my cursor on the word, hit a key combo that cuts the first half, then appends it to the end of the current word. Something like cw, but also yanking into the cut buffer and then appending to the current word (eg ea).
Nothing general and obvious comes to mind. This is more a novelty question than one of daily practical use, but preference is given to shortest answer with fewest plugins. (Hmm, like code golf for vim.)
You can use this function, it swaps any word of the form FooBar, foo_bar, or fooBar:
function! SwapWord()
" Swap the word under the cursor, ex:
" 'foo_bar' --> 'bar_foo',
" 'FooBar' --> 'BarFoo',
" 'fooBar' --> 'barFoo' (keeps case style)
let save_cursor = getcurpos()
let word = expand("<cword>")
let match_ = match(word, '_')
if match_ != -1
let repl = strpart(word, match_ + 1) . '_' . strpart(word, 0, match_)
else
let matchU = match(word, '\u', 1)
if matchU != -1
let was_lower = (match(word, '^\l') != -1)
if was_lower
let word = substitute(word, '^.', '\U\0', '')
endif
let repl = strpart(word, matchU) . strpart(word, 0, matchU)
if was_lower
let repl = substitute(repl, '^.', '\L\0', '')
endif
else
return
endif
endif
silent exe "normal ciw\<c-r>=repl\<cr>"
call setpos('.', save_cursor)
endf
Mapping example:
noremap <silent> gs :call SwapWord()<cr>
Are you talking about a single instance, globally across a file, or generically?
I would tend to just do a global search and replace, e.g.:
:1,$:s/$foobar/$barfoo/g
(for all lines, change $foobar to $barfoo, every instance on each line)
EDIT (single occurrence with cursor on the 'f'):
3xep
3xep (had some ~ in there before the re-edit of the question)
4xea_[ESC]px
Best I got for now. :)
nnoremap <Leader>s dwbP
Using Leader, s should now work.
dw : cut until the end of the word from cursor position
b : move cursor at the beginning of the word
P : paste the previously cut part at the front
It won't work for you last example though, you have to add another mapping to deal with _ .
(If you don't know what Leader is, see :help mapleader)

Vim script function: Unexpected characters in :let

So I'm learning vim script and trying to write a function that toggles (removes/adds) a character in a string and then returns the resulting string (the comments are just for stack overflow, I know how to properly comment in vimscript)
function! ToggleStringValue(input, value)
if a:input =~ value // if the value's there
let &a:input = substitute(a:input, a:value, "", "") // remove it
else
let &a:input .= a:value // otherwise append it
endif
return a:input
endfu
Example (pseudo) usage:
guioptions = ToggleStringValue(guioptions, "m") // should toggle menu bar on/off
But I'm getting an error on line 4 of the function Unexpected characters in :let
What am I doing wrong? and is this a good way of writing this function?
Edit: Correct versions after taking notes from Kent's answer:
function! ToggleStringValue(input, value)
let l:result = a:input
if l:result =~ a:value
let l:result = substitute(l:result, a:value, "", "")
else
let l:result .= a:value
endif
return l:result
endfu
function! ToggleMenuBar()
let &g:guioptions = ToggleStringValue(&g:guioptions, "m")
endfu
Your codes have several problems:
in vimscript, comment starts with ", not //, maybe you just put them there to make the question clear, I hope it is like that.
&var means an option, like let &number=0 to hide line number, same as set nonumber If the var was not option name, you will catch something like "Unknown Option" or "Option doesn't exist"
method/function argument with a:foo, they cannot be changed, they are readonly. That is, let a:foo="blah" won't go. You have to make a copy of them.

Vim script: check if the cursor is on the current word

For example in this situation:
function_call(arg1, arg2);
^Cursor is here
calling expand('<cword>') will return "arg2".
I need to filter out these cases, when cursor is on left from the current word's position.
Is there a way to do it?
An alternative using the position for (next) <cword> is:
(according to a comment by OP he want to know, perhaps the question should be updated with that request)
let cInd = strridx(getline('.'), expand("<cword>"), col('.') - 1)
let isOnKeyword = (cInd >= 0 && (cInd + strlen(expand("<cword>"))) >= (col('.') - 1))
The second part is needed if the next <cword> is also in an earlier position on the current line, in your example if the string arg2 should have been in the line also somewhere before the ,, that is hypothetical but should be considered.
Note that it's strridx not stridx.
Maybe by checking whether the cursor currently is on a keyword character:
let isOnKeyword = matchstr(getline('.'), '\%'.col('.').'c.') =~# '\k'

CamelCase Expansion in Vim like Intellij Idea?

In Intellij Idea, there's a feature. Let's say I have used a variable myCamelCase somewhere in my code. Then if I type mCC and press Ctrl-Enter or some such key combination, it expands to myCamelCase. Is there something similar in Vim?
Okay, forgive me for answering twice, but since my first attempt missed the point, I'll have another go. This is more complicated than I thought, but possibly not as complicated as I have made it (!).
This is now modified to suggest all matching variable names.
First of all, here's a function to generate the 'mCC' abbreviation from the 'myCamelCase' string:
function! Camel_Initials(camel)
let first_char = matchstr(a:camel,"^.")
let other_char = substitute(a:camel,"\\U","","g")
return first_char . other_char
endfunction
Now, here's a function that takes an abbreviation ('mCC') and scans the current buffer (backwards from the current line) for "words" that have this abbreviation. A list of all matches is returned:
function! Expand_Camel_Initials(abbrev)
let winview=winsaveview()
let candidate=a:abbrev
let matches=[]
try
let resline = line(".")
while resline >= 1
let sstr = '\<' . matchstr(a:abbrev,"^.") . '[a-zA-Z]*\>'
keepjumps let resline=search(sstr,"bW")
let candidate=expand("<cword>")
if candidate != a:abbrev && Camel_Initials(candidate) == a:abbrev
call add( matches, candidate )
endif
endwhile
finally
call winrestview(winview)
if len(matches) == 0
echo "No expansion found"
endif
return sort(candidate)
endtry
endfunction
Next, here's a custom-completion function that reads the word under the cursor and suggests the matches returned by the above functions:
function! Camel_Complete( findstart, base )
if a:findstart
let line = getline('.')
let start = col('.') - 1
while start > 0 && line[start - 1] =~ '[A-Za-z_]'
let start -= 1
endwhile
return start
else
return Expand_Camel_Initials( a:base )
endif
endfunction
To make use of this, you must define the "completefunc":
setlocal completefunc=Camel_Complete
To use insert-mode completion, type CTRL-X CTRL-U, but I usually map this to CTRL-L:
inoremap <c-l> <c-x><c-u>
With this code in your vimrc you should find that typing mCC followed by CTRL-L will make the expected replacement. If no matching expansion is found, the abbreviation is unchanged.
The code isn't water-tight, but it works in all the simple cases I tested. Hope it helps. Let me know if anything needs elucidating.
There is a plugin for this in Vim called vim-abolish. Use the map crc to expand to camel case.

Fast word count function in Vim

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?
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()}
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()}
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).
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!
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!
Since vim version 7.4.1042
Since vim version 7.4.1042, one can simply alter the statusline as follows:
set statusline+=%{wordcount().words}\ words
set laststatus=2 " enables the statusline.
Word count in 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:
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.
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\
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.
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.
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?
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
Since vim now supports this natively:
:echo wordcount().words
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!
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
A variation of Guy Gur-Ari's refinement that
only counts words if spell checking is enabled,
counts the number of selected words in visual mode
keeps mute outside of insert and normal mode, and
hopefully is more agnostic to the system language (when different from english)
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
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()},

Resources