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:
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
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:
= "bar" -- ruh roh raggy
end
foo
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
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.