Configuring Neovim with Fennel
Tags: lisp, neovim, programming
Published: 2023-11-10 14:25:35

![Configuring Neovim with Fennel](//images.ctfassets.net/fduzn3bxxb8f/8aFLumsdEGz5SMZrWOON1/385918748304079e05747f008da924cb/_.jpg)

I like changing the configuration of my Neovim quite a lot, and recently I have reached a plateau where my configuration does everything I want, most people would be happy once they reach this point but I was not because I like moving stuff so I started looking for things that looked cool and I found many people talking about Fennel…

### What is Fennel?

Fennel is a Lisp language that compiles to lua, it's easy to learn and since learning lisp has been on my todo list for quite some time after I tried Emacs, I saw this as a good opportunity to do it.

### Why Fennel?

If you like lisp languages or want to learn one, fennel is a good way to start since it's quite a simple language that compiles to another simple language (lua) while being fully compatible with it and having zero overhead once compiled.

It has many syntax improvements over lua, but the most remarkable features of Fennel are:

#### No global variables by default

One of the things I didn't like much about lua is how easy is to accidentally declare global variables by accident, when you look for many lua tutorials you see people using global variables without they knowing because it's simply the default behavior:

``` lua
-- This is a global variable
name = 'User 1'

-- Non-global variable
local name = 'User 1'
```

In most cases, you want to create a `local` variable, and fennel defaults to it every time you create a variable:

``` lisp
; Declare a local variable
(local name "User 1") ; Compiles to local name = "User 1"
(var language "fennel") ; Compiles to local language = "fennel"
```

#### Immutable variables by default

This behavior is becoming quite popular in other modern languages like `Rust`, which means that once you assign a value to a variable the value **cannot** change unless you explicitly allow it.

The advantage of this behavior is that it makes it easier to know how a variable is being used in your code just by looking at how it was defined

**Examples in lua:**

``` lua
-- There's no way to know if the value can change in the code by just looking at it
local language = "lua"

-- You might or might not see this somewhere in your code
language = "fennel"
```

**Fennel:**

``` lisp
; Once you see this, you know this is a constant and cannot change
(local language "fennel")

(set language "lua")
; Last line will generate the error:
; Compile error: expected var language
;
; (set language "lua")
; * Try declaring language using var instead of let/local.
; * Try introducing a new local instead of changing the value of language.
```

Of course, you can still declare mutable variables with `var`:

``` lisp
(var language "fennel")

(set language "lua") ; It works...
```

#### It doesn't declare variables with typos

In lua is very easy to redeclare new variables by mistake when you have a typo and when you do this you will probably only catch the error at runtime

``` lua
local my_variable = 1

myvariable = 2 -- There's a typo in this line and this will just create a new global variable called myvariable
```

In fennel this cannot happen since the compiler will warn you at compile time:

``` lisp
(var my_variable 1)

(set myvariable 2)
; Compile error: expected local myvariable
;
; (set myvariable 2)
; * Try looking for a typo.
; * Try looking for a local which is used out of its scope.
```

#### Tables have a more familiar syntax

Another small change is that fennel uses a more traditional way to represent arrays and tables

``` lisp
; Table or dictionary
(local person {:name "User 1"}) ; => local person = {name = "User 1"}

; Array
(local numbers [1 2 3]) ; => local numbers = {1, 2, 3}
```

#### Destructuring

Fennel supports destructuring like many other modern languages

``` lisp
(local person {:name "User 1" :age 12})

; Destructuring table
(local {:name person_name} person)

(print person_name) ; => User 1

(local my-numbers [1 2 3 4 5])

; Destructuring a list
; & c means the rest goes to this variable
(local [a b & c] my-numbers)

(print a) ; => 1
(print b) ; => 2
(print c) ; => [3 4 5]
```

#### Macros

Fennel is a lisp language which means it supports macros, if you ever read anything about lisp you see people talking about them as the holy grail

And indeed, macros are a powerful way to metaprogram and make languages easier to read. Macros are kinda normal functions but they get executed at compile time and output code.

With macros, you can extend the fennel language and create domain-specific function, for example, for Neovim, you could create a macro to set a vim option, like:

``` lisp
(set! tabstop 2)
; Or to set keymaps
(map! [:n] "-" (fn [] (print "Opened")) "Open parent directory")
```

This whole functionality requires an entire blog post, but you can read the extensive [fennel macros documentation](https://fennel-lang.org/macros)

## Picking a transpiler

Now that I convinced you to use Fennel, let's see how we can start configuring Neovim with it.

First, you need to find a way to compile the fennel code into lua code, there are many options for this:

- [tangerine.nvim](https://github.com/udayvir-singh/tangerine.nvim)
- [hotpot.nvim](https://github.com/rktjmp/hotpot.nvim)
- [aniseed](https://github.com/Olical/aniseed)

I picked *tangerine* since it's quite simple to setup and has a nice way to preview the compiled lua code which is very useful while you're learning fennel.

It also comes with an *optional* package called hibiscus with useful macros like `set!` and `map!`

## Configuring Tangerine and hibiscus with lazy

To start using tangerine.nvim we first need to install it, and since we need to load tangerine before anything else to output our lua files.

This is how I did it:

- Move my existing `init.lua` to another file inside the `/lua` folder, e.g: `/lua/main.lua`

- Replace the code in `init.lua` with: 

  The code below was partly taken from the [official documentation](https://github.com/udayvir-singh/tangerine.nvim#installation) and will:

  - Download automatically tangerine and hibiscus (optional)

  - Configure tangerine to generate the lua code when we save a fennel file or start neovim

  - It will store the compiled lua files in `.local/share/nvim/tangerine` for MacOS (this changes depending on the operating system)

    ``` lua
    local function bootstrap(url, ref)
      local name = url:gsub(".*/", "")
      local path = vim.fn.stdpath [[data]] .. "/lazy/" .. name

      if vim.fn.isdirectory(path) == 0 then
        print(name .. ": installing in data dir...")

        vim.fn.system { "git", "clone", url, path }
        if ref then
          vim.fn.system { "git", "-C", path, "checkout", ref }
        end

        vim.cmd [[redraw]]
        print(name .. ": finished installing")
      end
      vim.opt.runtimepath:prepend(path)
    end

    bootstrap("https://github.com/udayvir-singh/tangerine.nvim")

    -- Optional and only needed if you also want the macros
    bootstrap("https://github.com/udayvir-singh/hibiscus.nvim")

    require 'tangerine'.setup {
      target = vim.fn.stdpath [[data]] .. "/tangerine",

      -- compile files in &rtp
      rtpdirs = {
        "ftplugin",
      },

      compiler = {
        -- disable popup showing compiled files
        verbose = false,

        -- compile every time changes are made to fennel files or on entering vim
        hooks = { "onsave", "oninit" }
      },
    }
    ```

- Create a `init.fnl` file that will be loaded automatically by tangerine when neovim starts

- Optional: Move all your files in the `/lua` folder to a new `/fnl` folder By moving all your lua files to the `/fnl` folder you can *require* lua code from fennel and vice versa

**We also need to put `tangerine.nvim` in the list of Lazy plugins so it gets updated as a normal plugin**

In the end, you will end up with a folder structure like

```
.
├── fnl
│  ├── theme.fnl
│  ├── main.lua
├── init.fnl // The real init
├── init.lua // Load tangerine
```

## Writing the configuration

This is a part of my real init.fnl

``` lisp
(require :options) ; Normal neovim options
(require :theme) ; Load the theme config
(require :plugins) ; /fnl/plugins.lua This is still a lua file and will load all the plugins with Lazy
```

And here is a part of my options.fnl file

``` lisp
(import-macros {: set! : set+} :hibiscus.vim)

; ...

;; Indentation
(set! expandtab)
(set! shiftwidth 2)
(set! tabstop 2)

;; Line numbers
(set! number)
(set! relativenumber)
(set! numberwidth 3)
(set! numberwidth 3)

;; Whitespace
(set! list)
(set! listchars {:trail "·" :tab "→ " :nbsp "·"})

;; Insert-mode completion
(set+ :shortmess :c)

; ...
```

An example of setting mappings:

``` lisp
(import-macros {: map!} :hibiscus.vim)

(local oil (require :oil))

(oil.setup {:default_file_explorer true
            :keymaps {:q :actions.close}
            :view_options {:show_hidden true}})

(map! [:n] "-" oil.open "Open parent directory")
```

## Extra: Improving fennel DX in Neovim

### Install a code formatter

We will use `fnlfmt` to format our fnl code

Install it with your favorite package manager

#### Using brew

``` bash
brew install fnlfmt
```

### Install a LSP for fennel

If you're using language servers for completion and diagnostic then you will be glad to know there's one for fennel called [`fennel_language_server`](https://github.com/neovim/nvim-lspconfig/blob/master/doc/server_configurations.md#fennel_language_server) 

This is my configuration:

``` lua
require 'lspconfig.configs'.fennel_language_server = {
  default_config = {
    cmd = { 'fennel-language-server' },
    filetypes = { 'fennel' },
    single_file_support = true,
    -- source code resides in directory `fnl/`
    root_dir = lspconfig.util.root_pattern("fnl"),
    settings = {
      fennel = {
        workspace = {
          -- If you are using hotpot.nvim or aniseed,
          -- make the server aware of neovim runtime files.
          library = vim.api.nvim_list_runtime_paths(),
          checkThirdParty = false, -- THIS IS THE IMPORTANT LINE TO ADD
        },
        diagnostics = {
          globals = { 'vim' },
        },
      },
    },
  },
}

lspconfig.fennel_language_server.setup {
  on_attach = function(client, bufnr)
    -- Support formatting with fnlfmt
    vim.keymap.set('n', '<leader>cf', function()
      vim.lsp.buf.format({ async = true })
    end, { noremap = true, silent = true, buffer = bufnr, desc = "Format code" })
  end,
}
```

### Install [conjure](https://github.com/Olical/conjure) plugin

Conjure is an amazing plugin that lets you execute pieces of code inside Neovim, this is very useful when working with fennel but it also supports `lua`, `python` and many other languages.

To install it using `Lazy.nvim`:

``` lua
{
  "Olical/conjure",
  -- [Optional] cmp-conjure for cmp
  dependencies = {
    {
      "PaterJason/cmp-conjure",
    },
  },
  config = function()
    require("conjure.main").main()
    require("conjure.mapping")["on-filetype"]()
  end,
  init = function()
    -- Set configuration options here
    vim.g["conjure#debug"] = true
  end,
},
```

After you install it, `conjure` sets some default mapping to the file types it supports:

- `<localleader>er` Evaluate the root form under the cursor
- `<localleade>ee` Evaluate the current form under the cursor
- `<localleader>eb` Evaluate the current buffer contents

Read the [Conjure documentation](https://github.com/Olical/conjure/blob/master/doc/conjure.txt) for more details

## Conclusion

Configuring Neovim using Fennel has been a really enjoyable experience, partly made possible by using Conjure.

I liked writing fennel in Neovim so much that I even wrote my first neovim plugin using it [scratch-buffer.nvim](https://github.com/miguelcrespo/scratch-buffer.nvim).

But not everything has been great so far, here are some of the *downsides* I noticed so far:

- it's a bit annoying to have to transpile all the lua code you find on the internet to fennel, but this page has helped me a lot with it: https://fennel-lang.org/see
- Also the vim API signature is not available using the fennel-language-server
  In lua you can install [neodev](https://github.com/folke/neodev.nvim) and you will get some nice lsp suggestions with the vim API, this is sadly not possible in fennel right now
- When there's an error it's a bit annoying to debug since the lines that the error points are in the compiled lua code so you need to open it to understand the compiled lua code

## References

- Fennel official tutorial
  https://fennel-lang.org/tutorial

- Fennel reference
  With useful information about destructuring and more details in general: https://fennel-lang.org/reference

- Convert lua code to fennel and viceversa
  https://fennel-lang.org/see