Normal Mode

% is a motion that jumps to the respective match of a () [] {}. It was written for working on C originally, so obviously it also works on #if/#else/#endif, and on /* ... */. You can extend it to add in other matching open close tags that suit your fancy like <:>, but if the character is the same such as ":" it won’t work unfortunately.

For my own use in PHP, I wanted to add some support for matching (here|now)docs, and since I needed some regex firepower, I employed the bundled plugin matchit:

packadd! matchit

Now we can start to add in our desired behaviour!

:let b:match_words .= ',<<<["'']\=\(\h\w*\)["'']\=$:^\s*\1\>'
  • .= appends to the ftplugin’s existing pairs instead of clobbering them, and the leading , separates our pair from theirs.
  • ["'']\= matches 0 or 1 of " or '. '' is how you write a literal ' inside a single-quoted string.
  • \h\w* matches an identifier: \h is a letter or underscore, \w is a letter, digit or underscore.
  • \(...\) captures the identifier, and \1 reuses it in the end pattern.
  • $ and ^\s* anchor the opener to the end of its line and the closer to the start of its line (indent allowed).
  • \> is the end of a word, so SQL; matches but SQLX doesn’t.
  • the : separates the start pattern from the end pattern.

So now with % we can jump back and forth on the start/end markers for heredocs:

<<<SQL
SELECT * FROM users;
SQL;

or nowdocs:

<<<'HTML'
<div> I <3 blog.nerdware.ca </div>
HTML

Fun fact: I spent an hour of my life troubleshooting the above. I was only able to match on the first < of the <<<. I later found out that matchit uses a greedy matcher and after the first character, it was actually falling back to trying to match the original matchpairs <:> entry… what a wild Friday night. If you got that same issue and want to disable those you can use :setlocal matchpairs-=<:>

If prior to typing % you type a count, it will take you that percentage of the way through the file. I honestly don’t know when this would ever be useful, and I will give it a utility rating on par with g?.

Register

The % register is a special register in Vim that holds the name of your current buffer. I use it to copy the name of the current buffer into my clipboard with :let @+ = @%.

Fun fact: There are a handful of special registers, including + * % # " - : . _ / and =

Command-line Mode

As a range, % means the whole file, (the % in :%s/foo/bar/g).

Elsewhere on the command line, % expands to your buffer name. You can apply all sorts of modifiers to it:

  • %:S shell-escapes the output. I wish this were the default setting…
  • %:h gives you the directory.
  • %:p gives you the full path.
  • %:~ slots in ~ for the home directory.
  • %:e gives you the extension.

Note: There is some warning somewhere in the Friendly Manual about “%” and %:S working diffently when being passed to an external shell command … I don’t recall the distinction so if you care it can be an excercise to the reader

Here are some examples straight out of the friendly manual (:h filename-modifiers)!

When the file name is “src/version.c”, current dir “/home/mool/vim”:

:p                      /home/mool/vim/src/version.c
:p:.                    src/version.c
:p:~                    ~/vim/src/version.c
:h                      src
:p:h                    /home/mool/vim/src
:p:h:h                  /home/mool/vim
:t                      version.c
:p:t                    version.c
:r                      src/version
:p:r                    /home/mool/vim/src/version
:t:r                    version
:e                      c
:s?version?main?        src/main.c
:s?version?main?:p      /home/mool/vim/src/main.c
:p:gs?/?\\?             \home\mool\vim\src\version.c

Patterns

If you want to group part of a pattern without capturing it, you can use \%\(...\). For example, to make sure a SQL statement is either an update or an insert without caring which: \%(UPDATE\|INSERT\).... It is a niche use, but if you use Vim for scripting because you are like me and hate shell script, you may find the speed advantage helpful, and it doesn’t use up one of your nine \1-\9 backreferences (a benefit I have honestly never needed myself, but a benefit nevertheless).

There is also \%[], which matches as much of a sequence of optional atoms as it can, e.g. fu\%[nction] matches fu, fun, func, up to function.

Error Matching

A topic worth of a dedicated post

A really handy workflow in Vim is using makeprg and errorformat. Together they let you run your build commands, populate the quickfix list with the errors, and allow you to quickly navigate to them. In errorformat, % introduces the format items. Consider the following:

install
└─ install chess_engine
   └─ compile exe chess_engine Debug native 2 errors
src/main.zig:10:36: error: use of undeclared identifier 'argg'
        std.debug.print("{s}\n", .{argg});
                                   ^~~~
src/root.zig:9:18: error: expected ';' after statement
    Engine.move()
                 ^
error: 2 compilation errors
failed command: /usr/bin/zig build-exe -ODebug --dep chess_engine -Mroot=/home/carson/code/zig/chess-engine/src/main.zig -Mchess_engine=/home/carson/code/zig/chess-engine/src/root.zig --cache-dir .zig-cache --global-cache-dir /home/carson/.cache/zig --name chess_engine --zig-lib-dir /usr/lib/zig/ --listen=-

Build Summary: 0/3 steps succeeded (1 failed)
install transitive failure
└─ install chess_engine transitive failure
   └─ compile exe chess_engine Debug native 2 errors

error: the following build command failed with exit code 1:
.zig-cache/o/017f4cbe08099b1eed012c12388c10d5/build /usr/bin/zig /usr/lib/zig /home/carson/code/zig/chess-engine .zig-cache /home/carson/.cache/zig --seed 0x8ff340df -Z9454fa58f76246d2

We could set up our errorformat as follows:

:set makeprg=zig\ build
:set errorformat=%f:%l:%c:\ %t%*[a-z]:\ %m,%-G%.%#
:make
  • %f, %l, %c are the file, line and column.
  • %t grabs the error type from its first letter (e for error), and %*[a-z] eats the rest of the word.
  • %m is the message.
  • %-G%.%# ignores every other line (%.%# is errorformat for .*).

And then we get jumped straight to our first error in src/main.zig:10:36 with both that error and the other one populating our quickfix list.

Fluency with the make command in Vim and its various components is a beautiful thing because it means you can spend more time in Vim :)

Fin

So, next time you see a %, take a moment to thank them for all of their hard work.