The Most Important Vim Mapping (it just runs a macro)
‹‹ 2024-04-19 ››
This is a post about how I finally started using macros in neovim, after many times of trying them and giving up. If you don't use vim macros yet or would like to learn more, I definitely recommend watching That One Micro Talk on Macros (NeovimConf 2023) by Jesse Leite. In the video, Jesse introduces this wonderful mapping:
vim.keymap.set('n', 'Q', '@qj', {remap=true})
Ok, that was for neovim. In vimscript, it's
nmap Q @qj
And that's it! The most important mapping in my config! It just runs the macro
q
and goes down a line. Honestly I'm thinking about changing it to map to
@q
only and it would still be the most important mapping in my config. Let me
explain.
The Problems With Macros#
It's common to hear people talking about how powerful vim macros are, but I
always thought they were too clunky. It's easy to make a macro that will do
unexpected things when replayed and @q
/@@
are so awkward to type. This lead
me to search for alternatives like dot-repeating and substitution with :s
, but
these only work in simple cases and can be repetitive.
The Solution#
Then, everything changed when NeovimConf published the talk. Only Jesse Leite,
master of all macros, could teach us, and when the world needed him most, he was
there. Q
doesn't only reduce the barrier to replay a macro, it completely
destroys it. It's impressive how much cheaper macros become when they're so
spammable!
I also started getting much better at making macros that do well on replays, not
because Q
changes anything fundamentally, but because I was getting a lot more
practice.
Since I began using Q
, I've used macros in a number of interesting
scenarios, including:
Transforming gRPC service definitions into Go functions#
Suppose I had a gRPC service like the one below
service ThingService {
rpc AddThing(AddThingRequest) returns (AddThingResponse)
rpc DeleteThing(DeleteThingRequest) returns (DeleteThingResponse)
rpc RenameThing(RenameThingRequest) returns (RenameThingResponse)
rpc GetThing(GetThingRequest) returns (GetThingResponse)
}
To implement this service in Golang, I copied the 4 lines of rpcs (it was more
like 7 in the real case) into a .go
file and recorded a macro, turning it into
func (s *ThingService) AddThing(
ctx context.Context,
r *proto.AddThingRequest
) (*proto.AddThingResponse, error) {
return nil, nil
}
rpc DeleteThing(DeleteThingRequest) returns (DeleteThingResponse)
rpc RenameThing(RenameThingRequest) returns (RenameThingResponse)
rpc GetThing(GetThingRequest) returns (GetThingResponse)
For the other three functions, the macro does its job
func (s *ThingService) AddThing(
ctx context.Context,
r *proto.AddThingRequest,
) (*proto.AddThingResponse, error) {
return nil, nil
}
func (s *ThingService) DeleteThing(
ctx context.Context,
r *proto.DeleteThingRequest,
) (*proto.DeleteThingResponse, error) {
return nil, nil
}
func (s *ThingService) RenameThing(
ctx context.Context,
r *proto.RenameThingRequest,
) (*proto.RenameThingResponse, error) {
return nil, nil
}
func (s *ThingService) GetThing(
ctx context.Context,
r *proto.GetThingRequest,
) (*proto.GetThingResponse, error) {
return nil, nil
}
Writing the gRPC service example above#
That whole service definition is very repetitive! I actually initially wrote
service ThingService {
Add
Delete
Rename
Get
}
Recorded a macro to modify the first rpc into
service ThingService {
rpc AddThing(AddThingRequest) returns (AddThingResponse)
Delete
Rename
Get
}
and replayed the macro for the other 3 rpcs, remembering to yank the first word
to paste before ThingRequest
and ThingResponse
.
Adding a property to certain items in a yaml#
In another case, I had a huge yaml and needed to modify about 30 list items by
adding a new flag: true
property to them. For this, I opened two buffers
side-by-side -- yaml on the left, a list of IDs that should be modified on the
right. The macro starts on the right, hovering the current ID, then goes to the
left, searches and modifies an item and goes back to the right. By pressing Q
multiple times, we jump around the yaml making changes one by one, but always
going back to the next ID on the list!
Maybe this could be done using the quickfix list as well, but the macro is so simple I would probably do it this way again, if I need.
Caveats#
I argue macros are much more convenient after mapping Q
to @qj
, but they
can still feel clunky and hard to use, mostly because of replayability issues.
It does get better with practice, but there's definitely a learning curve. For
those of you who swear by multiple cursors, I'll give you that: they're much
easier to use. Oh and there's no "preview" for macros! If you get it wrong,
you'll only know after recording and you'll have to record again.
Using the right text-objects does make a big difference, though, because it can make motions more "generic". I find that chrisgrieser/nvim-various-textobjs has been very useful in that regard.
Conclusion#
Watch That One Micro Talk on Macros (NeovimConf 2023),
map Q
to @qj
and be happy.