|
| 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 | + 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