It all starts on the moon Lua
One of the most anticipated and loved features by Neovim users is the lua support.
This support officially came in version 0.5 of the neovim, which went on to allow the users to throw away stop using their init.vim
and set up the neovim with a nice init.lua
.
And a happy consequence of this is that we can not only use Lua, but we can use lua ecosystem packages and languages that compile to Lua
Hello, Peter Fennel
Fennel is one of these languages that compile to Lua, which means you will write Fennel code and the compiler will generate Lua code, which will be read and run by neovim.
Fennel -> Lua -> Neovim
But why Fennel?
This is a very common question that people ask when I say that I use Fennel.
It seems that's a common question that people ask everyone who uses Fennel because the official Fennel website has the answer to exactly this question.
I will summarize their response in a few points.
Fennel is less error-prone
Lua is a great language, but some points can facilitate the occurrence of errors.
One of the points is the ease with which you access or change a global variable.
If you create a variable without the local
keyword it is already a global variable. And to access the value of that variable, you just need to type the name of the variable, which can cause unexpected behavior, for example:
-- settings.lua
myVar = 'this is global'
-- init.lua
local myVal = 'local'
print('this is the value of local: ' .. myVar) -- Oops
Note that, due to a typing error in the word myVal
, replacing l
with r
we end up accessing the value of a global variable defined elsewhere.
Errors like this can be difficult to find out.
Fennel prevents errors like this by allowing access to global variables only through the _G
table.
When trying to simulate the above case in Fennel, the compiler will alert us that the myVar
variable does not exist.
(local myVal "local")
(print (.. "This is the value of local: " myVar))
:: COMPILE ERROR
xxx settings/globals.fnl
Compile error in settings/globals.fnl:42
unknown identifier in strict mode: myVar
* Try looking to see if there's a typo.
* Try using the _G table instead, eg. _G.myVar if you really want a global.
* Try moving this code to somewhere that myVar is in scope.
* Try binding myVar as a local in the scope of this code.
Another point, which can facilitate the occurrence of Lua errors, is the lack of validation of the number of arguments of a function.
local function myAnd(x, y)
return x and y
end
print(myAnd(true)) -- nil
Notice that I only passed 1 argument, and the function works with 2 parameters, the code in Lua ran without telling me that I forgot to pass another argument to the function.
In Fennel, we can use the lambda
keyword to create functions that validate parameters:
(lambda my-and [x y]
(and x y))
(print (my-and true)) ; Error [...] Missing argument y
Note: In Fennel, ;
is the character used to start a comment
(Syntax (from (Lisp!)))
This is a bit controversial because some people don't like Lisp's syntax, but it has some benefits:
- Everything is an expression, i.e. we don't have statements
- When dealing with operators, there is no ambiguity of what comes first, we have no "operator precedence". (In lua, for example,
A or B and C or D
)
These points make Fennel a very simple language to program and maintain.
Modernity and facilities
In addition to the points mentioned above, it is worth highlighting some interesting features that Fennel brings to make our lives easier.
With Fennel, we have destructuring, pattern matching, macros and more.
Destructuring
While on Lua we do:
-- Lua
local var = require'module'.var
local var2 = require'module'.var2
In fennel, we can simply do:
; Fennel
(local {: var : var2} (require :module))
Pattern matching
When we want to test the value of a variable several times, in Lua, we make an if
sequence:
-- Lua
local function get_desc(key)
if (key == "k1") then
return "Key 1"
elseif (key == "k2") then
return "Key 2"
elseif (key == "k3") then
return "Key 3"
else
return nil
end
Whereas, in fennel, we can use match
:
; Fennel
(lambda get-desc [key]
(match key
:k1 "Key 1"
:k2 "Key 2"
:k3 "Key 3"))
How to get started
Now that I've convinced you (at least I hope) to use Fennel, let's see how to start using it to configure Neovim!
We will use two plugins for this:
-
tangerine
udayvir-singh / tangerine.nvim
🍊 Sweet Fennel integration for Neovim
About
Tangerine provides a painless way to add fennel to your config.
Features
- 🔥 BLAZING fast, compile times in milliseconds
- 🌊 100% support for interactive evaluation
- 🎍 Control over when and how to compile
- 🎀 Natively loads
nvim/init.fnl
Comparison to other plugins
HOTPOT 🍲
- Abstracts too much away from the user.
- Hooks onto lua package searchers to compile [harder to debug].
ANISEED 🌿
- Excessively feature rich for use in dotfiles.
- Blindly compiles all files that it founds, resulting in slow load times.
Installation
- Create file
plugin/0-tangerine.lua
to bootstrap tangerine:
Important
If you are using lazy.nvim then you should create
init.lua
instead ofplugin/0-tangerine.lua
.Refer to #20 for more information.
-- ~/.config/nvim/plugin/0-tangerine.lua or ~/.config/nvim/init.lua -- pick your plugin manager local pack = "tangerine" or "packer" or "paq" or "lazy
… -
hibiscus
Hibiscus.nvim
🌺 Highly opinionated macros to elegantly write your neovim config.
Companion library for tangerine but it can also be used standalone.
Rational
- 🍬 Syntactic eye candy over hellscape of lua api
- 🎋 Provides missing features in both fennel and nvim api
Installation
- Create file
plugin/0-tangerine.lua
to bootstrap hibiscus:
NOTE: if you are using lazy plugin manager, you should create
/init.lua
instead.-- ~/.config/nvim/plugin/0-tangerine.lua or ~/.config/nvim/init.lua -- pick your plugin manager local pack = "tangerine" or "packer" or "paq" or "lazy" local function bootstrap(url, ref) local name = url:gsub(".*/", "") local path if pack == "lazy" then path = vim.fn.stdpath("data") .. "/lazy/" .. name vim.opt.rtp:prepend(path) else
…
Tangerine integrates Fennel with Neovim very transparently, compiling Fennel files to Lua and bringing some interesting tools.
hibiscus brings several macros related to the Neovim ecosystem that help us to write less
The first step is to create the ~/.config/nvim/plugin/0-tangerine.lua
file with the content:
local function bootstrap (name, url, path)
if vim.fn.isdirectory(path) == 0 then
print(name .. ": installing in data dir...")
vim.fn.system {"git", "clone", "--depth", "1", url, path}
vim.cmd [[redraw]]
print(name .. ": finished installing")
end
end
bootstrap (
"tangerine.nvim",
"https://github.com/udayvir-singh/tangerine.nvim",
vim.fn.stdpath "data" .. "/site/pack/packer/start/tangerine.nvim"
)
bootstrap (
"hibiscus.nvim",
"https://github.com/udayvir-singh/hibiscus.nvim",
vim.fn.stdpath "data" .. "/site/pack/packer/start/hibiscus.nvim"
)
require'tangerine'.setup{
compiler = {
verbose = false,
hooks = { "onsave", "oninit" }
}
}
This setup assumes you use Packer to manage your plugins, if you don't, check the Tangerine repository for how to install it in your plugin manager.
With this, when restarting Neovim, tangerine and Hibiscus will be downloaded and initialized.
This means you can now start configuring Neovim in Fennel by creating a ~/.config/nvim/init.fnl
file 🎉
When you save this file, tangerine will already compile it and generate the lua file to be loaded by Neovim, not requiring any additional configuration for this.
Tips for getting started with Fennel
Documentation is your friend!
The two best sources for understanding how Fennel works are Fennel's tutorial and reference.
I'll advance some simple things for you to understand the basics.
Parentheses
You will see a lot of parentheses in Fennel, they serve to delimit where an expression starts and ends.
For example, to declare a variable in the local scope, in Lua, you use the keyword local
, whereas in Fennel you call the function local
:
(local myVar "myValue") ; myVar = "myValue"
If the value of the variable is the result of a concatenation, we will not use the operator ..
, but the function ..
:
(local name "Fennel")
(local myVar (.. "Hello, " name)) ; myVar = "Hello, Fennel"
In short, every function you call will be enclosed in parentheses.
Neovim API
Everything you do with Lua, you do with Fennel, so the same call you make to a Neovim API in Lua, you'll make in Fennel.
This, on Lua:
-- Lua
print(vim.fn.stdpath"config")
this is it, in fennel:
(print (vim.fn.stdpath :config))
:symbol
You may have already noticed that, in some cases, I wrote some strings in Lua using :
in fennel (if you didn't, just look at the last example)
This is another way to write a string. However, to write in this format, the string cannot contain spaces.
(= :str "str") ; true
Tangerine Mappings
The plugin we use to integrate Fennel with Neovim has some mappings and commands that help us write code that will generate what we want. I'll list the ones I use the most below:
gL
It can be run both in normal mode and in visual mode.
This mapping shows the Lua code that your Fennel code will generate.
For example, if I press gL
after selecting the snippet:
(lambda add [x y]
(+ x y))
It opens a window containing the code in Lua:
local function add(x, y)
_G.assert((nil ~= y), "Missing argument y on globals.fnl:1")
_G.assert((nil ~= x), "Missing argument x on globals.fnl:1")
return (x + y)
end
return add
Very useful for quickly checking that the fennel code you are writing will generate the Lua code you expect.
Note: This mapping doesn't work very well in visual mode if you select a snippet that uses some macro unless the snippet has the macro import.
gO
This mapping opens the lua file compiled by the open fennel file, that is, if you have the plugins.fnl
file open and press gO
, it will open the plugins.lua
file that was generated by the compilation of plugins.fnl
.
Very useful for debugging.
:Fnl
You can run any code in Fennel with the :Fnl
command, just like :lua
.
:Fnl (print "hey")
Will print hey
, equivalent to :lua print("hey")
Now it's up to you
From here, you're ready to have some fun using Fennel (and it is).
Any questions, ask here in the comments!
Happy Vimming!
Top comments (0)