Neovim macros vs normal mode
In Neovim, which is better to use: macros or normal mode? If you answered macros, you might be surprised by the power of normal mode and the execute command when compared to managing macros with plugins. With a simple Lua table, you can get the most out of normal mode and emulate a macro library.
The macro commands that are too complicated to be written on-the-go can be saved
in a new file, such as config/macros.lua
. One example of a command I’ve saved
is one that converts traditional Typescript functions into const functions.
There’s a video of this in action below.
Why normal mode
When using normal mode you get to practice writing normal commands live while looking at the code, rather than try to decipher a dense string of characters or remember what you tagged a saved macro as. I would say it’s better to get good at writing normal commands with vim motions and learn to enjoy writing them at-will, rather than spend time organizing macros. There are macro plugins that help you organize them, but I found that editing and remembering which macro was which was cumbersome, and no plugin seemed to match up with the plugin I imagined in my head. If you do need a macro manager plugin, I would recommend telescope-macros though.
Motions
You should note that regardless of how you are recording your commands, is that
it’s better to use fFtT and hjkl than wb, since
targeting the beginning of a word is way more recognizable in command form than
contextual word navigation. For example, it’s difficult to parse the command
hhhhhjjkkkjdw
.
I find that using custom vim motions based on Treesitter queries is a good way
to manipulate the AST of your code with normal commands; and it starts to feel
quite magical. I recommend mini.ai
+ nvim-various-textobjs
.
When using global commands, treesitter-based motions make complicated syntax
changes easy to perform with the f
motion.
Global commands
One cool trick is to use a g
(global) command to make a change to every
location of a word in a file. For example, to use a normal command to upgrade
the syntax of an older Javascript file, such as from when React was still using
object-oriented syntax, we would execute the following command:
:g :g/function /execute "norm cwconst^[f(i = f{i=> "
Executing :g /function /execute "norm cwconst^[f(i = ^[f{i=> ^["
will change
the function syntax to arrow function syntax for all instances of the word
function
. Note that the ^]
symbols are special - in most terminals, these
escape codes can be inserted via Ctrl-V and then Esc. Note
how the escape codes are purple in the video.
Another trick is the :g/found/v/notfound/{cmd}
syntax — in this example,
a command is ran on lines that contain function
, but do not contain test
.
This a potentially way to delete all your code, but keep your tests, so exercise
caution when writing this kind of normal command!
I also found that recording macros using a plugin often requires multiple tries, which I found distracting. Recording macros also requires tagging the macros with a description, which added an extra step before re-using a newly created macro.
Another example
In my Wezterm config, I wanted to see if I could replace all instead of
a.[Wezterm method]
with act.[Wezterm method]
. A Wezterm method is a command
available in the Wezterm action API, like wezterm.action.SendKey
or
wezterm.action.SplitHorizontal
. I could have used find and replace with a
regex string, like %s/act.[A-Z+]/a.[A-Z+]/g
, but to help solve the “macros vs
normal mode” debate, I tried doing this with a normal mode command.
-
First, let’s search for a regex pattern in the command line via
/
. Upon pressing enter, this will set the “find” register (@\
) to the search string.\ a.[A-Z*] -
Then, we can jump to each regex match via
n
::171,$norm nlact^[" -
Result:
a.AdjustPanelSize
anda.ActivePaneDirection
were updated to have a prefix ofact.
instead ofa.
, as well as any other methods that started withact.
. The official wezterm documentation referencesact
, and I needed an example for this article. -
Alternative method: You can use
s/act.[A-Z*]/a.[A-Z*]/g
to replace the text as well, which requires writing regex in the command line.
Execute commands
It felt powerful to use the search register in a normal command with a line
range, but there are still improvements to be made to the technique. In normal
commands, it isn’t the easiest to deal with the escape characters, which make
the command more difficult to read. execute
commands are much easier to read,
since they don’t require escaping keyboard input characters. They’re also more
powerful, since they can chain different vim commands together. For example:
execute "159,165norm 0fc2dw"
This command means, if the cursor was directly below the *
character below,
running :execute "2,4norm fc2dw"
would move the cursor to the c
in config
on line 2, and delete the word config
on lines 2, 3, and 4.
In other words, it would change this:
* config.a = 1,cursor should be here
config.b = 2,config.c = 3
into this:
{a = 1,b = 2,c = 3}
Finding a plugin
In Neovim, macros are a string representation of keyboard input, which makes them hard to parse. To add a description to a macro, a plugin is needed. This turned out to be more tricky than I had expected, as this is a case when the most popular plugins on Github didn’t work well for me. The first result, NeoComposer, wasn’t what I was looking for because I had to choose a macro from a list with no descriptions. Macros are displayed as virtual text, and stored in registers, which I found to be easy to overwrite with other commands. I did enjoy being able to “scroll” through the macros in insert mode via C-n and
C-p though. If you can remember your macros and use them often, this plugin may be for you.
I considered forking the project, but I had discovered several other macro
plugins. The next most popular plugin was
Macrobatics, which looked to be
exactly what I wanted; but I didn’t enjoy the lack of a Lua API, as it’s
recommended in a modern Neovim setup to stick to Lua as Vimscript will become
deprecated over time. Additionally, I had trouble with the plugin since it
doesn’t work well with the lazy.nvim
package manager. After that, I searched
Github until I found one that actually worked well —
telescope-macros. Adding a telescope finder is a clever
and simple solution and I applaud the author, since an editable list is the only
thing needed to manage a macro library, and I find using Telescope for UI
components to be a good pattern to follow rather than reinventing the wheel.
If I have a macro assigned to the @l
register, using telescope-macros
, I can
apply it repeatedly via leader->ml: leader->m opens the
telescope macros list, l
finds the macro under the @l
register, and <cr>
closes the window and applies the macro. This is what’s used in the first video
in the article.
I felt some modifications were essential to make — for instance, adding a key
binding to [[☆ Telescope keymap]] that would close the modal and navigate to the
previewed file. Why isn’t that part of telescope by default? I searched for
examples and found none other than [[ThePrimeagen]]‘s telescope modifications,
and I took his get_preview
function, which would open the buffer from the
preview window in the editor.
However, his function didn’t work and I punted on the idea after trying and failing to get AI to help. Later on, I actually found Google Duet to be the most proficient as writing Neovim functions.