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 tovim.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 oflet g:mapleader = ','
;vim.g
is a table represeting global variablesvim.opt.encoding="utf-8"
is equivalent ofset encoding=utf-8
; (there's alsovim.o
for global options,vim.wo
for window options andvim.bo
for buffer options, but I haven't used them)vim.fn
is a table representing functions. You can refer to a functionthisIsMyFun
usingvim.fn.thisIsMyFun
orvim.fn["thisIsMyFun"]
and you can call it usingvim.fn.thisIsMyFun()
orvim.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
andset nowrap
you writevim.opt.wrap = true
andvim.opt.wrap = false
- home directory problems - I had issue using
~
as a reference to home directory for some backup files etc. so instead I setHOME
variable that I used by writingHOME = os.getenv("HOME")
- string concatenation uses
..
operator, so to refer to my backup dir I wrotevim.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 withnvim-lspinstall
andlspsaga.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 withnvim-compe
(autocompletion) I use them to replaceAle
andcoc.vim
,telescope.nvim
replaces any search plugin you use (ctrl-p
,fzf.vim
etc.)gitsigns
replacesvim-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
withvim.opt
(thanks to u/rainning0513) - both options will work, butvim.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)