~ views

Neovim macros vs normal mode


Telescope macro extension

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.

  1. 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*]
  2. Then, we can jump to each regex match via n:

    :171,$norm nlact^["
  3. Result: a.AdjustPanelSize and a.ActivePaneDirection were updated to have a prefix of act. instead of a., as well as any other methods that started with act.. The official wezterm documentation references act, and I needed an example for this article.

  4. 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.

Back to top