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:
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+
.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 theDown
motion is a/
, then instead of inserting\
like it's supposed to, insert aX
instead.
I think the current code handles the first condition, but not the second one.
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?