Completed
Push — master ( 82760b...c05ef7 )
by Lambda
02:32
created

Key   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 49
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 49
rs 10
c 1
b 0
f 0
wmc 5

2 Methods

Rating   Name   Duplication   Size   Complexity  
B parse() 0 33 4
A __str__() 0 2 1
1
"""Key module."""
2
from curses import ascii  # type: ignore
3
from typing import cast, Union, Tuple, Dict, NamedTuple     # noqa: F401
4
from neovim import Nvim
5
from .util import ensure_bytes, ensure_str, int2chr
6
7
8
KeyCode = Union[int, bytes]
9
KeyExpr = Union[KeyCode, str]
10
11
12
ESCAPE_QUOTE = str.maketrans({
13
    '"': '\\"',
14
})
15
16
CTRL_KEY = b'\x80\xfc\x04'
17
META_KEY = b'\x80\xfc\x08'
18
19
# https://github.com/vim/vim/blob/d58b0f982ad758c59abe47627216a15497e9c3c1/src/gui_w32.c#L389-L456
20
SPECIAL_KEYS = {k: getattr(ascii, k) for k in ascii.controlnames}
21
SPECIAL_KEYS.update(dict(
22
    BSLASH=ord('\\'),
23
    LT=ord('<'),
24
    UP=b'\x80ku',
25
    DOWN=b'\x80kd',
26
    LEFT=b'\x80kl',
27
    RIGHT=b'\x80kr',
28
    F1=b'\x80k1',
29
    F2=b'\x80k2',
30
    F3=b'\x80k3',
31
    F4=b'\x80k4',
32
    F5=b'\x80k5',
33
    F6=b'\x80k6',
34
    F7=b'\x80k7',
35
    F8=b'\x80k8',
36
    F9=b'\x80k9',
37
    F10=b'\x80k;',
38
    F11=b'\x80F1',
39
    F12=b'\x80F2',
40
    F13=b'\x80F3',
41
    F14=b'\x80F4',
42
    F15=b'\x80F5',
43
    F16=b'\x80F6',
44
    F17=b'\x80F7',
45
    F18=b'\x80F8',
46
    F19=b'\x80F9',
47
    F20=b'\x80FA',
48
    F21=b'\x80FB',
49
    F22=b'\x80FC',
50
    F23=b'\x80FD',
51
    F24=b'\x80FE',
52
    HELP=b'\x80%1',
53
    BACKSPACE=b'\x80kb',
54
    INSERT=b'\x80kI',
55
    DELETE=b'\x80kD',
56
    HOME=b'\x80kh',
57
    END=b'\x80@7',
58
    PAGEUP=b'\x80kP',
59
    PAGEDOWN=b'\x80kN',
60
))
61
# Add aliases used in Vim. This requires to be AFTER making swap dictionary
62
SPECIAL_KEYS.update(dict(
63
    NOP=SPECIAL_KEYS['NUL'],
64
    RETURN=SPECIAL_KEYS['CR'],
65
    ENTER=SPECIAL_KEYS['CR'],
66
    SPACE=SPECIAL_KEYS['SP'],
67
    BS=SPECIAL_KEYS['BACKSPACE'],
68
    INS=SPECIAL_KEYS['INSERT'],
69
    DEL=SPECIAL_KEYS['DELETE'],
70
))
71
72
73
KeyBase = NamedTuple('KeyBase', [
74
    ('code', KeyCode),
75
    ('char', str),
76
])
77
78
79
class Key(KeyBase):
80
    """Key class which indicate a single key.
81
82
    Attributes:
83
        code (int or bytes): A code of the key. A bytes is used when the key is
84
            a special key in Vim (a key which starts from 0x80 in getchar()).
85
        char (str): A printable represantation of the key. It might be an empty
86
            string when the key is not printable.
87
    """
88
89
    __slots__ = ()  # type: Tuple[str, ...]
90
    __cached = {}   # type: Dict[KeyExpr, Key]
91
92
    def __str__(self) -> str:
93
        return self.char
94
95
    @classmethod
96
    def parse(cls, nvim: Nvim, expr: KeyExpr) -> 'Key':
97
        """Parse a key expression and return a Key instance.
98
99
        It returns a Key instance of a key expression. The instance is cached
100
        to individual expression so that the instance is exactly equal when
101
        same expression is spcified.
102
103
        Args:
104
            expr (int, bytes, or str): A key expression.
105
106
        Example:
107
            >>> from unittest.mock import MagicMock
108
            >>> nvim = MagicMock()
109
            >>> nvim.options = {'encoding': 'utf-8'}
110
            >>> Key.parse(nvim, ord('a'))
111
            Key(code=97, char='a')
112
            >>> Key.parse(nvim, '<Insert>')
113
            Key(code=b'\x80kI', char='')
114
115
        Returns:
116
            Key: A Key instance.
117
        """
118
        if expr not in cls.__cached:
119
            code = _resolve(nvim, expr)
120
            if isinstance(code, int):
121
                char = int2chr(nvim, code)
122
            elif not code.startswith(b'\x80'):
123
                char = ensure_str(nvim, code)
124
            else:
125
                char = ''
126
            cls.__cached[expr] = cls(code, char)
127
        return cls.__cached[expr]
128
129
130
def _resolve(nvim: Nvim, expr: KeyExpr) -> KeyCode:
131
    if isinstance(expr, int):
132
        return expr
133
    elif isinstance(expr, str):
134
        return _resolve(nvim, ensure_bytes(nvim, expr))
135
    elif isinstance(expr, bytes):
136
        if len(expr) == 1:
137
            return ord(expr)
138
        elif expr.startswith(b'\x80'):
139
            return expr
140
    else:
141
        raise AttributeError((
142
            '`expr` (%s) requires to be an instance of int|bytes|str but '
143
            '"%s" has specified.'
144
        ) % (expr, type(expr)))
145
    # Special key
146
    if expr.startswith(b'<') or expr.endswith(b'>'):
147
        inner = expr[1:-1]
148
        code = _resolve_from_special_keys(nvim, inner)
149
        if code != inner:
150
            return code
151
    return expr
152
153
154
def _resolve_from_special_keys(nvim: Nvim, inner: bytes) -> KeyCode:
155
    inner_upper = inner.upper()
156
    inner_upper_str = ensure_str(nvim, inner_upper)
157
    if inner_upper_str in SPECIAL_KEYS:
158
        return SPECIAL_KEYS[inner_upper_str]
159
    elif inner_upper.startswith(b'C-'):
160
        if len(inner) == 3:
161
            if inner_upper[-1] in b'@ABCDEFGHIKLMNOPQRSTUVWXYZ[\\]^_?':
162
                return ascii.ctrl(inner[-1])
163
        return b''.join([
164
            CTRL_KEY,
165
            cast(bytes, _resolve_from_special_keys(nvim, inner[2:])),
166
        ])
167
    elif inner_upper.startswith(b'M-') or inner_upper.startswith(b'A-'):
168
        return b''.join([
169
            META_KEY,
170
            cast(bytes, _resolve_from_special_keys(nvim, inner[2:])),
171
        ])
172
    elif inner_upper == b'LEADER':
173
        leader = nvim.vars['mapleader']
174
        leader = ensure_bytes(nvim, leader)
175
        return _resolve(nvim, leader)
176
    elif inner_upper == b'LOCALLEADER':
177
        leader = nvim.vars['maplocalleader']
178
        leader = ensure_bytes(nvim, leader)
179
        return _resolve(nvim, leader)
180
    return inner
181