Here's what I'm currently using to do it:
nno <silent> m<cr> :<c-u>call <sid>break_down_mark()<cr>
nno <silent> m<bs> :<c-u>call <sid>break_down_clear()<cr>
nno <silent> m<tab> :<c-u>call <sid>break_down_draw()<cr>
fu! s:break_down_clear() abort
if exists('s:bd_marks')
call matchdelete(s:bd_marks.id)
unlet s:bd_marks
endif
endfu
fu! s:sort_coords(x,y) abort
return a:x.col - a:y.col
endfu
fu! s:break_down_draw() abort
if !exists('s:bd_marks.coords')
return
endif
let ve_save = &ve
set ve=all
let coords = s:bd_marks.coords
let nr_lines = len(coords)
call append(line('.'), repeat([''], nr_lines + 1))
if exists(':Commentary')
for j in range(0, nr_lines)
+
call setline(line('.'), "\<c-a>")
Commentary
call setline(line('.'), substitute(getline('.'), "\<c-a>", '', ''))
endfor
endif
let loclist = []
for i in sort(coords, 's:sort_coords')
exe 'norm! '. i.line .'G'
exe 'norm! '. i.col . '|j'
for j in range(1, nr_lines)
norm! r|j
endfor
exe 'norm! R+-- '
call add(loclist, {
\ 'bufnr': bufnr('%'),
\ 'lnum' : i.line + nr_lines + 1,
\ 'col' : i.col + 4,
\ })
let nr_lines -= 1
endfor
call setloclist(0, loclist)
lfirst
call s:break_down_clear()
let &ve = ve_save
endfu
fu! s:break_down_mark() abort
if !exists('s:bd_marks')
let s:bd_marks = {
\ 'coords' : [],
\ 'pattern' : '',
\ 'id' : 0,
\ }
else
if s:bd_marks.id
call matchdelete(s:bd_marks.id)
let s:bd_marks.pattern .= '|'
endif
endif
if !empty(s:bd_marks.coords) && line('.') == s:bd_marks.coords[-1].line
if count(s:bd_marks.coords, {
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ })
call filter(s:bd_marks.coords, "v:val != {
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }")
else
let s:bd_marks.coords += [{
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }]
endif
else
let s:bd_marks.coords = [{
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }]
endif
let s:bd_marks.pattern = '\v'
\ .join(
\ map(
\ deepcopy(s:bd_marks.coords),
\ "'%'.v:val.line.'l%'.v:val.col.'v.'"
\ ),
\ '|'
\ )
let s:bd_marks.id = !empty(s:bd_marks.coords)
\ ? matchadd('SpellBad', s:bd_marks.pattern)
\ : 0
endfu
How to use it
The previous code installs 3 key bindings:
m Enter mark the current character as the starting point of something
you want to describe
m Backspace delete all the marks
m Tab draw the diagram (`|`, `+`, `-`) for all the characters which
were marked; also, populate the location list of the current
window with their positions
After you hit m Tab
to draw the diagram, you can navigate between the different parts of your description with :lnext
and :lprevious
, or any key bindings bound to these commands. As an example, unimpaired binds them to [l
and ]l
.
A typical workflow would be to:
- mark the characters in which you're interested on a given line, hitting
m Enter
on each of them
- hit
m Tab
to draw the diagram
- navigate between the different parts of your command with
:lnext
/:lprevious
How it works
nno <silent> m<cr> :<c-u>call <sid>break_down_mark()<cr>
nno <silent> m<bs> :<c-u>call <sid>break_down_clear()<cr>
nno <silent> m<tab> :<c-u>call <sid>break_down_draw()<cr>
These lines install the 3 key bindings described earlier. They call the 3 functions s:break_down_mark()
, s:break_down_clear()
and s:break_down_draw()
.
fu! s:break_down_clear() abort
if exists('s:bd_marks')
call matchdelete(s:bd_marks.id)
unlet s:bd_marks
endif
endfu
This function is responsible for deleting all the information which were stored concerning the characters marked so far (unlet s:bd_marks
). It also removes the highlighting which was applied on them (call matchdelete(s:bd_marks.id)
).
fu! s:sort_coords(x,y) abort
return a:x.col - a:y.col
endfu
This function will be needed to sort the coordinates of the characters you mark, because it's possible that sometimes you go back on the line, to mark a character you forgot.
If you have the lambda feature (:echo has('lambda')
returns 1), then you could get rid of it. But in this case, you will have to replace the line:
for i in sort(coords, 's:sort_coords')
... with:
for i in sort(coords, {x,y -> x.col - y.col})
fu! s:break_down_mark() abort
...
endfu
This function is responsible for marking the current character as the start of an item in your future description.
if !exists('s:bd_marks')
let s:bd_marks = {
\ 'coords' : [],
\ 'pattern' : '',
\ 'id' : 0,
\ }
else
if s:bd_marks.id
call matchdelete(s:bd_marks.id)
let s:bd_marks.pattern .= '|'
endif
endif
This checks whether the script-local variable s:bd_marks
exists. If it doesn't, it means you're marking the first character in a line. So, it initializes the dictionary s:bd_marks
with 3 keys:
coords
, initial value = empty list
pattern
, initial value = empty pattern
id
, initial value = zero number
coords
will store the coordinates of your marks inside dictionaries. Each of them will contain 2 keys: line
and col
. Their values being the line address and the column number of the character which is being marked.
pattern
will store the pattern describing all the characters you've marked. It will be used by the matchadd()
function to highlight them.
id
is the identifier of the match created by matchadd()
. It's necessary to be able to delete it, either when when you've finished or when the match must be updated because you've just marked a new character.
If s:bd_marks
does exist, then it means you're not marking the first character in a line. You're marking a subsequent one.
So, s:break_down_mark()
removes the match (call matchdelete(s:bd_marks.id)
), and it adds a bar character (|
) at the end of the pattern, to prepare the next branch (let s:bd_marks.pattern .= '|'
).
The latter will describe the position of the current character, which is being marked.
if !empty(s:bd_marks.coords) && line('.') == s:bd_marks.coords[-1].line
...
else
...
endif
This if
block checks whether you're marking a character which is on the same line as the one containing the previous marked characters.
If you're marking a character on a different line, then the function resets the list of coordinates, with the ones of the current character:
let s:bd_marks.coords = [{
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }]
Because it can't draw the diagram for characters across different lines. They must all be on the same line.
Otherwise, if you're marking a character on the same line, the function processes this sub-block if
:
if count(s:bd_marks.coords, {
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ })
call filter(s:bd_marks.coords, "v:val != {
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }")
This checks whether you're marking a character which was already marked. If it was, then the function removes its coordinates from the list s:bd_marks.coords
.
This way, you can toggle the status of a mark, by hitting the same key binding m Enter
multiple times on the same character.
If the character was not already marked, then the function adds its coordinates to s:bd_marks.coords
:
let s:bd_marks.coords += [{
\ 'line' : line('.'),
\ 'col' : virtcol('.'),
\ }]
Finally, the function creates the pattern describing all the characters which were marked so far:
let s:bd_marks.pattern = '\v'
\ .join(
\ map(
\ deepcopy(s:bd_marks.coords),
\ "'%'.v:val.line.'l%'.v:val.col.'v.'"
\ ),
\ '|'
\ )
And it creates a match to highlight them all:
let s:bd_marks.id = !empty(s:bd_marks.coords)
\ ? matchadd('SpellBad', s:bd_marks.pattern)
\ : 0
It colors the characters with the SpellBad
highlighting group. If you don't like the color, you could replace SpellBad
with the name of another group, after looking at the output of the :hi
command.
fu! s:break_down_draw() abort
…
endfu
This function draws the diagram.
if !exists('s:bd_marks.coords')
return
endif
First, it checks whether s:bd_marks.coords
exists. If the latter doesn't exist, the function doesn't do anything and returns, because without coordinates, it can't draw anything.
let ve_save = &ve
set ve=all
It saves the value of your 'virtualedit'
option, and gives it the value all
so that it can draw freely.
let coords = s:bd_marks.coords
let nr_lines = len(coords)
call append(line('.'), repeat([''], nr_lines + 1))
It opens some new lines. The number should be just enough to draw the diagram without overwriting existing lines which were just below the long command you want to break down.
if exists(':Commentary')
for j in range(0, nr_lines)
+
call setline(line('.'), "\<c-a>")
Commentary
call setline(line('.'), substitute(getline('.'), "\<c-a>", '', ''))
endfor
endif
This part comments the new lines which have just been opened. It relies on the custom command :Commentary
, which is similar to the one defined in the vim-commentary plugin. However, by default :Commentary
doesn't comment an empty line, so the function temporarily inserts a random character (here C-a
) before :Commentary
is invoked, then it removes it.
If you don't use the vim-commentary
plugin, but another one, check out its documentation to find the name of the Ex command which comments a line from a script. I could be wrong, but I think in nerdcommenter it's :NERDComComment
, and in tcomment_vim it's :TComment
.
Then replace :Commentary
accordingly.
let loclist = []
for i in sort(coords, 's:sort_coords')
exe 'norm! '. i.line .'G'
exe 'norm! '. i.col . '|j'
for j in range(1, nr_lines)
norm! r|j
endfor
exe 'norm! R+-- '
call add(loclist, {
\ 'bufnr': bufnr('%'),
\ 'lnum' : i.line + nr_lines + 1,
\ 'col' : i.col + 4,
\ })
let nr_lines -= 1
endfor
This part is responsible for the creation of the list loclist
which will be used to populate the location list of your current window. It also draws the diagram with these 4 commands:
exe 'norm! '. i.line .'G' position the cursor on the line to break down
exe 'norm! '. i.col . '|j' position the cursor on the column of a character
from which a branch of the diagram must be drawn
norm! r|j draw the bar characters inside the vertical part of the branch
exe 'norm! R+-- ' draw the `+--` horizontal part of the branch
If you don't like the |
, +
and -
characters, and you prefer using different ones, you could change the last 2 commands.
call setloclist(0, loclist)
lfirst
This part populates the location list with loclist
and positions the cursor on its first entry, so that you are automatically positioned below the first character that you marked.
call s:break_down_clear()
let &ve = ve_save
Finally, the function removes the highlighting which was applied on the marked characters, and restore the value of your 'virtualedit'
option.
How it looks