1

For learning purpose, I'm trying to reimplement a small subset of the features provided by the DrawIt plugin.

So far, here's what I have:

" data "{{{

let s:draw_state = 'disabled'

let s:draw_chars = {
                   \ 'minus'     : '-',
                   \ 'pipe'      : '|',
                   \ 'plus'      : '+',
                   \ 'backslash' : '\',
                   \ 'slash'     : '/',
                   \ 'X'         : 'X',
                   \ }

"}}}
" mappings "{{{

nno <silent> m_        :<C-U>call <SID>draw_change_state(0)<CR>
nno <silent> m<space>  :<C-U>call <SID>draw_change_state(1)<CR>
nno <silent> m<Bar>    :<C-U>call <SID>draw_stop()<CR>

"}}}
" beyond_last_line"{{{

fu! s:draw_beyond_last_line(key) abort
    return (a:key ==? '<Down>' || a:key ==? '<PageDown>' || a:key ==? '<End>' || a:key ==# 'v' )
            \ && s:draw_state ==# 'drawing' && line('.') == line('$')
endfu

"}}}
" change_state "{{{

fu! s:draw_change_state(erasing_mode) abort

    if s:draw_state ==# 'disabled'
        let s:ve_save = &ve
        let s:draw_original_mappings = s:save_mappings([
                                                       \ '<Left>',
                                                       \ '<Right>',
                                                       \ '<Down>',
                                                       \ '<Up>',
                                                       \ '<S-Left>',
                                                       \ '<S-Right>',
                                                       \ '<S-Down>',
                                                       \ '<S-Up>',
                                                       \ '<',
                                                       \ '>',
                                                       \ 'v',
                                                       \ '^',
                                                       \ ],
                                                       \
                                                       \   'n',
                                                       \       1)

        " The last argument passed to `s:save_mappings()` is 1. "{{{
        " This is very important. It means that we save global mappings.
        " We aren't interested in buffer-local ones.
        " Why?
        " It would be difficult to restore them, we would need to first restore
        " the focus to the buffer where they were initially saved.
        " And they could only be used in the current buffer, not in others.
        "
        " I prefer to not bother.
        " If the user mapped the keys locally, our global mapping will work
        " everywhere except in the current buffer and buffers where they
        " installed similar buffer-local mappings.
        "
        " Trying to support this case would create too much complexity.
        " We would need to override the buffer-local mappings from the user in
        " every buffer where they exist. It would probably require an autocmd,
        " watching some event, like `BufEnter,BufNewFile`.
        " It would have to check whether the user did install a buffer-local
        " mapping using the keys we're interested in, and in that case,
        " save the info about the mapping as well as the position of the buffer
        " in the buffer list.
        "
        " Once the user stops drawing, we would then need to parse all this
        " info, to give the focus to various buffers and restore the mappings
        " in them.
        " Then, we would need to restore the layout… FUBAR
        "
        " }}}

    endif

    let s:draw_state = {
                       \ 'disabled' : a:erasing_mode ? 'erasing'  : 'drawing' ,
                       \ 'drawing'  : a:erasing_mode ? 'erasing'  : 'disabled',
                       \ 'erasing'  : a:erasing_mode ? 'disabled' : 'drawing' ,
                       \ }[s:draw_state]

    call s:draw_update_mappings()
endfu

"}}}
" install_mappings "{{{

fu! s:draw_install_mappings() abort
    let args = ' <nowait> <silent> '

    for l:key in ['Left>', 'Right>', 'Down>', 'Up>', 'PageDown>', 'PageUp>', 'Home>', 'End>']
        exe 'nno '.args.' <'.l:key.' :<C-U>call <SID>draw_it('.string('<lt>'.l:key).')<CR>'
    endfor

    for l:key in ['S-Left>', 'S-Right>', 'S-Down>', 'S-Up>']
        exe 'nno '.args.' <'.l:key.' :<C-U>call <SID>draw_shift_arrow_motion("<lt>'.l:key.'")<CR>'
    endfor

    for l:key in ['<', '>', 'v', '^']
        exe 'nno '.args.l:key.' :<C-U>call <SID>draw_it('.string(l:key).')<CR>'
    endfor
endfu

"}}}
" intersection "{{{

fu! s:draw_intersection(key) abort
    if count(['<Left>', '<Right>', '<Down>', '<Up>'], a:key)
        return '+'

    elseif count(['<End>', '<Home>', '<PageDown>', '<PageUp>'], a:key)
        return 'X'
    endif
endfu

"}}}
" is_changing_dir "{{{

fu! s:draw_is_changing_dir(key) abort
    return getline('.')[col('.')-1] ==# s:draw_chars[s:draw_opposite(a:key)]
endfu

"}}}
" it "{{{

fu! s:draw_it(key) abort

    if s:draw_beyond_last_line(a:key)
        call append('.', '')
    endif

    if count([
             \ '<Left>',
             \ '<Right>',
             \ '<Down>',
             \ '<Up>',
             \ '<PageDown>',
             \ '<PageUp>',
             \ '<End>',
             \ '<Home>'
             \ ],
             \     a:key)

        let keys = (
                 \   s:draw_is_changing_dir(a:key)
                 \     ? s:draw_replace_char(s:draw_intersection(a:key))
                 \     : ''
                 \ )
                 \ .s:draw_motion(a:key)
                 \ .s:draw_replace_char(a:key)

    elseif count(['^', 'v', '<', '>'], a:key)

        let keys = 'r'.a:key.s:draw_motion(a:key).'r'.a:key

    endif

    call feedkeys(keys, 'in')
endfu

"}}}
" key2name "{{{

fu! s:draw_key2name(key) abort
    if a:key ==? '<Left>' || a:key ==? '<Right>'
        return 'minus'

    elseif a:key ==? '<Down>' || a:key ==? '<Up>'
        return 'pipe'

    elseif a:key ==? '<End>' || a:key ==? '<PageUp>'
        return 'slash'

    elseif a:key ==? '<Home>' || a:key ==? '<PageDown>'
        return 'backslash'

    elseif a:key ==? '+'
        return 'plus'

    elseif a:key ==# 'X'
        return 'X'

    endif
endfu

"}}}
" motion "{{{

fu! s:draw_motion(key) abort
    if a:key ==? '<Left>'
        return "\<Left>"

    elseif a:key ==? '<Right>'
        return "\<Right>"

    elseif a:key ==? '<Down>'
        return "\<Down>"

    elseif a:key ==? '<Up>'
        return "\<Up>"

    elseif a:key ==? '<PageDown>'
        return "\<Right>\<Down>"

    elseif a:key ==? '<PageUp>'
        return "\<Right>\<Up>"

    elseif a:key ==? '<Home>'
        return "\<Left>\<Up>"

    elseif a:key ==? '<End>'
        return "\<Left>\<Down>"

    elseif a:key ==# 'v'
        return "\<Down>"

    elseif a:key ==# '^'
        return "\<Up>"

    elseif a:key ==# '<'
        return "\<Left>"

    elseif a:key ==# '>'
        return "\<Right>"

    endif
endfu

"}}}
" opposite "{{{

fu! s:draw_opposite(key) abort
    if a:key ==? '<Down>' || a:key ==? '<Up>'
        return 'minus'

    elseif a:key ==? '<Left>' || a:key ==? '<Right>'
        return 'pipe'

    elseif a:key ==? '<Home>' || a:key ==? '<PageDown>'
        return 'slash'

    elseif a:key ==? '<End>' || a:key ==? '<PageUp>'
        return 'backslash'
    endif
endfu

"}}}
" remove_mappings "{{{

fu! s:draw_remove_mappings() abort
    for l:key in [
                 \ '<Left>',
                 \ '<Right>',
                 \ '<Down>',
                 \ '<Up>',
                 \ '<S-Left>',
                 \ '<S-Right>',
                 \ '<S-Down>',
                 \ '<S-Up>',
                 \ '<PageDown>',
                 \ '<PageUp>',
                 \ '^',
                 \ 'v',
                 \ '<',
                 \ '>',
                 \ ]

        " Why unmap silently?
        "
        " Because we could be dumb and ask to disable the drawing mode manually
        " (`m|`), even though it's already disabled.
        " It could raise errors, if the keys are already unmapped (the user didn't
        " map them to anything by default).

        sil! exe 'nunmap '.l:key
    endfor

        call s:restore_mappings(s:draw_original_mappings)
endfu

"}}}
" replace_char"{{{

fu! s:draw_replace_char(key) abort
    " This function is called before and then after a motion (left, up, …).
    " It must return the character to draw.
    "
    " When it's called AFTER a motion, and we're erasing, the character HAS TO
    " be a space.
    " When it's called BEFORE a motion, and we're erasing, we COULD (should?)
    " return nothing.
    "
    " Nevertheless, we let the function return a space.
    " It doesn't seem to cause an issue.
    " This way, we don't have to pass a 2nd argument to know when it's called
    " (before or after a motion).

    return 'r'
         \ .(
         \   s:draw_state ==# 'erasing'
         \   ? ' '
         \   : s:draw_chars[s:draw_key2name(a:key)]
         \  )
endfu

"}}}
" restore_mappings "{{{

" Warning:
" Don't try to restore a buffer local mapping unless you're sure that, when
" `s:restore_mappings()` is called, you're in the same buffer where
" `s:save_mappings()` was originally called.
"
" If you aren't in the same buffer, you could install a buffer-local mapping
" inside a buffer where this mapping didn't exist before.
" It could cause unexpected behavior on the user's system.
"
" Usage:
" call s:restore_mappings(my_saved_mappings)
"
" `my_saved_mappings` is a dictionary obtained earlier by calling
" `s:save_mappings()`.
" Its keys are the keys used in the mappings.
" Its values are the info about those mappings stored in sub-dictionaries.
"
" There's nothing special to pass to `s:restore_mappings()`, no other
" argument, no wrapping inside a 3rd dictionary, or anything. Just this dictionary.

fu! s:restore_mappings(mappings) abort

    for mapping in values(a:mappings)
        if !empty(mapping)
            exe    (mapping.noremap ? ' nnoremap ' : ' nmap ')
               \ . (mapping.buffer  ? ' <buffer> ' : '')
               \ . (mapping.expr    ? ' <expr>   ' : '')
               \ . (mapping.nowait  ? ' <nowait> ' : '')
               \ . (mapping.silent  ? ' <silent> ' : '')
               \ .  mapping.lhs
               \ . ' '
               \ . substitute(mapping.rhs, '<SID>', '<SNR>'.mapping.sid.'_', 'g')
        endif
    endfor

endfu

"}}}
" save_mappings "{{{

" Usage:
"
"     let my_global_mappings = s:save_mappings(['key1', 'key2', …], 'n', 1)
"     let my_local_mappings  = s:save_mappings(['key1', 'key2', …], 'n', 0)
"

" Output example: "{{{
"
"     { '<left>' :
"                \
"                \ {'silent': 0,
"                \ 'noremap': 1,
"                \ 'lhs': '<Left>',
"                \ 'mode': 'n',
"                \ 'nowait': 0,
"                \ 'expr': 0,
"                \ 'sid': 7,
"                \ 'rhs': ':echo ''foo''<CR>',
"                \ 'buffer': 1},
"                \
"     \ '<right>':
"                \
"                \ { 'silent': 0,
"                \ 'noremap': 1,
"                \ 'lhs': '<Right>',
"                \ 'mode': 'n',
"                \ 'nowait': 0,
"                \ 'expr': 0,
"                \ 'sid': 7,
"                \ 'rhs': ':echo ''bar''<CR>',
"                \ 'buffer': 1,
"                \ },
"                \}
"
" }}}

fu! s:save_mappings(keys, mode, global) abort
    let mappings = {}

    " If a key is used in a global mapping and a local one, by default,
    " `maparg()` only returns information about the local one.
    " We want to be able to get info about a global mapping even if a local
    " one shadows it.
    " To do that, we will temporarily unmap the local mapping.

    if a:global
        for l:key in a:keys
            let buf_local_map = maparg(l:key, a:mode, 0, 1)

            " temporarily unmap the local mapping
            sil! exe a:mode.'unmap <buffer> '.l:key

            " save info about the global one
            let mappings[l:key] = maparg(l:key, a:mode, 0, 1)

            " restore the local one
            call s:restore_mappings({l:key : buf_local_map})
        endfor

    " TRY to return info local mappings.
    " If they exist it will work, otherwise it will return info about global
    " mappings.
    else
        for l:key in a:keys
            let mappings[l:key] = maparg(l:key, a:mode, 0, 1)
        endfor
    endif

    return mappings
endfu


"}}}
" shift_arrow_motion "{{{

fu! s:draw_shift_arrow_motion(key) abort
    if a:key ==? '<S-Down>' && line('.') == line('$')
        call append('.', '')
    endif
    call feedkeys(eval('"'.'\'.substitute(a:key, '\cs-', '', '').'"'), 'in')
endfu

"}}}
" stop "{{{

fu! s:draw_stop() abort
    let s:draw_state = 'disabled'
    call s:draw_remove_mappings()
    let &ve = s:ve_save
    echom '[Drawing/Erasing] disabled'
endfu

"}}}
" update_mappings"{{{

fu! s:draw_update_mappings() abort
    if s:draw_state ==# 'disabled'
        call s:draw_stop()

    else
        call s:draw_install_mappings()
        set ve=all
        echom '['.substitute(s:draw_state, '.', '\u&', '').'] '.'enabled'

    endif
endfu

"}}}

Here are the mappings I'm currently using:

m_            start/toggle drawing
m<space>      start/toggle erasing
m|            stop everything

<left>        insert `-` and move left
<right>       insert `-` and move right
<down>        insert `|` and move down
<up>          insert `|` and move up

<s-left>      move left
<s-right>     move right
<s-down>      move down
<s-up>        move up

<Home>        insert `\` and move up
<PageUp>      insert `/` and move up
<End>         insert `/` and move down
<PageDown>    insert `\` and move down
v             insert `v` and move down
^             insert `^` and move up
<             insert `<` and move left
>             insert `>` and move right

But I don't know how to handle intersections. What I would like to achieve is the following:

  1. If the character I'm inserting is not parallel to the one where I was just before, replace the latter with an intersection. For example, if I'm on a - and I hit <Up> or <Down>, it should insert a | which is not parallel to -. So, - should be replaced with +.

  2. If the character I'm inserting is replacing an existing character which is not parallel, do the same thing. For example, if I hit <PageDown> to move down and insert a \, but the character on which I land after the Down motion is a /, then instead of inserting \ like it's supposed to, insert a X instead.


I think the current code handles the first condition, but not the second one.

For example:enter image description here

The corners of the drawings on the left are correctly drawn using the + and X characters, but the ones on the right don't handle the intersection at all.

Here's what the upper drawing on the right does:

             |              
       |     |              
       |     |       |      
       |     |       |      
       |     |       |      
-------|-----|-------|------
       |     |       |      
       |     |       |      
       |             |      

Here's what I want:

             |              
       |     |              
       |     |       |      
       |     |       |      
       |     |       |      
-------+-----+-------+------
       |     |       |      
       |     |       |      
       |             |  

Here's what the lower drawing on the right does:

\           
 \          
  \         
   \      / 
    \    / /
     \  /// 
      \///  
      ///   
     ///\   
    ///  \  
    //    \ 
           \

Here's what I want:

\           
 \          
  \         
   \      / 
    \    / /
     \  /// 
      \///  
      /X/   
     ///\   
    ///  \  
    //    \ 
           \

The problem comes from this function:

fu! s:draw_it(key) abort

    if s:draw_beyond_last_line(a:key)
        call append('.', '')
    endif

    if count([
             \ '<Left>',
             \ '<Right>',
             \ '<Down>',
             \ '<Up>',
             \ '<PageDown>',
             \ '<PageUp>',
             \ '<End>',
             \ '<Home>'
             \ ],
             \     a:key)

        let keys = (
                 \   s:draw_is_changing_dir(a:key)
                 \     ? s:draw_replace_char(s:draw_intersection(a:key))
                 \     : ''
                 \ )
                 \ .s:draw_motion(a:key)
                 \ .s:draw_replace_char(a:key)

    elseif count(['^', 'v', '<', '>'], a:key)

        let keys = 'r'.a:key.s:draw_motion(a:key).'r'.a:key

    endif

    call feedkeys(keys, 'in')
endfu

And more specifically from this part:

    let keys = (
             \   s:draw_is_changing_dir(a:key)
             \     ? s:draw_replace_char(s:draw_intersection(a:key))
             \     : ''
             \ )
             \ .s:draw_motion(a:key)
             \ .s:draw_replace_char(a:key)

The purpose of this part is to assign the right value to the variable keys. It's supposed to contain the keys to type. They are written in the typeahead buffer at the end of the function, using feedkeys().


Currently, it can be divided into 3 parts:

           (
             \   s:draw_is_changing_dir(a:key)
             \     ? s:draw_replace_char(s:draw_intersection(a:key))
             \     : ''
             \ )

Replace the current character with an intersection if the direction is changing, or do nothing.

             \ .s:draw_motion(a:key)

Move the cursor.

             \ .s:draw_replace_char(a:key)

Replace the character under the cursor.


The value of a:key contains the key that was typed. For example, if I hit <Down>, it should contain the string '<Down>'.

The function s:draw_replace_char() should return 2 keys to replace the character under the cursor. The first key is always r, the replace command. The second one should be the character to draw (-, +, \, /, ...).

The function s:draw_is_changing_dir() returns 0 or 1. It returns 1 if the character to draw is not parallel to the one under the cursor.

Before moving the cursor, I test the output of this function to decide whether I should first replace the current character with an intersection or do nothing.

The function s:draw_intersection() returns the character standing for an intersection, + or X, depending on the key which was hit. For example, if I hit <Down>, the function should return the string '+'. And if I hit <PageDown>, it should return 'X'.

The function s:draw_motion() returns the keys to move the cursor in a direction. For example, If I hit <PageDown>, it should return "\<Right>\<Down>".

How can I rewrite the assignment of keys so that it handles the 2 conditions described earlier, regarding intersections?

1

Sorry, that's too much code to read through. It looks complicated though. Just check the character under the cursor and draw appropriately.

set ve=all

fun! Drawchar(dir)
    let c = {'h':'-', 'l':'-', 'j':'|', 'k':'|'}[a:dir]
    norm!yl
    if @" =~ '[-|+]' && @" != c
        let c = '+'
    endif
    exe 'norm!r' . c
endfun

fun! Draw(dir)
    call Drawchar(a:dir)
    exe 'norm!' . a:dir
    call Drawchar(a:dir)
endfun

nno <s-right> :call Draw('l')<cr>
nno <s-left> :call Draw('h')<cr>
nno <s-down> :call Draw('j')<cr>
nno <s-up> :call Draw('k')<cr>

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.