Using Neovim and Configuring it with Lua
Neovim is a fork of Vim created in 2015. It strove to split the text editing engine from the UI, to add features then missing from Vim such as embedded terminals and asynchronous operations, and to allow configuration in Lua rather than the baroque and Vim-specific VimL aka VimScript.
As of the 0.5 release, it has delivered on these goals. An ecosystem of third party UIs has flourished, the new features were implemented and saw usage, and it can now be configured entirely in Lua. Later releases such as 0.8 have added even more nicities.
By Jason Long, neovim, CC BY 3.0, https://commons.wikimedia.org/w/index.php?curid=38131852
Why use a fork of Vim rather than the plethora of other editors out there? I’ve given my reasons in the previous article, but to summarise: it depends on just a terminal as opposed to a whole web browser engine like Visual Studio code; it has ergonomic keybindings owing to its modal editor origins; its UI/engine split lets third parties develop UIs independently; it comes from POSIX vi, allowing core sysadmin skills to be reused in one’s primary editor; and modifying it can be done with immediate hackability rather than only formally loaded extensions with boilerplate.
I’ll document the process for configuring Neovim with Lua, and try to assume the reader hasn’t extensive familiarity with the vi family of editors. That said, this is a guide for configuring Neovim, not how to use the vi family of editors on a day-to-day basis.
A (Far too Short) Crash Course in Neovim
In case you’re unfamiliar with the vi family of editors, of which Neovim is a member, there are numerous places to start.
The fundamentals are:
- Neovim runs in a terminal by default. That means its program,
nvim
, must be in yourPATH
if you got it directly from the GitHub release. Once its there, invoke it by enteringnvim
. Pass in file paths as arguments to open them directly. - Neovim (and Vim and vi) run in “modes”. Each mode has a different set of keyboard shortcuts, which they call “keybindings”.
- You start in “normal” mode. Keys perform operations rather than being inserted
into the document. For example,
dd
deletes the current line rather than typingdd
into the current position in the document. - To insert characters, you switch into a different mode, one called “insert
mode”. You can do this from normal mode by typing
i
. - Finished inserting text? Press the escape key to flip back from insert mode to normal mode.
- More complex commands can be entered using “command mode”. To move to it from command mode, type a colon.
- Like insert mode, one can switch back to normal mode using the escape key.
- From command mode, you can quit the editor by invoking the
quit
command. Once in command mode, just type the command you wish to invoke and press enter once done. - All commands can be abbreviated; for example, few will type out
write
to write the file out orquit
to quit Neovim. Instead, abbreviations can be used when they aren’t ambiguous, e.g.q
forquit
andw
forwrite
. Quitting is especially useful to know.
This is the fundamental reason why vi-based editors are so ergonomic and efficient: complex operations can be typed in a dedicated mode, meaning they don’t need complex combinations of modifier keys.
In Praise of Lua
Is an editor truly to praise when it chooses Lua as its primary extension language? A language infamous for defaulting to global variables and using 1-based indices for arrays, no less. Where are its classes? Why doesn’t it have built-in language support for namespaces?
On the contrary, I’ve come to believe it’s a language that punches well above its weight.
Compactness Delivered with an Approachable Syntax
It’s a small language. In an era when almost every mainstream programming language suffers from language committees adding constant features for the sake of it, a small language that just stays the same is a blessing from a previous era. It’s compact enough to keep in your head. Even the additions post 5.1 – which LuaJIT sticks to – have been conservative.
Its syntax is simple. It has an ALGOL-style syntax using English keywords. Despite the clear benefits of Lisp, its esoteric syntax is off-putting to experienced developers and newcomers alike. And unlike Python, it hasn’t added so many unnecessary features that its word-heavy syntax has become a Python-like word soup. Modern C-style languages tend to accumulate edge cases as they graft increasing features atop a core syntax not designed for it.
Simplicity in Scoping and Encapsulation
Apart from making variables global by default, its scoping system is
refreshingly simple. Unlike JavaScript, there’s no scope
hoisting
or temporal dead
zones to worry
about. It also differs from Kotlin, Java, and Swift in that there isn’t a
laundry list of scope-altering features such as classes, structs, namespaces,
and statics. There’s Scheme-like lexical scope (whose captured variables in a
closure can be modified by the closure’s
fenv
), and there are tables
including one representing global variables. When one Lua snippet require
s a
Lua file, the file can return
a table of module entries at the end,
importing one module into the other without resorting to global variables.
Its metatables are flexible and powerful enough to implement most object systems, reflection mechanisms, namespaces, and proxies. It’s a single unified data structure used ubiquitously across Lua. As opposed to JavaScript, the language hasn’t fell victim to committees constantly throwing in new features that its built-in data structures can already provide.
DSLs
Part of Lua’s heritage rests in data configuration languages, which it
demonstrates with its concise data definitions. The syntactical sugar on top is
simple: tables and strings passed to functions can drop the parentheses; colons
can be used to pass implicit self
s to functions stored in tables (i.e.
methods); and tables can drop square brackets and quotation marks from simple
string keys.

By Alexandre Nakonechnyj (Graphic design) and Lua team (PostScript code) - svg from PostScript Source (see below) created from Lumu, Public Domain, https://commons.wikimedia.org/w/index.php?curid=37714918
A few select pieces of well-targeted syntactical sugar make for a more elegant language than those trying to hard-code in new syntax for every desired feature.
Concurrency
The asymmetric coroutine feature of Lua lets it to do concurrency without
littering code with async
, await
, yield
, or suspend
keywords at every
layer of the callstack. As editor extensions get increasingly complicated and
dependent on outside processes such as LSP servers, this is useful for
editor extensions.
This capability is already being used extensively in the Neovim Lua package ecosystem.
Performance
LuaJIT is an impressively quick language implementation. While modern JavaScript engines may surpass it, the same cannot be said for the de facto implementations of Python and Ruby.
It is a target for other languages such as Teal and Fennel, likely due to Lua’s reach and LuaJIT’s renown performance.
Configuring Neovim with Lua
Lua’s website describes the language in depth. As Neovim uses LuaJIT, you should check the 5.1 reference.
To configure Neovim, the first step is to ensure you have at least version 0.8. If you’re downloading from somewhere that traditionally has older versions of software, such as Debian’s repositories, double-check. If in doubt, get the latest stable GitHub release.
Once you have installed that version, you can drop our new init.lua
configuration file in a few possible locations. Neovim respects the XDG
specifications,
so let’s go for ~/.config/nvim/init.lua
.
Global Options
Let’s take a look at an example of some basic options:
local opt = vim.opt
opt.backup = false
opt.cursorline = true
opt.expandtab = true
opt.hidden = true
opt.hlsearch = true
opt.incsearch = true
opt.number = true
opt.path:append '**'
opt.scrolloff = 5
opt.shiftwidth = 4
opt.smartcase = true
opt.smartindent = true
opt.softtabstop = 4
opt.swapfile = false
opt.tabstop = 4
opt.textwidth = 80
opt.wildmenu = true
opt.wrap = true
opt.writebackup = false
vim.opt
is a Lua table that stores global options, similar to the JSON-backed
properties in Visual Studio Code. In traditional Vi or Vim you’d set these like
set nobackup
or set tabstop=4
.
As a metatable it can do useful things such
as take array assignments for certain multi-value options or support appending
with method calls like append
.
For those not used to the vi editor family, some of the option names may seem a
old fashioned, e.g. “shiftwidth”. These can be researched individually; when
you’re comfortable with what they do, the structure of the above snippet can be
followed to configure them. For example, expandtab
determines whether spaces
or hard tabs are used for indenting, and cursorline
enables highlighting on
the whole current line.
Some features are not exposed as options. For example, changing the colour
scheme is actually a command instead: colorscheme blue
. You can tab-complete
in command mode, so tab after colorscheme
to see what other color schemes
come out of the box aside from blue
. You can add third party colour schemes
alongside the preincluded ones.
Neovim documents each of these options, to be browsed at your leisure.
Keyboard Shortcuts
Older editors tend to call keyboard shortcuts keybindings.
Neovim inherited Vim’s notion of a “leader key”. It means a single keybinding to
preface our own. It’s similar to Emac’s idea of non-standard keybindings being
prefixed with C-x
. Vim’s is more ergonomic; because Vim is modal, it means
modifier-less keys such as Space can be used as the leader key, opening up
combinations such as Space-v
for, say, splitting a window vertically. It’s
easy to hit with a thumb, and no pinky strain for modifier keys.
Let’s define the leader key as space.
vim.g.mapleader = ' '
vim.g
represents Vim’s global variables. These are understood by the editor
itself rather than Lua – Lua has its own notion of global variables. They are
also readable in command mode and VimL via a g:
prefix, e.g. :echo g:mapleader
. mapleader
is a global Vim variable that defines the current
leader key.
Let’s now set some convenient keybindings to allow the likes of splitting the
window with just space followed by v
when in normal mode:
local set = vim.keymap.set
set('n', '<leader>h', '<c-w>h')
set('n', '<leader>j', '<c-w>j')
set('n', '<leader>k', '<c-w>k')
set('n', '<leader>l', '<c-w>l')
set('n', '<leader>v', '<c-w>v')
set('n', '<leader>s', '<c-w>s')
set('n', '<leader>c', '<c-w>c')
set('n', '<leader>o', '<c-w>o')
Voilà! Now all of our window-splitting and navigation can be done with two keystrokes each, one of which is usually under our thumb.
Packages
To go beyond basic editing, we’ll want some packages. Neovim can use most of Vim’s packages, bar a handful of very new ones that take advantage of Vim 9 exclusive features. But why would we use Neovim just to use packages written in VimL? Thankfully, packages written primarily in Lua cover most of what we need:-
packer.nvim
: improved package management built atop Neovim’s built-in package manager. We’ll use this to install the remaining packages.nvim-tree.lua
: a file explorer tree on the left-side of the editor.gitsigns.nvim
: markings on the left side for git additions, modifications, and removals.nvim-dap
andnvim-dap-ui
: Debugger Adapter Protocol to provide integration with debuggers.telescope.nvim
: filter and select from a set of providers e.g. buffers, the filesystem, and ripgrep results. In plain English: a fuzzy finder for files and a nice UI for finding items among files.nvim-lspconfig
: integrate with Language Server Protocol servers for your tech stacks of choice, table stakes for modern editors and IDEs unless they have enough clout to reimplement parsers for everything in-house such as Jetbrains.nvim-treesitter
: semantic understanding of code structure rather than crappy old 100% regexp-based systems. Provides modern, accurate syntax highlighting among other things.nvim-scrollview
: a scrollbar, but even in a terminal.barbar.nvim
: modern tabs, even in a terminal.LuaSnip
: a code snippets system.nvim-cmp
andcmp-nvim-lsp
: autocompletion, integrated with LSP.lualine.nvim
: a useful status line at the bottom of the editor.nvim-minimap
: a Sublime Text- and Code-style minimap, but it tucks itself into a corner rather than taking over a whole side of the screen.kommentary
: comment current lines, but with awareness provided by the likes oftreesitter
.neogit
: perform git operations easily directly from the editor. A shameless clone of Emac’smagit
mode, but why not?magit
is superb, so it’s the right plugin to clone.
First, install packer.nvim
to get enhanced package management.
git clone \
--depth 1 \
https://github.com/wbthomason/packer.nvim \
"$HOME"/.local/share/nvim/site/pack/packer/start/packer.nvim
Declare the packages you’re interested in. The declaration look like this:
local packages = {
{
'wbthomason/packer.nvim',
},
{
'arcticicestudio/nord-vim',
config = function()
vim.cmd.colorscheme 'nord'
end,
},
}
Each package declaration is a Lua table, i.e. wrapped with braces. The first string in the package is its name which states from where on GitHub to pull it; use a full repository URI for non-GitHub git repositories.
A config
function in a package configures the package after it has been
loaded.
Finally, configure packer and point it to your packages declaration:
vim.cmd.packadd 'packer.nvim'
local packer = require 'packer'
packer.init {
git = { clone_timeout = 60 * 5 }
}
packer.use(packages) -- Define `packages`
Plugin developers sometimes commit breaking changes. To avoid them, you can pin commits and tags in the declaration:
local packages = {
{
'wbthomason/packer.nvim',
commit = '3f950aeed3bd908e33fd59643e8f3be05b719df6',
},
{
'arcticicestudio/nord-vim',
commit = '8d8b9bf86bbc715a055b54cb53f0643fd664caa4',
config = function()
vim.cmd.colorscheme 'nord'
end,
},
}
Save the configuration using the write
command, whose abbreviation is w
.
Reload the configuration in the current editor with the command source $MYVIMRC
. Finally, run the PackerSync
command and the packages will be
pulled down and installed.
Unlike Visual Studio Code, many extensions need explicit initialisation code.
Sometimes it’s a basic setup
call like this:
{
'lewis6991/gitsigns.nvim',
version = 'v0.2',
config = function()
require 'gitsigns'.setup()
end,
},
Other times, more complex configuration will be passed to the setup
function which will be documented in the extension’s README:
{
'nvim-treesitter/nvim-treesitter',
commit = '34de06d4e8fc46090325dcaa3e8d74e295dd8ef1',
config = function()
require 'nvim-treesitter.configs'.setup {
context_commentstring = {
enable = true,
},
}
end,
},
Once you know how to configure options and install packages in Neovim, combined
with knowing how to use vi family editors day-to-day, everything else is a
search engine query away. Of particular note are autogroups
which allow
configuration specific to languages, e.g. using spaces to indent except for
Go.
Putting it All Together
My complete Neovim configuration can be found on my GitLab
account.
It’s part of a Dockerfile, meaning you can jump straight into it with Docker
using docker run -it --rm -v "$PWD":/home/user/workspace registry.gitlab.com/louis.jackman/dockerfiles/base-dev:0.0.23
.
This “screenshot” is pasted as HTML directly from my terminal emulator, so the UI looks more janky than it actually is. Of note are the features we’d take for granted in modern editors: a side view of the file tree, tabs, fuzzy finding of files, autocomplete of file paths, syntax colouring, and more.
▎post.md ▎.gitlab-ci.yml ▎config.toml ▎Makefile ▎README.md ▎rawhtml.html ▎ 2╭─────────────────────────── Results ────────────────────────────╮ 2│ │ 2│ │ ★│ │ │ │ │ │ │ content/posts/2021/10/09/a-brief-history-of-text-editors-from-│data at ★ lay│ content/posts/2018/11/25/your-ci-pipeline-has-the-skeleton-key│ ◌ res│ content/posts/2019/01/11/lambda-syntax-in-mainstream-programmi│ scrip│ themes/hermit/exampleSite/content/posts/post-with-featured-ima│ stati│ themes/hermit/assets/scss/_normalize.scss │ theme│ themes/hermit/assets/scss/_animate.scss │ .gita│ themes/hermit/static/site.webmanifest │ .giti│ themes/hermit/images/screenshot.png │ .gitl│ themes/hermit/images/hermit.png │ .gitm│ themes/hermit/assets/js/main.js │ confi│ themes/hermit/images/tn.png │ COPYI│ static/louis-jackman-cv.pdf │ Makef│ content/posts/2018/07/29/time-attacks-why-being-efficient-can-│ READM│ static/louis-jackman-public-pgp-key.asc │ ~ │> Makefile │ ~ ╰────────────────────────────────────────────────────────────────╯names ar ~ ╭────────────────────────── Find Files ──────────────────────────╮ ~ │> make 2018/ 15 / 99 │in navig ~ ╰──────── 2019/ ────────────────────────────────────────╯ ~ 2020/ se like posts. <vimTree[-] 14 2021/ 22:0 :e content/posts/2020/
Once Neovim is set up, it possesses all the features of a modern editor. It’s performant, has a decent extension language, supports LSP, is immediately hackable, has ergonomic keybindings, runs over SSH and in a TTY attached to a container, and roots its keybindings in the ubiquitous POSIX vi standard. Add to that a growing ecosystem of fancy third party GUIs. Unlike modern Vim, its ecosystem focuses on using one language well rather than using external integrations written in anything under the sun from Python to Node.js and even Deno.
If you’d like to keep up with Neovim, follow its news and development.