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, interval=0.033): |
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
|
|
|
interval (float): Interval in seconds (Default: 0.033) |
326
|
|
|
|
327
|
|
|
Returns: |
328
|
|
|
Keystroke: A resolved keystroke. |
329
|
|
|
|
330
|
|
|
""" |
331
|
|
|
previous = None |
332
|
|
|
while True: |
333
|
|
|
code = _getcode( |
334
|
|
|
nvim, |
335
|
|
|
datetime.now() + timeoutlen if timeoutlen else None, |
336
|
|
|
callback=callback, |
337
|
|
|
interval=interval, |
338
|
|
|
) |
339
|
|
|
if code is None and previous is None: |
340
|
|
|
# timeout without input |
341
|
|
|
continue |
342
|
|
|
elif code is None: |
343
|
|
|
# timeout |
344
|
|
|
return self.resolve(nvim, previous, nowait=True) or previous |
345
|
|
|
previous = Keystroke((previous or ()) + (Key.parse(nvim, code),)) |
346
|
|
|
keystroke = self.resolve(nvim, previous, nowait=False) |
347
|
|
|
if keystroke: |
348
|
|
|
# resolved |
349
|
|
|
return keystroke |
350
|
|
|
|
351
|
|
|
@classmethod |
352
|
|
|
def from_rules(cls, nvim, rules): |
353
|
|
|
"""Create a keymap instance from a rule tuple. |
354
|
|
|
|
355
|
|
|
Args: |
356
|
|
|
nvim (neovim.Nvim): A ``neovim.Nvim`` instance. |
357
|
|
|
rules (tuple): A tuple of rules. |
358
|
|
|
|
359
|
|
|
Example: |
360
|
|
|
>>> from .keystroke import Keystroke |
361
|
|
|
>>> from unittest.mock import MagicMock |
362
|
|
|
>>> nvim = MagicMock() |
363
|
|
|
>>> nvim.options = {'encoding': 'utf-8'} |
364
|
|
|
>>> lhs1 = Keystroke.parse(nvim, '<C-H>') |
365
|
|
|
>>> lhs2 = Keystroke.parse(nvim, '<C-D>') |
366
|
|
|
>>> lhs3 = Keystroke.parse(nvim, '<C-M>') |
367
|
|
|
>>> rhs1 = Keystroke.parse(nvim, '<BS>') |
368
|
|
|
>>> rhs2 = Keystroke.parse(nvim, '<DEL>') |
369
|
|
|
>>> rhs3 = Keystroke.parse(nvim, '<CR>') |
370
|
|
|
>>> keymap = Keymap.from_rules(nvim, [ |
371
|
|
|
... (lhs1, rhs1), |
372
|
|
|
... (lhs2, rhs2, 'noremap'), |
373
|
|
|
... (lhs3, rhs3, 'nowait'), |
374
|
|
|
... ]) |
375
|
|
|
|
376
|
|
|
Returns: |
377
|
|
|
Keymap: A keymap instance |
378
|
|
|
""" |
379
|
|
|
keymap = cls() |
380
|
|
|
keymap.register_from_rules(nvim, rules) |
381
|
|
|
return keymap |
382
|
|
|
|
383
|
|
|
|
384
|
|
|
def _getcode(nvim, timeout, callback=None, interval=0.033): |
385
|
|
|
while not timeout or timeout > datetime.now(): |
386
|
|
|
if callback: |
387
|
|
|
callback() |
388
|
|
|
code = getchar(nvim, False) |
389
|
|
|
if code != 0: |
390
|
|
|
return code |
391
|
|
|
time.sleep(interval) |
392
|
|
|
return None |
393
|
|
|
|
394
|
|
|
|
395
|
|
|
DEFAULT_KEYMAP_RULES = ( |
396
|
|
|
('<C-B>', '<prompt:move_caret_to_head>', 'noremap'), |
397
|
|
|
('<C-E>', '<prompt:move_caret_to_tail>', 'noremap'), |
398
|
|
|
('<BS>', '<prompt:delete_char_before_caret>', 'noremap'), |
399
|
|
|
('<C-H>', '<prompt:delete_char_before_caret>', 'noremap'), |
400
|
|
|
('<S-TAB>', '<prompt:assign_previous_text>', 'noremap'), |
401
|
|
|
('<C-J>', '<prompt:accept>', 'noremap'), |
402
|
|
|
('<C-K>', '<prompt:insert_digraph>', 'noremap'), |
403
|
|
|
('<CR>', '<prompt:accept>', 'noremap'), |
404
|
|
|
('<C-M>', '<prompt:accept>', 'noremap'), |
405
|
|
|
('<C-N>', '<prompt:assign_next_text>', 'noremap'), |
406
|
|
|
('<C-P>', '<prompt:assign_previous_text>', 'noremap'), |
407
|
|
|
('<C-Q>', '<prompt:insert_special>', 'noremap'), |
408
|
|
|
('<C-R>', '<prompt:paste_from_register>', 'noremap'), |
409
|
|
|
('<C-U>', '<prompt:delete_entire_text>', 'noremap'), |
410
|
|
|
('<C-V>', '<prompt:insert_special>', 'noremap'), |
411
|
|
|
('<C-W>', '<prompt:delete_word_before_caret>', 'noremap'), |
412
|
|
|
('<ESC>', '<prompt:cancel>', 'noremap'), |
413
|
|
|
('<DEL>', '<prompt:delete_char_under_caret>', 'noremap'), |
414
|
|
|
('<Left>', '<prompt:move_caret_to_left>', 'noremap'), |
415
|
|
|
('<S-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'), |
416
|
|
|
('<C-Left>', '<prompt:move_caret_to_one_word_left>', 'noremap'), |
417
|
|
|
('<Right>', '<prompt:move_caret_to_right>', 'noremap'), |
418
|
|
|
('<S-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'), |
419
|
|
|
('<C-Right>', '<prompt:move_caret_to_one_word_right>', 'noremap'), |
420
|
|
|
('<Up>', '<prompt:assign_previous_matched_text>', 'noremap'), |
421
|
|
|
('<S-Up>', '<prompt:assign_previous_text>', 'noremap'), |
422
|
|
|
('<Down>', '<prompt:assign_next_matched_text>', 'noremap'), |
423
|
|
|
('<S-Down>', '<prompt:assign_next_text>', 'noremap'), |
424
|
|
|
('<Home>', '<prompt:move_caret_to_head>', 'noremap'), |
425
|
|
|
('<End>', '<prompt:move_caret_to_tail>', 'noremap'), |
426
|
|
|
('<PageDown>', '<prompt:assign_next_text>', 'noremap'), |
427
|
|
|
('<PageUp>', '<prompt:assign_previous_text>', 'noremap'), |
428
|
|
|
('<INSERT>', '<prompt:toggle_insert_mode>', 'noremap'), |
429
|
|
|
) |
430
|
|
|
|