How to conveniently select all content inside a Markdown Fenced Code Block using Vim?
For example, given a code block:
```
.wrapper {
display: flex;
}
.wrapper > div {
flex: 1;
}
```
Assume the cursor is inside the block.
To select in Vim, I think we can have:
?```<CR>j
v/```<CR>k
(alternatively, select by paragraphs till the end)
v}}k
I am looking for simpler keystrokes or ideas, perhaps something similar to vip
or vi{
.
Assuming the cursor is on the opening fence, I think the shortest method would be:
*kV''j
With the cursor on the closing fence, it would be:
#jV''k
With the cursor inside the code block, the approach would be different:
/```/-<CR>V??+<CR>
If that satisfies your needs, then you can stop reading now. If not, maybe we can try to come up with a comprehensive solution!
Let's start by breaking down the problem:
Then let's try our hands at the three cases:
The cursor is on the opening fence
Figuring out if the cursor is on a ```
is trivial:
function! IsFence()
return getline('.') == '```'
endfunction!
But, assuming this Markdown content:
hello
```
one
two
```
```
three
four
```
goodbye
how can we figure out if we are on an opening fence or on a closing fence? Well, there are a few ways but the one I chose is to count the number of fences starting with the current one: if it is even, we are on an opening fence.
function! IsOpeningFence()
return IsFence() && getline(line('.'),'$')->filter({ _, val -> val =~ '^```'})->len() % 2 == 0
endfunction
Good!
The cursor is on the closing fence
Now that we can determine if the cursor is on a fence and if it is an opening one, determining if it is a closing fence is trivial:
function! IsClosingFence()
return IsFence() && !IsOpeningFence()
endfunction
Good!
The cursor is between fences
This one is a little bit trickier. There are, again, a few ways to go about it but the one I chose is to ask Vim whether the highlight group markdownCodeBlock
is currently active:
function! IsbetweenFences()
return synID(line("."), col("."), 0)->synIDattr('name') =~? 'markdownCodeBlock'
endfunction
Good!
NOTE: this snippet assumes that you use the built-in Markdown syntax script. If you don't, substitute markdownCodeBlock
with whatever works for you.
Now that we can figure out which case we are in, let's see what we can do for each one.
The cursor is on the opening fence
if IsOpeningFence()
normal *kV''j
endif
The cursor is on the closing fence
if IsClosingFence()
normal #jV''k
endif
The cursor is between fences
Our original macro is not as trivial as the others to use with :help normal
so we are going to try a different method:
if IsBetweenFences()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
endif
where we…
```
at the beginning of a line (the closing fence),```
at the beginning of a line (the opening fence) while leaving a mark behind us,OK, let's put our decision tree together:
if IsOpeningFence()
normal *kV''j
elseif IsBetweenFences()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
elseif IsClosingFence()
normal #jV''k
else
return
endif
and consider the fact that what we do when we are between fences could also work when we are on the opening fence, which makes one branch redundant:
if isOepeningFence() || IsBetweenFences()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
elseif IsClosingFence()
normal #jV''k
else
return
endif
One last thing, it would be neater if we used the same approach in both branches so let's do it:
if IsOpeningFence() || IsInside()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
elseif IsClosingFence()
call search('^```', 'Wbs')
normal +
normal V''k
else
return
endif
Great! Let's put it all together in one single function:
function! SelectInnerCodeBlock()
function! IsFence()
return getline('.') =~ '^```'
endfunction
function! IsOpeningFence()
return IsFence() && getline(line('.'),'$')->filter({ _, val -> val =~ '^```'})->len() % 2 == 0
endfunction
function! IsBetweenFences()
return synID(line("."), col("."), 0)->synIDattr('name') =~? 'markdownCodeBlock'
endfunction
function! IsClosingFence()
return IsFence() && !IsOpeningFence()
endfunction
if IsOpeningFence() || IsBetweenFences()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
elseif IsClosingFence()
call search('^```', 'Wbs')
normal +
normal V''k
else
return
endif
endfunction
and map it in visual mode (for use with v
) and operator-pending mode (for use with y
, d
, etc.):
xnoremap <silent> iC :<C-u>call SelectInnerCodeBlock()<CR>
onoremap <silent> iC :<C-u>call SelectInnerCodeBlock()<CR>
and see if it works:
The last thing we need to do is make sure that our custom pseudo-text object is restricted to Markdown buffers. For that, we move our code to ~/.vim/after/ftplugin/markdown.vim
, with a few minor changes:
SelectInnerCodeBlock()
becomes script-local, see :help :s
and :help <SID>
,:help <buffer>
." in ~/.vim/after/ftplugin/markdown.vim
function! s:SelectInnerCodeBlock()
function! IsFence()
return getline('.') =~ '^```'
endfunction
function! IsOpeningFence()
return IsFence() && getline(line('.'),'$')->filter({ _, val -> val =~ '^```'})->len() % 2 == 0
endfunction
function! IsBetweenFences()
return synID(line("."), col("."), 0)->synIDattr('name') =~? 'markdownCodeBlock'
endfunction
function! IsClosingFence()
return IsFence() && !IsOpeningFence()
endfunction
if IsOpeningFence() || IsBetweenFences()
call search('^```', 'W')
normal -
call search('^```', 'Wbs')
normal +
normal V''
elseif IsClosingFence()
call search('^```', 'Wbs')
normal +
normal V''k
else
return
endif
endfunction
xnoremap <buffer> <silent> iC :<C-u>call <SID>SelectInnerCodeBlock()<CR>
onoremap <buffer> <silent> iC :<C-u>call <SID>SelectInnerCodeBlock()<CR>