Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Vim mapping to underline a markdown title

Tags:

vim

I am trying to do something I expect to be simple, but I don't get why it does not work.

For the next lines in a markdown file, I want to define a mapping to underline a line, for instance :

line 0
# line 1
## line 2
### line 3
#### line 4
##### line 5
###### line 6
####### line 7
######## line 8

What I want is the following : when the cursor is on the line 0 or 1 or whatever, I just want to add :

  • <u> at the beginning of line if the line has no # --or-- <u> after the last # of the line
  • </u> at the end of line

So what I want is that when I press U on any lines, it adds the tags <u> and </u>, like the following :

<u> line 0 </u>
# <u> line 1 </u>
## <u> line 2 </u>
### <u> line 3 </u>
#### <u> line 4 </u>
##### <u> line 5 </u>
###### <u> line 6 </u>
####### <u> line 7 </u>
######## <u> line 8 </u>

On keyboard, I can simply do ^f#;;;;;;;;;;;;;;a <u><C-c>A </u><C-c>, and it works fine.

On my .vimrc, why nnoremap U ^f#;;;;;;;;;;;;;;a <u><C-c>A </u><C-c> does not work ?

(note: I wrote a lot of semicolons in case the heading is very long)

like image 616
vimchun Avatar asked Dec 31 '25 06:12

vimchun


1 Answers

It's the abundance of semi-colons that breaks the mapping. Quoting from :help map-error:

Note that when an error is encountered (that causes an error message or might cause a beep) the rest of the mapping is not executed.

A ; with no # to jump to causes an error and aborts the mapping. You can cut back on the semi-colons and see that it works if (and only if) the right number of hashes is given.

An almost-working but much better alternative would be to use an appropriate motion, see :help motion.txt like e to go to the end of a word instead of a lots of semi-colons. Note this will still fail if there isn't a single # present in the current line. Using e would be cleaner but still fail.

We can get a working mapping using the :substitute command and a regular expression that allows for an optional number of hashes. It's an Ex command and thus avoids unnecessary mode switches.

:nnoremap U :s@^#*\s*\zs.*@<u> & </u>@<CR>

Note that I used @ instead of the familiar / to separate pattern and replacement string not to have to escape the slash in </u>.

The pattern itself is the start of line anchor ^ followed by any number of # and any number of white space \s. The \zs atom starts the match (i.e. we ignore anything before it). The .* is just a greedy match-all.

The replacement string is <u> & </u>, that's the two tags with the match in between.

All of this is documented in :help :substitute and :help pattern, especially :help /^, :help /\zs and :help s/\&.

As mentioned in the comment, the above won't work on Visual Studio Code with VSCodeVim plugin. The reason seems to be that it does not recognize the \zs atom. We can write a pattern with two capture groups that work the same

:s@^(#*\s*)(.*)@\1<u> \2 </u>@

On Vim, we would have to escape the parentheses for capture groups (unless we're using very magic mode). This does not seem to be the case for VSCodeVim.

You might also be interested in don't stop mapping on not found

It should be noted that - since .* matches everything - it will highlight the entire buffer if the hlsearch option is set. To prevent this, you can follow the :substitute command up with :nohlsearch to turn highlighting off. You might also want to preserve the previous value of the search register / in a variable and restore it afterwards. This is explained in detail in Search and replace without changing the highlighting?

Putting it all together, we get the following mapping:

:nnoremap U :let old_search=@/<bar>s@^#*\s*\zs.*@<u> & </u>@<bar>let @/=old_search<CR>
like image 134
Friedrich Avatar answered Jan 05 '26 07:01

Friedrich



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!