Common Lisp client for Neovim
cl-neovim is a Neovim client library for writing Neovim plugins using Common Lisp.
cl-neovim is available from Quicklisp, so simply evaluating
* (ql:quickload :cl-neovim)
from your SBCL repl should properly load it and all the remaining dependencies.
Installing plugin host
The previous step only installed cl-neovim for usage from the REPL. The easiest way to also install lisp host (required to use plugins written in Common Lisp) is to use vim-plug. Add
into your init.vim, run
:PlugInstall from within Neovim, restart Neovim and run
:UpdateRemotePlugins. If everything worked correctly, calling
:Lisp (print 42) command (after restarting one more time) should output
42 into your prompt.
If you are having trouble getting the host to work, and if you are using latest version of Neovim (≥ v0.1.5), you can call
:CheckHealth lisp command from Neovim, which should help you debug your problems.
Using the package
To use the package from the REPL, first run Neovim and make it listen to some address:
$ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim
start your SBCL REPL and enter:
* (ql:quickload :cl-neovim) * (nvim:connect :file "/tmp/nvim") * (nvim:command "echo 'Hello from Common Lisp!'")
which should display "Hello from Common Lisp!" into your Neovim's prompt.
cl-neovim looks for lisp plugins inside
$VIMRUNTIME/rplugin/lisp/ directory. Note that simply loading plugins in your init.vim is not enough -- the first time around (and every time your callback specifications change) you will need to run
:UpdateRemotePlugins from within Neovim to register the plugins.
The following is a (slightly convoluted) translation of python plugin example from :h remote-plugin-example; simply put it in
(defpackage #:sample-plugin (:use #:cl)) (in-package #:sample-plugin) (defparameter *calls* 0 "Counter for calls.") (defun increment-calls () (if (= *calls* 5) (error "Too many calls!") (incf *calls*))) (nvim:defcommand/s lisp-sample-cmd (&rest args &opts (range r) bang) (increment-calls) (setf (nvim:current-line) (format nil "Command: Called ~A times, args: ~A, range: ~A, bang: ~A" *calls* args r bang))) (nvim:defautocmd/s buf-enter (filename) (declare (opts (pattern "*.lisp") (eval "expand(\"<afile>\")"))) (increment-calls) (setf (nvim:current-line) (format nil "Autocmd: Called ~A times, file: ~A" *calls* filename))) (nvim:defun "LispSampleFun" (&rest args &opts (eval line-n)) (declare (opts (eval "line(\".\")-1"))) (increment-calls) (setf (nvim:current-line) (format nil "Function: Called ~A times, args: ~A, eval: ~A" *calls* args line-n)))
A more serious plugin
For plugins that require a more serious structure, cl-neovim registers
.asd files in the root directory of the plugin, which means you can structure them as you wish. The only thing you will need is to add a
rplugin/lisp/[plugin-name].lisp file which (quick)loads your plugin. For example:
;;;; lisp-sample-plugin.asd (in-package #:cl-user) (asdf:defsystem #:lisp-sample-plugin :depends-on (#:cl-neovim) :serial T :components ((:module "src" :components ((:file "package") (:file "main")))))
;;;; src/package.lisp (in-package :cl-user) (defpackage #:lisp-sample-plugin (:use #:cl))
;;;; src/main.lisp (in-package #:lisp-sample-plugin) (nvim:defcommand sample-callback () (setf (nvim:current-line) "Hi nvim!"))
;;;; rplugin/lisp/lisp-sample-plugin.lisp (ql:quickload :lisp-sample-plugin)
cl-neovim allows you to connect to Neovim using either named pipes via
#'connect and it's
:file parameter, or using tcp address if you specify
:port arguments instead. Function also binds the connection to the
*nvim-instance* variable and returns an instance of
nvim class, which you can optionally pass as the final argument to all of the functions below in case you need to be connected to multiple instances of Neovim at once.
Package basically exports every function exposed by Neovim's api. You can find the full listing in package.lisp.
If you are familiar with the api Neovim exposes, some things in cl-neovim are renamed for nicer interface. Specifically:
- underscores are replaced with hyphens;
- names starting with
vimhave that prefix removed;
- predicates containing
ishave that replaced by suffix
setare removed from names.
vim_get_current_line is now just
Setter functions (those with
set in their names) are implemented as inversions of their respective
get counterparts via
setf macro. So, to set current line to "some new line", you would use
(setf (nvim:current-line) "some new line").
By default all the calls are synchronous, meaning they block the execution of the thread until Neovim returns the result. You can optionally use asynchronous versions by appending
/a to the name; these calls won't block the thread, but they also ignore all the errors and return values.
If you want to manually call Neovim api functions (that is, by string), you can use
#'call/a for synchronous and asynchronous calls respectively, where the first argument of either call is either a instance of
nvim class that gets returned by
t (and, equivalently,
*nvim-instance*) for last connected instance.
Callbacks for Neovim are of the form:
callback-type name (args) documentation declare-opts? form* callback-type ::= defcommand | defautocmd | defun ; asynchronous versions | defcommand/s | defautocmd/s | defun/s ; synchronous versions args ::= lambda-list [&opts args-opt*]? args-opt ::= option | (option alternative-name) declare-opts ::= (declare (opts declare-opt*)) declare-opt ::= option | (option value)
callback-type specifies the type of callback registered with Neovim:
defcommand for commands,
defautocmd for autocommands and
defun for functions. These functions are all asynchronous, meaning Neovim will call them and instantly return control back to the user, completely ignoring their return values and any errors. If you would like Neovim to block user input until your callback is done executing, use the
name can be a string, in which case it is registered with Neovim verbatim, or a symbol, in which case the
hyphen-separated-name is registered with Neovim as
args is basically just a regular lambda-list, but you can also append it with
&opts keyword and specify names for arguments for additional options Neovim passes along with the regular arguments when called.
args-opt production's options are, for:
(range | count) | bang | register | eval;
- autocmds: none (values from
evalget passed as normal arguments into lambda-list); and
While these are full option names, you can also specify alternative names for them by wrapping them into a list of
(option alternative-name). Unless you explicitly specify differently via
declare-opts, these options get set to some common-sense default values.
declare-opts is a declaration used to let Neovim know about expected behaviour of the callback and explicitly tell it which options you want it to pass along in the calls. Valid options in
declare-opt are for:
nargs | complete | (range | count) | bang | register | eval;
pattern | eval; and
range | eval.
Note that you can specify just the name of the option in which case default values are assumed, or an
(option value) list if you want to assign custom values for options.
Tips for writing plugins
cl-neovim is slightly different from most other Neovim client libraries in that it allows the developer to use the full power of REPL to continuously run and test all code, including callbacks. So, while you can simply write plugins by constantly restarting Neovim (and calling
:UpdateRemotePlugins when necessary), you can be much more efficient by:
- starting Neovim with
$ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim;
- connecting to it via REPL:
* (nvim:connect :file "/tmp/nvim"); and
- writing your plugins as you would write other lisp programs by constantly evaluating your subprograms in REPL.
Evaluating the callbacks in the REPL will get them registered with the connected Neovim instance, and in order to test them, you can trigger them in Neovim and then use
(nvim:listen-once) in REPL to listen to messages from Neovim. E.g. for the
sample-callback we specified above, you would evaluate the
(nvim:defcommand sample-callback ...) form in the REPL, run
:SampleCallback from Neovim and evaluate
(listen-once) in the REPL, after which line under cursor in Neovim should change to "Hi nvim!".
(listen-once) is slightly more work than one would like, I suggest you trigger the callback from the REPL itself -- that is by calling
(nvim:command "SampleCallback") (or
(nvim:call-function "SampleCallback" '()) for functions), which runs
listen-once for you behind the scenes.
Debugging your plugins
Only 'printf' debugging is truly supported: printing to standard output from the REPL properly prints to the
*standard-output*, but ignores it when plugin is ran using plugin host; that is unless Neovim is started with
$NVIM_LISP_LOG_FILE set, in which case all the output is redirected to that file. Note that while plugin host should properly close the file when Neovim shuts down, if it for whatever reason fails to do so (or if you want instant updates to log file), simply use
(force-output) after printing, so you don't lose buffered output.
Additionally you can set
DEBUG to log messages passed between cl-neovim and Neovim itself, or to
DEBUG1 to also track actual bytes. If you want to see messages passed between Neovim and cl-neovim running in your REPL, you can manually enable logging by evaluating
(nvim:enable-logging :level :debug) (or
:debug1 for bytes).
There are two aspects to testing cl-neovim: testing how it works from the REPL and how it works from host. To test REPL, run Neovim with
$ NVIM_LISTEN_ADDRESS=/tmp/nvim nvim. Then, run REPL with same
NVIM_LISTEN_ADDRESS specified, e.g.
$ NVIM_LISTEN_ADDRESS=/tmp/nvim sbcl. After that, evaluate
* (ql:quickload :cl-neovim) * (asdf:test-system :cl-neovim)
to actually run the tests.
On the other hand, to test the plugin host, you need to add
to your init.vim (.nvimrc), run
:UpdateRemotePlugins from Neovim, restart it, and finally run
from Neovim to run the tests. After you are done with testing the host, it is recommended that you remove the
let g:lisp_host_enable_tests=1 line from your init.vim and run
:UpdateRemotePlugins again, otherwise your Neovim will have a bunch of useless (auto)commands and functions registered.
Are very welcome. I would be more than happy to merge pull requests or just hear your criticism/ideas to improve cl-neovim.
Copyright (c) 2015 Andrej Dolenc
Licensed under the MIT License.