tsbohc/zest.nvim

github github
fennel plugin
star 61
stars
alert-circle 2
open issues
users 4
subscribers
git-branch 1
forks
CREATED

2021-05-07

UPDATED

19 days ago

packer

require('packer').startup(function()
  use 'tsbohc/zest.nvim'
end)

paq

require "paq" { 
  'tsbohc/zest.nvim'
}

zest.nvim

These are your father's parentheses.
Elegant weapons for a more... civilized age.
ā€” xkcd/297

An opinionated library of macros that aims to streamline the process of configuring neovim with fennel, a lisp that compiles to lua.

For a full config example, see my dotfiles.

a short pitch

  • Provide a syntactically sweet way of interacting with select parts of lua api
  • Seamlessly integrate lua functions into keymaps, autocmds, etc
  • Be primarily a library of macros, do as much as possible at compile time
  • Output code that is readable and efficient
  • Remain compatible with everything, yet standalone

WIP If you have any feedback or ideas on how to improve zest, please share them with me! You can reach me in an issue or at @tsbohc on the conjure discord.

Deprecation notice I'll be overhauling the macros some time later this month. The old macros will stay for a while though. The -fn macros will be merged into regular ones.

setup

as a companion library

If you're already using a plugin that integrates fennel into neovim, such as aniseed or hotpot, follow these instructions:

  • Install with your favourite package manager

    (use :tsbohc/zest.nvim)
    
  • Before using any of the macros, run zest.setup with no arguments

    (let [zest (require :zest)]
    (zest.setup))
    
  • Import and alias the macros you wish to use in the current file

    (import-macros
    {:opt-prepend opt^} :zest.macros)
    

standalone

When installed on its own, zest can be configured to mirror the source directory tree to target. When a relevant file is saved, zest will display a message and recompile it.

Unless configured, zest will not initialise its compiler.

(let [zest (require :zest)
      h vim.env.HOME]
  (zest.setup
    {:target (.. h "/.garden/etc/nvim.d/lua")
     :source (.. h "/.garden/etc/nvim.d/fnl")
     :verbose-compiler true
     :disable-compiler false}))

macros

In each example, the top block contains the fennel code written in the configuration, while the bottom one shows the lua code that neovim will execute.

The examples are refreshed with every change to zest and are always up to date.

vlua

  • Store a function and return its v:lua, excluding the parentheses
(local v (vlua my_fn))
local v
do
  local ZEST_N_0_ = _G._zest.v["#"]
  local ZEST_ID_0_ = ("_" .. ZEST_N_0_)
  _G._zest["v"][ZEST_ID_0_] = my_fn
  _G._zest["v"]["#"] = (ZEST_N_0_ + 1)
  v = ("v:lua._zest.v." .. ZEST_ID_0_)
end

vlua-format

  • A string.format wrapper for vlua
(vim.cmd
  (vlua-format
    ":com -nargs=* Mycmd :call %s(<f-args>)"
    (fn [...]
      (print ...))))
local function _0_(...)
  local ZEST_N_0_ = _G._zest.v["#"]
  local ZEST_ID_0_ = ("_" .. ZEST_N_0_)
  local function _1_(...)
    return print(...)
  end
  _G._zest["v"][ZEST_ID_0_] = _1_
  _G._zest["v"]["#"] = (ZEST_N_0_ + 1)
  return ("v:lua._zest.v." .. ZEST_ID_0_)
end
vim.cmd(string.format(":com -nargs=* Mycmd :call %s(<f-args>)", _0_(...)))

options

  • A complete vim.opt wrapper
(opt-local-append completeopt ["menuone" "noselect"])
do end (vim.opt_local.completeopt):append({"menuone", "noselect"})

Full list of opt- macros:

opt-set      opt-local-set      opt-global-set
opt-get      opt-local-get      opt-global-get
opt-append   opt-local-append   opt-global-append
opt-prepend  opt-local-prepend  opt-global-prepend
opt-remove   opt-local-remove   opt-global-remove

keymaps

def-keymap

  • Map literals
(def-keymap :H [nv] "0")
do
  local ZEST_OPTS_0_ = {noremap = true}
  vim.api.nvim_set_keymap("n", "H", "0", ZEST_OPTS_0_)
  vim.api.nvim_set_keymap("v", "H", "0", ZEST_OPTS_0_)
end
  • Map lua expressions
(each [_ k (ipairs [:h :j :k :l])]
  (def-keymap (.. "<c-" k ">") [n] (.. "<c-w>" k)))
for _, k in ipairs({"h", "j", "k", "l"}) do
  vim.api.nvim_set_keymap("n", ("<c-" .. k .. ">"), ("<c-w>" .. k), {noremap = true})
end
  • Map pairs
(def-keymap [n]
  {:<ScrollWheelUp>   "<c-y>"
   :<ScrollWheelDown> "<c-e>"})
do
  local ZEST_OPTS_0_ = {noremap = true}
  vim.api.nvim_set_keymap("n", "<ScrollWheelUp>", "<c-y>", ZEST_OPTS_0_)
  vim.api.nvim_set_keymap("n", "<ScrollWheelDown>", "<c-e>", ZEST_OPTS_0_)
end

To disable noremap, include :remap after the modes.

def-keymap-fn

  • Define a function and map it to a key
(def-keymap-fn :<c-m> [n]
  (print "hello from fennel!"))
do
  local ZEST_VLUA_0_
  do
    local ZEST_ID_0_ = "_60_99_45_109_62_110_"
    local function _0_()
      return print("hello from fennel!")
    end
    _G._zest["keymap"][ZEST_ID_0_] = _0_
    ZEST_VLUA_0_ = ("v:lua._zest.keymap." .. ZEST_ID_0_)
  end
  local ZEST_RHS_0_ = (":call " .. ZEST_VLUA_0_ .. "()<cr>")
  vim.api.nvim_set_keymap("n", "<c-m>", ZEST_RHS_0_, {noremap = true})
end
  • Define an expression as a function
(def-keymap-fn :k [nv :expr]
  (if (> vim.v.count 0) "k" "gk"))
do
  local ZEST_VLUA_0_
  do
    local ZEST_ID_0_ = "_107_110_118_"
    local function _0_()
      if (vim.v.count > 0) then
        return "k"
      else
        return "gk"
      end
    end
    _G._zest["keymap"][ZEST_ID_0_] = _0_
    ZEST_VLUA_0_ = ("v:lua._zest.keymap." .. ZEST_ID_0_)
  end
  local ZEST_RHS_0_ = (ZEST_VLUA_0_ .. "()")
  local ZEST_OPTS_0_ = {expr = true, noremap = true}
  vim.api.nvim_set_keymap("n", "k", ZEST_RHS_0_, ZEST_OPTS_0_)
  vim.api.nvim_set_keymap("v", "k", ZEST_RHS_0_, ZEST_OPTS_0_)
end

autocmds

def-augroup

  • Define an augroup with autocmd! included
(def-augroup :my-augroup)
do
  vim.cmd("augroup my-augroup")
  vim.cmd("autocmd!")
  vim.cmd("augroup END")
end

def-autocmd

  • Define an autocommand
(def-autocmd [:BufNewFile my_event] [:*.html :*.xml]
  "setlocal nowrap")
vim.cmd(("au " .. table.concat({"BufNewFile", my_event}, ",") .. " *.html,*.xml setlocal nowrap"))

def-autocmd-fn

  • Define a function and bind it as an autocommand
(def-augroup :restore-position
  (def-autocmd-fn :BufReadPost "*"
    (when (and (> (vim.fn.line "'\"") 1)
               (<= (vim.fn.line "'\"") (vim.fn.line "$")))
      (vim.cmd "normal! g'\""))))
do
  vim.cmd("augroup restore-position")
  vim.cmd("autocmd!")
  do
    local ZEST_VLUA_0_
    do
      local ZEST_N_0_ = _G._zest.autocmd["#"]
      local ZEST_ID_0_ = ("_" .. ZEST_N_0_)
      local function _0_()
        if ((vim.fn.line("'\"") > 1) and (vim.fn.line("'\"") <= vim.fn.line("$"))) then
          return vim.cmd("normal! g'\"")
        end
      end
      _G._zest["autocmd"][ZEST_ID_0_] = _0_
      _G._zest["autocmd"]["#"] = (ZEST_N_0_ + 1)
      ZEST_VLUA_0_ = ("v:lua._zest.autocmd." .. ZEST_ID_0_)
    end
    vim.cmd(("autocmd BufReadPost * :call " .. ZEST_VLUA_0_ .. "()"))
  end
  vim.cmd("augroup END")
end

def-augroup-dirty

  • Define an augroup without autocmd!
(def-augroup-dirty :my-dirty-augroup)
do
  vim.cmd("augroup my-dirty-augroup")
  vim.cmd("augroup END")
end

commands

def-command-fn

  • Assign a function to an ex command
(def-command-fn :MyCmd [...]
  (print ...))
do
  local ZEST_VLUA_0_
  do
    local ZEST_ID_0_ = "_77_121_67_109_100_"
    local function _0_(...)
      return print(...)
    end
    _G._zest["command"][ZEST_ID_0_] = _0_
    ZEST_VLUA_0_ = ("v:lua._zest.command." .. ZEST_ID_0_)
  end
  vim.cmd(("command -nargs=* MyCmd :call " .. ZEST_VLUA_0_ .. "(<f-args>)"))
end

Arguments are handled automatically like so:

[]       -nargs=0    --
[x]      -nargs=1 <q-args>
[...]    -nargs=* <f-args>
[x ...]  -nargs=* <f-args>
[x y]    -nargs=* <f-args>

notes

a tale of two macros

At compile time, there is no good way of knowing if a variable contains a function or a string. I think so, at least (enlighten me!). This means that the type of the argument has to be supplied to the macro explicitly.

This is the reason for the having both def-keymap and def-keymap-fn, for example.

That said, def-keymap and others can accept functions if they have been wrapped in vlua:

(fn my-fn []
  (print "dinosaurs"))

(def-keymap :<c-m> [n]
  (vlua-format
    ":call %s()<cr>"
    my-fn))

text objects

When it comes to defining text objects, they can be considered fancy keymaps. Here're the definitions of inner line and around line:

(def-keymap :il [xo :silent]
  (string.format ":<c-u>normal! %s<cr>"
    "g_v^"))
(def-keymap :al [xo :silent]
  (vlua-format ":<c-u>call %s()<cr>"
    (fn [] (vim.cmd "normal! $v0"))))

text operators

Text operators are the fanciest of keymaps. Here's a minimal example:

(fn def-operator [k f]
  (let [v-lua (vlua f)]
    (def-keymap k [n :silent] (string.format ":set operatorfunc=%s<cr>g@" v-lua))
    (def-keymap k [v :silent] (string.format ":<c-u>call %s(visualmode())<cr>" v-lua))
    (def-keymap (.. k k) [n :silent] (string.format ":<c-u>call %s(v:count1)<cr>" v-lua))))

(def-operator :q
  (fn [x] (print x))

complex autocmds

If you want to create complex autocmds, use vlua:

(vim.cmd
  (vlua-format
    (.. ":autocmd " ponder " * <buffer=42> ++once :call %s()")
    print-answer))

thanks

zest embeds fennel.lua -- I do not claim any ownership over this file