Completed
Push — master ( 9bef6a...d8c15c )
by Lambda
56s
created

Action.clear()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
c 0
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 register_from_rules(self, rules) -> None:
53
        """Register action callbacks from rules.
54
55
        Args:
56
            rules (Iterable): An iterator which returns rules. A rule is a
57
                (name, callback) tuple.
58
59
        Example:
60
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
61
            >>> action = Action()
62
            >>> action.register_from_rules([
63
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
64
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
65
            ... ])
66
        """
67
        for rule in rules:
68
            self.register(*rule)
69
70
    def call(self, prompt, action):
71
        """Call a callback of specified action.
72
73
        Args:
74
            prompt (Prompt): A ``prompt.prompt.Prompt`` instance.
75
            name (str): An action name.
76
77
        Example:
78
            >>> from unittest.mock import MagicMock
79
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
80
            >>> prompt = MagicMock()
81
            >>> action = Action()
82
            >>> action.register_from_rules([
83
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
84
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
85
            ...     ('prompt:do', lambda prompt, params: params),
86
            ... ])
87
            >>> action.call(prompt, 'prompt:accept')
88
            1
89
            >>> action.call(prompt, 'prompt:cancel')
90
            2
91
            >>> action.call(prompt, 'prompt:do:foo')
92
            'foo'
93
            >>> action.call(prompt, 'unknown:accept')
94
            1
95
            >>> action.call(prompt, 'unknown:unknown')
96
            Traceback (most recent call last):
97
              ...
98
            AttributeError: No action "unknown:unknown" has registered.
99
100
        Returns:
101
            None or int: None or int which represent the prompt status.
102
        """
103
        m = ACTION_PATTERN.match(action)
104
        name = m.group('name')
105
        label = m.group('label')
106
        params = m.group('params') or ''
107
        alternative_name = 'prompt:' + label
108
        # fallback to the prompt's builtin action if no name found in registry
109
        if name not in self.registry and alternative_name in self.registry:
110
            name = alternative_name
111
        # Execute action or raise AttributeError
112
        if name in self.registry:
113
            fn = self.registry[name]
114
            return fn(prompt, params)
115
        raise AttributeError(
116
            'No action "%s" has registered.' % name
117
        )
118
119
    @classmethod
120
    def from_rules(cls, rules):
121
        """Create a new action instance from rules.
122
123
        Args:
124
            rules (Iterable): An iterator which returns rules. A rule is a
125
                (name, callback) tuple.
126
127
        Example:
128
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
129
            >>> Action.from_rules([
130
            ...     ('prompt:accept', lambda prompt, params: STATUS_ACCEPT),
131
            ...     ('prompt:cancel', lambda prompt, params: STATUS_CANCEL),
132
            ... ])
133
            <....action.Action object at ...>
134
135
        Returns:
136
            Action: An action instance.
137
        """
138
        action = cls()
139
        action.register_from_rules(rules)
140
        return action
141
142
143
# Default actions -------------------------------------------------------------
144
def _accept(prompt, params):
145
    from .prompt import STATUS_ACCEPT
146
    return STATUS_ACCEPT
147
148
149
def _cancel(prompt, params):
150
    from .prompt import STATUS_CANCEL
151
    return STATUS_CANCEL
152
153
154
def _toggle_insert_mode(prompt, params):
155
    from .prompt import INSERT_MODE_INSERT, INSERT_MODE_REPLACE
156
    if prompt.insert_mode == INSERT_MODE_INSERT:
157
        prompt.insert_mode = INSERT_MODE_REPLACE
158
    else:
159
        prompt.insert_mode = INSERT_MODE_INSERT
160
161
162
def _delete_char_before_caret(prompt, params):
163
    if prompt.caret.locus == 0:
164
        return
165
    prompt.text = ''.join([
166
        prompt.caret.get_backward_text()[:-1],
167
        prompt.caret.get_selected_text(),
168
        prompt.caret.get_forward_text(),
169
    ])
170
    prompt.caret.locus -= 1
171
172
173
def _delete_word_before_caret(prompt, params):
174
    if prompt.caret.locus == 0:
175
        return
176
    # Use vim's substitute to respect 'iskeyword'
177
    original_backward_text = prompt.caret.get_backward_text()
178
    backward_text = prompt.nvim.call(
179
        'substitute',
180
        original_backward_text, '\k\+\s*$', '', '',
181
    )
182
    prompt.text = ''.join([
183
        backward_text,
184
        prompt.caret.get_selected_text(),
185
        prompt.caret.get_forward_text(),
186
    ])
187
    prompt.caret.locus -= len(original_backward_text) - len(backward_text)
188
189
190
def _delete_char_under_caret(prompt, params):
191
    prompt.text = ''.join([
192
        prompt.caret.get_backward_text(),
193
        prompt.caret.get_forward_text(),
194
    ])
195
196
197
def _delete_text_after_caret(prompt, params):
198
    prompt.text = prompt.caret.get_backward_text()
199
    prompt.caret.locus = prompt.caret.tail
200
201
202
def _delete_entire_text(prompt, params):
203
    prompt.text = ''
204
    prompt.caret.locus = 0
205
206
207
def _move_caret_to_left(prompt, params):
208
    prompt.caret.locus -= 1
209
210
211
def _move_caret_to_one_word_left(prompt, params):
212
    # Use vim's substitute to respect 'iskeyword'
213
    original_text = prompt.caret.get_backward_text()
214
    substituted_text = prompt.nvim.call(
215
        'substitute',
216
        original_text, '\k\+\s\?$', '', '',
217
    )
218
    offset = len(original_text) - len(substituted_text)
219
    prompt.caret.locus -= 1 if not offset else offset
220
221
222
def _move_caret_to_right(prompt, params):
223
    prompt.caret.locus += 1
224
225
226
def _move_caret_to_one_word_right(prompt, params):
227
    # Use vim's substitute to respect 'iskeyword'
228
    original_text = prompt.caret.get_forward_text()
229
    substituted_text = prompt.nvim.call(
230
        'substitute',
231
        original_text, '^\k\+', '', '',
232
    )
233
    prompt.caret.locus += 1 + len(original_text) - len(substituted_text)
234
235
236
def _move_caret_to_head(prompt, params):
237
    prompt.caret.locus = prompt.caret.head
238
239
240
def _move_caret_to_lead(prompt, params):
241
    prompt.caret.locus = prompt.caret.lead
242
243
244
def _move_caret_to_tail(prompt, params):
245
    prompt.caret.locus = prompt.caret.tail
246
247
248
def _assign_previous_text(prompt, params):
249
    prompt.text = prompt.history.previous()
250
    prompt.caret.locus = prompt.caret.tail
251
252
253
def _assign_next_text(prompt, params):
254
    prompt.text = prompt.history.next()
255
    prompt.caret.locus = prompt.caret.tail
256
257
258
def _assign_previous_matched_text(prompt, params):
259
    prompt.text = prompt.history.previous_match()
260
    prompt.caret.locus = prompt.caret.tail
261
262
263
def _assign_next_matched_text(prompt, params):
264
    prompt.text = prompt.history.next_match()
265
    prompt.caret.locus = prompt.caret.tail
266
267
268
def _paste_from_register(prompt, params):
269
    state = prompt.store()
270
    prompt.update_text('"')
271
    prompt.redraw_prompt()
272
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
273
    prompt.restore(state)
274
    val = prompt.nvim.call('getreg', reg)
275
    prompt.update_text(val)
276
277
278
def _paste_from_default_register(prompt, params):
279
    val = prompt.nvim.call('getreg', prompt.nvim.vvars['register'])
280
    prompt.update_text(val)
281
282
283
def _yank_to_register(prompt, params):
284
    state = prompt.store()
285
    prompt.update_text("'")
286
    prompt.redraw_prompt()
287
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
288
    prompt.restore(state)
289
    prompt.nvim.call('setreg', reg, prompt.text)
290
291
292
def _yank_to_default_register(prompt, params):
293
    prompt.nvim.call('setreg', prompt.nvim.vvars['register'], prompt.text)
294
295
296
def _insert_special(prompt, params):
297
    state = prompt.store()
298
    prompt.update_text('^')
299
    prompt.redraw_prompt()
300
    code = getchar(prompt.nvim)
301
    prompt.restore(state)
302
    # Substitute special keys into control char
303
    if code == b'\x80kb':
304
        code = 0x08  # ^H
305
    char = int2repr(prompt.nvim, code)
306
    prompt.update_text(char)
307
308
309
def _insert_digraph(prompt, params):
310
    state = prompt.store()
311
    prompt.update_text('?')
312
    prompt.redraw_prompt()
313
    digraph = Digraph()
314
    char = digraph.retrieve(prompt.nvim)
315
    prompt.restore(state)
316
    prompt.update_text(char)
317
318
319
DEFAULT_ACTION = Action.from_rules([
320
    ('prompt:accept', _accept),
321
    ('prompt:cancel', _cancel),
322
    ('prompt:toggle_insert_mode', _toggle_insert_mode),
323
    ('prompt:delete_char_before_caret', _delete_char_before_caret),
324
    ('prompt:delete_word_before_caret', _delete_word_before_caret),
325
    ('prompt:delete_char_under_caret', _delete_char_under_caret),
326
    ('prompt:delete_text_after_caret', _delete_text_after_caret),
327
    ('prompt:delete_entire_text', _delete_entire_text),
328
    ('prompt:move_caret_to_left', _move_caret_to_left),
329
    ('prompt:move_caret_to_one_word_left', _move_caret_to_one_word_left),
330
    ('prompt:move_caret_to_right', _move_caret_to_right),
331
    ('prompt:move_caret_to_one_word_right', _move_caret_to_one_word_right),
332
    ('prompt:move_caret_to_head', _move_caret_to_head),
333
    ('prompt:move_caret_to_lead', _move_caret_to_lead),
334
    ('prompt:move_caret_to_tail', _move_caret_to_tail),
335
    ('prompt:assign_previous_text', _assign_previous_text),
336
    ('prompt:assign_next_text', _assign_next_text),
337
    ('prompt:assign_previous_matched_text', _assign_previous_matched_text),
338
    ('prompt:assign_next_matched_text', _assign_next_matched_text),
339
    ('prompt:paste_from_register', _paste_from_register),
340
    ('prompt:paste_from_default_register', _paste_from_default_register),
341
    ('prompt:yank_to_register', _yank_to_register),
342
    ('prompt:yank_to_default_register', _yank_to_default_register),
343
    ('prompt:insert_special', _insert_special),
344
    ('prompt:insert_digraph', _insert_digraph),
345
])
346