From init.vim to init.lua - a crash course

Earlier this year maintainers of Neovim have released version 0.5 which, among other features, allows developers to configure their editor using Lua instead of VimL. In this article I'll share a few basic rules on how to transition from one configuration to another. This is not a complete guide, but it covers almost 100% of what I needed in order to completely move away from init.vim to init.lua. In the bottom of the article you're also find a link to my config files so that you can look at them and copy if needed.

Lua in 1 minute

First, it's good to spend 10-15 minutes learning Lua in order to easily write the new config. I used the Learn X in Y minutes page, but I guess anything works. If you want to spend 1 minute instead, here you go:

-- this is a comment
num = 22 -- this global variable represents a number
local num2 = 33 -- local variable
str1 = 'this is a string'
str2 = "and so is this"
str3 = [[ and this is a string too ]]
str4 = "string " .. "concatenation"
val = true and not false -- booleans and logical operators

if str1 == 'something' then
  print("YES")
elseif str2 ~= 'is not equal' then
  print('Maybe')
else
  print('no')
end

function printText(text)
  print(text)
  return true
end

tab1 = { 'this', 'is, 'a', 'table' } -- aka array
-- tables are both arrays and dictionaries
tab2 = { also = 'this is a table' }
tab2["new_key"] = "new value"

print(tab2["also"])

require('plugins') -- will find and execute plugins.lua file

There's of course way more than that, but for me this + copying some stuff from plugins documentation was enough to write my config files.

Config basics

Ok, now onto the config. In Vim we use a number of functions that are dedicated to the editor configuration (the whole language is dedicated to it, really). In Lua we're using a general programming language and we'll use an API to interact with Neovim configuration:

  • vim.cmd("set notimeout") - this is a safety net, whatever string you pass as parameter to vim.cmd will be interpreted as VimL. For multiple lines, wrap string in double brackets:
vim.cmd([[
set notimeout
set encoding=utf-8
]])
  • vim.g.mapleader = "," is equivalent of let g:mapleader = ','; vim.g is a table represeting global variables
  • vim.opt.encoding="utf-8" is equivalent of set encoding=utf-8; (there's also vim.o for global options, vim.wo for window options and vim.bo for buffer options, but I haven't used them)
  • vim.fn is a table representing functions. You can refer to a function thisIsMyFun using vim.fn.thisIsMyFun or vim.fn["thisIsMyFun"] and you can call it using vim.fn.thisIsMyFun() or vim.fn["thisIsMyFun"]()
  • vim.api is a collection of API functions. I used only one: vim.api.nvim_set_keymap that maps certain key combinations to some functions (more about it below)

Moving settings to Lua

Moving most of the settings is pretty straightforward. You just replace set x = y with vim.opt.x = "y". There are however some catches:

  • pairs of boolean settings are merged into one setting, e.g. instead of set wrap and set nowrap you write vim.opt.wrap = true and vim.opt.wrap = false
  • home directory problems - I had issue using ~ as a reference to home directory for some backup files etc. so instead I set HOME variable that I used by writing HOME = os.getenv("HOME")
  • string concatenation uses .. operator, so to refer to my backup dir I wrote vim.opt.backupdir = HOME .. "/.vim/backup"
  • double backslash - if you want to pass a special character \t to Vim, you need to write it as "\\t" in Lua

Mapping keys

The Lua API has a function to map keys to some functions. The function signature is vim.api.nvim_set_keymap(mode, keys, mapping, options), where mode refers to a letter representing editor mode ( n for normal, i for insert etc.) just like in original vim functions like nmap or imap, keys is a string representing a combination of keys, mapping is a string representing what the keys map to, and options are a table where you can pass some additional settings. Example:

vim.api.nvim_set_keymap(
  "n",
  "<leader>a",
  ":Git blame<cr>",
  { noremap = true }
 )

is equivalent of nnoremap <leader>a :Git blame<cr>.

I didn't check what are all the options that can be passed in the 4th argument, 2 that I used are noremap = true and silent = true.

I also wrote myself a few simple functions to avoid typing vim.api... every time:

function map(mode, shortcut, command)
  vim.api.nvim_set_keymap(mode, shortcut, command, { noremap = true, silent = true })
end

function nmap(shortcut, command)
  map('n', shortcut, command)
end

function imap(shortcut, command)
  map('i', shortcut, command)
end

With these functions my example above became just nmap("<leader>a", "<cmd>Git blame<cr>").

Package manager

It's very probable that you already use some package manager for Neovim, and when moving to Lua you don't need to change it, you can just wrap your whole plugin list in vim.cmd and continue using it as before.

I decided to try a new manager called Packer, which is written in Lua and requires Neovim 0.5. The installation was a bit troublesome, because I didn't know what packpath is and that Packer requires some very specific names of the directories to find packages. Anyway, I moved it to ~/.config/nvim/pack/packer/start/packer.nvim directory and it worked nicely (though I'm sure there's a better way to install it).

Besides the installation, Packer is both easy to use and has all the features I need (and a lot that I don't need). The basic config example looks like this:

return require('packer').startup(function()
  use 'wbthomason/packer.nvim'

  -- common
  use 'tpope/vim-fugitive' -- Git commands
  use { 'tpope/vim-rails', ft = "ruby" } -- only load when opening Ruby file
end)

If you need some more advanced functionality, I recommend checking the documentation.

Other plugins

There is a number of other plugins that are available for Neovim 0.5 and I found myself replacing some of plugins I had used before with the new alternatives. I won't cover them here (maybe in another post), but here are a few that you can check out:

  • nvim-lspconfig together with nvim-lspinstall and lspsaga.nvim use the new, built-in LSP in Neovim and provide some useful functions. Together they allow me to easily install and use language servers (e.g. to display function documentation or jump to a definition). Together with nvim-compe (autocompletion) I use them to replace Ale and coc.vim,
  • telescope.nvim replaces any search plugin you use ( ctrl-p, fzf.vim etc.)
  • gitsigns replaces vim-gitgutter

There are a few more like lualine.vim that can replace powerline or airline, but I'm yet to try it out.

Summary

The whole process of moving 350 lines of init.vim to init.lua took me around 2h, including time to organize the files (Lua allows you to use multiple config files, see my example below) and excluding time to play with new plugins. I spent around 1 hour moving 90-95% of the content, and another hour solving some issues like home directory or some broken config. In the end I found the whole process rather quick and definitely rewarding, though I'm sure a lot of things can be done better. If you plan to use new capabilities of Neovim 0.5 I definitely recommend moving your config to Lua.

My config in VimL and Lua: https://github.com/arnvald/viml-to-lua

Update (February 2022):

  • replaced vim.o with vim.opt (thanks to u/rainning0513) - both options will work, but vim.opt is recommended
  • replaced <cmd> with : when comparing Vim with Lua to keep it consistent, since both options behave the same (again, thanks to u/rainning0513)

Update (April 2023)

  • replaced compe library with nvim-cmp by the same author, and updated the config (a lot of settings have changed)
  • replaced vinegar with oil.nvim, a more modern replacement that besides navigation allows creating/deleting files and supports dev icons