Completed
Push — master ( 0be2af...ace01b )
by Lambda
50s
created

_delete_text_before_caret()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
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
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
    if prompt.caret.locus == 0:
200
        return
201
    # Use vim's substitute to respect 'iskeyword'
202
    original_backward_text = prompt.caret.get_backward_text()
203
    backward_text = prompt.nvim.call(
204
        'substitute',
205
        original_backward_text, '\k\+\s*$', '', '',
206
    )
207
    prompt.text = ''.join([
208
        backward_text,
209
        prompt.caret.get_selected_text(),
210
        prompt.caret.get_forward_text(),
211
    ])
212
    prompt.caret.locus -= len(original_backward_text) - len(backward_text)
213
214
215
def _delete_char_after_caret(prompt, params):
216
    if prompt.caret.locus == prompt.caret.tail:
217
        return
218
    prompt.text = ''.join([
219
        prompt.caret.get_backward_text(),
220
        prompt.caret.get_selected_text(),
221
        prompt.caret.get_forward_text()[1:],
222
    ])
223
224
225
def _delete_word_after_caret(prompt, params):
226
    if prompt.caret.locus == prompt.caret.tail:
227
        return
228
    # Use vim's substitute to respect 'iskeyword'
229
    forward_text = prompt.nvim.call(
230
        'substitute',
231
        prompt.caret.get_forward_text(),
232
        '^\s*\k\+', '', '',
233
    )
234
    prompt.text = ''.join([
235
        prompt.caret.get_backward_text(),
236
        prompt.caret.get_selected_text(),
237
        forward_text
238
    ])
239
240
241
def _delete_char_under_caret(prompt, params):
242
    prompt.text = ''.join([
243
        prompt.caret.get_backward_text(),
244
        prompt.caret.get_forward_text(),
245
    ])
246
247
248
def _delete_word_under_caret(prompt, params):
249
    # Use vim's substitute to respect 'iskeyword'
250
    if prompt.text == '':
251
        return
252
    if prompt.caret.get_selected_text() == ' ':
253
        backward_text = prompt.caret.get_backward_text().rstrip()
254
        forward_text = prompt.caret.get_forward_text().lstrip()
255
    else:
256
        backward_text = prompt.nvim.call(
257
            'substitute',
258
            prompt.caret.get_backward_text(),
259
            '\k\+$', '', '',
260
        )
261
        forward_text = prompt.nvim.call(
262
            'substitute',
263
            prompt.caret.get_forward_text(),
264
            '^\k\+', '', '',
265
        )
266
    prompt.text = ''.join([
267
        backward_text,
268
        forward_text,
269
    ])
270
    prompt.caret.locus = len(backward_text)
271
272
273
def _delete_text_before_caret(prompt, params):
274
    prompt.text = prompt.caret.get_forward_text()
275
    prompt.caret.locus = prompt.caret.head
276
277
278
def _delete_text_after_caret(prompt, params):
279
    prompt.text = prompt.caret.get_backward_text()
280
    prompt.caret.locus = prompt.caret.tail
281
282
283
def _delete_entire_text(prompt, params):
284
    prompt.text = ''
285
    prompt.caret.locus = 0
286
287
288
def _move_caret_to_left(prompt, params):
289
    prompt.caret.locus -= 1
290
291
292
def _move_caret_to_one_word_left(prompt, params):
293
    # Use vim's substitute to respect 'iskeyword'
294
    original_text = prompt.caret.get_backward_text()
295
    substituted_text = prompt.nvim.call(
296
        'substitute',
297
        original_text, '\k\+\s\?$', '', '',
298
    )
299
    offset = len(original_text) - len(substituted_text)
300
    prompt.caret.locus -= 1 if not offset else offset
301
302
303
def _move_caret_to_left_anchor(prompt, params):
304
    # Like 't' in normal mode
305
    anchor = int2char(prompt.nvim, getchar(prompt.nvim))
306
    index = prompt.caret.get_backward_text().rfind(anchor)
307
    if index != -1:
308
        prompt.caret.locus = index
309
310
311
def _move_caret_to_right(prompt, params):
312
    prompt.caret.locus += 1
313
314
315
def _move_caret_to_one_word_right(prompt, params):
316
    # Use vim's substitute to respect 'iskeyword'
317
    original_text = prompt.caret.get_forward_text()
318
    substituted_text = prompt.nvim.call(
319
        'substitute',
320
        original_text, '^\k\+', '', '',
321
    )
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