Saying Goodbye to Vimscript

guide lua vim

After using Neovim exclusively for the past several years, I’ve decided it’s finally time to migrate all my configuration files from Vimscript to Lua. I no longer wish to remember the syntax peculiarities of an archaic language not used anywhere else. Similarly, I can’t think of a reason to ever return to vanilla Vim. In this post, I summarize what needs to be done to switch to lua for others thinking of taking the dive.

File Layout

To get started with Lua, replace your init.vim file with init.lua. Note that if you have both files present at once, Neovim will complain and your configuration will not load. The scripts contained in the ftplugin directory can also be converted to Lua in the same way. Finally, it is also possible to split the configuration into multiple files by creating a lua subdirectory and using require to import scripts contained therein.

require('common')
require('lsp')
require('plugins')
require('util')
require('autocmd')

With these changes, your directory structure may look similar to this:

├── ftplugin
│   ├── c.lua
│   ├── cpp.lua
│   ├── go.lua
│   ├── html.lua
│   ├── markdown.lua
│   └── yaml.lua
├── init.lua
├── lua
│   ├── autocmd.lua
│   ├── common.lua
│   ├── ginit.lua
│   ├── lsp.lua
│   ├── plugins.lua
│   └── util.lua
└── pack
    └── plugins

If you use Neovim Qt, you may be wondering about the presence of ginit.lua from the above file listing. Unfortunately, Neovim Qt does not support a Lua-only configuration path and insists on using ginit.vim for GUI-specific configuration. Furthermore, attempting to include Neovim Qt specific commands in init.lua will cause errors on load (tests for GUI presence via has do not appear to work). Fortunately, we can to use auto command to conditionally require the ginit.lua file only when running under a GUI.

vim.api.nvim_create_autocmd(
    'UIEnter', {
        callback = function()
            if vim.v.event.chan == 1 then
                require('ginit')
            end
        end,
        once = true,
    }
)

Color Scheme and Leader

The color scheme can be set by executing the same colorscheme command as in Vim. There are two different ways to do this, with the latter looking a bit cleaner than the former. This pattern of treating the command name as a callable method in cmd extends to other commands (including ones exposed by plugins) as well.

vim.cmd('colorscheme solarized8')

-- or with some syntax sugar...

vim.cmd.colorscheme('solarized8')

The leader key is configured with a global variable, just like in Vim.

vim.g.mapleader = ' '

Core Options

Consider comparing your configuration against the defaults listed in the documentation. Neovim ships with mostly sane defaults and you can reduce the size of your configuration file by skipping redundant settings. Note that boolean settings in Vim that start with no when false (such as nonumber) are represented by assignments instead.

vim.opt.autowrite = true
vim.opt.completeopt = 'menuone,noselect'
vim.opt.expandtab = true
vim.opt.fileformats = 'unix,dos,mac'
vim.opt.foldenable = false
vim.opt.guicursor = 'n:blinkon0'
vim.opt.ignorecase = true
vim.opt.linebreak = true
vim.opt.modeline = false
vim.opt.modelines = 0
vim.opt.number = true
vim.opt.shiftround = true
vim.opt.shiftwidth = 4
vim.opt.showmode = false
vim.opt.smartcase = true
vim.opt.swapfile = false
vim.opt.tabstop = 4
vim.opt.termguicolors = true
vim.opt.updatetime = 300
vim.opt.wrap = false
vim.opt.writebackup = false

Plugin Configuration

Plugins written for Vim are generally configured with global variables; these are exposed in the vim.g table. Vimscript supports namespaces via the # character, which can cause syntax issues for Lua. To workaround this problem, use the square bracket index operator on the table instead.

-- vim-airline
vim.g['airline#extensions#tabline#enabled'] = 1
vim.g['airline_symbols_ascii'] = 1

-- vim-dirvish
vim.g.dirvish_mode = ':sort ,^.*[\\/],'

-- vim-go
vim.g.go_diagnostics_enabled = 0
vim.g.go_imports_autosave = 0
vim.g.go_metalinter_enabled = {}
vim.g.go_null_module_warning = 0
vim.g.go_version_warning = 0

Key Mappings

Key mappings are also reasonably straightforward. The main difference from vanilla Vim is that commands must start with with a <cmd> tag instead of the : prefix. When defining key mappings for a combination of modes (such as normal and visual), the mode parameter has to be specified as a Lua table; a comma-delimited string will not work.

-- common keymaps
vim.keymap.set('i', '<c-c>', '<esc>')
vim.keymap.set('n', '<bs>', '<cmd>bd<cr>')
vim.keymap.set('n', '<c-c><c-c>', '<cmd>nohlsearch<cr>')
vim.keymap.set('n', '<leader><leader>', '<cmd>b#<cr>')
vim.keymap.set('n', '<leader>w', '<cmd>w<cr>')
vim.keymap.set('n', '<leader>x', '<cmd>x<cr>')
vim.keymap.set('n', 'j', 'gj')
vim.keymap.set('n', 'k', 'gk')

-- clipboard keymaps
vim.keymap.set({'n', 'v'}, '<leader>P', '"+P')
vim.keymap.set({'n', 'v'}, '<leader>Y', '"+y$')
vim.keymap.set({'n', 'v'}, '<leader>d', '"+d')
vim.keymap.set({'n', 'v'}, '<leader>d', '"+dd')
vim.keymap.set({'n', 'v'}, '<leader>p', '"+p')
vim.keymap.set({'n', 'v'}, '<leader>y', '"+y')
vim.keymap.set({'n', 'v'}, '<leader>yy', '"+yy')

-- split keymaps
vim.keymap.set('n', '<a-=>', '<c-w><c-=>')
vim.keymap.set('n', '<a-h>', '<c-w><')
vim.keymap.set('n', '<a-j>', '<c-w>+')
vim.keymap.set('n', '<a-k>', '<c-w>-')
vim.keymap.set('n', '<a-l>', '<c-w>>')
vim.keymap.set('n', '<c-h>', '<c-w>h')
vim.keymap.set('n', '<c-j>', '<c-w>j')
vim.keymap.set('n', '<c-k>', '<c-w>k')
vim.keymap.set('n', '<c-l>', '<c-w>l')

-- fzf.vim keymaps
vim.keymap.set('n', '<leader>fg', vim.cmd.GFiles)
vim.keymap.set('n', '<leader>fh', vim.cmd.History)
vim.keymap.set('n', '<leader>fb', vim.cmd.Buffers)
vim.keymap.set('n', '<leader>fl', vim.cmd.Lines)

Auto Commands

Auto commands are defined as a list of conditions followed by a table specifying what you want the action to do. Unlike key mappings, it is possible to declare the condition list as either a table or a comma-delimited string.

vim.api.nvim_create_autocmd(
    'BufRead,BufNewFile', {
        pattern = '*.gohtml',
        command = 'set filetype=html'
    }
)

-- or using the table syntax...

vim.api.nvim_create_autocmd(
    {'BufRead', 'BufNewFile'}, {
        pattern = '*.gohtml',
        command = 'set filetype=html'
    }
)

Useful Resources