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
|
|
|
|