Completed
Push — master ( ace01b...aa6711 )
by Lambda
01:08
created

_delete_word_under_caret()   B

Complexity

Conditions 4

Size

Total Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 4
dl 0
loc 24
rs 8.6845
c 2
b 0
f 0
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(r'(?:%s+|%s+)\s*$' % pattern_set)
204
    original_backward_text = prompt.caret.get_backward_text()
205
    backward_text = pattern.sub('', original_backward_text, count=1)
206
    prompt.text = ''.join([
207
        backward_text,
208
        prompt.caret.get_selected_text(),
209
        prompt.caret.get_forward_text(),
210
    ])
211
    prompt.caret.locus -= len(original_backward_text) - len(backward_text)
212
213
214
def _delete_char_after_caret(prompt, params):
215
    if prompt.caret.locus == prompt.caret.tail:
216
        return
217
    prompt.text = ''.join([
218
        prompt.caret.get_backward_text(),
219
        prompt.caret.get_selected_text(),
220
        prompt.caret.get_forward_text()[1:],
221
    ])
222
223
224
def _delete_word_after_caret(prompt, params):
225
    # NOTE: Respect the behavior of 'w' in Normal mode.
226
    if prompt.caret.locus == prompt.caret.tail:
227
        return
228
    pattern_set = build_keyword_pattern_set(prompt.nvim)
229
    pattern = re.compile(r'^(?:%s+|%s+|)\s*' % pattern_set)
230
    forward_text = pattern.sub('', prompt.caret.get_forward_text(), count=1)
231
    prompt.text = ''.join([
232
        prompt.caret.get_backward_text(),
233
        prompt.caret.get_selected_text(),
234
        forward_text
235
    ])
236
237
238
def _delete_char_under_caret(prompt, params):
239
    prompt.text = ''.join([
240
        prompt.caret.get_backward_text(),
241
        prompt.caret.get_forward_text(),
242
    ])
243
244
245
def _delete_word_under_caret(prompt, params):
246
    # NOTE: Respect the behavior of 'diw' in Normal mode.
247
    if prompt.text == '':
248
        return
249
    pattern_set = build_keyword_pattern_set(prompt.nvim)
250
    pattern = re.compile(pattern_set.pattern)
251
    inverse = re.compile(pattern_set.inverse)
252
    selected_text = prompt.caret.get_selected_text()
253
    if pattern.match(selected_text):
254
        pattern_b = re.compile(r'%s+$' % pattern_set.pattern)
255
        pattern_a = re.compile(r'^%s+' % pattern_set.pattern)
256
    elif inverse.match(selected_text):
257
        pattern_b = re.compile(r'%s+$' % pattern_set.inverse)
258
        pattern_a = re.compile(r'^%s+' % pattern_set.inverse)
259
    else:
260
        pattern_b = re.compile(r'\s+$')
261
        pattern_a = re.compile(r'^\s+')
262
    backward_text = pattern_b.sub('', prompt.caret.get_backward_text())
263
    forward_text = pattern_a.sub('', prompt.caret.get_forward_text())
264
    prompt.text = ''.join([
265
        backward_text,
266
        forward_text,
267
    ])
268
    prompt.caret.locus = len(backward_text)
269
270
271
def _delete_text_before_caret(prompt, params):
272
    prompt.text = prompt.caret.get_forward_text()
273
    prompt.caret.locus = prompt.caret.head
274
275
276
def _delete_text_after_caret(prompt, params):
277
    prompt.text = prompt.caret.get_backward_text()
278
    prompt.caret.locus = prompt.caret.tail
279
280
281
def _delete_entire_text(prompt, params):
282
    prompt.text = ''
283
    prompt.caret.locus = 0
284
285
286
def _move_caret_to_left(prompt, params):
287
    prompt.caret.locus -= 1
288
289
290
def _move_caret_to_one_word_left(prompt, params):
291
    # NOTE:
292
    # At least Neovim 0.2.0 or Vim 8.0, <S-Left> in command line does not
293
    # respect 'iskeyword' and a definition of the 'word' seems a chunk of
294
    # printable characters.
295
    pattern = re.compile('\S+\s?$')
296
    original_text = prompt.caret.get_backward_text()
297
    substituted_text = pattern.sub('', original_text)
298
    offset = len(original_text) - len(substituted_text)
299
    prompt.caret.locus -= 1 if not offset else offset
300
301
302
def _move_caret_to_left_anchor(prompt, params):
303
    # Like 't' in normal mode
304
    anchor = int2char(prompt.nvim, getchar(prompt.nvim))
305
    index = prompt.caret.get_backward_text().rfind(anchor)
306
    if index != -1:
307
        prompt.caret.locus = index
308
309
310
def _move_caret_to_right(prompt, params):
311
    prompt.caret.locus += 1
312
313
314
def _move_caret_to_one_word_right(prompt, params):
315
    # NOTE:
316
    # At least Neovim 0.2.0 or Vim 8.0, <S-Left> in command line does not
317
    # respect 'iskeyword' and a definition of the 'word' seems a chunk of
318
    # printable characters.
319
    pattern = re.compile('^\S+')
320
    original_text = prompt.caret.get_forward_text()
321
    substituted_text = pattern.sub('', original_text)
322
    prompt.caret.locus += 1 + len(original_text) - len(substituted_text)
323
324
325
def _move_caret_to_right_anchor(prompt, params):
326
    # Like 't' in normal mode
327
    anchor = int2char(prompt.nvim, getchar(prompt.nvim))
328
    index = prompt.caret.get_forward_text().find(anchor)
329
    if index != -1:
330
        prompt.caret.locus = sum([
331
            len(prompt.caret.get_backward_text()),
332
            len(prompt.caret.get_selected_text()),
333
            index,
334
        ])
335
336
337
def _move_caret_to_head(prompt, params):
338
    prompt.caret.locus = prompt.caret.head
339
340
341
def _move_caret_to_lead(prompt, params):
342
    prompt.caret.locus = prompt.caret.lead
343
344
345
def _move_caret_to_tail(prompt, params):
346
    prompt.caret.locus = prompt.caret.tail
347
348
349
def _assign_previous_text(prompt, params):
350
    prompt.text = prompt.history.previous()
351
    prompt.caret.locus = prompt.caret.tail
352
353
354
def _assign_next_text(prompt, params):
355
    prompt.text = prompt.history.next()
356
    prompt.caret.locus = prompt.caret.tail
357
358
359
def _assign_previous_matched_text(prompt, params):
360
    prompt.text = prompt.history.previous_match()
361
    prompt.caret.locus = prompt.caret.tail
362
363
364
def _assign_next_matched_text(prompt, params):
365
    prompt.text = prompt.history.next_match()
366
    prompt.caret.locus = prompt.caret.tail
367
368
369
def _paste_from_register(prompt, params):
370
    state = prompt.store()
371
    prompt.update_text('"')
372
    prompt.redraw_prompt()
373
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
374
    prompt.restore(state)
375
    val = prompt.nvim.call('getreg', reg)
376
    prompt.update_text(val)
377
378
379
def _paste_from_default_register(prompt, params):
380
    val = prompt.nvim.call('getreg', prompt.nvim.vvars['register'])
381
    prompt.update_text(val)
382
383
384
def _yank_to_register(prompt, params):
385
    state = prompt.store()
386
    prompt.update_text("'")
387
    prompt.redraw_prompt()
388
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
389
    prompt.restore(state)
390
    prompt.nvim.call('setreg', reg, prompt.text)
391
392
393
def _yank_to_default_register(prompt, params):
394
    prompt.nvim.call('setreg', prompt.nvim.vvars['register'], prompt.text)
395
396
397
def _insert_special(prompt, params):
398
    state = prompt.store()
399
    prompt.update_text('^')
400
    prompt.redraw_prompt()
401
    code = getchar(prompt.nvim)
402
    prompt.restore(state)
403
    # Substitute special keys into control char
404
    if code == b'\x80kb':
405
        code = 0x08  # ^H
406
    char = int2repr(prompt.nvim, code)
407
    prompt.update_text(char)
408
409
410
def _insert_digraph(prompt, params):
411
    state = prompt.store()
412
    prompt.update_text('?')
413
    prompt.redraw_prompt()
414
    digraph = Digraph()
415
    char = digraph.retrieve(prompt.nvim)
416
    prompt.restore(state)
417
    prompt.update_text(char)
418
419
420
DEFAULT_ACTION = Action.from_rules([
421
    ('prompt:accept', _accept),
422
    ('prompt:cancel', _cancel),
423
    ('prompt:toggle_insert_mode', _toggle_insert_mode),
424
    ('prompt:delete_char_before_caret', _delete_char_before_caret),
425
    ('prompt:delete_word_before_caret', _delete_word_before_caret),
426
    ('prompt:delete_char_after_caret', _delete_char_after_caret),
427
    ('prompt:delete_word_after_caret', _delete_word_after_caret),
428
    ('prompt:delete_char_under_caret', _delete_char_under_caret),
429
    ('prompt:delete_word_under_caret', _delete_word_under_caret),
430
    ('prompt:delete_text_before_caret', _delete_text_before_caret),
431
    ('prompt:delete_text_after_caret', _delete_text_after_caret),
432
    ('prompt:delete_entire_text', _delete_entire_text),
433
    ('prompt:move_caret_to_left', _move_caret_to_left),
434
    ('prompt:move_caret_to_one_word_left', _move_caret_to_one_word_left),
435
    ('prompt:move_caret_to_left_anchor', _move_caret_to_left_anchor),
436
    ('prompt:move_caret_to_right', _move_caret_to_right),
437
    ('prompt:move_caret_to_one_word_right', _move_caret_to_one_word_right),
438
    ('prompt:move_caret_to_right_anchor', _move_caret_to_right_anchor),
439
    ('prompt:move_caret_to_head', _move_caret_to_head),
440
    ('prompt:move_caret_to_lead', _move_caret_to_lead),
441
    ('prompt:move_caret_to_tail', _move_caret_to_tail),
442
    ('prompt:assign_previous_text', _assign_previous_text),
443
    ('prompt:assign_next_text', _assign_next_text),
444
    ('prompt:assign_previous_matched_text', _assign_previous_matched_text),
445
    ('prompt:assign_next_matched_text', _assign_next_matched_text),
446
    ('prompt:paste_from_register', _paste_from_register),
447
    ('prompt:paste_from_default_register', _paste_from_default_register),
448
    ('prompt:yank_to_register', _yank_to_register),
449
    ('prompt:yank_to_default_register', _yank_to_default_register),
450
    ('prompt:insert_special', _insert_special),
451
    ('prompt:insert_digraph', _insert_digraph),
452
])
453