Dan Coady

if you just want to know how to convert your vimscript to lua then go to the section “getting started”

so i’m a neovim user, horrible i know. there’s a lot of reasons why i use neovim but i don’t really care to try and convince anyone to use it or vim. fact of the matter is that neovim is… difficult. you end up spending hours and hours fiddling with your config just to have something vaguely useful compared to say visual studio code or any jetbrains ide. still, i use neovim regardless, and one of the reasons why i’d like to highlight here is that neovim lets me configure it to work exactly to my workflow, making it incredibly ergonomic.

this is important, because at work i’m not just compiling and running code + a little bit of git here and there. i need to also clear the eeprom of devices, flash devices over dfu and fwup, dispatch rpc requests, scan for open ports on devices, and more things i’m probably forgetting. this is the kinda stuff that i do day to day, and they all require commands that frankly i don’t really remember off the top of my head. i do have a lot of these things aliased in my .zshenv which is fine and all, but some things are projects specific (eg. different projects may have different offsets and lengths for the bootloader and main app so i can’t just use one command to wipe the eeprom of any device) and so it helps to have my editor take care of this for me. and look, you can do this in a lot of ides, sure, but it’s kind of painful to figure out their special way of letting you do something as simple as running a python script in a specific directory. neovim is plain and simple, which makes it dead easy to perform these kinds of tasks.

the main language used to configure both vim and neovim is vimscript, and it… well it serves its purpose. one of the cool things about it is that from command mode you’re just running vimscript. this means that if you do something like this from command mode:

:set noexpandtab
:%retab!

then you can put exactly that into your config, maybe something like this:

function RetabCurrentFile()
    set noexpandtab
    %retab!
end

command Retab call RetabCurrentFile()

so that you can run :Retab and your current buffer will convert all your spaces to tabs. this generally makes writing your config pretty easy, since what you’re running in neovim to do things will be the exact same thing you throw into your config, but it also starts getting a bit nasty the more you try to do with it. this is why using lua is just a bit nicer (if a bit more painful to get started with initially) than using vimscript. lua is just an actual programming language that was intended to have actual meaningful programming workloads done with it. this means you end up being able to write much more complex logic without getting too lost in the weeds of the quirks of vimscript as a language, plus lua is just much more widely used so it has way more learning resources and helpful libraries online if you end up needing them. so let’s have a look at how to convert your config from vimscript to lua.

getting started

now i’ll fully admit that i followed along with heiker’s medium post and imaginary robot’s blog post to give myself most of a base to work off, and i fully encourage you to do the same since they’re both excellent articles with some good tips on how to make this all work, plus i don’t really want to rehash what they’ve already said.

there’s also my neovim config which you can use as a reference for some stuff. i’ve tried to comment it half decently so that it’s not too hard to understand what’s being done.

now, assuming you’ve read the above blog posts i want to go through some things that they didn’t cover which i found to be helpful/necessary for my own config.

setting your shell

this is huge for me since i use zsh both at work and at home, but by default neovim uses… i think sh? anyway not important, what is important is that it doesn’t use your default shell and that’s annoying for me since i have a bunch of aliases in my .zshenv which i would like to have access to via neovim. this is the solution i came up with:

-- set shell to zsh, with bash as a fallback
local shell = vim.fn.system({"which", "zsh"})
local nullsToCull = 1
vim.opt.shell = string.sub(shell, 1, string.len(shell) - nullsToCull)
if vim.opt.shell == "" or vim.opt.shell == nil then
    shell = vim.fn.system({"which", "bash"})
    vim.opt.shell = string.sub(shell, 1, string.len(shell) - nullsToCull)
end

please excuse that my name casing isn’t consistent throughout. i write my config half at work, half at home, and in both places i use different casing styles.

anyway, this sets my shell in neovim to zsh, with bash as a fallback just in case whatever machine i’m on doesn’t have zsh for whatever reason. the important thing to note here is that i have to do:

string.sub(shell, 1, string.len(shell) -nullsToCull)

this is because of a weird quirk with both neovim and vim where they’ll place null characters all over the place. in this case, the result of vim.fn.system() contains a trailing null character at the end, and you need to get rid of this otherwise neovim will crack it at you because it can’t execute /bin/zsh^@.

auto commands

i don’t really go crazy with these, just like to have a few so that i can configure buffers on a per-type basis. the main ones i have are to configure my terminal and quickfix buffers so that they don’t have line numbers. in vimscript they looked like this:

" open terminal/quickfix without line numbers
autocmd TermOpen * setlocal nonumber norelativenumber
autocmd BufNew,BufRead Quickfix setlocal nonumber norelativenumber

and in lua they look like this:

-- open terminal/quickfix without line numbers
local function remove_line_numbers()
    vim.opt_local.number = false
    vim.opt_local.relativenumber = false
end

vim.api.nvim_create_autocmd({ "TermOpen" },
    {
        pattern = "*",
        callback = remove_line_numbers,
    })
vim.api.nvim_create_autocmd({ "BufNew", "BufRead" },
    {
        pattern = "Quickfix",
        callback = remove_line_numbers,
    })

it’s more lines, sure, but i prefer it honestly. i think this way it’s a lot clearer what’s happening. of course, you can check :h nvim_create_autocmd() for more information on what’s available since this really just scratches the surface of the api for creating auto commands.

user commands

this is something i use all the time since it’s insanely helpful being able to create your own commands to do specific things either in your editor or in your project itself. i already kinda touched on them before, but let me give a better example below:

-- command to run a workspace command alias
vim.api.nvim_create_user_command("WorkspaceCmd",
    function(opt)
        -- stuff to run the selected workspace command
    end,
    {
        nargs = 1,
        complete = function(lead, _, _)
            local res = {}

            local lead_lower = string.lower(lead)
            for k, _ in pairs(Workspace.context.opts.cmds) do
                local cmd_alias_lower = string.lower(k)
                if Helpers.string_starts_with(cmd_alias_lower, lead_lower) then
                    res[#res + 1] = k
                end
            end

            return res
        end,
    }
)

now unfortunately i have no comparison with vimscript here since this is something new to my config. let’s run through what’s going on here anyway though.

the first argument is the name of the command (so in neovim you’d call this with :WorkspaceCmd), and the second argument is the callback for when we run the command. the third argument is the most interesting here though, since it’s the command-attributes. of course, more info available from :h nvim_create_user_command(), but the two things i have set are nargs which enforces the amount of arguments required, and complete which is the callback made for tab autocompletion. in this case i’ve gone ahead and done a bit of logic to populate the autocomplete results based on if any of the listed commands start with what you’ve already typed for the argument.

extra tidbits and tips

with that out of the way, here’s a random assortment of tips and tricks to make your time in lua and neovim a bit better.

lua-isms

scopes

lua by default declares things in the global scope which is… a choice that was made. honestly this is one of my least favourite things about lua because it means doing something like this:

function my_function()
    foo = "bar" -- ruh roh raggy
end

will declare foo in the global scope, despite the declaration happening within the scope of my_function. as a rule of thumb, i try to declare everything as local, including functions, unless i know i want it to be global. for example:

local function my_function()
    local foo = "bar" -- :)
end

including lua modules

you’ll often see including multiple files called including modules when you look into lua stuff online. there’s some rules behind which directories are searched when you call require(), but let me just save you the trouble and tell you to structure you neovim config like this:

- init.lua
- lua/
    - other_file.lua
    - another_file.lua

so that in your init.lua you can safely do require("other_file"). believe me, i learned this one the hard way.

global variables

there’s a bit of a convention in lua to name globals in pascal case, so i guess bear that in mind if it’s something that matters to you. one thing that i do find useful though is the idea of namespaces, since it lets me explicitly say where a function or variable has come from if it’s not from the current file. the accepted pattern for this is to use global tables:

Helpers = {}

Helpers.foo = function()
    print("hello, world!")
end

Helpers.foo() -- prints "hello, world!"

oh, and learn about tables. everything is a damn table in lua.

plugins

i’m a big fan of plugins, mostly cause i’m lazy. here’s a list of some plugins i like and how i use them:

mason.nvim

very nice lil package manager for neovim, mostly for hooking up things like language servers to neovim. or at least, that’s what i use it for. this way i barely even have to think about managing my language servers, i just run :Mason and i get a nice lil ui for managing everything.

you can find it here.

vim-dispatch

basically necessary for any vim or neovim user as far as i’m concerned. it’s so damn simple, and i only really use :Dispatch from it, but it’s hugely useful.

for a bit of context, normally when you run shell commands or run :make your entire editor is taken over and you just have to wait for the command to finish doing it’s thing. with vim-dispatch you can instead run :Dispatch <command> or :Make and all of a sudden your focus isn’t taken away from your active buffer, and the output of your command is in a quickfix window. genius!

seriously, i cannot stress enough just how useful this plugin is.

you can find it here.

vim-fugitive

another tim pope plugin (that man is a wizard, seriously) which i think is just necessary for anyone using vim or neovim. this time it’s just some nice git command abstractions. some of them are plain and simple like :Git status which will… well just run git status. but some of them are really cool like Git blame which will open a second buffer next to your active buffer and show you the blame for that file. but it’s not only that! it also lets you press enter on any of the commits listed in the blame and it’ll show you the diff for that commit in another buffer. so cool! so useful! and this is just a small slice of what is possible with vim-fugitive, so get on it!

you can find it here.

telescope.nvim

one of my coworkers told me about this when he came in one day to show me his new neovim config that he had written so he could move away from visual studio code, and i was kinda blown away.

in short, telescope.nvim lets you quickly (and i mean quickly) search through so many things. the ones i use are searching through my open buffers, the file names in my cwd, and the file contents in my cwd. this plugin has changed the way i work so much that i’d also say it’s a fairly necessary addition to anyone’s neovim setup. seriously, all i have to do now is type \fg from normal mode and i can search through the contents of all the files in my cwd and it is blazing fast thanks to ripgrep (which is also just a great tool in general).

you can find it here.

some closing thoughts

alright it’s getting late and i just wanted to quickly get these thoughts out before i went to bed. perhaps in the future i’ll write a more in depth post on why i use neovim and why maybe you should too. i do truly think that it’s a great editor once you have things set up, it’s just the getting set up part that majorly sucks. hopefully from reading this you’ve gotten some good insight on how you might approach converting your config over to lua, or maybe how you might improve your config further.