Completed
Push — master ( d6ee56...e81bdf )
by Lambda
58s
created

Definition.parse()   B

Complexity

Conditions 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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