DEV Community

Cover image for Configuring Neovim with Fennel
Miguel Crespo
Miguel Crespo

Posted on • Originally published at miguelcrespo.co

Configuring Neovim with Fennel

Configuring Neovim with Fennel

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:

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

-- Non-global variable
local name = 'User 1'
Enter fullscreen mode Exit fullscreen mode

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

; Declare a local variable
(local name "User 1") ; Compiles to local name = "User 1"
(var language "fennel") ; Compiles to local language = "fennel"
Enter fullscreen mode Exit fullscreen mode

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:

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

Fennel:

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

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

(var language "fennel")

(set language "lua") ; It works...
Enter fullscreen mode Exit fullscreen mode

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

local my_variable = 1

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

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

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

Tables have a more familiar syntax

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

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

; Array
(local numbers [1 2 3]) ; => local numbers = {1, 2, 3}
Enter fullscreen mode Exit fullscreen mode

Destructuring

Fennel supports destructuring like many other modern languages

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

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:

(set! tabstop 2)
; Or to set keymaps
(map! [:n] "-" (fn [] (print "Opened")) "Open parent directory")
Enter fullscreen mode Exit fullscreen mode

This whole functionality requires an entire blog post, but you can read the extensive fennel macros documentation

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:

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 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)

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

Writing the configuration

This is a part of my real init.fnl

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

And here is a part of my options.fnl file

(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)

; ...
Enter fullscreen mode Exit fullscreen mode

An example of setting mappings:

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

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

brew install fnlfmt
Enter fullscreen mode Exit fullscreen mode

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

This is my configuration:

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

Install 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:

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

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 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.

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 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

Top comments (0)