Completed
Push — master ( 9bef6a...d8c15c )
by Lambda
56s
created

Keymap.clear()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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