Neovim, and Other Editor-Related Thoughts [editor]
- spore
Neovim, and Other Editor-Related Thoughts [editor]
- spore
Why Fennel? Because Lua is too verbose to my taste.
And among the alternatives, Fennel For instance: in Lua is in Fennel. Now you can see the benefits, as I have many similar cases in my
actual config. I use a little plugin called nfnl1, which
automatically compiles Fennel code to Lua on save. After installing the plugin, a Neovim config in Fennel is as simple as: Upon saving, Since this plugin compiles Fennel to Lua ahead of time, it’s not needed to load
a config developed with it. Actually there’s no need to load it at all in
filetypes other than Fennel. In my config with
lazy.nvim I wrote: And it works as intended. Neovim gets its Lua, I get my Fennel and they lived
happily thereafter. There are other solutions like aniseed and hotpot.nvim. I chose Fennel itself already ships with
many useful macros and forms. But I still
find them not covering some pain points of mine. So here is my secret sauce.
Bon appétit! In my opinion, it is an anti-feature that Lua mixes arrays and maps, seeing
them as tables. Fennel authors agree with me, so they didn’t include a syntax
for mixed tables. Most of the time it’s fine, until you find that some
libraries and plugins consume data in this shape. It’s okay for me if the data looks like Usage: which translates to in Lua. The implementation is trivial, but it took me some time to come up with this
name which is short and pretty enough to be practical. Lua and many other languages support “chaining” field access and function
calls, with infix dot operator and postfix application syntax. And
unsurprisingly, some APIs are designed around this convenience. Fennel doesn’t fully support this: “direct” dot access is only available for
identifiers, so for example These restrictions make sense, since so many niceties of S-expressions would be
gone if they were allowed. On the other hand I never agree with the idea that
Lisps need to have extra amount of parens comparing to other languages. The
only thing missing here is a syntax that’s concise and intuitive enough. Let’s figure out what can be done, starting with this Lua expression mixing
field accesses and calls: Naively translating the above expression we get this in Fennel: which screams “NOOOO” to me in every character. What about some built-in
macros? A smaller NO. The threading macro in Fennel, unlike its origin in Clojure,
doesn’t support placeholders. So the thread is forced to be broken when we
apply It’s finally readable, but still much more verbose than the original version.
If only we can use the exact same syntax from Lua… Oh wait, can’t we? A macro1 that treats symbols as fields and parens as calls, chaining
them together – that’s exactly the same behavior as the Lua syntax, in Lisp
flavor. Turns out it’s not hard to write a syntax transformation for this.
Now I can finally have my peace, looking at in my config. In my opinion it’s a wrong choice to conflate indexing with field access in
Fennel: indeed they are the same thing under the hood, but accessing fields
with a static symbol is a very different pattern than indexing with a value. I
mean, that’s precisely why Lua have them separated. Forcing I still appreciate that Fennel has a good macro system, so that I can help
myself with the situation. But since it have already inherited half the
syntactic mess from Clojure, I’d like it a bit more “magical” to deal with
interops better. It would be ideal for me if Fennel can follow Lua more
closely, having This macro is similar to the Clojure Note that macros presented in this section are assumed to be in
Fennel macro modules, defined
as at top of the file.
See its README for this Upcoming: Fennel REPL; fennel-ls 1. Neovim Configuration in Fennel [editor/fennel-on-neovim]
1.1. Getting Started (with nfnl) [editor/fennel-getting-started]
{
config = function()
if vim.o.columns < 60 then
return "short"
else
return "full"
end
end
}
{:config #(if (< vim.o.columns 60)
"short"
"full")}
# in the config directory
# you can also use an empty file, which saves you from future permission prompts
echo {} > .nfnl.fnl
nvim init.fnl
nfnl
will generate init.lua
from init.fnl
(it won’t override
pre-existing files though). If you are migrating from a Lua-based config, you
may find it easier to write an init_.fnl
and check the compiled Lua before
swapping it in place. It’s rather trivial to write Fennel code equivalent to
Lua, so it won’t take long after you get used to the language. And the
generated file is decently readable, so check it out when in doubt.[{1 "Olical/nfnl" :ft "fennel"}]
nfnl
because it’s minimal and robust. 1.2. Useful Macros [editor/fennel-macros]
1.2.1.
|
, for Mixed Array and Map [editor/fennel-macros/bar]{elem_at_1, key = value, ...}
since
{1 elem_at_1 :key value ...}
is not too bad, but I’m not going to accept {1 x 2 y 3 z :k v}
because it feels redundant and noisy. So I made a simple macro
for this case:(fn | [& args]
(let [len (length args)]
(table.move args 1 (- len 1) 1 (. args len))))
(| 1 2 3 {:a 4 :b 5})
{1, 2, 3, a = 4, b = 5}
1.2.2.
dot
, for Chaining Field Accesses and Calls [editor/fennel-macros/dot]The side where grass is greener
require("plugin").setup({ ... })
(vim.lsp.buf.hover)
works but ((require :plugin).setup ...)
does not; the alternative looks like ((. (require :plugin) :setup) x)
which is horrendous. (Note that here setup
becomes a
string and extra parens is required for dot access with the .
macro.)There shall be a way
f(a).b(x).c
(. ((. (f a) :b) x) :c)
;; with threading macros (no placeholders)
(-> ((-> (f a) (. :b)) x)
(. :c))
x
to function f(a).b
. With a better threading macro it can be as good
as:;; with SRFI-197 `chain` style macros
(chain (f a)
(. _ :b)
(_ x)
(. _ :c))
;; what if...?
(dot (f a) (b x) c)
(dot x)
maps to x
, (dot x a)
is x.a
, (dot x (f a))
is x.f(a)
, and
more arguments do the same transformation to the previous expression.(fn dot [x & args]
(fn transform [x form]
(case form
;; (dot x y) => (. x "y")
(where form (sym? form)) `(. ,x ,(tostring form))
;; bonus rule: (dot x [y z]) => (. x y z)
(where exprs (sequence? exprs)) `(. ,x ,(unpack exprs))
;; (dot x (f a)) => ((. x "f") a)
[symbol & rest] `((. ,x ,(tostring symbol))
,(unpack rest))
;; (dot x ()) => (x)
[] `(,x)))
(case args
;; (dot x form rest ...) => (dot (dot x form) rest ...)
[form & rest] (dot (transform x form) (unpack rest))
;; (dot x) => x
[] x))
(dot (require :plugin) (setup {}))
Final thoughts
.
to have
expressions (evaluating to strings) as parameters forfeits the chance of
accepting anything special, leaving the only choice to be nesting parens
outside..
as my dot
macro and renaming the original .
to
something like ref
...
, but that name is taken by
a Lua operator so I didn’t pick a symbolic name for it.fn
and imported with import-macros
. Additionally, due to the compilation
mechanism of nfnl
, macro modules need to have a special header like;; [nfnl-macro]
nfnl
-specific detail.