neovim

Neovim is a highly configurable text editor inspired by Vim. If you aren't familiar with Vim, it's a keyboard-centered, modal editor that allows for intuitive editing. If you want to learn how to use it, install it, open it, and then type :Tutor to go through a tutorial. After a long time of using LazyVim, a Neovim "distribution" (premade configuration), I decided to write my own Neovim configuration. It was challenging but worth it in the end. Since many people struggle with this, I wrote this article as an opinionated overview on how to configure Neovim. As a disclaimer, I do reference Unix commands often that only work on Unix systems like MacOS, Linux, BSD, etc. If you use Windows, you will have to find the equivalent commands for these. Personally, I use Windows but I have WSL installed, which allows you to run an integrated Linux environment in Windows. You also need some basic utilities like git installed.

Introduction

Neovim is usually configured in Lua but my config (which can be found here) uses Fennel, which is a Lisp dialect that compiles to Lua. If you want to use Lua instead, you can reference the lua/ directory of my config, which has the compiled Lua code (albeit ugly). First, let's make the folder that will house our Neovim config.

mkdir -p $HOME/.config/nvim
cd $HOME/.config/nvim

Then, let's make an init.lua file.

touch init.lua

Since we are using Fennel, this file will only bootstrap some necessary things. Here is my init.lua:

local function bootstrap(url, ref)
    local name = url:gsub(".*/", "")
    local path = vim.fn.stdpath("data") .. "/lazy/" .. name
    vim.opt.rtp:prepend(path)

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

        vim.fn.system({ "git", "clone", "--filter=blob:none", url, path })
        if ref then
            vim.fn.system({ "git", "-C", path, "checkout", ref })
        end

        vim.cmd("redraw")
        print(name .. ": finished installing")
    end
end

bootstrap("https://github.com/folke/lazy.nvim", "v10.15")
bootstrap("https://github.com/udayvir-singh/tangerine.nvim", "v2.8")
bootstrap("https://github.com/udayvir-singh/hibiscus.nvim", "v1.7")

require("tangerine").setup({
    compiler = {
        verbose = false,
        hooks = { "onsave", "oninit" },
    },
})

This code bootstraps the following:

I also add a hook in tangerine to compile the Fennel into Lua everytime I save a Fennel file or open Neovim. Let's also make a directory to store our Fennel config files.

mkdir fnl

init.fnl

Now, let's actually start configuring Neovim. If you are following this in Lua, you will want to still put this in init.lua but we will put this in init.fnl. Let's make the file first:

touch init.fnl

This is my init.fnl:

(local lazy (require :lazy))
(import-macros {: g!} :hibiscus.vim)

(g! :mapleader " ")
(g! :maplocalleader "\\")

(lazy.setup :plugins {:performance {:reset_packpath false}})

(require :config)

First, we setup the leader keys before setting up any plugins. Then, we setup lazy.nvim with our plugins (it will look in the fnl/plugins/ which we will worry about later). Also, if you are using Fennel, remember to set reset_packpath to false as this can mess with tangerine. Lastly, we require config (really fnl/config/), which we will also setup later.

Macros

Macros are, in my opinion, the best part of using a Lisp dialect. Hibiscus already provides some macros but you are free to define some of your own. Here are some I have in my fnl/macros.fnl:

(fn hl! [group val]
    `(vim.api.nvim_set_hl 0 ,group ,val))

(fn plug! [plugin ?opts]
    (doto (or ?opts {}) (tset 1 plugin)))

(fn require! [plugin item]
    `(. (require ,plugin) ,item))

(fn vim! [cmd]
    (sym (.. :vim. cmd)))

{: hl! : plug! : require! : vim!}

These are simple macros that save some keystrokes. The plug! macro may be confusing but all it does is it avoids Fennel's ugly syntax for mixed tables and allows you to write Lazy plugin specs as (plug! package opts) instead of {1 package ...}.

Config

This directory will house our Neovim configuration (excluding plugins).

mkdir fnl/config
cd fnl/config

Now, let's make an init.fnl file

touch init.fnl

All this file does is exports all the other files in this directory so that we only have to import config in our init.fnl. It is very similar to index.js in JavaScript or __init__.py in Python. My fnl/config/init.fnl file looks like this:

(require :config.options)
(require :config.keymaps)
(require :config.highlights)
(require :config.autocmds)

Whenever you make a new file in fnl/config/, just remember to import it in init.fnl. My config is quite long so I won't put it here, but you can see it in my repository.

Plugins

Let's make a plugins folder first.

mkdir fnl/plugins
cd fnl/plugins

All our plugin specs will go here. Unlike fnl/config/, we do not need an init.fnl file because Lazy will automatically load all the files in this directory. I use a ton of plugins so I won't go over them all. However, I've seen that most people struggle most with configuring LSP (Language Server Protocol), so I'll go over that briefly.

My configuration includes automatic setup of LSP servers. I do this with a combination of the following:

If this seems too daunting for you, there's also lsp-zero which provides an easy and painless way to configure LSP. Their guide for configuring LSP without lsp-zero is also good if you don't want to use it.

Conclusion

Configuring Neovim can be challenging but the best way to go about it is to break it down and configure one thing at a time. If you are still unsure about anything, just reference my config or anyone else's. I personally used LazyVim as a reference when I was writing mine. Good luck!