Skip to content
Sweeter Neovim with Fennel
Lua Makefile
Branch: master
Clone or download
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github Add CoC and funding Nov 30, 2019
fnl/aniseed Print stack traces when AOT compiling Dec 1, 2019
lua/aniseed Bump nvim.lua version again Dec 3, 2019
test/fnl More doc tweaks Nov 22, 2019
.gitignore Remove symlinks, fixes #1 Dec 2, 2019
Makefile Bump nvim.lua version again Dec 3, 2019
README.adoc Bump version and improve wording Dec 3, 2019
UNLICENSE Initial commit Nov 10, 2019

README.adoc

Aniseed

Sweeter Neovim with Fennel

Read my post, Neovim configuration and plugins in Fennel (Lisp), to get some more background on Aniseed.

Write Fennel code and have Neovim compile and execute it via Lua. Configure and control your Neovim through the wonders of Lisp!

The project started as an experiment to see if I could replace my Neovim configuration in my dotfiles as well as parts of Conjure with Fennel.

If you’re interested in Aniseed, you may find nvim-local-fennel really useful too! It compiles and executes .lnvim.fnl files from your current working directory and above, allowing you to write machine or project local configuration in Fennel extremely easily. It also acts as a good example of how you can use Aniseed to develop your own plugins.

Installation

Use your favourite plugin manager, I highly recommend vim-plug if you don’t have one already.

Pin the plugin to a tag then subscribe to new releases through GitHub. I’d hate to accidentally break your entire configuration through a bad release or breaking change. I follow semver, any breaking changes will incur a major version bump, breaking changes and upgrade steps will be listed in the release notes.

Plug 'Olical/aniseed', { 'tag': 'v1.2.0' }

Compiling ahead of time

We can compile a single Fennel file to Lua through the aniseed.compile module.

lua require("aniseed.compile").file("config.fnl", "config.lua")

To compile an entire directory of files matching a glob, such as your Neovim configuration or plugin source code, we can use the glob function in the same module.

lua require("aniseed.compile").glob("**/*.fnl", "~/.config/nvim/fnl", "~/.config/nvim/lua")

This will compile all Fennel files found under the fnl directory into Lua and spit them out into the lua directory under the same directory structures as their source.

These functions will skip any source files which haven’t changed since you last compiled them. If you wish to bypass this check you can pass an opts of {force = true} to either function as the last argument.

lua require("aniseed.compile").file("config.fnl", "config.lua", {force = true})

Evaluating with mappings

Ahead of time compiling covers most bases, but if you’ve ever written a Lisp you know how great REPL integration is. Aniseed provides some interactive evaluation, it’s not quite Conjure but it’s still useful.

To set up the mappings you’ll need to require aniseed.mapping and execute the init function contained within it. Then you can map your own keys to the <Plug> mappings it defined.

(local mapping (require :aniseed.mapping))
(mapping.init)

;; You can do this through regular Lua if you want.
;; lua require("aniseed.mapping").init()
Type Name Description

Command

AniseedEval

Executes the Fennel passed to it, such as :AniseedEval (+ 10 20). There’s a function of the same name that this maps to.

Command

AniseedEvalRange

Executes the Fennel in the given range, such as :%AniseedEvalRange or :1,5AniseedEvalRange. There’s a function of the same name that this maps to.

Command

AniseedEvalFile

Executes the Fennel contained in the file specified by the given path, won’t AOT compile for future calls so it’s really best suited to small one off things. There’s a function of the same name that this maps to.

Normal mapping

<Plug>(AniseedEval)

Should be followed by a motion to indicate what you want evaluated. Triggering this mapping with 10j would evaluate the next 10 lines, for example. If you have vim-sexp you could use af to indicate your current form or aF for the outer most form. You can even map to <Plug>(AniseedEval)af to create a mapping that evaluates your current form quickly.

Normal mapping

<Plug>(AniseedEvalCurrentFile)

Evaluates the current file from disk.

Visual mapping

<Plug>(AniseedEvalSelection)

Evaluates whatever you have visually selected.

Here’s the mappings I use written in Fennel, feel free to take what you want or just write your own in VimL!

;; We have access to https://github.com/norcalli/nvim.lua
;; Aniseed is really just combining cool things to be greater than the sum of it's parts.
(local nvim (require :aniseed.nvim))

;; Where <localleader> is bound to comma.
;; I'm using the colon syntax which translates to strings just to be fancy.
;; Imagine each :keyword as "keyword".

(fn map! [mode from to suffix]
  "Map some keys (prefixed by <localleader>) to a command."
  (nvim.ex.autocmd
    :FileType :fennel
    (.. mode :map) :<buffer>
    (.. :<localleader> from)
    (.. to (or suffix ""))))

(fn plug-map! [mode from to suffix]
  "Adds a map in terms of map! but wraps the target command in <Plug>(...)."
  (map! mode from (.. "<Plug>(" to ")") suffix))

(nvim.ex.augroup :aniseed)
(nvim.ex.autocmd_)

;; ,E<motion> - evaluate a motion.
(plug-map! :n :E :AniseedEval)

;; ,ee - evaluate the current form.
(plug-map! :n :ee :AniseedEval :af)

;; ,er - evaluate the outermost form.
(plug-map! :n :er :AniseedEval :aF)

;; ,ef - evaluate the current file from disk.
(plug-map! :n :ef :AniseedEvalCurrentFile)

;; ,eb - evaluate the buffer from Neovim.
(map! :n :eb ":%AniseedEvalRange<cr>")

;; ,ee - evaluate the current visual selection.
(plug-map! :v :ee :AniseedEvalSelection)

(nvim.ex.augroup :END)

Example usage

Given a simple Fennel program and the mappings I described above, we could evaluate the following with ,ef or ,eb to evaluate the file from disk or the buffer.

(fn add [a b]
  (+ a b))

(print (add 10 20))

{:add add}

Sadly we can’t evaluate the add function and then the call to it like we would in Clojure with Conjure, that would require a concept of namespaces to give the evaluation some context.

I actually considered bundling my own namespace macro system in with Aniseed to allow this, I may well still do that some day if I can work out various issues with the concept.

The last line in the file defines the return value for the module. Modules, by convention, return a table of functions to expose, you could now call add from other Fennel or Lua (they’re the same really) modules by requiring your file.

Extras

Module reloading

Lua may not have namespaces, but it’s module system will be enough for most purposes. One annoying thing about the system is that when you require a module it gets cached in a table. Subsequent requires return that cached value, not your new changes to the file.

If you would like your module to be redefined interactively while you work on it, simply put an :aniseed/module key in the table returned from your module. Aniseed will update your modules reference in the table for the name you give it.

For example, if we had the add module from above which we wished to change and reload without restarting Neovim, we could add the following to it.

(local core (require :aniseed.core))

(fn add [a b]
  (+ a b))

;; Pretty print the output.
;; Just like Clojure's pr function.
;; There's a pr-str too!
(core.pr (add 10 20))

{:aniseed/module :add
 :add add}

Now when you evaluate the file the global add module will be updated with your changes as you see them in your buffer. Remember to then go re-evaluate any other modules that require this one, they’ll be holding the old version of the module in their local variables.

This is another thing I might be able to improve with a hypothetical namespace system akin to Clojure’s.

Clojure inspired utility functions

For now, the best documentation of the internal functions and API is the code. You’ll find a bunch of Clojure like functions in aniseed.core, you can find that in fnl/aniseed/core.fnl.

There’s also aniseed.string, aniseed.fs and aniseed.nvim.util. The last of which contains some helpful functions for creating bridge functions between VimL and Lua which is really useful for defining mappings, for example.

If you require more and you think the community would benefit from more ported Clojure functions, why not submit a pull request adding them.

Bundled libraries

There’s a few dependencies which have some files copied into the lua/aniseed directory, they’re extremely useful, Aniseed relies on them internally but you also have access to them.

  • aniseed.fennel - Fennel itself, the entire compiler.

  • aniseed.view - the view function from Fennel, I’d recommend accessing it through aniseed.core and the pr or pr-str functions though.

  • aniseed.nvim - nvim.lua is a collection of extremely helpful mappings to vim.api.*, I find it a lot easier to read than vanilla API access.

Development

Aniseed is written in Fennel and compiled to Lua by the Makefile (please read it before executing it!), the compiled files are committed to the repository. Bear this in mind when considering a contribution!

You can use make test to ensure the Fennel files in the test directory can be compiled and executed without error. It’s a very light test that won’t catch much but it’s better than nothing as a high level smoke test.

Unlicenced

The following files are excluded from my license and ownership:

  • lua/aniseed/fennel.lua

  • lua/aniseed/view.lua

  • lua/aniseed/nvim.lua

These files come from Fennel and nvim.lua, I did not write them, all other files are from me and unlicenced. The aforementioned files should be considered under their respective project licences. They are copied into this repo to allow the plugin to work with systems that don’t support symlinks correctly.

Find the full unlicense in the UNLICENSE file, but here’s a snippet.

This is free and unencumbered software released into the public domain.

Anyone is free to copy, modify, publish, use, compile, sell, or distribute this software, either in source code form or as a compiled binary, for any purpose, commercial or non-commercial, and by any means.

You can’t perform that action at this time.