Completed
Push — master ( 1945b2...a8a1aa )
by Lambda
01:08
created

_delete_word_before_caret()   A

Complexity

Conditions 2

Size

Total Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 15
rs 9.4285
1
"""Action module."""
2
from .prompt import STATUS_PROGRESS
3
from .digraph import Digraph
4
from .util import safeget, int2char, int2repr, getchar
5
6
7
class Action:
8
    """Action class which holds action callbacks.
9
10
    Attributes:
11
        registry (dict): An action dictionary.
12
    """
13
14
    __slots__ = ('registry',)
15
16
    def __init__(self):
17
        """Constructor."""
18
        self.registry = {}
19
20
    def register(self, name, callback):
21
        """Register action callback to a specified name.
22
23
        Args:
24
            name (str): An action name which follow {namespace}:{action name}
25
            callback (Callable): An action callback which take a
26
                ``prompt.prompt.Prompt`` instance and return None or int.
27
28
        Example:
29
            >>> from .prompt import STATUS_ACCEPT
30
            >>> action = Action()
31
            >>> action.register('prompt:accept', lambda prompt: STATUS_ACCEPT)
32
        """
33
        self.registry[name] = callback
34
35
    def register_from_rules(self, rules) -> None:
36
        """Register action callbacks from rules.
37
38
        Args:
39
            rules (Iterable): An iterator which returns rules. A rule is a
40
                (name, callback) tuple.
41
42
        Example:
43
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
44
            >>> action = Action()
45
            >>> action.register_from_rules([
46
            ...     ('prompt:accept', lambda prompt: STATUS_ACCEPT),
47
            ...     ('prompt:cancel', lambda prompt: STATUS_CANCEL),
48
            ... ])
49
        """
50
        for rule in rules:
51
            self.register(*rule)
52
53
    def call(self, prompt, name):
54
        """Call a callback of specified action.
55
56
        Args:
57
            prompt (Prompt): A ``prompt.prompt.Prompt`` instance.
58
            name (str): An action name.
59
60
        Example:
61
            >>> from unittest.mock import MagicMock
62
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
63
            >>> prompt = MagicMock()
64
            >>> action = Action()
65
            >>> action.register_from_rules([
66
            ...     ('prompt:accept', lambda prompt: STATUS_ACCEPT),
67
            ...     ('prompt:cancel', lambda prompt: STATUS_CANCEL),
68
            ... ])
69
            >>> action.call(prompt, 'prompt:accept')
70
            1
71
72
        Returns:
73
            None or int: None or int which represent the prompt status.
74
        """
75
        if name in self.registry:
76
            fn = self.registry[name]
77
            return fn(prompt)
78
        elif name.startswith('call:'):
79
            return _call(prompt, name[5:])
80
        raise AttributeError(
81
            'No action "%s" has registered.' % name
82
        )
83
84
    @classmethod
85
    def from_rules(cls, rules):
86
        """Create a new action instance from rules.
87
88
        Args:
89
            rules (Iterable): An iterator which returns rules. A rule is a
90
                (name, callback) tuple.
91
92
        Example:
93
            >>> from .prompt import STATUS_ACCEPT, STATUS_CANCEL
94
            >>> Action.from_rules([
95
            ...     ('prompt:accept', lambda prompt: STATUS_ACCEPT),
96
            ...     ('prompt:cancel', lambda prompt: STATUS_CANCEL),
97
            ... ])
98
            <....action.Action object at ...>
99
100
        Returns:
101
            Action: An action instance.
102
        """
103
        action = cls()
104
        action.register_from_rules(rules)
105
        return action
106
107
108
# Special actions -------------------------------------------------------------
109
def _call(prompt, fname):
110
    result = prompt.nvim.call(fname, prompt.context.to_dict())
111
    if result is None:
112
        return STATUS_PROGRESS
113
    elif isinstance(result, int):
114
        return result
115
    elif isinstance(result, dict):
116
        prompt.context.extend(result)
117
    elif isinstance(result, (list, tuple)):
118
        status = safeget(result, 0, default=STATUS_PROGRESS)
119
        prompt.context.extend(safeget(result, 1, default={}))
120
        return status
121
    else:
122
        raise AttributeError(
123
            "A function '%s' does not return a correct value." % fname
124
        )
125
126
127
# Default actions -------------------------------------------------------------
128
def _accept(prompt):
129
    from .prompt import STATUS_ACCEPT
130
    return STATUS_ACCEPT
131
132
133
def _cancel(prompt):
134
    from .prompt import STATUS_CANCEL
135
    return STATUS_CANCEL
136
137
138
def _toggle_insert_mode(prompt):
139
    from .prompt import INSERT_MODE_INSERT, INSERT_MODE_REPLACE
140
    if prompt.insert_mode == INSERT_MODE_INSERT:
141
        prompt.insert_mode = INSERT_MODE_REPLACE
142
    else:
143
        prompt.insert_mode = INSERT_MODE_INSERT
144
145
146
def _delete_char_before_caret(prompt):
147
    if prompt.caret.locus == 0:
148
        return
149
    prompt.context.text = ''.join([
150
        prompt.caret.get_backward_text()[:-1],
151
        prompt.caret.get_selected_text(),
152
        prompt.caret.get_forward_text(),
153
    ])
154
    prompt.caret.locus -= 1
155
156
157
def _delete_word_before_caret(prompt):
158
    if prompt.caret.locus == 0:
159
        return
160
    # Use vim's substitute to respect 'iskeyword'
161
    original_backward_text = prompt.caret.get_backward_text()
162
    backward_text = prompt.nvim.call(
163
        'substitute',
164
        original_backward_text, '\k\+\s*$', '', '',
165
    )
166
    prompt.context.text = ''.join([
167
        backward_text,
168
        prompt.caret.get_selected_text(),
169
        prompt.caret.get_forward_text(),
170
    ])
171
    prompt.caret.locus -= len(original_backward_text) - len(backward_text)
172
173
174
def _delete_char_under_caret(prompt):
175
    prompt.context.text = ''.join([
176
        prompt.caret.get_backward_text(),
177
        prompt.caret.get_forward_text(),
178
    ])
179
180
181
def _delete_text_after_caret(prompt):
182
    prompt.context.text = prompt.caret.get_backward_text()
183
    prompt.caret.locus = prompt.caret.tail
184
185
186
def _delete_entire_text(prompt):
187
    prompt.context.text = ''
188
    prompt.caret.locus = prompt.caret.tail
189
190
191
def _move_caret_to_left(prompt):
192
    prompt.caret.locus -= 1
193
194
195
def _move_caret_to_right(prompt):
196
    prompt.caret.locus += 1
197
198
199
def _move_caret_to_head(prompt):
200
    prompt.caret.locus = prompt.caret.head
201
202
203
def _move_caret_to_lead(prompt):
204
    prompt.caret.locus = prompt.caret.lead
205
206
207
def _move_caret_to_tail(prompt):
208
    prompt.caret.locus = prompt.caret.tail
209
210
211
def _assign_previous_text(prompt):
212
    prompt.text = prompt.history.previous()
213
214
215
def _assign_next_text(prompt):
216
    prompt.text = prompt.history.next()
217
218
219
def _assign_previous_matched_text(prompt):
220
    prompt.text = prompt.history.previous_match()
221
222
223
def _assign_next_matched_text(prompt):
224
    prompt.text = prompt.history.next_match()
225
226
227
def _paste_from_register(prompt):
228
    context = prompt.context.to_dict()
229
    prompt.update_text('"')
230
    prompt.redraw_prompt()
231
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
232
    prompt.context.extend(context)
233
    val = prompt.nvim.call('getreg', reg)
234
    prompt.update_text(val)
235
236
237
def _paste_from_default_register(prompt):
238
    val = prompt.nvim.call('getreg', prompt.nvim.vvars['register'])
239
    prompt.update_text(val)
240
241
242
def _yank_to_register(prompt):
243
    context = prompt.context.to_dict()
244
    prompt.update_text("'")
245
    prompt.redraw_prompt()
246
    reg = int2char(prompt.nvim, getchar(prompt.nvim))
247
    prompt.context.extend(context)
248
    prompt.nvim.call('setreg', reg, prompt.text)
249
250
251
def _yank_to_default_register(prompt):
252
    prompt.nvim.call('setreg', prompt.nvim.vvars['register'], prompt.text)
253
254
255
def _insert_special(prompt):
256
    context = prompt.context.to_dict()
257
    prompt.update_text('^')
258
    prompt.redraw_prompt()
259
    code = getchar(prompt.nvim)
260
    prompt.context.extend(context)
261
    # Substitute special keys into control char
262
    if code == b'\x80kb':
263
        code = 0x08  # ^H
264
    char = int2repr(prompt.nvim, code)
265
    prompt.update_text(char)
266
267
268
def _insert_digraph(prompt):
269
    context = prompt.context.to_dict()
270
    prompt.update_text('?')
271
    prompt.redraw_prompt()
272
    digraph = Digraph()
273
    char = digraph.retrieve(prompt.nvim)
274
    prompt.context.extend(context)
275
    prompt.update_text(char)
276
277
278
DEFAULT_ACTION = Action.from_rules([
279
    ('prompt:accept', _accept),
280
    ('prompt:cancel', _cancel),
281
    ('prompt:toggle_insert_mode', _toggle_insert_mode),
282
    ('prompt:delete_char_before_caret', _delete_char_before_caret),
283
    ('prompt:delete_word_before_caret', _delete_word_before_caret),
284
    ('prompt:delete_char_under_caret', _delete_char_under_caret),
285
    ('prompt:delete_text_after_caret', _delete_text_after_caret),
286
    ('prompt:delete_entire_text', _delete_entire_text),
287
    ('prompt:move_caret_to_left', _move_caret_to_left),
288
    ('prompt:move_caret_to_right', _move_caret_to_right),
289
    ('prompt:move_caret_to_head', _move_caret_to_head),
290
    ('prompt:move_caret_to_lead', _move_caret_to_lead),
291
    ('prompt:move_caret_to_tail', _move_caret_to_tail),
292
    ('prompt:assign_previous_text', _assign_previous_text),
293
    ('prompt:assign_next_text', _assign_next_text),
294
    ('prompt:assign_previous_matched_text', _assign_previous_matched_text),
295
    ('prompt:assign_next_matched_text', _assign_next_matched_text),
296
    ('prompt:paste_from_register', _paste_from_register),
297
    ('prompt:paste_from_default_register', _paste_from_default_register),
298
    ('prompt:yank_to_register', _yank_to_register),
299
    ('prompt:yank_to_default_register', _yank_to_default_register),
300
    ('prompt:insert_special', _insert_special),
301
    ('prompt:insert_digraph', _insert_digraph),
302
])
303