Completed
Push — master ( dfe802...608c2f )
by Lambda
01:39
created

_getcode()   B

Complexity

Conditions 5

Size

Total Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
c 3
b 0
f 0
dl 0
loc 9
rs 8.5454
1
"""Keymap."""
2
import time
3
from collections import namedtuple
4
from operator import itemgetter
5
from datetime import datetime
6
from .key import Key
7
from .keystroke import Keystroke
8
from .util import getchar
9
10
11
DefinitionBase = namedtuple('DefinitionBase', [
12
    'lhs',
13
    'rhs',
14
    'noremap',
15
    'nowait',
16
    'expr',
17
])
18
19
20
class Definition(DefinitionBase):
21
    """An individual keymap definition."""
22
23
    __slots__ = ()
24
25
    def __new__(cls, lhs, rhs, noremap=False, nowait=False, expr=False):
26
        if expr and not isinstance(rhs, str):
27
            raise AttributeError(
28
                '"rhs" of "expr" mapping requires to be a str.'
29
            )
30
        return super().__new__(cls, lhs, rhs, noremap, nowait, expr)
31
32
    @classmethod
33
    def parse(cls, nvim, rule):
34
        """Parse a rule (list) and return a definition instance."""
35
        if len(rule) == 2:
36
            lhs, rhs = rule
37
            flags = ''
38
        elif len(rule) == 3:
39
            lhs, rhs, flags = rule
40
        else:
41
            raise AttributeError(
42
                'To many arguments are specified.'
43
            )
44
        flags = flags.split()
45
        kwargs = {}
46
        for flag in flags:
47
            if flag not in ['noremap', 'nowait', 'expr']:
48
                raise AttributeError(
49
                    'Unknown flag "%s" has specified.' % flag
50
                )
51
            kwargs[flag] = True
52
        lhs = Keystroke.parse(nvim, lhs)
53
        if not kwargs.get('expr', False):
54
            rhs = Keystroke.parse(nvim, rhs)
55
        return cls(lhs, rhs, **kwargs)
56
57
58
class Keymap:
59
    """Keymap."""
60
61
    __slots__ = ('registry',)
62
63
    def __init__(self):
64
        """Constructor."""
65
        self.registry = {}
66
67
    def register(self, definition):
68
        """Register a keymap.
69
70
        Args:
71
            definition (Definition): A definition instance.
72
73
        Example:
74
            >>> from .keystroke import Keystroke
75
            >>> from unittest.mock import MagicMock
76
            >>> nvim = MagicMock()
77
            >>> nvim.options = {'encoding': 'utf-8'}
78
            >>> keymap = Keymap()
79
            >>> keymap.register(Definition(
80
            ...     Keystroke.parse(nvim, '<C-H>'),
81
            ...     Keystroke.parse(nvim, '<BS>'),
82
            ... ))
83
            >>> keymap.register(Definition(
84
            ...     Keystroke.parse(nvim, '<C-H>'),
85
            ...     Keystroke.parse(nvim, '<BS>'),
86
            ...     noremap=True,
87
            ... ))
88
            >>> keymap.register(Definition(
89
            ...     Keystroke.parse(nvim, '<C-H>'),
90
            ...     Keystroke.parse(nvim, '<BS>'),
91
            ...     nowait=True,
92
            ... ))
93
            >>> keymap.register(Definition(
94
            ...     Keystroke.parse(nvim, '<C-H>'),
95
            ...     Keystroke.parse(nvim, '<BS>'),
96
            ...     noremap=True,
97
            ...     nowait=True,
98
            ... ))
99
100
        """
101
        self.registry[definition.lhs] = definition
102
103
    def register_from_rule(self, nvim, rule):
104
        """Register a keymap from a rule.
105
106
        Args:
107
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
108
            rule (tuple): A rule tuple.
109
110
        Example:
111
            >>> from .keystroke import Keystroke
112
            >>> from unittest.mock import MagicMock
113
            >>> nvim = MagicMock()
114
            >>> nvim.options = {'encoding': 'utf-8'}
115
            >>> keymap = Keymap()
116
            >>> keymap.register_from_rule(nvim, ['<C-H>', '<BS>'])
117
            >>> keymap.register_from_rule(nvim, [
118
            ...     '<C-H>',
119
            ...     '<BS>',
120
            ...     'noremap',
121
            ... ])
122
            >>> keymap.register_from_rule(nvim, [
123
            ...     '<C-H>',
124
            ...     '<BS>',
125
            ...     'noremap nowait',
126
            ... ])
127
128
        """
129
        self.register(Definition.parse(nvim, rule))
130
131
    def register_from_rules(self, nvim, rules):
132
        """Register keymaps from raw rule tuple.
133
134
        Args:
135
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
136
            rules (tuple): A tuple of rules.
137
138
        Example:
139
            >>> from .keystroke import Keystroke
140
            >>> from unittest.mock import MagicMock
141
            >>> nvim = MagicMock()
142
            >>> nvim.options = {'encoding': 'utf-8'}
143
            >>> lhs1 = Keystroke.parse(nvim, '<C-H>')
144
            >>> lhs2 = Keystroke.parse(nvim, '<C-D>')
145
            >>> lhs3 = Keystroke.parse(nvim, '<C-M>')
146
            >>> rhs1 = Keystroke.parse(nvim, '<BS>')
147
            >>> rhs2 = Keystroke.parse(nvim, '<DEL>')
148
            >>> rhs3 = Keystroke.parse(nvim, '<CR>')
149
            >>> keymap = Keymap()
150
            >>> keymap.register_from_rules(nvim, [
151
            ...     (lhs1, rhs1),
152
            ...     (lhs2, rhs2, 'noremap'),
153
            ...     (lhs3, rhs3, 'nowait'),
154
            ... ])
155
156
        """
157
        for rule in rules:
158
            self.register_from_rule(nvim, rule)
159
160
    def filter(self, lhs):
161
        """Filter keymaps by ``lhs`` Keystroke and return a sorted candidates.
162
163
        Args:
164
            lhs (Keystroke): A left hand side Keystroke instance.
165
166
        Example:
167
            >>> from .keystroke import Keystroke
168
            >>> from unittest.mock import MagicMock
169
            >>> nvim = MagicMock()
170
            >>> nvim.options = {'encoding': 'utf-8'}
171
            >>> k = lambda x: Keystroke.parse(nvim, x)
172
            >>> keymap = Keymap()
173
            >>> keymap.register_from_rules(nvim, [
174
            ...     ('<C-A><C-A>', '<prompt:A>'),
175
            ...     ('<C-A><C-B>', '<prompt:B>'),
176
            ...     ('<C-B><C-A>', '<prompt:C>'),
177
            ... ])
178
            >>> candidates = keymap.filter(k(''))
179
            >>> len(candidates)
180
            3
181
            >>> candidates[0]
182
            Definition(..., rhs=(Key(code=b'<prompt:A>', ...)
183
            >>> candidates[1]
184
            Definition(..., rhs=(Key(code=b'<prompt:B>', ...)
185
            >>> candidates[2]
186
            Definition(..., rhs=(Key(code=b'<prompt:C>', ...)
187
            >>> candidates = keymap.filter(k('<C-A>'))
188
            >>> len(candidates)
189
            2
190
            >>> candidates[0]
191
            Definition(..., rhs=(Key(code=b'<prompt:A>', ...)
192
            >>> candidates[1]
193
            Definition(..., rhs=(Key(code=b'<prompt:B>', ...)
194
            >>> candidates = keymap.filter(k('<C-A><C-A>'))
195
            >>> len(candidates)
196
            1
197
            >>> candidates[0]
198
            Definition(..., rhs=(Key(code=b'<prompt:A>', ...)
199
200
        Returns:
201
            Iterator[Definition]: Sorted Definition instances which starts from
202
                `lhs` Keystroke instance
203
        """
204
        candidates = (
205
            self.registry[k]
206
            for k in self.registry.keys() if k.startswith(lhs)
207
        )
208
        return sorted(candidates, key=itemgetter(0))
209
210
    def resolve(self, nvim, lhs, nowait=False):
211
        """Resolve ``lhs`` Keystroke instance and return resolved keystroke.
212
213
        Args:
214
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
215
            lhs (Keystroke): A left hand side Keystroke instance.
216
            nowait (bool): Return a first exact matched keystroke even there
217
                are multiple keystroke instances are matched.
218
219
        Example:
220
            >>> from .keystroke import Keystroke
221
            >>> from unittest.mock import MagicMock
222
            >>> nvim = MagicMock()
223
            >>> nvim.options = {'encoding': 'utf-8'}
224
            >>> k = lambda x: Keystroke.parse(nvim, x)
225
            >>> keymap = Keymap()
226
            >>> keymap.register_from_rules(nvim, [
227
            ...     ('<C-A><C-A>', '<prompt:A>'),
228
            ...     ('<C-A><C-B>', '<prompt:B>'),
229
            ...     ('<C-B><C-A>', '<C-A><C-A>', ''),
230
            ...     ('<C-B><C-B>', '<C-A><C-B>', 'noremap'),
231
            ...     ('<C-C>', '<prompt:C>', ''),
232
            ...     ('<C-C><C-A>', '<prompt:C1>'),
233
            ...     ('<C-C><C-B>', '<prompt:C2>'),
234
            ...     ('<C-D>', '<prompt:D>', 'nowait'),
235
            ...     ('<C-D><C-A>', '<prompt:D1>'),
236
            ...     ('<C-D><C-B>', '<prompt:D2>'),
237
            ... ])
238
            >>> # No mapping starts from <C-C> so <C-C> is returned
239
            >>> keymap.resolve(nvim, k('<C-Z>'))
240
            (Key(code=26, ...),)
241
            >>> # No single keystroke is resolved in the following case so None
242
            >>> # will be returned.
243
            >>> keymap.resolve(nvim, k('')) is None
244
            True
245
            >>> keymap.resolve(nvim, k('<C-A>')) is None
246
            True
247
            >>> # A single keystroke is resolved so rhs is returned.
248
            >>> # will be returned.
249
            >>> keymap.resolve(nvim, k('<C-A><C-A>'))
250
            (Key(code=b'<prompt:A>', ...),)
251
            >>> keymap.resolve(nvim, k('<C-A><C-B>'))
252
            (Key(code=b'<prompt:B>', ...),)
253
            >>> # noremap = False so recursively resolved
254
            >>> keymap.resolve(nvim, k('<C-B><C-A>'))
255
            (Key(code=b'<prompt:A>', ...),)
256
            >>> # noremap = True so resolved only once
257
            >>> keymap.resolve(nvim, k('<C-B><C-B>'))
258
            (Key(code=1, ...), Key(code=2, ...))
259
            >>> # nowait = False so no single keystroke could be resolved.
260
            >>> keymap.resolve(nvim, k('<C-C>')) is None
261
            True
262
            >>> # nowait = True so the first matched candidate is returned.
263
            >>> keymap.resolve(nvim, k('<C-D>'))
264
            (Key(code=b'<prompt:D>', ...),)
265
266
        Returns:
267
            None or Keystroke: None if no single keystroke instance is
268
                resolved. Otherwise return a resolved keystroke instance or
269
                ``lhs`` itself if no mapping is available for ``lhs``
270
                keystroke.
271
        """
272
        candidates = list(self.filter(lhs))
273
        n = len(candidates)
274
        if n == 0:
275
            return lhs
276
        elif n == 1:
277
            definition = candidates[0]
278
            if definition.lhs == lhs:
279
                return self._resolve(nvim, definition)
280
        elif nowait:
281
            # Use the first matched candidate if lhs is equal
282
            definition = candidates[0]
283
            if definition.lhs == lhs:
284
                return self._resolve(nvim, definition)
285
        else:
286
            # Check if the current first candidate is defined as nowait
287
            definition = candidates[0]
288
            if definition.nowait and definition.lhs == lhs:
289
                return self._resolve(nvim, definition)
290
        return None
291
292
    def _resolve(self, nvim, definition):
293
        if definition.expr:
294
            rhs = Keystroke.parse(nvim, nvim.eval(definition.rhs))
295
        else:
296
            rhs = definition.rhs
297
        if definition.noremap:
298
            return rhs
299
        return self.resolve(nvim, rhs, nowait=True)
300
301
    def harvest(self, nvim, timeoutlen, callback=None):
302
        """Harvest a keystroke from getchar in Vim and return resolved.
303
304
        It reads 'timeout' and 'timeoutlen' options in Vim and harvest a
305
        keystroke as Vim does. For example, if there is a key mapping for
306
        <C-X><C-F>, it waits 'timeoutlen' milliseconds after user hit <C-X>.
307
        If user continue <C-F> within timeout, it returns <C-X><C-F>. Otherwise
308
        it returns <C-X> before user continue <C-F>.
309
        If 'timeout' options is 0, it wait the next hit forever.
310
311
        Note that it returns a key immediately if the key is not a part of the
312
        registered mappings.
313
314
        Args:
315
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
316
317
        Returns:
318
            Keystroke: A resolved keystroke.
319
320
        """
321
        previous = None
322
        while True:
323
            code = _getcode(
324
                nvim,
325
                datetime.now() + timeoutlen if timeoutlen else None
326
            )
327
            if code is None and previous is None:
328
                # timeout without input
329
                continue
330
            elif code is None:
331
                # timeout
332
                return self.resolve(nvim, previous, nowait=True) or previous
333
            previous = Keystroke((previous or ()) + (Key.parse(nvim, code),))
334
            keystroke = self.resolve(nvim, previous, nowait=False)
335
            if keystroke:
336
                # resolved
337
                return keystroke
338
339
    @classmethod
340
    def from_rules(cls, nvim, rules):
341
        """Create a keymap instance from a rule tuple.
342
343
        Args:
344
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
345
            rules (tuple): A tuple of rules.
346
347
        Example:
348
            >>> from .keystroke import Keystroke
349
            >>> from unittest.mock import MagicMock
350
            >>> nvim = MagicMock()
351
            >>> nvim.options = {'encoding': 'utf-8'}
352
            >>> lhs1 = Keystroke.parse(nvim, '<C-H>')
353
            >>> lhs2 = Keystroke.parse(nvim, '<C-D>')
354
            >>> lhs3 = Keystroke.parse(nvim, '<C-M>')
355
            >>> rhs1 = Keystroke.parse(nvim, '<BS>')
356
            >>> rhs2 = Keystroke.parse(nvim, '<DEL>')
357
            >>> rhs3 = Keystroke.parse(nvim, '<CR>')
358
            >>> keymap = Keymap.from_rules(nvim, [
359
            ...     (lhs1, rhs1),
360
            ...     (lhs2, rhs2, 'noremap'),
361
            ...     (lhs3, rhs3, 'nowait'),
362
            ... ])
363
364
        Returns:
365
            Keymap: A keymap instance
366
        """
367
        keymap = cls()
368
        keymap.register_from_rules(nvim, rules)
369
        return keymap
370
371
372
def _getcode(nvim, timeout, callback=None):
373
    while not timeout or timeout > datetime.now():
374
        code = getchar(nvim, False)
375
        if code != 0:
376
            return code
377
        if callback:
378
            callback()
379
        time.sleep(0.01)
380
    return None
381
382
383
DEFAULT_KEYMAP_RULES = (
384
    ('<C-B>', '<prompt:move_caret_to_head>', 'noremap'),
385
    ('<C-E>', '<prompt:move_caret_to_tail>', 'noremap'),
386
    ('<BS>', '<prompt:delete_char_before_caret>', 'noremap'),
387
    ('<C-H>', '<prompt:delete_char_before_caret>', 'noremap'),
388
    ('<S-TAB>', '<prompt:assign_previous_text>', 'noremap'),
389
    ('<C-J>', '<prompt:accept>', 'noremap'),
390
    ('<C-K>', '<prompt:insert_digraph>', 'noremap'),
391
    ('<CR>', '<prompt:accept>', 'noremap'),
392
    ('<C-M>', '<prompt:accept>', 'noremap'),
393
    ('<C-N>', '<prompt:assign_next_text>', 'noremap'),
394
    ('<C-P>', '<prompt:assign_previous_text>', 'noremap'),
395
    ('<C-Q>', '<prompt:insert_special>', 'noremap'),
396
    ('<C-R>', '<prompt:paste_from_register>', 'noremap'),
397
    ('<C-U>', '<prompt:delete_entire_text>', 'noremap'),
398
    ('<C-V>', '<prompt:insert_special>', 'noremap'),
399
    ('<C-W>', '<prompt:delete_word_before_caret>', 'noremap'),
400
    ('<ESC>', '<prompt:cancel>', 'noremap'),
401
    ('<DEL>', '<prompt:delete_char_under_caret>', 'noremap'),
402
    ('<Left>', '<prompt:move_caret_to_left>', 'noremap'),
403
    ('<S-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'),
404
    ('<C-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'),
405
    ('<Right>', '<prompt:move_caret_to_right>', 'noremap'),
406
    ('<S-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'),
407
    ('<C-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'),
408
    ('<Up>', '<prompt:assign_previous_matched_text>', 'noremap'),
409
    ('<S-Up>', '<prompt:assign_previous_text>', 'noremap'),
410
    ('<Down>', '<prompt:assign_next_matched_text>', 'noremap'),
411
    ('<S-Down>', '<prompt:assign_next_text>', 'noremap'),
412
    ('<Home>', '<prompt:move_caret_to_head>', 'noremap'),
413
    ('<End>', '<prompt:move_caret_to_tail>', 'noremap'),
414
    ('<PageDown>', '<prompt:assign_next_text>', 'noremap'),
415
    ('<PageUp>', '<prompt:assign_previous_text>', 'noremap'),
416
    ('<INSERT>', '<prompt:toggle_insert_mode>', 'noremap'),
417
)
418