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