Skip to content

Commit ad2d66a

Browse files
committed
Implement keymapping helpers.
Resolves #221.
1 parent b31525d commit ad2d66a

6 files changed

+685
-0
lines changed

autoload/maktaba/keymapping.vim

+284
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
""
2+
" @dict KeyMapping
3+
" A maktaba representation of a vim key mapping, which is used to configure and
4+
" unmap it from vim.
5+
6+
7+
if !exists('s:next_keymap_id')
8+
let s:next_keymap_id = 0
9+
endif
10+
11+
if !exists('s:KEYMAPS_BY_ID')
12+
let s:KEYMAPS_BY_ID = {}
13+
endif
14+
15+
16+
function! s:ReserveKeyMapId() abort
17+
let l:keymap_id = s:next_keymap_id
18+
let s:next_keymap_id += 1
19+
return l:keymap_id
20+
endfunction
21+
22+
23+
function! s:GetMappingById(id) abort
24+
try
25+
return s:KEYMAPS_BY_ID[a:id]
26+
catch /E716:/
27+
return 0
28+
endtry
29+
endfunction
30+
31+
32+
function! s:GetFuncCallKeystrokes(funcstr, mode) abort
33+
if a:mode ==# 'n'
34+
return printf(':call %s<CR>', a:funcstr)
35+
elseif a:mode ==# 'i'
36+
return printf('<C-\><C-o>:call %s<CR>', a:funcstr)
37+
elseif a:mode ==# 'v'
38+
" Uses "gv" at the end to re-enter visual mode.
39+
return printf(':<C-u>call %s<CR>gv', a:funcstr)
40+
elseif a:mode ==# 's'
41+
" Uses "gv<C-g>" at the end to re-enter select mode.
42+
return printf('<C-\><C-o>:<C-u>call %s<CR>gv<C-g>', a:funcstr)
43+
endif
44+
throw maktaba#error#NotImplemented(
45+
\ 'MapOnce not implemented for mode %s', a:mode)
46+
endfunction
47+
48+
49+
""
50+
" @dict KeyMapping
51+
" Unmaps the mapping in vim.
52+
" Returns 1 if mapping was found and unmapped, 0 if mapping was gone already.
53+
function! maktaba#keymapping#Unmap() dict abort
54+
if self.IsMapped()
55+
let l:arg_prefix = self._maparg.buffer ? '<buffer> ' : ''
56+
call execute(printf(
57+
\ 'silent %sunmap %s%s',
58+
\ self._maparg.mode,
59+
\ l:arg_prefix,
60+
\ self._maparg.lhs))
61+
if has_key(s:KEYMAPS_BY_ID, self._id)
62+
unlet s:KEYMAPS_BY_ID[self._id]
63+
endif
64+
return 1
65+
else
66+
return 0
67+
endif
68+
endfunction
69+
70+
71+
""
72+
" @dict KeyMapping
73+
" Returns 1 if the mapping is still defined, 0 otherwise
74+
"
75+
" Caveat: This detection can currently false positive if the original mapping
76+
" was unmapped but then another similar one mapped afterwards.
77+
function! maktaba#keymapping#IsMapped() dict abort
78+
let l:foundmap = maparg(self._lhs, self._mode, 0, 1)
79+
return !empty(l:foundmap) && l:foundmap == self._maparg
80+
endfunction
81+
82+
83+
""
84+
" @dict KeyMapping
85+
" Return a copy of the spec used to issue this mapping.
86+
function! maktaba#keymapping#GetSpec() dict abort
87+
return copy(self._spec)
88+
endfunction
89+
90+
91+
let s:IsMapped = function('maktaba#keymapping#IsMapped')
92+
let s:Unmap = function('maktaba#keymapping#Unmap')
93+
let s:GetSpec = function('maktaba#keymapping#GetSpec')
94+
95+
96+
""
97+
" Set up a key mapping in vim, mapping key sequence {lhs} to replacement
98+
" sequence {rhs} in the given [mode]. This is a convenience wrapper for
99+
" @function(#Spec) and its |KeyMappingSpec.Map| that supports the basic mapping
100+
" options. It is equivalent to calling: >
101+
" :call maktaba#keymapping#Spec({lhs}, {rhs}, [mode]).Map()
102+
" <
103+
"
104+
" See those functions for usage and behavior details.
105+
"
106+
" @default mode=all of 'n', 'v', and 'o' (vim's default)
107+
function! maktaba#keymapping#Map(lhs, rhs, ...) abort
108+
if a:0 >= 1
109+
let l:spec = maktaba#keymappingspec#Spec(a:lhs, a:rhs, a:1)
110+
else
111+
let l:spec = maktaba#keymappingspec#Spec(a:lhs, a:rhs)
112+
endif
113+
return l:spec.Map()
114+
endfunction
115+
116+
117+
""
118+
" @private
119+
" Unmap the one-shot mapping identified by {id} (an internal ID generated in the
120+
" implementation) and mapped with @function(KeyMappingSpec.MapOnce) or
121+
" MapOnceWithTimeout.
122+
" Returns 1 if mapping was found and unmapped, 0 if mapping was gone already.
123+
function! maktaba#keymapping#UnmapById(id) abort
124+
let l:keymap = s:GetMappingById(a:id)
125+
if l:keymap is 0
126+
return 0
127+
endif
128+
call l:keymap.Unmap()
129+
return 1
130+
endfunction
131+
132+
133+
""
134+
" @private
135+
" Performs the actions needed for a MapOnceWithTimestamp mapping, unmapping it
136+
" by {id} if it's still mapped and conditionally mapping a simpler version of
137+
" itself to be triggered by the upcoming LHS keystrokes (mapped if
138+
" {timeout_start} + 'timeoutlen' hasn't elapsed).
139+
function! maktaba#keymapping#UnwrapForIdAndTimeoutWithRhs(
140+
\ id, timeout_start, orig_rhs) abort
141+
let l:keymap = s:GetMappingById(a:id)
142+
call l:keymap.Unmap()
143+
if reltimefloat(reltime(a:timeout_start)) < &timeoutlen
144+
" Timeout hasn't elapsed.
145+
" Remap a version of {orig_rhs} to be invoked immediately.
146+
let l:spec_without_timestamp_or_remap = l:keymap.GetSpec().WithRemap(0)
147+
let l:spec_without_timestamp_or_remap._rhs = a:orig_rhs
148+
call l:spec_without_timestamp_or_remap.MapOnce()
149+
else
150+
" Timeout has elapsed.
151+
" Register nothing, so we fall back to original {rhs}.
152+
endif
153+
endfunction
154+
155+
156+
""
157+
" @private
158+
" Creates a skeleton @dict(KeyMapping) from {spec}.
159+
" Internal helper only intended to be called by @function(KeyMappingSpec.Map).
160+
function! maktaba#keymapping#PopulateFromSpec(spec) abort
161+
return {
162+
\ '_id': s:ReserveKeyMapId(),
163+
\ '_spec': a:spec,
164+
\ '_lhs': a:spec._lhs,
165+
\ '_mode': a:spec._mode,
166+
\ '_is_noremap': a:spec._is_noremap,
167+
\ '_is_bufmap': a:spec._is_bufmap,
168+
\ 'IsMapped': s:IsMapped,
169+
\ 'Unmap': s:Unmap,
170+
\ 'GetSpec': s:GetSpec,
171+
\ '_DoMap': function('maktaba#keymapping#MapSelf'),
172+
\ '_DoMapOnce': function('maktaba#keymapping#MapSelfOnce'),
173+
\ '_DoMapOnceWithTimeout':
174+
\ function('maktaba#keymapping#MapSelfOnceWithTimeout'),
175+
\ }
176+
endfunction
177+
178+
179+
""
180+
" @private
181+
" @dict KeyMapping
182+
" Defines the key mapping in vim via the |:map| commands for the keymap in self.
183+
" Core internal implementation of @function(KeyMappingSpec.Map).
184+
function! maktaba#keymapping#MapSelf() dict abort
185+
" TODO(dbarnett): Perform a sweep for expired mapping timeouts before trying
186+
" to register more mappings (which might conflict).
187+
let l:spec = self._spec
188+
let s:KEYMAPS_BY_ID[self._id] = self
189+
execute printf('%s%smap %s %s %s',
190+
\ l:spec._mode,
191+
\ l:spec._is_noremap ? 'nore' : '',
192+
\ join(l:spec._args, ' '),
193+
\ l:spec._lhs,
194+
\ l:spec._rhs)
195+
let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1)
196+
endfunction
197+
198+
199+
""
200+
" @private
201+
" @dict KeyMapping
202+
" Define a buffer-local one-shot vim mapping from spec that will only trigger
203+
" once and then unmap itself.
204+
"
205+
" @throws NotImplemented if used with `WithRemap(1)`
206+
function! maktaba#keymapping#MapSelfOnce() dict abort
207+
let l:spec = self._spec
208+
if !l:spec._is_noremap
209+
throw maktaba#error#NotImplemented(
210+
\ "MapOnce doesn't support recursive mappings")
211+
endif
212+
let s:KEYMAPS_BY_ID[self._id] = self
213+
execute printf(
214+
\ '%snoremap %s %s %s%s',
215+
\ self._mode,
216+
\ join(l:spec._args, ' '),
217+
\ l:spec._lhs,
218+
\ s:GetFuncCallKeystrokes(
219+
\ 'maktaba#keymapping#UnmapById(' . self._id . ')',
220+
\ self._mode),
221+
\ l:spec._rhs)
222+
let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1)
223+
endfunction
224+
225+
226+
""
227+
" @private
228+
" @dict KeyMapping
229+
" Define a short-lived vim mapping from spec that will only trigger once and
230+
" will also expire if 'timeoutlen' duration expires with 'timeout' setting
231+
" active. See |KeyMappingSpec.MapOnceWithTimeout()| for details.
232+
"
233+
" @throws NotImplemented if used with `WithRemap(1)`
234+
function! maktaba#keymapping#MapSelfOnceWithTimeout() dict abort
235+
if !self._spec._is_noremap
236+
throw maktaba#error#NotImplemented(
237+
\ "MapOnceWithTimeout doesn't support recursive mappings")
238+
endif
239+
240+
" Handle cases for !has('reltime') and 'notimeout', which map without timeout.
241+
if !has('reltime')
242+
call s:plugin.logger.Info(
243+
\ 'Vim is missing +reltime feature. '
244+
\ . 'MapOnceWithTimeout fell back to mapping without timeout')
245+
call self._DoMapOnce()
246+
return
247+
elseif !&timeout
248+
call self._DoMapOnce()
249+
return
250+
endif
251+
" Handle case for timeoutlen=0, which "times out" immediately and skips the
252+
" mapping entirely. Handle will always have IsMapped()=0.
253+
if &timeoutlen == 0
254+
return
255+
endif
256+
257+
" This conditionally sends keystrokes by using a recursive mapping that will
258+
" check reltime/timeout and then invoke either
259+
" (a) a version of itself with no time check, if timeout hasn't elapsed, or
260+
" (b) a fallback to the behavior if the mapping hadn't existed.
261+
" The recursive wrapper always starts by unmapping itself and mapping an
262+
" unwrapped RHS mapping, which avoids recursing indefinitely.
263+
let l:spec = self._spec
264+
let s:KEYMAPS_BY_ID[self._id] = self
265+
" Escapes any special keystroke sequences (example: convert <Esc> to <LT>Esc>)
266+
" since they would be passed to map as special keysrokes instead of part of
267+
" the arg string.
268+
let l:escaped_rhs = substitute(l:spec._rhs, '\m<\([^>]*\)>', '<LT>\1>', 'g')
269+
" TODO(dbarnett): Also schedule a timer_start job if +timers is available to
270+
" sweep away expired maps after timeout expires.
271+
execute printf(
272+
\ '%smap %s %s %s%s',
273+
\ self._mode,
274+
\ join(['<nowait>', '<silent>'] + l:spec._args, ' '),
275+
\ l:spec._lhs,
276+
\ s:GetFuncCallKeystrokes(printf(
277+
\ 'maktaba#keymapping#UnwrapForIdAndTimeoutWithRhs(%d, %s, %s)',
278+
\ self._id,
279+
\ string(reltime()),
280+
\ string(l:escaped_rhs)),
281+
\ self._mode),
282+
\ l:spec._rhs)
283+
let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1)
284+
endfunction

0 commit comments

Comments
 (0)