-
Notifications
You must be signed in to change notification settings - Fork 40
Implement keymapping helpers #222
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dbarnett
wants to merge
3
commits into
master
Choose a base branch
from
keymap_reltime
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| "" | ||
| " @dict KeyMapping | ||
| " A maktaba representation of a Vim key mapping, which is used to configure and | ||
| " unmap it from Vim. | ||
|
|
||
|
|
||
| if !exists('s:next_keymap_id') | ||
| let s:next_keymap_id = 0 | ||
| endif | ||
|
|
||
| if !exists('s:KEYMAPS_BY_ID') | ||
| let s:KEYMAPS_BY_ID = {} | ||
| endif | ||
|
|
||
|
|
||
| function! s:ReserveKeyMapId() abort | ||
| let l:keymap_id = s:next_keymap_id | ||
| let s:next_keymap_id += 1 | ||
| return l:keymap_id | ||
| endfunction | ||
|
|
||
|
|
||
| function! s:GetMappingById(id) abort | ||
| try | ||
| return s:KEYMAPS_BY_ID[a:id] | ||
| catch /E716:/ | ||
| return 0 | ||
| endtry | ||
| endfunction | ||
|
|
||
|
|
||
| function! s:GetFuncCallKeystrokes(funcstr, mode) abort | ||
| if a:mode ==# 'n' | ||
| return printf(':call %s<CR>', a:funcstr) | ||
| elseif a:mode ==# 'i' | ||
| return printf('<C-\><C-o>:call %s<CR>', a:funcstr) | ||
| elseif a:mode ==# 'v' | ||
| " Needs C-u to remove "'<,'>" range inserted for commands from visual mode. | ||
| " Uses "gv" at the end to re-enter visual mode. | ||
| return printf(':<C-u>call %s<CR>gv', a:funcstr) | ||
| elseif a:mode ==# 's' | ||
dbarnett marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| " Uses "gv<C-g>" at the end to re-enter select mode. | ||
| return printf('<C-\><C-o>:<C-u>call %s<CR>gv<C-g>', a:funcstr) | ||
| endif | ||
| throw maktaba#error#NotImplemented( | ||
| \ 'MapOnce not implemented for mode %s', a:mode) | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @dict KeyMapping | ||
| " Returns 1 if the mapping is still defined, 0 otherwise | ||
| function! maktaba#keymapping#IsMapped() dict abort | ||
| let l:foundmap = maparg(self._lhs, self._mode, 0, 1) | ||
| return !empty(l:foundmap) && l:foundmap == self._maparg | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @dict KeyMapping | ||
| " Unmaps the mapping in Vim. | ||
| " Returns 1 if mapping was found and unmapped, 0 if mapping was gone already. | ||
| function! maktaba#keymapping#Unmap() dict abort | ||
| if self.IsMapped() | ||
| let l:arg_prefix = self._maparg.buffer ? '<buffer> ' : '' | ||
| execute printf( | ||
| \ 'silent %sunmap %s%s', | ||
| \ self._maparg.mode, | ||
| \ l:arg_prefix, | ||
| \ self._maparg.lhs) | ||
| if has_key(s:KEYMAPS_BY_ID, self._id) | ||
| unlet s:KEYMAPS_BY_ID[self._id] | ||
| endif | ||
| return 1 | ||
| else | ||
| return 0 | ||
| endif | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @dict KeyMapping | ||
| " Return a copy of the spec used to issue this mapping. | ||
| function! maktaba#keymapping#GetSpec() dict abort | ||
| return copy(self._spec) | ||
| endfunction | ||
|
|
||
|
|
||
| let s:IsMapped = function('maktaba#keymapping#IsMapped') | ||
| let s:Unmap = function('maktaba#keymapping#Unmap') | ||
| let s:GetSpec = function('maktaba#keymapping#GetSpec') | ||
|
|
||
|
|
||
| "" | ||
| " Set up a key mapping in Vim, mapping key sequence {lhs} to replacement | ||
| " sequence {rhs} in the given [mode]. It is equivalent to calling: > | ||
| " :call maktaba#keymappingspec#Spec({lhs}, {rhs}, [mode]).Map() | ||
| " < | ||
| " | ||
| " See the |maktaba#keymappingspec#Spec()| and |KeyMappingSpec.Map()| functions | ||
| " for usage and behavior details. | ||
| " | ||
| " @default mode=all of 'n', 'v', and 'o' (Vim's default) | ||
| function! maktaba#keymapping#Map(lhs, rhs, ...) abort | ||
| if a:0 >= 1 | ||
| let l:spec = maktaba#keymappingspec#Spec(a:lhs, a:rhs, a:1) | ||
| else | ||
| let l:spec = maktaba#keymappingspec#Spec(a:lhs, a:rhs) | ||
| endif | ||
| return l:spec.Map() | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " Unmap the one-shot mapping identified by {id} (an internal ID generated in the | ||
| " implementation) and mapped with @function(KeyMappingSpec.MapOnce) or | ||
| " MapOnceWithTimeout. | ||
| " Returns 1 if mapping was found and unmapped, 0 if mapping was gone already. | ||
| function! maktaba#keymapping#UnmapById(id) abort | ||
| let l:keymap = s:GetMappingById(a:id) | ||
| if l:keymap is 0 | ||
| return 0 | ||
| endif | ||
| call l:keymap.Unmap() | ||
| return 1 | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " Performs the actions needed for a MapOnceWithTimestamp mapping, unmapping it | ||
| " by {id} if it's still mapped and conditionally mapping a simpler version of | ||
| " itself to be triggered by the upcoming LHS keystrokes (mapped if | ||
| " {timeout_start} + 'timeoutlen' hasn't elapsed). | ||
| function! maktaba#keymapping#UnwrapForIdAndTimeoutWithRhs( | ||
| \ id, timeout_start, orig_rhs) abort | ||
| let l:keymap = s:GetMappingById(a:id) | ||
| call l:keymap.Unmap() | ||
| if reltimefloat(reltime(a:timeout_start)) < &timeoutlen | ||
| " Timeout hasn't elapsed. | ||
| " Remap a version of {orig_rhs} to be invoked immediately. | ||
| let l:spec_without_timestamp_or_remap = l:keymap.GetSpec().WithRemap(0) | ||
| let l:spec_without_timestamp_or_remap._rhs = a:orig_rhs | ||
| call l:spec_without_timestamp_or_remap.MapOnce() | ||
| else | ||
| " Timeout has elapsed. | ||
| " Register nothing, so we fall back to original {rhs}. | ||
| endif | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " Creates a skeleton @dict(KeyMapping) from {spec}. | ||
| " Internal helper only intended to be called by @function(KeyMappingSpec.Map). | ||
| function! maktaba#keymapping#PopulateFromSpec(spec) abort | ||
| return { | ||
| \ '_id': s:ReserveKeyMapId(), | ||
| \ '_spec': a:spec, | ||
| \ '_lhs': a:spec._lhs, | ||
| \ '_mode': a:spec._mode, | ||
| \ '_is_noremap': a:spec._is_noremap, | ||
| \ '_is_bufmap': a:spec._is_bufmap, | ||
| \ 'IsMapped': s:IsMapped, | ||
| \ 'Unmap': s:Unmap, | ||
| \ 'GetSpec': s:GetSpec, | ||
| \ '_DoMap': function('maktaba#keymapping#MapSelf'), | ||
| \ '_DoMapOnce': function('maktaba#keymapping#MapSelfOnce'), | ||
| \ '_DoMapOnceWithTimeout': | ||
| \ function('maktaba#keymapping#MapSelfOnceWithTimeout'), | ||
| \ } | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " @dict KeyMapping | ||
| " Defines the key mapping in Vim via the |:map| commands for the keymap in self. | ||
| " Core internal implementation of @function(KeyMappingSpec.Map). | ||
| function! maktaba#keymapping#MapSelf() dict abort | ||
| " TODO(dbarnett): Perform a sweep for expired mapping timeouts before trying | ||
| " to register more mappings (which might conflict). | ||
| let l:spec = self._spec | ||
| let s:KEYMAPS_BY_ID[self._id] = self | ||
| execute printf('%s%smap %s %s %s', | ||
| \ l:spec._mode, | ||
| \ l:spec._is_noremap ? 'nore' : '', | ||
| \ join(l:spec._args, ' '), | ||
| \ l:spec._lhs, | ||
| \ l:spec._rhs) | ||
| let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1) | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " @dict KeyMapping | ||
| " Define a buffer-local one-shot Vim mapping from spec that will only trigger | ||
| " once and then unmap itself. | ||
| " | ||
| " @throws NotImplemented if used with `WithRemap(1)` | ||
| function! maktaba#keymapping#MapSelfOnce() dict abort | ||
| let l:spec = self._spec | ||
| if !l:spec._is_noremap | ||
| throw maktaba#error#NotImplemented( | ||
| \ "MapOnce doesn't support recursive mappings") | ||
| endif | ||
| let s:KEYMAPS_BY_ID[self._id] = self | ||
| execute printf( | ||
| \ '%snoremap %s %s %s%s', | ||
| \ self._mode, | ||
| \ join(l:spec._args, ' '), | ||
| \ l:spec._lhs, | ||
| \ s:GetFuncCallKeystrokes( | ||
| \ 'maktaba#keymapping#UnmapById(' . self._id . ')', | ||
| \ self._mode), | ||
| \ l:spec._rhs) | ||
| let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1) | ||
| endfunction | ||
|
|
||
|
|
||
| "" | ||
| " @private | ||
| " @dict KeyMapping | ||
| " Define a short-lived Vim mapping from spec that will only trigger once and | ||
| " will also expire if 'timeoutlen' duration expires with 'timeout' setting | ||
| " active. See |KeyMappingSpec.MapOnceWithTimeout()| for details. | ||
| " | ||
| " @throws NotImplemented if used with `WithRemap(1)` | ||
| function! maktaba#keymapping#MapSelfOnceWithTimeout() dict abort | ||
| if !self._spec._is_noremap | ||
| throw maktaba#error#NotImplemented( | ||
| \ "MapOnceWithTimeout doesn't support recursive mappings") | ||
| endif | ||
|
|
||
| " Handle cases for !has('reltime') and 'notimeout', which map without timeout. | ||
| if !has('reltime') | ||
| call s:plugin.logger.Info( | ||
| \ 'Vim is missing +reltime feature. ' | ||
| \ . 'MapOnceWithTimeout fell back to mapping without timeout') | ||
| call self._DoMapOnce() | ||
| return | ||
| elseif !&timeout | ||
| call self._DoMapOnce() | ||
| return | ||
| endif | ||
| " Handle case for timeoutlen=0, which "times out" immediately and skips the | ||
| " mapping entirely. Handle will always have IsMapped()=0. | ||
| if &timeoutlen == 0 | ||
| return | ||
| endif | ||
|
|
||
| " This conditionally sends keystrokes by using a recursive mapping that will | ||
| " check reltime/timeout and then invoke either | ||
| " (a) a version of itself with no time check, if timeout hasn't elapsed, or | ||
| " (b) a fallback to the behavior if the mapping hadn't existed. | ||
| " The recursive wrapper always starts by unmapping itself and mapping an | ||
| " unwrapped RHS mapping, which avoids recursing indefinitely. | ||
| let l:spec = self._spec | ||
| let s:KEYMAPS_BY_ID[self._id] = self | ||
| " Escapes any special keystroke sequences (example: convert <Esc> to <LT>Esc>) | ||
| " since they would be passed to map as special keysrokes instead of part of | ||
| " the arg string. | ||
| let l:escaped_rhs = substitute(l:spec._rhs, '\m<\([^>]*\)>', '<LT>\1>', 'g') | ||
| " TODO(dbarnett): Also schedule a timer_start job if +timers is available to | ||
| " sweep away expired maps after timeout expires. | ||
| execute printf( | ||
| \ '%smap %s %s %s%s', | ||
| \ self._mode, | ||
| \ join(['<nowait>', '<silent>'] + l:spec._args, ' '), | ||
| \ l:spec._lhs, | ||
| \ s:GetFuncCallKeystrokes(printf( | ||
| \ 'maktaba#keymapping#UnwrapForIdAndTimeoutWithRhs(%d, %s, %s)', | ||
| \ self._id, | ||
| \ string(reltime()), | ||
| \ string(l:escaped_rhs)), | ||
| \ self._mode), | ||
| \ l:spec._rhs) | ||
| let self._maparg = maparg(l:spec._lhs, self._mode, 0, 1) | ||
| endfunction | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.