Vimscript: Functions
Writing Vim plugins often calls for writing custom functions. Luckily, vimscript lets us do this. Functions in vimscript are capable of most things you would expect: they can operate on parameters, can produce side effects, and can return values. Vimscript even supports writing higher-order functions. To use them safely and effectively in our plugin scripts, however, we have to know how to organize them. If we are not careful, for example, we can easily start redefining other plugins’ functions. In this post, we will tackle basic vimscript function syntax and also vimscript’s rudimentary support for writing modular plugin code, which lets us safely limit a function’s scope.
Basic syntax
Here is the basic vimscript function syntax (see :help function
and
:help user-functions
):
function[!] {name}([arguments])
{commands}
endfunction
The function’s name, the {name}
, must start with a capital letter (or an
s:
–which we will get to in the next section). Vim’s manual explains that the
capital letter restriction prevents confusion with built-in functions, which
start with lowercase letters.
The body of the function, the {commands}
, are a sequence of “Ex commands.”
These are the commands you enter in normal mode by first typing :
–the manual
will sometimes even refer to them as “colon commands,” and in this post we do too.
In a function body you can omit the colons.
When Vim loads a plugin script, any functions declared with the optional !
will overwrite
(or rather redefine) any previous same-named function within scope. (More on
this in a moment.)
Within the plugin context
Plugins extend Vim’s functionality through custom
key-mappings and custom colon commands (among other things). The simplest
plugins are the .vim
script files we place in the directory /home/.vim/plugin
(if you are on a Unix machine). When Vim loads, it looks in the
this directory and sources any .vim
files it finds. When Vim
finishes sourcing our plugin files, any functions we defined that follow the
basic syntax given in the last section become attached to the global scope, and
they can be invoked using the :call
command. (Of course, it is unlikely you
will call functions this way. It is more useful to call functions as part of
some custom key mapping or custom colon command.)
If you have used other scripting languages, Vim’s sourcing process should sound
familiar. In JavaScript, for example, when a browser loads a script file, any
functions (or variables) declared outside of another function scope get
attached to the global scope (i.e., the browser’s window
object). Also, like
vimscript, JavaScript’s function names refer to the function declaration that
used the name last. For example, the following bit of valid JavaScript outputs
Hola
.
function sayHello() {
console.log('Hello');
};
// Same name? Not a problem. Sayonara, previous definition.
function sayHello() {
console.log('Hola');
};
sayHello(); // Outputs "Hola"
Another example is the Python REPL’s execfile
function, which sources a given
script, also with similar side effects to Vim’s model. Name
collisions among different scripts’ functions is an obvious problem in this sourcing
process, but vimscript does provide a few mechanisms for dealing with it.
The s:
scope annotation
First, we can declare functions using the s:
scope annotation. This limits a
function’s scope to the file that declares it. An s:
-scoped function can be
used anywhere within the file but not without. For example, in the following
mini-plugin all functions have been marked with s:
and are only available
within the DoMyHw.vim
file. This is good since s:gcd
in particular is so
generically named that some other script may define such a function and break
our plugin (or we there’s).
" Contents of /home/.vim/plugin/DoMyHw.vim
function! s:solveGcdExpression()
try
" Note: cursor is at the end of the expression after marking limits
call s:markExpressionLimits()
catch
echom "The cursor is not on a GCD expression"
return
endtry
" `` moves cursor to the start of the expression
normal v``y
let l:expression = @"
let [l:a, l:b] = matchlist(l:expression, s:EXP_BODY_PATTERN)[1:2]
let l:answerString = " = " . s:gcd(l:a, l:b)
" `` moves cursor to the end of the expression
exec "normal ``a" . l:answerString . "\<ESC>"
endfunction
function! s:markExpressionLimits()
let l:notOnExpression = "ERROR: Cursor not on a gcd expression"
if (search(s:EXP_PREFIX_PATTERN, "sbc", line(".")) == 0)
throw l:notOnExpression
endif
if (search(s:EXP_PATTERN, "se", line(".")) == 0)
throw l:notOnExpression
endif
endfunction
let s:EXP_PREFIX_PATTERN = '\(gcd(\|(\)'
let s:EXP_BODY_PATTERN = '\s*\(\d\+\)\s*,\s*\(\d\+\)\s*)'
let s:EXP_PATTERN = s:EXP_PREFIX_PATTERN . s:EXP_BODY_PATTERN
function! s:gcd(m, n)
let [l:max, l:min] = sort([a:m, a:n], {a, b -> b - a})
if l:min == 0
return l:max
else
return s:gcd(l:min, l:max % l:min)
endif
endfunction
" Public commands
command! SolveGcd :call s:solveGcdExpression()
nnoremap <Leader>sg :SolveGcd<CR>
In some sense .vim
files act as modules where the file provides encapsulation
and where the global declarations are the module exports.
Autoload scripts
The second mechanism for avoiding name collisions are “autoload” scripts. These
are scripts that we have placed in /home/.vim/autoload
. Public functions in
autoload scripts have the syntax:
function[!] {filepath}#{name}([arguments])
{commands}
endfunction
Notice we have the original function syntax with the addition of the {filepath}#
prefix.
The {filepath}
indicates the path to the script declaring the function
relative to the /home/.vim/autoload
directory. The syntax for the path uses
#
as directory separators instead of slashes. Also, we drop the script’s
.vim
extension in the file path.
When Vim encounters a function of this form, it finds and sources the file
/home/.vim/autload{filepath}.vim
, replacing the #
s in {filepath} with
/
s. Thus, we avoid namespace collisions by having unique {filepath}
prefixes.
These autload requirements functions are best understood by example. Below, is
the result of moving s:gcd
to the autoload script intmath.vim
.
" Contents of /home/.vim/autoload/intmath.vim
function! intmath#gcd(m, n)
let [l:max, l:min] = sort([a:m, a:n], {a, b -> b - a})
if l:min == 0
return l:max
else
return intmath#gcd(l:min, l:max % l:min)
endif
endfunction
This function is now called in the original DoMyHw.vim
using the full name
intmath#gcd
. (Note that ...
indicates sections of omitted code.)
" Contents of /home/.vim/plugin/DoMyHw.vim
function! s:solveGcdExpression()
...
let l:answerString = " = " . intmath#gcd(l:a, l:b)
...
endfunction
...
" Public commands
command! SolveGcd :call s:solveGcdExpression()
nnoremap <Leader>sg :SolveGcd<CR>
Note that the relative path to intmath.vim
under /home/.vim/autoload
is simply
intmath.vim
, so the function prefix is intmath#
. If intmath.vim
would
have been located at /home/.vim/autoload/mymath/intfuncs.vim
, then our prefix
would have been mymath#intfuncs#
, and we woud have called the function using
mymath#intfuncs#gcd
.
This file-path-prefix scheme is roughly analogous to Java’s package system minus the use of import statements. Java package names correspond to a file path. Without import statements, we would have to refer to each function and class in Java by its fully qualified package name. Thinking of autoload script’s in terms Java’s package system, we can view Vim autoload scripts as “packages,” and we call functions from these packages using their fully qualified name.
Vimscript functions are a good way to decompose plugin functionality. Like in
other scripting languages, sourcing vimscript presents some namespacing
issues, but hopefully this post has shown how vimscript’s scoping and autoload
constructs help us modularize our code and keep the global namespace
unpolluted. At this point, it might be worth checking out the source code for
some nontrivial Vim plugins such as Tim Pope’s git plugin
fugitive or Yosuke Kurami’s TypeScript
plugin tsuquyomi to see how this is done
at scale. Notice in particular how fugutive opts for a monolithic pugin file using
the s:
annotation where tsuquyomi uses autoload scripts.