DEV Community

Cover image for Setting Up Neovim with Fennel
Douglas Massolari
Douglas Massolari

Posted on

Setting Up Neovim with Fennel

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

In fennel, we can simply do:

; Fennel
(local {: var : var2} (require :module))
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Whereas, in fennel, we can use match:

; Fennel
(lambda get-desc [key]
  (match key
    :k1 "Key 1"
    :k2 "Key 2"
    :k3 "Key 3"))
Enter fullscreen mode Exit fullscreen mode

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:

  1. tangerine

    GitHub logo udayvir-singh / tangerine.nvim

    🍊 Sweet Fennel integration for Neovim

    🍊 Tangerine 🍊

    Neovim version GNU Neovim in Emacs version

    About β€’ Installation β€’ Setup β€’ Commands β€’ API β€’ Development

    About

    Tangerine provides a painless way to add fennel to your config.

    Features

    • πŸ”₯ BLAZING fast, compile times in milliseconds
    • 🌊 200% 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

    1. Create file plugin/0-tangerine.lua to bootstrap tangerine:

    NOTE: if you are using lazy plugin manager, you should create /init.lua instead.

    -- ~/.config/nvim/plugin/0-tangerine.lua
    -- pick your plugin manager, default [standalone]
    local pack = "tangerine" or "packer" or "paq"
    
    local function bootstrap(url, ref)
        local name =
    …
    Enter fullscreen mode Exit fullscreen mode
  2. hibiscus

    GitHub logo udayvir-singh / hibiscus.nvim

    🌺 Flavored Fennel Macros for Neovim

    Hibiscus.nvim

    🌺 Highly opinionated macros to elegantly write your neovim config.

    Companion library for tangerine but it can also be used standalone.

    Neovim version

    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:
    -- ~/.config/nvim/plugin/0-tangerine.lua
    -- pick your plugin manager, default [standalone]
    local pack = "tangerine" or "packer" or "paq"
    
    local function bootstrap(url, ref)
        local name = url:gsub(".*/", "")
        local path = vim.fn.stdpath [[data]] .. "/site/pack/".. pack .. "/start/" .. name
    
        if vim.fn.isdirectory(path) == 0 then
            print(name .. ": installing in data dir...")
    
            vim.fn.system {"git", "clone", url,
    …
    Enter fullscreen mode Exit fullscreen mode

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" }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

this is it, in fennel:

(print (vim.fn.stdpath :config))
Enter fullscreen mode Exit fullscreen mode

: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
Enter fullscreen mode Exit fullscreen mode

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))
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)