Neovim plugin that allows you to easily write your .vimrc in lua or any lua based language
Vimpeccable is a plugin for Neovim that allows you to easily replace your vimscript-based
.vimrc/
init.vimwith a lua-based one instead. Vimpeccable adds to the existing Neovim lua API by adding new lua commands to easily map keys directly to lua.
NOTE: We recommend using the latest development preview release of Neovim. While the plugin itself is compatible with the current Neovim stable release, the example vimrcs shown in this documentation are not.
Given the following .vimrc:
set ignorecase set smartcase set incsearchset history=5000
set tabstop=4 set shiftwidth=4
let mapleader = "<space>"
nnoremap hw :echo 'hello world'
" Toggle line numbers nnoremap n :setlocal number!
" Keep the cursor in place while joining lines nnoremap J mzJ`z
nnoremap ev :vsplit ~/.config/nvim/init.vim
colorscheme gruvbox
When using Vimpeccable, you could instead write it in lua or any lua-based language as well. For example, you could write it in lua:
vim.o.ignorecase = true vim.o.smartcase = true vim.o.incsearch = truevim.o.history = 5000
vim.o.tabstop = 4 vim.o.shiftwidth = vim.o.tabstop vim.g.mapleader = " "
vim.cmd('colorscheme gruvbox')
-- Note that we are using 'vimp' (not 'vim') below to add the maps -- vimp is shorthand for vimpeccable
vimp.nnoremap('hw', function() print('hello') print('world') end)
-- Toggle line numbers -- Note here that we are directly mapping a lua function -- to the n keys vimp.nnoremap('n', function() vim.wo.number = not vim.wo.number end)
-- Keep the cursor in place while joining lines vimp.nnoremap('J', 'mzJ`z')
vimp.nnoremap('ev', [[:vsplit ~/.config/nvim/init.vim]]) -- Or alternatively: -- vimp.nnoremap('ev', function() -- vim.cmd('vsplit ~/.config/nvim/init.vim') -- end)
vim.cmd('colorscheme gruvbox')
Or you could write it in MoonScript:
vim.o.ignorecase = true vim.o.smartcase = true vim.o.incsearch = truevim.o.history = 5000
vim.o.tabstop = 4 vim.o.shiftwidth = vim.o.tabstop
vim.g.mapleader = " "
vim.cmd('colorscheme gruvbox')
-- Note that we are using 'vimp' (not 'vim') below to add the maps -- vimp is shorthand for vimpeccable
-- Toggle line numbers -- Note here that we are directly mapping a moonscript function -- to the n keys vimp.nnoremap 'n', -> vim.wo.number = not vim.wo.number
-- Keep the cursor in place while joining lines vimp.nnoremap 'J', 'mzJ`z'
vimp.nnoremap 'hw', -> -- Note that we can easily create multi-line functions here print('hello') print('world')
-- Edit the primary vimrc vimp.nnoremap 'ev', -> vim.cmd('vsplit ~/.config/nvim/init.vim') -- This would work too: -- vimp.nnoremap 'ev', [[:vsplit ~/.config/nvim/init.vim]] -- Or this: -- vimp.nnoremap 'ev', ':vsplit ~/.config/nvim/init.vim'
You can also use any other lua-based language such as fennel, Teal, etc. in similar fashion.
To use the example lua vimrc displayed above, you can start by changing your neovim
init.vimfile to the following:
call plug#begin() Plug 'svermeulen/vimpeccable' Plug 'svermeulen/vimpeccable-lua-vimrc-example' Plug 'morhetz/gruvbox' call plug#end()
For the purposes of this example we will use vim-plug but you are of course free to use whichever plugin manager you prefer.
Then you can open Neovim and execute
:PlugInstall, and then you should be able to execute all the maps from the example (eg.
hwto print 'hello world')
What we've done here is that we've packaged up our vimrc into a plugin named
vimpeccable-lua-vimrc-example. To see how that works, open up the
~/.config/nvim/plugged/vimpeccable-lua-vimrc-exampledirectory. You should see two files:
/lua/vimrc.luaand
/plugin/vimrc.vim. If you open up
vimrc.vimyou'll see that all it does is the load
vimrc.lualike this:
lua require('vimrc')
This file is necessary because Neovim does not have support for a lua based entry point yet, however this is coming soon. In the meantime, we need to bootstrap our lua based vimrc with this
vimrc.vimfile instead.
Note that the reason this works is because vim will automatically source all
.vimfiles found inside the
plugindirectories of each plugin that we've added via
vim-plugabove. And when executing
lua require('vimrc'), neovim will look for a file named
vimrc.luain all the
luadirectories in each plugin as well.
To view the
vimrc.luafile, press
ev. As you can see, this is the same as the quickstart lua config example posted above.
You can also implement your vimrc using any language that compiles to lua, such as MoonScript. You can do this by changing your neovim
init.vimfile to the following:
call plug#begin() Plug 'svermeulen/nvim-moonmaker' Plug 'svermeulen/vimpeccable' Plug 'svermeulen/vimpeccable-moonscript-vimrc-example' Plug 'morhetz/gruvbox' call plug#end()
Before opening neovim you will also need to make sure that you have MoonScript installed and
mooncis available on the command line. Then you can open up neovim, execute
:PlugInstall, and then you should be able to execute all the same maps from the example (eg.
hwto print 'hello world')
Note that in this case we added an extra plugin above named
nvim-moonmaker. This plugin does the work of lazily compiling our moonscript files to lua, which is necessary because neovim does not support moonscript out of the box. See the nvim-moonmaker page for more details.
To view the
vimrc.moonfile, press
ev, which you will see is the same as the quickstart moonscript config example posted above.
Vimpeccable mirrors the standard vim API and so has all the variations of
nnoremap,
nmap,
xnnoremap, etc. that you probably are already familiar with.
The standard format to add a mapping in vimscript is:
[MODE](nore?)map [OPTIONS] [LHS] [RHS]
Where: -
MODEcan be one of
x,
v,
s,
o,
i,
c,
t-
noreis optional and determines whether the command is 'recursive' or not. Recursive here would, for example, allow executing other user-defined maps triggered from a user defined map. -
OPTIONScan be one or more options such as , , etc.
Examples:
nnoremap hw :echo 'hello world'" Note that we need to use recursive here we are mapping to a non-default RHS nmap c Commentary xmap c Commentary
nnoremap t :call g:DoCustomThing()
Vimpeccable mirrors the above except that it is a lua method call and therefore requires that each parameter is seperated by commas:
vimp.[MODE](nore?)map [OPTIONS?], [LHS], [RHS]
Examples:
-- Note that in lua we can represent strings either with quotes or with double square brackets vimp.nnoremap('hw', [[:echo 'hello world']])vimp.nmap('c', 'Commentary') vimp.xmap('c', 'Commentary')
-- Also note that we need to pass the options as a list instead of as seperate parameters -- Also note that unlike vimscript, the options are not surrounded with angle brackets vimp.nnoremap({'expr', 'silent'}, '1', [[:call g:DoCustomThing()]])
-- Or, alternatively, implement DoCustomThing in lua instead: vimp.nnoremap({'expr', 'silent'}, '1', function() -- Add logic here end)
Vimpeccable also comes with extra methods named
bindand
rbindwhich allow passing the mode as a parameter instead of needing to use different methods:
vimp.bind('n', 'hw', [[:echo 'hello world']])-- plugs need to use rbind vimp.rbind('nx', 'c', 'Commentary')
Note that the only difference here is that
rbindis 'recursive' so allows the use of custom user maps as part of the RHS value.
These methods can be especially useful to allow binding multiple modes at the same time. Note also that you can pass multiple values for LHS like this as well:
vimp.rbind('nx', {'c', 'gc'}, 'Commentary')
Which in vimscript would require 4 different statements for each variation.
For many vimmers, It is common to regularly be making tweaks to your vimrc.
In order to make edits at runtime without requiring a full restart of vim, often what people do is open up their vimrc and then simply execute
:so %to re-source it. The lua equivalent of this would be
:luafile %, however, if we were to attempt this when using vimpeccable we would get errors complaining about duplicate maps. This is a feature, not a bug, and is helpful to avoid accidentally clobbering existing maps. But how would we reload our vimpeccable config at runtime then?
To show how this is done, let's use the following config in our neovim
init.vim:
call plug#begin() Plug 'svermeulen/vimpeccable' Plug 'svermeulen/vimpeccable-lua-vimrc-advanced-example' Plug 'morhetz/gruvbox' call plug#end()
Here, we're using the
vimpeccable-lua-vimrc-advanced-exampleplugin, which contains a map to reload our vimrc. After replacing your
init.vimwith the above, if you then open nvim, run
:PlugInstalland then press
evyou should see the following vimrc file:
util = require('vimrc.util')-- ... -- -- ...
-- r = reload vimrc plugin vimp.nnoremap('r', function() -- Remove all previously added vimpeccable maps vimp.unmap_all() -- Unload the lua namespace so that the next time require('vimrc') or require('vimrc.X') is called -- it will reload the file -- By default, require() will only load the lua file the first time it is called and thereafter -- pull it from a cache util.unload_lua_namespace('vimrc') -- Make sure all open buffers are saved vim.cmd('silent wa') -- Execute our vimrc lua file again to add back our maps require('vimrc')
print("Reloaded vimrc!") end)
To test our new
rreload mapping, try changing the
hwmapping to print something different, then press
rand then
hwto see the new text.
You might also notice that we have a new file inside our
~/.config/nvim/plugged/vimpeccable-lua-vimrc-advanced-exampledirectory at
/lua/vimrc/util.luathat we are referencing above with the line
util = require('vimrc.util'). As your vimrc grows in complexity, you may want to split it up into multiple files, which we can do quite easily in lua by using the
requiremethod.
Note that
util.luawill also be reloaded every time we execute
r, as well as any other lua file underneath the
vimrcfolder. See the comments above inside the
rmapping for an explanation of what each line does.
Note that an equivalent example for moonscript can also be found by using the following
init.viminstead:
call plug#begin() Plug 'svermeulen/nvim-moonmaker' Plug 'svermeulen/vimpeccable' Plug 'svermeulen/vimpeccable-moonscript-vimrc-advanced-example' Plug 'morhetz/gruvbox' call plug#end()
Vimpeccable can also optionally make custom maps repeatable with the
.key. For example, given the following maps:
vimp.bind('[e', ':move--') vimp.bind(']e', ':move+')
You might want to be able to hit
]e..to move the current line three lines down. By default this would not work. You can fix this by making it repeatable by just passing in the
repeatableoption like this:
vimp.bind({'repeatable'}, '[e', ':move--') vimp.bind({'repeatable'}, ']e', ':move+')
Note that this feature requires that vim-repeat is installed.
By default, vimpeccable will reject any maps that are already taken. To see what that looks like, try adding the following map to the same
vimrc.lua(assuming you're using the
vimpeccable-lua-vimrc-advanced-exampleconfig from above):
vimp.bind('hw', function() print('hi!') end)
If you then execute
r, you should see the following error or similar:
This is because we have already defined a map for
hwabove this line. Note that this error will not stop the rest of our config from loading. By default, Vimpeccable will simply log the error and continue, to ensure as much as your config can be loaded as possible.
In some cases you might want to override the previous mapping anyway, which you can do by passing in the
overrideoption like this:
vimp.bind({'override'}, 'hw', function() print('hi!') end)
If you then reload with
r, and press
hw, you should now see the new output.
Vimpeccable will also automatically detect maps that 'shadow' each other as well. For example, if we change our map to this instead:
vimp.bind('h', function() print('hi!') end)
And then attempt to reload again with
r, we will get a similar error:
This is different from vim's default behaviour. If we added these maps using vimscript like this instead:
nnoremap hw :echo 'hello world' nnoremap h :echo 'hi!'
Then every time we execute
h, there would be a delay before we see the 'hi!' text printed, because vim needs to wait to see if you're in the process of executing
hwinstead.
If you find yourself using a lot of leader maps, you might notice that it is not possible to cancel a leader operation without sometimes causing unintended side effects. For example, given the following map:
vimp.bind('ddb', function() print("Executed map!") end)
If you then type
ddand then hit any key other than
b, you will find that the current line is deleted. This is because vim will do its best to try and match what you've already typed to another existing map, and in this case it chooses
ddto delete the current line. A similar problem occurs if we type
dand then hit any other key other than d, except in this case vim decides to just move the cursor one character to the right.
You can avoid these problems by adding the following to the bottom of your
vimrc.lua:
vimp.add_chord_cancellations('n', '')
Now if we reload with
r, then hit
dd, then the line will no longer be deleted. And similarly, if we hit
d, nothing will happen anymore.
Under the hood, what vimpeccable is actually doing here is adding maps for
ddand
dand explicitly mapping them to do nothing.
Vimpeccable also supports buffer local maps. Given this vimscript map:
nnoremap t1 :echo 'buffer local map!'
As you might expect, the equivalent in lua would be:
vimp.nnoremap({'buffer'}, 't1', [[:echo 'buffer local map!']]) vimp.nnoremap({'buffer'}, 't2', [[:echo 'another buffer local map!']])
Or alternatively:
vimp.add_buffer_maps(function() vimp.nnoremap('t1', function() print('lua map!') end) vimp.nnoremap('t2', function() print('lua map two!') end) end)
You can also specify the exact buffer if you know the buffer id like this:
vimp.add_buffer_maps(bufferId, function() vimp.nnoremap('t1', function() print('lua map!') end) vimp.nnoremap('t2', function() print('lua map two!') end) end)
For a full example, install
vimpeccable-lua-vimrc-advanced-exampleas explained in the previous section, and then look at the files
/ftplugin/lua.vimand
/lua/vimrc/ft/lua.lua.
Or, alternatively, in MoonScript instead of lua:
vimp.add_buffer_maps -> vimp.nnoremap 't1', -> print('lua map!') vimp.nnoremap 't2', -> print('lua map two!')
In some cases it might be better to define a custom action as a vim command rather than mapping it to a key. This way we don't use up any open key maps and our custom commands are discoverable on the command line by pressing tab (which can be easier than having to remember whatever leader map we chose). For example, you might want to define the following user commands in vimscript:
function! g:OpenFileOnGithub() echom "Open the URL on github for current file on current line" endfunctionfunction! g:RenameFile(newName) echom "Todo - rename current file to " . a:newName endfunction
command! -nargs=0 SvOpenFileOnGithub call g:OpenFileOnGithub() command! -nargs=* SvRename call g:RenameFile()
Note here that I'm using
Svas a prefix on my commands so that I can just type
Svon the command line to see the full list.
To do this in lua with vimpeccable instead, you could do this:
vimp.map_command('SvOpenFileOnGithub', function() print("Todo - Open the URL on github for current file on current line") end)vimp.map_command('SvRename', function(newName) print("Todo - rename current file to " .. newName) end)
Or, if using MoonScript:
vimp.map_command 'SvOpenFileOnGithub', -> print("Todo - Open the URL on github for current file on current line")vimp.map_command 'SvRename', (newName) -> print("Todo - rename current file to " .. newName)
Note that vimpeccable will automatically fill in the
nargsvalue for the command based on the given function signature.