Giter Club home page Giter Club logo

cl-neovim's Introduction

cl-neovim

Build Status Coverage Status Quicklisp dist MIT licensed

cl-neovim is a Neovim client library for writing Neovim plugins using Common Lisp.

Installation

Prerequisites for cl-neovim are SBCL along with Quicklisp, and libuv1-dev, which you should be able to install with your package manager or by manually compiling libuv.

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

Plug 'adolenc/cl-neovim'

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.

Example plugins

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.

Simple plugin

The following is a (slightly convoluted) translation of python plugin example from :h remote-plugin-example; simply put it in $VIMRUNTIME/rplugin/lisp/sample-plugin.lisp:

(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)

Exported symbols

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 :host and :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.

Neovim's API

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 vim have that prefix removed;
  • predicates containing is have that replaced by suffix p;
  • get and set are removed from names.

For example, vim_get_current_line is now just current-line, buffer_get_line becomes buffer-line and window_is_valid is window-valid-p.

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/s and #'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 #'connect, or t (and, equivalently, *nvim-instance*) for last connected instance.

Callbacks

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 /s variants.

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 CamelCaseName.

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:

  • commands: (range | count) | bang | register | eval;
  • autocmds: none (values from eval get passed as normal arguments into lambda-list); and
  • functions: eval.

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:

  • commands: nargs | complete | (range | count) | bang | register | eval;
  • autocmds: pattern | eval; and
  • functions: 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 specified: $ 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!".

Because (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 NVIM_LISP_LOG_LEVEL to 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).

Running tests

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

let g:lisp_host_enable_tests=1

to your init.vim (.nvimrc), run :UpdateRemotePlugins from Neovim, restart it, and finally run

: LispHostRunTests

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.

Contributions

Are very welcome. I would be more than happy to merge pull requests or just hear your criticism/ideas to improve cl-neovim.

License

Copyright (c) 2015 Andrej Dolenc

Licensed under the MIT License.

cl-neovim's People

Contributors

adolenc avatar dundargoc avatar hiphish avatar jurov avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

cl-neovim's Issues

Problems with defun/defautocmd/defcommand

There are still a few things that need to be addressed to make the interface for defun, defautocmd and defcommand a bit simpler/more consistent for the user:

  • placeholder gensyms used to properly parse the options passed by nvim should be made ignorable to avoid useless warnings
  • we can probably figure out the nargs option by just checking if lambda-list contains &rest symbol or alternatively by counting how many symbols it contains
  • if user specifies an option o in arglist after &opts, we can assume he wants the default in declare statement for o -- declare can be used only when user wants special behavior
  • declares unrelated to opts should not just be ignored as they are now
  • make return-from work
  • synchronous versions should make sure return values are encodable by mpk
  • I think there is still a problem with slime's indentation not working as one would expect
  • booleans that get passed from neovim (such as those for bang) should be passed to user as T or NIL instead of 1 or 0

Autocommands hang in nvim-0.2.2

Steps to reproduce:
In Lisp REPL:

(ql:quickload :cl-neovim)
(nvim:connect)
(nvim:defautocmd/s buf-enter (stuff) nil)

Now any buffer switching will completely hang neovim. It waits forever for synchronous rpc call to return.

Perhaps there's no event loop on lisp side and it is necessary to start it, how?

Errors when result of `:Lisp` contains apostrophe `'`

Calling the :Lisp ex-command in Neovim with an argument that evaluates to a string containing an apostrophe (') throws errors. For example, executing :Lisp (print "That is nice") works fine, but :Lisp (print "That's nice") does not.

The issue is the way output is echoed, you use (nvim:command (format nil "echo '~A'" output)) and if the output contains an apostrophe it breaks everything. We need to either escape the apostrophe by doubling it, or used a different API function.

I propose the latter in my PR #14 by using the nvim:out-write function. The output still needs to be formatted through "~A~&" to put a terminating linefeed at the end, or else the output won't show. Using ~& ensures that no additional linefeed will be created if the string already ends in one.

nvim:call-function evaluates ARG instead of passing them

For example (nvim:buffer-line 1 1) works, but trying to do the same by (nvim:call-function 'getbufline '(1 1 )) yields error:

 in: 1 1
     (1 1)
 
 caught ERROR:
   illegal function call


Frames:
  0.  ((:METHOD MESSAGEPACK-RPC.EVENT-LOOP:JOIN (MESSAGEPACK-RPC.EVENT-LOOP:FUTURE)) #<MESSAGEPACK-RPC.EVENT-LOOP:FUTURE {10042B5863}>) [fast-method]
  1.  (SB-INT:SIMPLE-EVAL-IN-LEXENV (CL-NEOVIM:CALL-FUNCTION (QUOTE GETBUFLINE) (QUOTE (1 1))) #<NULL-LEXENV>)
  2.  (EVAL (CL-NEOVIM:CALL-FUNCTION (QUOTE GETBUFLINE) (QUOTE (1 1))))

Plugin Architecture

Right now plugin host simply looks for *.lisp files in rplugin/lisp/ directories. This works for trivial plugins such as the sample plugin, but is probably also not the way to go for more complex ones. I think for more complex plugins cl-neovim should support looking for (and requiring) *.asd files instead.

There is also still a question about using quicklisp to handle loading of dependencies/plugins. On one hand this introduces a pretty substantial lag when loading up lisp host, but on the other hand it makes everything really convenient for end user.

Add Tests

Given how important it is that cl-neovim works as well and consistently as possible, I need to add a bunch of stress tests, especially making sure that host architecture (and stdin/stdout communication) works as expected.

Connecting to multiple instances of neovim

This is a very low priority issue and I probably won't implement it unless there is actual request for it.

Right now cl-neovim can only connect to a single instance of neovim at a time. This is different from the reference Python implementation where one can connect to as many running instances as he wants. I am pretty sure this is useless for host architecture and I am unsure of the usefulness in other cases, but I would love to be proved otherwise.

Q: plans to (re)implement SLIME/SLIMV via the "host" lisp?

Hi Andrej,

I started a CL RPC library for neovim quite some time ago but then didn't find any more time for it; it's nice to see that you're working on that front!

Do you have any plans to (re)implement SLIME resp. SLIMV via that one? Or perhaps an emacs lisp compatibility layer?

Debugging

Current implementation of debugging support (for plugins started via host) is unfortunately completely unusable. The way this would ideally work is that user would start neovim with $NVIM_LISP_DEBUG=filename, and every output to something like nvim:debug would write to specified file.

Also, host should catch everything being written to standard output from inside plugins in order to not break the communication with neovim.

Can't get callback to work

Hi! I can't get to call/s the function that I defined without getting an error:

(nvim:enable-logging :level :debug)
CL-USER> (nvim:defun/s "my_test_function" (x y) (print (list x y)))
 <DEBUG> [22:42:25] cl-neovim - (cl-neovim -> nvim) [req] (80 "vim_call_function" ("lisp#RegisterRepl" (3)))
 <DEBUG> [22:42:25] cl-neovim - (nvim -> cl-neovim) [rsp] (1 80 NIL 0)
 <DEBUG> [22:42:25] cl-neovim - (cl-neovim -> nvim) [req] (81 "vim_call_function" ("lisp#RegisterReplCallback" (3 #<HASH-TABLE :TEST EQUAL :COUNT 4 {1005333B33}>)))
 <DEBUG> [22:42:25] cl-neovim - (nvim -> cl-neovim) [rsp] (1 81 NIL 0)
 <DEBUG> [22:42:25] cl-neovim - Registering callback `":function:my_test_function"'
#<FUNCTION (LAMBDA (&REST #:R805)) {52FD8F4B}>
CL-USER> (nvim:call/s nvim:*nvim-instance* "my_test_function" 1 2)
 <DEBUG> [22:42:37] cl-neovim - (cl-neovim -> nvim) [req] (82 "my_test_function" (1 2))
 <DEBUG> [22:42:37] cl-neovim - (nvim -> cl-neovim) [rsp] (1 82 (0 "Invalid method: my_test_function") NIL)
; Debugger entered on #<MESSAGEPACK-RPC.CONDITIONS:RPC-ERROR {100533EE93}>

The setup, though, seems to work otherwise, I can call the already defined functions just fine and

:Lisp (print 42)

prints 42 in nvim as expected.

Any clue what this could be?

Fix Threading (and msgpack-rpc.lisp in general)

Right now each new asynchronous message from Neovim spawns a real new thread to handle it. This is obviously not the way to go; in the reference Python implementation this is solved by using green threads instead, which seems much more sensible.

I am also not completely sure whether or not async calls should return a future (as per msgpack-rpc spec) or just completely ignore the result (which seems to be what the Python implementation does, if I understand it correctly).

The current event loops are also very hacky and should probably be rewritten.

`:UpdateRemotePlugins` crashes Neovim

I followed all the steps of installation and when executing :UpdateRemotePlugins it crashed Neovim. I have Quicklisp installed to a non-standard location, ~/.local/share/quicklisp/setup.lisp, so I set let g:lisp_host_quicklisp_setup = '~/.local/share/quicklisp/setup.lisp' in my init.vim right before Plug 'adolenc/cl-neovim'.

The first time I had tried it :UpdateRemotePlugins failed (as expected) because I hadn't set g:lisp_host_quicklisp_setup, so the error has to be somewhere after reading the setup.lisp file.

The crash displays:

Assertion failed: (!stream->closed), function wstream_write, file /tmp/neovim-20160912-2107-m91gva/src/nvim/event/wstream.c, line 74.
Abort trap: 6

Before the crash Neovim would appear to be hanging for a while, displaying only

remote/host: python3 host registered plugins ['deoplete', 'chromatica']

Executing Lisp expressions like :Lisp (print (+ 2 3)) seems to work, so I don't know if something is missing. Running :LispHostRunTests doesn't print anything when it's done, so no idea what's going on there, it just opens a new (empty) file called test.lisp_host_testa. Here is my rplugin.vim file (after removing the testing part): http://pastebin.com/NUF8SZUt

Fix generate-api.lisp

generate-api.lisp currently doesn't work due to a bunch of changes in the api, so it should be fixed.

We should also add some way for user to regenerate the api manually (probably via some asdf command).

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.