Action.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 1
c 3
b 0
f 0
dl 0
loc 3
rs 10
1
"""Prompt action module."""
2
import re
3
from .digraph import Digraph
4
from .util import getchar, int2char, int2repr, build_keyword_pattern_set
5
6
7
ACTION_PATTERN = re.compile(
8
    r'(?P<name>(?:\w+):(?P<label>\w+))(?::(?P<params>.+))?'
9
)
10
"""Action name pattern."""
11
12
13
class Action:
14
    """Action class which hold action callbacks.
15
16
    Note:
17
        This class defines ``__slots__`` attribute so sub-class must override
18
        the attribute to extend available attributes.
19
20
    Attributes:
21
        registry (dict): An action dictionary.
22
    """
23
24
    __slots__ = ('registry',)
25
26
    def __init__(self):
27
        """Constructor."""
28
        self.registry = {}
29
30
    def clear(self):
31
        """Clear registered actions."""
32
        self.registry.clear()
33
34
    def register(self, name, callback):
35
        """Register action callback to a specified name.
36
37
        Args:
38
            name (str): An action name which follow
39
                {namespace}:{action name}:{params}
40
            callback (Callable[Prompt, str]): An action callback which take a
41
                ``prompt.prompt.Prompt`` instance, str and return None or int.
42
43
        Example:
44
            >>> from .prompt import STATUS_ACCEPT
45
            >>> action = Action()
46
            >>> action.register(
47
            ...     'prompt:accept', lambda prompt, params: STATUS_ACCEPT
48
            ... )
49
        """
50
        self.registry[name] = callback
51
52
    def unregister(self, name, fail_silently=False):
53
        """Unregister a specified named action when exists.
54
55
        Args:
56
            name (str): An action name which follow
57
                {namespace}:{action name}
58
            fail_silently (bool): Do not raise KeyError even the name is
59
                missing in a registry
60
61
        Example:
62
            >>> from .prompt import STATUS_ACCEPT
63
            >>> action = Action()
64
            >>> action.register(
65
            ...     'prompt:accept', lambda prompt, params: STATUS_ACCEPT
66
            ... )
67
            >>> action.unregister(
68
            ...     'prompt:accept',
69
            ... )
70
        """
71
        try:
72
            del self.registry[name]
73
        except KeyError as e:
74
            if not fail_silently:
75
                raise e
76
77
    def register_from_rules(self, rules) -> None:
78
        """Register action callbacks from rules.
79
80
        Args:
81
            rules (Iterable): An iterator which returns rules. A rule is a
82
                (name, callback) tuple.
83
84
        Example:
85
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
86
            >>> action = Action()
87
            >>> action.register_from_rules([
88
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
89
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
90
            ... ])
91
        """
92
        for rule in rules:
93
            self.register(*rule)
94
95
    def call(self, prompt, action):
96
        """Call a callback of specified action.
97
98
        Args:
99
            prompt (Prompt): A ``prompt.prompt.Prompt`` instance.
100
            name (str): An action name.
101
102
        Example:
103
            >>> from unittest.mock import MagicMock
104
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
105
            >>> prompt = MagicMock()
106
            >>> action = Action()
107
            >>> action.register_from_rules([
108
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
109
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
110
            ...     ('prompt:do', lambda prompt, params: params),
111
            ... ])
112
            >>> action.call(prompt, 'prompt:accept')
113
            1
114
            >>> action.call(prompt, 'prompt:cancel')
115
            2
116
            >>> action.call(prompt, 'prompt:do:foo')
117
            'foo'
118
            >>> action.call(prompt, 'unknown:accept')
119
            1
120
            >>> action.call(prompt, 'unknown:unknown')
121
            Traceback (most recent call last):
122
              ...
123
            AttributeError: No action "unknown:unknown" has registered.
124
125
        Returns:
126
            None or int: None or int which represent the prompt status.
127
        """
128
        m = ACTION_PATTERN.match(action)
129
        name = m.group('name')
130
        label = m.group('label')
131
        params = m.group('params') or ''
132
        alternative_name = 'prompt:' + label
133
        # fallback to the prompt's builtin action if no name found in registry
134
        if name not in self.registry and alternative_name in self.registry:
135
            name = alternative_name
136
        # Execute action or raise AttributeError
137
        if name in self.registry:
138
            fn = self.registry[name]
139
            return fn(prompt, params)
140
        raise AttributeError(
141
            'No action "%s" has registered.' % name
142
        )
143
144
    @classmethod
145
    def from_rules(cls, rules):
146
        """Create a new action instance from rules.
147
148
        Args:
149
            rules (Iterable): An iterator which returns rules. A rule is a
150
                (name, callback) tuple.
151
152
        Example:
153
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
154
            >>> Action.from_rules([
155
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
156
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
157
            ... ])
158
            <....action.Action object at ...>
159
160
        Returns:
161
            Action: An action instance.
162
        """
163
        action = cls()
164
        action.register_from_rules(rules)
165
        return action
166
167
168
# Default actions -------------------------------------------------------------
169
def _accept(prompt, params):
170
    from .prompt import STATUS_ACCEPT
171
    return STATUS_ACCEPT
172
173
174
def _cancel(prompt, params):
175
    from .prompt import STATUS_CANCEL
176
    return STATUS_CANCEL
177
178
179
def _toggle_insert_mode(prompt, params):
180
    from .prompt import INSERT_MODE_INSERT, INSERT_MODE_REPLACE
181
    if prompt.insert_mode == INSERT_MODE_INSERT:
182
        prompt.insert_mode = INSERT_MODE_REPLACE
183
    else:
184
        prompt.insert_mode = INSERT_MODE_INSERT
185
186
187
def _delete_char_before_caret(prompt, params):
188
    if prompt.caret.locus == 0:
189
        return
190
    prompt.text = ''.join([
191
        prompt.caret.get_backward_text()[:-1],
192
        prompt.caret.get_selected_text(),
193
        prompt.caret.get_forward_text(),
194
    ])
195
    prompt.caret.locus -= 1
196
197
198
def _delete_word_before_caret(prompt, params):
199
    # NOTE: Respect the behavior of 'b' in Normal mode.
200
    if prompt.caret.locus == 0:
201
        return
202
    pattern_set = build_keyword_pattern_set(prompt.nvim)
203
    pattern = re.compile(
204
        r'(?:|%s+|%s+|[^\s\x20-\xff]+)\s*$' % pattern_set,
205
    )
206
    original_backward_text = prompt.caret.get_backward_text()
207
    backward_text = pattern.sub('', original_backward_text)
208
    prompt.text = ''.join([
209
        backward_text,
210
        prompt.caret.get_selected_text(),
211
        prompt.caret.get_forward_text(),
212
    ])
213
    prompt.caret.locus -= len(original_backward_text) - len(backward_text)
214
215
216
def _delete_char_after_caret(prompt, params):
217
    if prompt.caret.locus == prompt.caret.tail:
218
        return
219
    prompt.text = ''.join([
220
        prompt.caret.get_backward_text(),
221
        prompt.caret.get_selected_text(),
222
        prompt.caret.get_forward_text()[1:],
223
    ])
224
225
226
def _delete_word_after_caret(prompt, params):
227
    # NOTE: Respect the behavior of 'w' in Normal mode.
228
    if prompt.caret.locus == prompt.caret.tail:
229
        return
230
    pattern_set = build_keyword_pattern_set(prompt.nvim)
231
    pattern = re.compile(
232
        r'^(?:%s+|%s+|[^\s\x20-\xff]+|)\s*' % pattern_set
233
    )
234
    forward_text = pattern.sub('', prompt.caret.get_forward_text())
235
    prompt.text = ''.join([
236
        prompt.caret.get_backward_text(),
237
        prompt.caret.get_selected_text(),
238
        forward_text
239
    ])
240
241
242
def _delete_char_under_caret(prompt, params):
243
    prompt.text = ''.join([
244
        prompt.caret.get_backward_text(),
245
        prompt.caret.get_forward_text(),
246
    ])
247
248
249
def _delete_word_under_caret(prompt, params):
250
    # NOTE: Respect the behavior of 'diw' in Normal mode.
251
    if prompt.text == '':
252
        return
253
    pattern_set = build_keyword_pattern_set(prompt.nvim)
254
    pattern = re.compile(pattern_set.pattern)
255
    inverse = re.compile(pattern_set.inverse)
256
    non_ascii = re.compile(r'[^\s\x20-\xff]')
257
    selected_text = prompt.caret.get_selected_text()
258
    if selected_text == '':
259
        # The caret is at the end of the text
260
        pattern_b = re.compile(r'.$')
261
        pattern_a = re.compile('')
262
    elif pattern.match(selected_text):
263
        pattern_b = re.compile(r'%s+$' % pattern_set.pattern)
264
        pattern_a = re.compile(r'^%s+' % pattern_set.pattern)
265
    elif inverse.match(selected_text):
266
        pattern_b = re.compile(r'%s+$' % pattern_set.inverse)
267
        pattern_a = re.compile(r'^%s+' % pattern_set.inverse)
268
    elif non_ascii.match(selected_text):
269
        pattern_b = re.compile(r'[^\s\x20-\xff]+$')
270
        pattern_a = re.compile(r'^[^\s\x20-\xff]+')
271
    else:
272
        pattern_b = re.compile(r'\s+$')
273
        pattern_a = re.compile(r'^\s+')
274
    backward_text = pattern_b.sub('', prompt.caret.get_backward_text())
275
    forward_text = pattern_a.sub('', prompt.caret.get_forward_text())
276
    prompt.text = ''.join([
277
        backward_text,
278
        forward_text,
279
    ])
280
    prompt.caret.locus = len(backward_text)
281
282
283
def _delete_text_before_caret(prompt, params):
284
    prompt.text = prompt.caret.get_forward_text()
285
    prompt.caret.locus = prompt.caret.head
286
287
288
def _delete_text_after_caret(prompt, params):
289
    prompt.text = prompt.caret.get_backward_text()
290
    prompt.caret.locus = prompt.caret.tail
291
292
293
def _delete_entire_text(prompt, params):
294
    prompt.text = ''
295
    prompt.caret.locus = 0
296
297
298
def _move_caret_to_left(prompt, params):
299
    prompt.caret.locus -= 1
300
301
302
def _move_caret_to_one_word_left(prompt, params):
303
    # NOTE:
304
    # At least Neovim 0.2.0 or Vim 8.0, <S-Left> in command line does not
305
    # respect 'iskeyword' and a definition of the 'word' seems a chunk of
306
    # printable characters.
307
    pattern = re.compile('\S+\s?$')
308
    original_text = prompt.caret.get_backward_text()
309
    substituted_text = pattern.sub('', original_text)
310
    offset = len(original_text) - len(substituted_text)
311
    prompt.caret.locus -= 1 if not offset else offset
312
313
314
def _move_caret_to_left_anchor(prompt, params):
315
    # Like 't' in normal mode
316
    anchor = int2char(prompt.nvim, getchar(prompt.nvim))
317
    index = prompt.caret.get_backward_text().rfind(anchor)
318
    if index != -1:
319
        prompt.caret.locus = index
320
321
322
def _move_caret_to_right(prompt, params):
323
    prompt.caret.locus += 1
324
325
326
def _move_caret_to_one_word_right(prompt, params):
327
    # NOTE:
328
    # At least Neovim 0.2.0 or Vim 8.0, <S-Left> in command line does not
329
    # respect 'iskeyword' and a definition of the 'word' seems a chunk of
330
    # printable characters.
331
    pattern = re.compile('^\S+')
332
    original_text = prompt.caret.get_forward_text()
333
    substituted_text = pattern.sub('', original_text)
334
    prompt.caret.locus += 1 + len(original_text) - len(substituted_text)
335
336
337
def _move_caret_to_right_anchor(prompt, params):
338
    # Like 't' in normal mode
339
    anchor = int2char(prompt.nvim, getchar(prompt.nvim))
340
    index = prompt.caret.get_forward_text().find(anchor)
341
    if index != -1:
342
        prompt.caret.locus = sum([
343
            len(prompt.caret.get_backward_text()),
344
            len(prompt.caret.get_selected_text()),
345
            index,
346
        ])
347
348
349
def _move_caret_to_head(prompt, params):
350
    prompt.caret.locus = prompt.caret.head
351
352
353
def _move_caret_to_lead(prompt, params):
354
    prompt.caret.locus = prompt.caret.lead
355
356
357
def _move_caret_to_tail(prompt, params):
358
    prompt.caret.locus = prompt.caret.tail
359
360
361
def _assign_previous_text(prompt, params):
362
    prompt.text = prompt.history.previous()
363
    prompt.caret.locus = prompt.caret.tail
364
365
366
def _assign_next_text(prompt, params):
367
    prompt.text = prompt.history.next()
368
    prompt.caret.locus = prompt.caret.tail
369
370
371
def _assign_previous_matched_text(prompt, params):
372
    prompt.text = prompt.history.previous_match()
373
    prompt.caret.locus = prompt.caret.tail
374
375
376
def _assign_next_matched_text(prompt, params):
377
    prompt.text = prompt.history.next_match()
378
    prompt.caret.locus = prompt.caret.tail
379
380
381
def _paste_from_register(prompt, params):
382
    state = prompt.store()
383
    prompt.update_text('"')
384
    prompt.redraw_prompt()
385
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
386
    prompt.restore(state)
387
    val = prompt.nvim.call('getreg', reg)
388
    prompt.update_text(val)
389
390
391
def _paste_from_default_register(prompt, params):
392
    val = prompt.nvim.call('getreg', prompt.nvim.vvars['register'])
393
    prompt.update_text(val)
394
395
396
def _yank_to_register(prompt, params):
397
    state = prompt.store()
398
    prompt.update_text("'")
399
    prompt.redraw_prompt()
400
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
401
    prompt.restore(state)
402
    prompt.nvim.call('setreg', reg, prompt.text)
403
404
405
def _yank_to_default_register(prompt, params):
406
    prompt.nvim.call('setreg', prompt.nvim.vvars['register'], prompt.text)
407
408
409
def _insert_special(prompt, params):
410
    state = prompt.store()
411
    prompt.update_text('^')
412
    prompt.redraw_prompt()
413
    code = getchar(prompt.nvim)
414
    prompt.restore(state)
415
    # Substitute special keys into control char
416
    if code == b'\x80kb':
417
        code = 0x08  # ^H
418
    char = int2repr(prompt.nvim, code)
419
    prompt.update_text(char)
420
421
422
def _insert_digraph(prompt, params):
423
    state = prompt.store()
424
    prompt.update_text('?')
425
    prompt.redraw_prompt()
426
    digraph = Digraph()
427
    char = digraph.retrieve(prompt.nvim)
428
    prompt.restore(state)
429
    prompt.update_text(char)
430
431
432
DEFAULT_ACTION = Action.from_rules([
433
    ('prompt:accept', _accept),
434
    ('prompt:cancel', _cancel),
435
    ('prompt:toggle_insert_mode', _toggle_insert_mode),
436
    ('prompt:delete_char_before_caret', _delete_char_before_caret),
437
    ('prompt:delete_word_before_caret', _delete_word_before_caret),
438
    ('prompt:delete_char_after_caret', _delete_char_after_caret),
439
    ('prompt:delete_word_after_caret', _delete_word_after_caret),
440
    ('prompt:delete_char_under_caret', _delete_char_under_caret),
441
    ('prompt:delete_word_under_caret', _delete_word_under_caret),
442
    ('prompt:delete_text_before_caret', _delete_text_before_caret),
443
    ('prompt:delete_text_after_caret', _delete_text_after_caret),
444
    ('prompt:delete_entire_text', _delete_entire_text),
445
    ('prompt:move_caret_to_left', _move_caret_to_left),
446
    ('prompt:move_caret_to_one_word_left', _move_caret_to_one_word_left),
447
    ('prompt:move_caret_to_left_anchor', _move_caret_to_left_anchor),
448
    ('prompt:move_caret_to_right', _move_caret_to_right),
449
    ('prompt:move_caret_to_one_word_right', _move_caret_to_one_word_right),
450
    ('prompt:move_caret_to_right_anchor', _move_caret_to_right_anchor),
451
    ('prompt:move_caret_to_head', _move_caret_to_head),
452
    ('prompt:move_caret_to_lead', _move_caret_to_lead),
453
    ('prompt:move_caret_to_tail', _move_caret_to_tail),
454
    ('prompt:assign_previous_text', _assign_previous_text),
455
    ('prompt:assign_next_text', _assign_next_text),
456
    ('prompt:assign_previous_matched_text', _assign_previous_matched_text),
457
    ('prompt:assign_next_matched_text', _assign_next_matched_text),
458
    ('prompt:paste_from_register', _paste_from_register),
459
    ('prompt:paste_from_default_register', _paste_from_default_register),
460
    ('prompt:yank_to_register', _yank_to_register),
461
    ('prompt:yank_to_default_register', _yank_to_default_register),
462
    ('prompt:insert_special', _insert_special),
463
    ('prompt:insert_digraph', _insert_digraph),
464
])
465