Source code for prompt.keymap

"""Keymap."""
import time
from collections import namedtuple
from operator import itemgetter
from datetime import datetime
from .key import Key
from .keystroke import Keystroke
from .util import getchar


DefinitionBase = namedtuple('DefinitionBase', [
    'lhs',
    'rhs',
    'noremap',
    'nowait',
    'expr',
])


[docs]class Definition(DefinitionBase): """An individual keymap definition.""" __slots__ = () def __new__(cls, lhs, rhs, noremap=False, nowait=False, expr=False): if expr and not isinstance(rhs, str): raise AttributeError( '"rhs" of "expr" mapping requires to be a str.' ) return super().__new__(cls, lhs, rhs, noremap, nowait, expr) @classmethod
[docs] def parse(cls, nvim, rule): """Parse a rule (list) and return a definition instance.""" if len(rule) == 2: lhs, rhs = rule flags = '' elif len(rule) == 3: lhs, rhs, flags = rule else: raise AttributeError( 'To many arguments are specified.' ) flags = flags.split() kwargs = {} for flag in flags: if flag not in ['noremap', 'nowait', 'expr']: raise AttributeError( 'Unknown flag "%s" has specified.' % flag ) kwargs[flag] = True lhs = Keystroke.parse(nvim, lhs) if not kwargs.get('expr', False): rhs = Keystroke.parse(nvim, rhs) return cls(lhs, rhs, **kwargs)
[docs]class Keymap: """Keymap.""" __slots__ = ('registry',) def __init__(self): """Constructor.""" self.registry = {}
[docs] def register(self, definition): """Register a keymap. Args: definition (Definition): A definition instance. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> keymap = Keymap() >>> keymap.register(Definition( ... Keystroke.parse(nvim, '<C-H>'), ... Keystroke.parse(nvim, '<BS>'), ... )) >>> keymap.register(Definition( ... Keystroke.parse(nvim, '<C-H>'), ... Keystroke.parse(nvim, '<BS>'), ... noremap=True, ... )) >>> keymap.register(Definition( ... Keystroke.parse(nvim, '<C-H>'), ... Keystroke.parse(nvim, '<BS>'), ... nowait=True, ... )) >>> keymap.register(Definition( ... Keystroke.parse(nvim, '<C-H>'), ... Keystroke.parse(nvim, '<BS>'), ... noremap=True, ... nowait=True, ... )) """ self.registry[definition.lhs] = definition
[docs] def register_from_rule(self, nvim, rule): """Register a keymap from a rule. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. rule (tuple): A rule tuple. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> keymap = Keymap() >>> keymap.register_from_rule(nvim, ['<C-H>', '<BS>']) >>> keymap.register_from_rule(nvim, [ ... '<C-H>', ... '<BS>', ... 'noremap', ... ]) >>> keymap.register_from_rule(nvim, [ ... '<C-H>', ... '<BS>', ... 'noremap nowait', ... ]) """ self.register(Definition.parse(nvim, rule))
[docs] def register_from_rules(self, nvim, rules): """Register keymaps from raw rule tuple. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. rules (tuple): A tuple of rules. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> lhs1 = Keystroke.parse(nvim, '<C-H>') >>> lhs2 = Keystroke.parse(nvim, '<C-D>') >>> lhs3 = Keystroke.parse(nvim, '<C-M>') >>> rhs1 = Keystroke.parse(nvim, '<BS>') >>> rhs2 = Keystroke.parse(nvim, '<DEL>') >>> rhs3 = Keystroke.parse(nvim, '<CR>') >>> keymap = Keymap() >>> keymap.register_from_rules(nvim, [ ... (lhs1, rhs1), ... (lhs2, rhs2, 'noremap'), ... (lhs3, rhs3, 'nowait'), ... ]) """ for rule in rules: self.register_from_rule(nvim, rule)
[docs] def filter(self, lhs): """Filter keymaps by ``lhs`` Keystroke and return a sorted candidates. Args: lhs (Keystroke): A left hand side Keystroke instance. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> k = lambda x: Keystroke.parse(nvim, x) >>> keymap = Keymap() >>> keymap.register_from_rules(nvim, [ ... ('<C-A><C-A>', '<prompt:A>'), ... ('<C-A><C-B>', '<prompt:B>'), ... ('<C-B><C-A>', '<prompt:C>'), ... ]) >>> candidates = keymap.filter(k('')) >>> len(candidates) 3 >>> candidates[0] Definition(..., rhs=(Key(code=b'<prompt:A>', ...) >>> candidates[1] Definition(..., rhs=(Key(code=b'<prompt:B>', ...) >>> candidates[2] Definition(..., rhs=(Key(code=b'<prompt:C>', ...) >>> candidates = keymap.filter(k('<C-A>')) >>> len(candidates) 2 >>> candidates[0] Definition(..., rhs=(Key(code=b'<prompt:A>', ...) >>> candidates[1] Definition(..., rhs=(Key(code=b'<prompt:B>', ...) >>> candidates = keymap.filter(k('<C-A><C-A>')) >>> len(candidates) 1 >>> candidates[0] Definition(..., rhs=(Key(code=b'<prompt:A>', ...) Returns: Iterator[Definition]: Sorted Definition instances which starts from `lhs` Keystroke instance """ candidates = ( self.registry[k] for k in self.registry.keys() if k.startswith(lhs) ) return sorted(candidates, key=itemgetter(0))
[docs] def resolve(self, nvim, lhs, nowait=False): """Resolve ``lhs`` Keystroke instance and return resolved keystroke. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. lhs (Keystroke): A left hand side Keystroke instance. nowait (bool): Return a first exact matched keystroke even there are multiple keystroke instances are matched. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> k = lambda x: Keystroke.parse(nvim, x) >>> keymap = Keymap() >>> keymap.register_from_rules(nvim, [ ... ('<C-A><C-A>', '<prompt:A>'), ... ('<C-A><C-B>', '<prompt:B>'), ... ('<C-B><C-A>', '<C-A><C-A>', ''), ... ('<C-B><C-B>', '<C-A><C-B>', 'noremap'), ... ('<C-C>', '<prompt:C>', ''), ... ('<C-C><C-A>', '<prompt:C1>'), ... ('<C-C><C-B>', '<prompt:C2>'), ... ('<C-D>', '<prompt:D>', 'nowait'), ... ('<C-D><C-A>', '<prompt:D1>'), ... ('<C-D><C-B>', '<prompt:D2>'), ... ]) >>> # No mapping starts from <C-C> so <C-C> is returned >>> keymap.resolve(nvim, k('<C-Z>')) (Key(code=26, ...),) >>> # No single keystroke is resolved in the following case so None >>> # will be returned. >>> keymap.resolve(nvim, k('')) is None True >>> keymap.resolve(nvim, k('<C-A>')) is None True >>> # A single keystroke is resolved so rhs is returned. >>> # will be returned. >>> keymap.resolve(nvim, k('<C-A><C-A>')) (Key(code=b'<prompt:A>', ...),) >>> keymap.resolve(nvim, k('<C-A><C-B>')) (Key(code=b'<prompt:B>', ...),) >>> # noremap = False so recursively resolved >>> keymap.resolve(nvim, k('<C-B><C-A>')) (Key(code=b'<prompt:A>', ...),) >>> # noremap = True so resolved only once >>> keymap.resolve(nvim, k('<C-B><C-B>')) (Key(code=1, ...), Key(code=2, ...)) >>> # nowait = False so no single keystroke could be resolved. >>> keymap.resolve(nvim, k('<C-C>')) is None True >>> # nowait = True so the first matched candidate is returned. >>> keymap.resolve(nvim, k('<C-D>')) (Key(code=b'<prompt:D>', ...),) Returns: None or Keystroke: None if no single keystroke instance is resolved. Otherwise return a resolved keystroke instance or ``lhs`` itself if no mapping is available for ``lhs`` keystroke. """ candidates = list(self.filter(lhs)) n = len(candidates) if n == 0: return lhs elif n == 1: definition = candidates[0] if definition.lhs == lhs: return self._resolve(nvim, definition) elif nowait: # Use the first matched candidate if lhs is equal definition = candidates[0] if definition.lhs == lhs: return self._resolve(nvim, definition) else: # Check if the current first candidate is defined as nowait definition = candidates[0] if definition.nowait and definition.lhs == lhs: return self._resolve(nvim, definition) return None
def _resolve(self, nvim, definition): if definition.expr: rhs = Keystroke.parse(nvim, nvim.eval(definition.rhs)) else: rhs = definition.rhs if definition.noremap: return rhs return self.resolve(nvim, rhs, nowait=True)
[docs] def harvest(self, nvim, timeoutlen): """Harvest a keystroke from getchar in Vim and return resolved. It reads 'timeout' and 'timeoutlen' options in Vim and harvest a keystroke as Vim does. For example, if there is a key mapping for <C-X><C-F>, it waits 'timeoutlen' milliseconds after user hit <C-X>. If user continue <C-F> within timeout, it returns <C-X><C-F>. Otherwise it returns <C-X> before user continue <C-F>. If 'timeout' options is 0, it wait the next hit forever. Note that it returns a key immediately if the key is not a part of the registered mappings. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. Returns: Keystroke: A resolved keystroke. """ previous = None while True: code = _getcode( nvim, datetime.now() + timeoutlen if timeoutlen else None ) if code is None and previous is None: # timeout without input continue elif code is None: # timeout return self.resolve(nvim, previous, nowait=True) or previous previous = Keystroke((previous or ()) + (Key.parse(nvim, code),)) keystroke = self.resolve(nvim, previous, nowait=False) if keystroke: # resolved return keystroke
@classmethod
[docs] def from_rules(cls, nvim, rules): """Create a keymap instance from a rule tuple. Args: nvim (neovim.Nvim): A ``neovim.Nvim`` instance. rules (tuple): A tuple of rules. Example: >>> from .keystroke import Keystroke >>> from unittest.mock import MagicMock >>> nvim = MagicMock() >>> nvim.options = {'encoding': 'utf-8'} >>> lhs1 = Keystroke.parse(nvim, '<C-H>') >>> lhs2 = Keystroke.parse(nvim, '<C-D>') >>> lhs3 = Keystroke.parse(nvim, '<C-M>') >>> rhs1 = Keystroke.parse(nvim, '<BS>') >>> rhs2 = Keystroke.parse(nvim, '<DEL>') >>> rhs3 = Keystroke.parse(nvim, '<CR>') >>> keymap = Keymap.from_rules(nvim, [ ... (lhs1, rhs1), ... (lhs2, rhs2, 'noremap'), ... (lhs3, rhs3, 'nowait'), ... ]) Returns: Keymap: A keymap instance """ keymap = cls() keymap.register_from_rules(nvim, rules) return keymap
def _getcode(nvim, timeout): while not timeout or timeout > datetime.now(): code = getchar(nvim, False) if code != 0: return code time.sleep(0.01) return None DEFAULT_KEYMAP_RULES = ( ('<C-B>', '<prompt:move_caret_to_head>', 'noremap'), ('<C-E>', '<prompt:move_caret_to_tail>', 'noremap'), ('<BS>', '<prompt:delete_char_before_caret>', 'noremap'), ('<C-H>', '<prompt:delete_char_before_caret>', 'noremap'), ('<S-TAB>', '<prompt:assign_previous_text>', 'noremap'), ('<C-J>', '<prompt:accept>', 'noremap'), ('<C-K>', '<prompt:insert_digraph>', 'noremap'), ('<CR>', '<prompt:accept>', 'noremap'), ('<C-M>', '<prompt:accept>', 'noremap'), ('<C-N>', '<prompt:assign_next_text>', 'noremap'), ('<C-P>', '<prompt:assign_previous_text>', 'noremap'), ('<C-Q>', '<prompt:insert_special>', 'noremap'), ('<C-R>', '<prompt:paste_from_register>', 'noremap'), ('<C-U>', '<prompt:delete_entire_text>', 'noremap'), ('<C-V>', '<prompt:insert_special>', 'noremap'), ('<C-W>', '<prompt:delete_word_before_caret>', 'noremap'), ('<ESC>', '<prompt:cancel>', 'noremap'), ('<DEL>', '<prompt:delete_char_under_caret>', 'noremap'), ('<Left>', '<prompt:move_caret_to_left>', 'noremap'), ('<S-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'), ('<C-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'), ('<Right>', '<prompt:move_caret_to_right>', 'noremap'), ('<S-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'), ('<C-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'), ('<Up>', '<prompt:assign_previous_matched_text>', 'noremap'), ('<S-Up>', '<prompt:assign_previous_text>', 'noremap'), ('<Down>', '<prompt:assign_next_matched_text>', 'noremap'), ('<S-Down>', '<prompt:assign_next_text>', 'noremap'), ('<Home>', '<prompt:move_caret_to_head>', 'noremap'), ('<End>', '<prompt:move_caret_to_tail>', 'noremap'), ('<PageDown>', '<prompt:assign_next_text>', 'noremap'), ('<PageUp>', '<prompt:assign_previous_text>', 'noremap'), ('<INSERT>', '<prompt:toggle_insert_mode>', 'noremap'), )