3

I found the following description of a command which sets up which items must appear in the status line, here:

set statusline=%F%m%r%h%w[%L][%{&ff}]%y[%p%%][%04l,%04v]
"              | | | | |  |   |      |  |     |    |
"              | | | | |  |   |      |  |     |    +-- current column
"              | | | | |  |   |      |  |     +-- current line
"              | | | | |  |   |      |  +-- current % into file
"              | | | | |  |   |      +-- current syntax
"              | | | | |  |   +-- current fileformat
"              | | | | |  +-- number of lines
"              | | | | +-- preview flag in square brackets
"              | | | +-- help flag in square brackets
"              | | +-- readonly flag in square brackets
"              | +-- modified flag in square brackets
"              +-- full path to file in the buffer

I would like to reproduce the same sort of diagram which is used here to break down the description of other commands that I could find in the future. But drawing the |, + and - characters can take some time.

How to automate the process, so that I can focus on the contents of the description, rather than the diagram?

2 Answers 2

6

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

enter image description here

1

One way of avoiding this is to simply build your statusline incrementally:

" filename (relative path)
set statusline=%1*%f\ 
" file format
set statusline+=%2*[%{&ff}]
" file type
set statusline+=%3*%y
" modified flag
set statusline+=%4*%m
" read only flag
set statusline+=%9*%r
" help file flag
set statusline+=%5*%h
" preview window flag
set statusline+=%5*%w
" buffer number (note trailing space)
set statusline+=%6*\ buf:%n\ 
" col number,line number / number of lines (note trailing space)
set statusline+=%7*[%v,%l/%L]\ 
" percentage through file
set statusline+=%8*[%p%%]
1
  • The statusline example is just for demonstration. Being able to automate drawing of ASCII art in this way is still useful in contexts when you can't break the line you're annotating up so easily.
    – Rich
    Commented Feb 22, 2018 at 9:51

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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