I'd like to create a vim function to automatically add the code highlight tags, e.g. ```python and ``` around fenced code blocks to have syntax highlighting in my markdown previewer (I'm using iamcco/markdown-preview.nvim which is great). The best I've got so far is using ^\S.*$\n\(^$\n\)\+\(\s\{4,}.*$\n\|^$\n\)\+ to match the code blocks and a preceding non-empty line. I then go on to use (defined as a function)

let lang = input('enter the highlighting language : ') 
inoremap jj <Esc>
g@^\S.*$\n\(^$\n\)\+\(\s\{4,}.*$\n\|^$\n\)\+@ execute "normal :delmarks!" | execute "normal jmai```".lang."jjld$}Pmb" | 'a,'bs/^\s\{4}//g

trying to add the highlighting flags and removing the four leading spaces. However, it is not working properly since } in the third line jumps to the first empty line inside the code blocks instead of the end. For example, for the code blocks (four leading spaces are explicitly shown)

    def foo(*args):
        print args

    foo(1, 2, 3) # (1, 2, 3)

the above function ends up with

def foo(*args):
  print args
    foo(1, 2, 3) # (1, 2, 3)

How do I properly jump to the end of the match and fix the function? Or does anybody has better alternatives in mind?

  • 1
    If you're matching a whole block with a single regex, I believe the best way to transform it will be to use a :s command. To unindent the block, you can use \= on the replacement and the substitute() function to perform the further substitution. – filbranden May 14 '20 at 14:33
  • I can write an answer for that, but I'm still missing a piece of it... Why do you match on an unindented line for the first line? What is that trying to accomplish? – filbranden May 14 '20 at 14:34
  • Can you just do :call search('', 'e') – Rich May 14 '20 at 15:02
  • @filbranden The purpose of adding a preceding non-empty line in the match pattern is to match each code blocks with empty lines inside only once. This extra line is not what I want, though. My knowledge on vim is still limited, and I have to cook what I have. I am still not able to implement your idea of using :s to do the workflow, adding language tags first and then unindent in one time. An complete answer will be appreciated! – Kevin Powell May 15 '20 at 1:13
  • @Rich May be it is part of your complete answer. Replacing } with :call search('', 'e') in my cmd part, however, fails to jump to the end. – Kevin Powell May 15 '20 at 1:16

This substitution works:

:%s/\v^ {4}.*\ze(\n( {4}.*\ze)?)*/\="```".lang."\r".substitute(submatch(0), '\v(^|\n\zs) {4}', '', 'g')."\r```"/

Note that even though the regex is not anchored at the first line that starts with four spaces. But that is fine if we're using it with a :%s command, since that command will not rescan text it has already replaced, so it will perform the substitution only once on each such block.

For the replacement, we're using \= to use an expression and then the substitite() function to strip four space indent from each of the lines.

Let's break it down. First, the search pattern:

/\v^ {4}.*\ze(\n( {4}.*\ze)?)*

And let's look at one item at a time:

  • \v: Use "verymagic", so we don't need to use backslashes to escape most special symbols.
  • ^ {4}.*: Match a line starting with at least four spaces.
  • \ze: End the match at this point. We'll end the match at the last \ze we find. This ensures we will always end the match on an indented line and not one a blank line that follows.
  • (\n...)*: Matches zero or more additional lines of the match inside this group.
  • (...)?: Make the additional match optional, so that we'll match blank lines, where a \n is followed by another \n.
  • {4}.*\ze: This is exactly the same match from the start, we're again matching a line indented by four spaces. When we do, we set the end of the match to the end of this last line matched.

Now for the replacement:

\="```".lang."\r".substitute(submatch(0), '\v(^|\n\zs) {4}', '', 'g')."\r```"

Breaking it down:

  • \=: Use an expression for the replacement.
  • "```".lang."\r": This is the header line, using three backquotes followed by the language name in the lang variable, which you got with input(), then a newline.
  • substitute(...): Do a further substitution inside the replacement.
  • submatch(0): The text that matched the pattern.
  • '\v(^|\n\zs) {4}': This regex matches the four initial spaces, either at the beginning of the string, or at the beginning of a line (right after a newline character.) See :help string-match for details on how we need to match the \ns explicitly with substitute().
  • '': Replace the spaces with nothing, to remove that extra indentation.
  • 'g': Replace all matches, in all lines that start with four spaces.
  • ."\r```": End by adding a newline followed by the three backquotes at the end of the block.
  • 1
    Great thanks! It does the magic! The search pattern in substitute() seems to be inconsistent with that in :s. While your '\v(^|\n\zs) {4}' correctly remove indenting, \v^ {4} which works in :s fails here. – Kevin Powell May 15 '20 at 5:35
  • @KevinPowell Yeah in substitute() the ^ anchor only matches the start of the whole string. I think that's documented somewhere... 😁 – filbranden May 15 '20 at 9:45
  • I see. In this way, substitute() is similar to '<,'>s in visual selection in that, with g flag, even a single anchor will do the replacement for every line. – Kevin Powell May 15 '20 at 13:43
  • @KevinPowell See :help string-match for details on how we need to match the \ns explicitly with substitute(). – filbranden May 15 '20 at 13:54
  • 1
    Got it. Thanks a lot! – Kevin Powell May 15 '20 at 14:31

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.