I'm trying to write a simple function that wraps a visual selection of lines with Markdown ``` lines. Here's what I currently have:

function! MarkCodeBlock()
    " Add Markdown code-block delimiters to begin and end of current visual group.
    execute "normal! `<O```\<esc>yy`>p"
endfunction

vnoremap <leader>_ :call MarkCodeBlock()<CR>

It mostly works, but it appears to invoke MarkCodeBlock() once for each line in the visual selection. As an example, suppose I have the following block:

This is not a code line
This is a code line
This is a code line
This is a code line
This is not a code line

where the middle three lines are visually selected. On invoking the map sequence, I end up with this:

This is not a code line
```
```
```
This is a code line
This is a code line
This is a code line
```
```
```
This is not a code line

but I want this:

This is not a code line
```
This is a code line
This is a code line
This is a code line
```
This is not a code line

Any ideas on how to achieve this?

up vote 4 down vote accepted

When you press leader _, you enter command-line mode from visual mode.
If you try to enter command-line mode from visual mode manually, you'll see that Vim automatically inserts this range:

:'<,'>
 ├┘ ├┘
 │  └ mark put automatically on the last line of the visual selection
 └ mark put automatically on the first line of the visual selection (see `:h '<`)

So, when :call calls your MarkCodeBlock() function, Vim has automatically prefixed it with a range.
And, from :h func-range:

If [range] is excluded, ":{range}call" will call the function for each line in the range, with the cursor on the start of each line.

Because of this, your function is called once for every line in the range.

You have 2 possibilities:

  1. Eliminate the range by making your mapping press C-u (see :h c^u)

:

vnoremap <leader>_ :<c-u>call MarkCodeBlock()<CR>
                    ^^^^^
  1. Make Vim know that the function can handle the range itself and doesn't need :call to re-invoke it for every line in the range. You can do so by passing the range argument to :function

:

function! MarkCodeBlock() range
                          ^^^^^
    ...
endfunction

A few possible improvements/suggestions:

:vnoremap is applied in 2 modes: visual and select.
But you're probably interested only in visual mode, because (from :h mapmode-s):

NOTE: Mapping a printable character in Select mode may confuse the user.

And the lhs of your mapping contains printable characters.

If you want your mapping to be applied only in visual mode, use :xnoremap instead of :vnoremap:

xnoremap <leader>_ :<c-u>call MarkCodeBlock()<CR>
^

If your leader namespace becomes too crowded, and you need to offload some of the mappings to another namespace, you could use m in visual mode. A m<key> mapping won't shadow any Vim's built-in command, since you can't put a mark in visual mode.

xnoremap m_ :<c-u>call MarkCodeBlock()<CR>
         ^

Your MarkCodeBlock() function is public, which means that its definition could collide with another public function with the same name. You could make it “private” (local to the script) by prefixing its name with s: in your definition, and with <sid> in your mapping. From :h <sid>:

When defining a function in a script, "s:" can be prepended to the name to make it local to the script. But when a mapping is executed from outside of the script, it doesn't know in which script the function was defined. To avoid this problem, use "" instead of "s:". The same translation is done as for mappings. This makes it possible to define a call to the function in a mapping.

xnoremap m_ :<c-u>call <sid>MarkCodeBlock()<CR>
                       ^^^^^

function! s:MarkCodeBlock()
          ^^
    ...
endfunction

If an error is raised while the statements in your function are being executed, you may want Vim to stop executing the next ones (this gives shorter stack traces). In this case, you can pass the abort argument to :function (see :h :func-abort):

function! s:MarkCodeBlock() abort
                            ^^^^^
    ...
endfunction

Instead of using :normal to add lines in your buffer, you could use append() (and line(); see :h append() and :h line()). In general, the latter has the benefit of not making the cursor move.

execute "normal! `<O```\<esc>yy`>p"

⇔

call append(line("'<")-1, '```')
call append(line("'>"), '```')

If your Vim version includes the patch 8.1.0037, you can do the same to any buffer, not just the current one, using the appendbufline() function instead of append().


All in all, this would give:

function! s:MarkCodeBlock() abort
    " Add Markdown code-block delimiters to begin and end of current visual group.
    call append(line("'<")-1, '```')
    call append(line("'>"), '```')
endfunction
xnoremap m_ :<c-u>call <sid>MarkCodeBlock()<CR>
  • 1
    Or something like this: xnoremap <key> :<c-u>'>put='```'<bar>'<put!='```'<cr> – Peter Rincker Oct 10 at 23:12
  • +1, :put is shorter here. :put and append() have their pros and cons. :put is more handy on the command-line because it's shorter to type. Also, it moves the cursor on the last line pasted. This is handy when you have multiple blocks of text to paste one right after the other. OTOH, sometimes you don't want the cursor to move. Also, it's not silent (so you have to prefix it with :silent). If the expression contains double quotes, you have to escape them to prevent Vim from interpreting them as command terminations. – user938271 Oct 10 at 23:36
  • And it pollutes the expression register (although, usually you probably don't care about what it contains). So, usually I tend to type :put in an interactive usage, and write append(), appendbufline(), setline(), setbufline() in scripts. But there're exceptions. – user938271 Oct 10 at 23:36
  • Just wanted to say that after using Vim for probably over a decade, I have only recently gotten deep into Vim scripts, plugins, and other configuration fun. I'm hugely thankful for the time you took to write this response, and find it tremendously helpful. THANK YOU! – Steve Hollasch Oct 12 at 1:17

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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