Vim Plugin using Vim9 Script
If you are a longtime user of Vim there are occasions where you may want to extend the functionality of Vim by writing your own plugin. The release of Vim9 script has made the task less intimidating since the new scripting language resembles Python. This blog post is not a beginner guide but rather a collection of thoughts from my experience developing autosuggest and vimcomplete.
If you are on the fence deciding whether to implement your idea in Lua for Neovim or Vim9script for Vim, I have some opinions. Even though legacy Vim script works on both Vim and Neovim I intentionally did not learn it because, well, it is just weird and unreadable.
Both Lua and Vim9script are compiled into bytecode (unlike legacy script). I wrote a few non-trivial plugins in Lua before switching over to Vim9script. I prefer the latter because the code tends to be more compact, has more advanced language features for functional programming, has better regex support, and offers smoother interface to Vim’s APIs. But Lua is its own fun language to program in and Neovim keeps experimenting with new features. Ultimately it boils down to preference. If Vim9script tickles your curiosity then read on.
Directory Structure
The first step in writing a plugin is to organize your folders. Vim expects certain folder names. Here is a typical organization for github hosted repositories.
myplugin
├── LICENSE
├── README.md
├── autoload
│ ├── foo.vim
│ └── bar.vim
├── doc
│ └── myplugin.txt
└── plugin
└── myplugin.vim
Your main directory name should be the name of the plugin. Under that
directory, the plugin should have a plugin
and an autoload
directory:
- The
plugin
directory sets up the plugin. It should include the commands and keybindings that you want in your plugin. The files in this directory are sourced in alphabetically order (:h load-plugins
). - The
autoload
directory holds the meat of the plugin. It is only loaded when one of the commands defined in theplugin
directory gets called. On-demand loading keeps Vim’s initialization faster. doc
directory is optional but highly recommended. Learn the Vim help file syntax. You could include even more information here than inREADME.md
.- In addition, you may need an
import
directory if you wish to export functions for use in other plugins.
Note: Vim has extensive documentation. Anytime you have doubt over foo
try
:helpgrep foo
or :h foo<tab>
(with wildmenu
) or use
autosuggest.
Include the following boilerplate code at the top of plugin/myplugin.vim
.
if !has('vim9script') || v:version < 900
" Needs Vim version 9.0 and above
finish
endif
vim9script
g:loaded_myplugin = true
All other files should include vim9script
at the top. In order to use
functions defined in another script you have to use the import
directive.
From a script in plugin
directory you could define a command as follows.
import autoload '../autoload/foo.vim'
command! -nargs=0 MyCommand foo.SomeFunction()
In the above example, SomeFunction()
needs to be exported (:h vim9-export
)
from foo.vim
.
Also, make use of User
(:h User
) auto-command event to synchronize parts of initialization.
Language Features
Vim9script is fairly easy to learn. You can also pick up some advanced insights here. If you are familiar with Python there is VimScript For Python Developers. Finally, here are some suggestions to make your programming task more fun.
Lambda Expressions
If you are doing any type of data manipulation lambda expressions (:h lambda
)
come in handy. You can use them with usual suspects filter()
, map()
, reduce()
, sort()
etc.
Functions can be chained using ->
operator. Use the arg->func()
idiom consistently
throughout. Keep your code readable and bloat-free.
Meta Tables
Vim9script will have class (:h class
) in the near future. In the meantime, you can emulate
an object (encapsulation) using a simple dictionary and function references
(:h funcref()
, :h function()
).
def NewMyObject(someArg: bool): dict<any>
var contents = {
property1: [],
property2: someArg,
}
contents->extend({
functionName1: function(FunctionName1, [contents]),
})
return contents
enddef
def functionName1(obj: dict<any>, optionalArg: number)
var foo = obj.property1
enddef
var myObj = NewMyObject(true)
var fnArg = 22
myObj.functionName1(fnArg)
Options
Users of your plugin may need to set options. Use a dictionary to encapsulate all options. It is best not to use a global variable for each option, since they pollute global namespace. You can use a global function that takes a dictionary argument to set the options. There is also a possibility to use exported function, but this limits the users to only use Vim9script for configuration. So the former method is preferred.
In your autoload/options.vim
you can define the options.
export var myOptions: dict<any> = {
option1: '',
option2: false,
option3: [],
}
In plugin/myplugin.vim
define a global function to set options.
import autoload '../autoload/options.vim'
def! g:PluginNameOptionsSet(opts: dict<any>)
options.myOptions->extend(opts)
enddef
Strings
Be familiar with ==#
, ==?
, =~#
, =~?
, !~
, match()
, matchstr()
,
matchlist()
, \c
, \v
, "
vs '
etc. See help files.
Use $'sometext {var}'
as opposed to "sometext " .. var
.
Debug
For the most part echom
in scripts is adequate. You can view the messages
using :messages
. For checking regex you can use :echo
'teststring' =~ 'pattern'
or :echo matchstr('foo', 'pattern')
.
Disassemble
Sometimes you may ask yourself if it is worth caching a dictionary key
value outside a loop, and discover that Vim9 compiler does not do loop optimization.
You can verify using :disassembly
that it uses LOADSCRIPT and
USEDICT instructions inside the loop when value is not
cached.
% vim -Nu NONE -S <(cat <<'EOF' 1 :(
vim9script
var foo = {x: 1, y: 2}
def Func()
var fooy = foo.y
for i in range(5)
# echom fooy # good: using cached value
echom foo.y # not good
endfor
enddef
disa Func
EOF
)
Asynchronous Execution
Vim offers job_start()
for truly parallel execution (on a multicore system). You can spawn a new
process and wait for output. However, there is also a lightweight option that offers
concurrent execution. It may sound non-intuitive but you can use
timer_start()
with 0
timeout to schedule your function for execution at a
later time when Vim’s main-loop is free. You can also pass arguments to
callback, for example, timer_start(0, function(MyWorker, [arg1, arg2]))
.
Typically you break down your long running task into batches (say you want to
search a few thousand lines then search in batches of a thousand lines each).
Each batch can be scheduled using timer_start()
as above, with one of the arguments to
MyWorker()
being the index into a batch data array. These tasks should be chained by
having MyWorker()
call the next timer_start()
(for the next batch). The
magic here is that Vim schedules any newly typed keystrokes between each
MyWorker()
invocation. Even though Vim is single-threaded your plugin remains
responsive to keystrokes even while working on a long running task.
For demonstration, see autosuggest or ngram-complete.
Performance Measurement and Timing
Calling reltime()
before and after a code section measures execution time.
var start = reltime()
# ... do something ...
echom $'Elapsed time: {start->reltime()->reltimestr()}'
You can abort a task if it goes over a timeout. Use reltimefloat()
for
that.
const timeout = 2000 # millisec
var start = reltime()
while (start->reltime()->reltimefloat() * 1000) < timeout
# ... process a batch ...
endwhile
Expose APIs to Other Plugins
Say you want other plugins to be able to call FooAPI()
function of your
plugin. Create a directory named import
at the top level and a file inside it
(say fooplugin.vim
). Inside this file you can expose your APIs by exporting
functions.
vim9script
import '../autoload/myfoo.vim'
export def FooAPI()
myfoo.MyService()
enddef
Other plugins can import your API.
import 'fooplugin.vim'
def MyFunc()
fooplugin.FooAPI()
enddef
For demonstration, see API in vimcomplete used in ngram-complete.
Autocompletion for Vim9 Script
LSP for Vim9 script does not exist yet. In the meantime, you can use the autocompletion extension for Vim9 script language vimscript-complete.