Completed
Push — master ( dfe802...608c2f )
by Lambda
01:39
created

_build_echon_expr()   B

Complexity

Conditions 5

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
c 2
b 0
f 0
dl 0
loc 10
rs 8.5454
1
"""Prompt module."""
2
import re
3
import copy
4
from datetime import timedelta
5
from .util import build_echon_expr, ESCAPE_ECHO
6
7
ACTION_KEYSTROKE_PATTERN = re.compile(r'<(\w+:\w+)>')
8
9
STATUS_PROGRESS = 0
10
STATUS_ACCEPT = 1
11
STATUS_CANCEL = 2
12
STATUS_ERROR = 3
13
14
INSERT_MODE_INSERT = 1
15
INSERT_MODE_REPLACE = 2
16
17
18
class Prompt:
19
    """Prompt class."""
20
21
    prefix = ''
22
23
    def __init__(self, nvim, context):
24
        """Constructor.
25
26
        Args:
27
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
28
            context (Context): A ``prompt.context.Context`` instance.
29
        """
30
        from .caret import Caret
31
        from .history import History
32
        from .keymap import DEFAULT_KEYMAP_RULES, Keymap
33
        from .action import DEFAULT_ACTION
34
        self.nvim = nvim
35
        self.insert_mode = INSERT_MODE_INSERT
36
        self.context = context
37
        self.caret = Caret(context)
38
        self.history = History(self)
39
        self.action = copy.copy(DEFAULT_ACTION)
40
        self.keymap = Keymap.from_rules(nvim, DEFAULT_KEYMAP_RULES)
41
42
    @property
43
    def text(self):
44
        """str: A current context text.
45
46
        It automatically adjust the current caret locus to the tail of the text
47
        if any text is assigned.
48
49
        It calls the following overridable methods in order of the appearance.
50
51
        - on_init - Only once
52
        - on_update
53
        - on_redraw
54
        - on_keypress
55
        - on_term - Only once
56
57
        Example:
58
            >>> from .context import Context
59
            >>> from unittest.mock import MagicMock
60
            >>> nvim = MagicMock()
61
            >>> nvim.options = {'encoding': 'utf-8'}
62
            >>> context = Context()
63
            >>> context.text = "Hello"
64
            >>> context.caret_locus = 3
65
            >>> prompt = Prompt(nvim, context)
66
            >>> prompt.text
67
            'Hello'
68
            >>> prompt.caret.locus
69
            3
70
            >>> prompt.text = "FooFooFoo"
71
            >>> prompt.text
72
            'FooFooFoo'
73
            >>> prompt.caret.locus
74
            9
75
        """
76
        return self.context.text
77
78
    @text.setter
79
    def text(self, value):
80
        self.context.text = value
81
        self.caret.locus = len(value)
82
83
    def apply_custom_mappings_from_vim_variable(self, varname):
84
        """Apply custom key mappings from Vim variable.
85
86
        Args:
87
            varname (str): A global Vim's variable name
88
        """
89
        if varname in self.nvim.vars:
90
            custom_mappings = self.nvim.vars[varname]
91
            for rule in custom_mappings:
92
                self.keymap.register_from_rule(self.nvim, rule)
93
94
    def insert_text(self, text):
95
        """Insert text after the caret.
96
97
        Args:
98
            text (str): A text which will be inserted after the caret.
99
100
        Example:
101
            >>> from .context import Context
102
            >>> from unittest.mock import MagicMock
103
            >>> nvim = MagicMock()
104
            >>> nvim.options = {'encoding': 'utf-8'}
105
            >>> context = Context()
106
            >>> context.text = "Hello Goodbye"
107
            >>> context.caret_locus = 3
108
            >>> prompt = Prompt(nvim, context)
109
            >>> prompt.insert_text('AA')
110
            >>> prompt.text
111
            'HelAAlo Goodbye'
112
        """
113
        locus = self.caret.locus
114
        self.text = ''.join([
115
            self.caret.get_backward_text(),
116
            text,
117
            self.caret.get_selected_text(),
118
            self.caret.get_forward_text(),
119
        ])
120
        self.caret.locus = locus + len(text)
121
122
    def replace_text(self, text):
123
        """Replace text after the caret.
124
125
        Args:
126
            text (str): A text which will be replaced after the caret.
127
128
        Example:
129
            >>> from .context import Context
130
            >>> from unittest.mock import MagicMock
131
            >>> nvim = MagicMock()
132
            >>> nvim.options = {'encoding': 'utf-8'}
133
            >>> context = Context()
134
            >>> context.text = "Hello Goodbye"
135
            >>> context.caret_locus = 3
136
            >>> prompt = Prompt(nvim, context)
137
            >>> prompt.replace_text('AA')
138
            >>> prompt.text
139
            'HelAA Goodbye'
140
        """
141
        locus = self.caret.locus
142
        self.text = ''.join([
143
            self.caret.get_backward_text(),
144
            text,
145
            self.caret.get_forward_text()[len(text) - 1:],
146
        ])
147
        self.caret.locus = locus + len(text)
148
149
    def update_text(self, text):
150
        """Insert or replace text after the caret.
151
152
        Args:
153
            text (str): A text which will be replaced after the caret.
154
155
        Example:
156
            >>> from .context import Context
157
            >>> from unittest.mock import MagicMock
158
            >>> nvim = MagicMock()
159
            >>> nvim.options = {'encoding': 'utf-8'}
160
            >>> context = Context()
161
            >>> context.text = "Hello Goodbye"
162
            >>> context.caret_locus = 3
163
            >>> prompt = Prompt(nvim, context)
164
            >>> prompt.insert_mode = INSERT_MODE_INSERT
165
            >>> prompt.update_text('AA')
166
            >>> prompt.text
167
            'HelAAlo Goodbye'
168
            >>> prompt.insert_mode = INSERT_MODE_REPLACE
169
            >>> prompt.update_text('BB')
170
            >>> prompt.text
171
            'HelAABB Goodbye'
172
        """
173
        if self.insert_mode == INSERT_MODE_INSERT:
174
            self.insert_text(text)
175
        else:
176
            self.replace_text(text)
177
178
    def redraw_prompt(self):
179
        # NOTE:
180
        # There is a highlight name 'Cursor' but some sometime the visibility
181
        # is quite low (e.g. tender) so use 'IncSearch' instead while the
182
        # visibility is quite good and most recent colorscheme care about it.
183
        backward_text = self.caret.get_backward_text()
184
        selected_text = self.caret.get_selected_text()
185
        forward_text = self.caret.get_forward_text()
186
        self.nvim.command('|'.join([
187
            'redraw',
188
            build_echon_expr(self.prefix, 'Question'),
189
            build_echon_expr(backward_text, 'None'),
190
            build_echon_expr(selected_text, 'IncSearch'),
191
            build_echon_expr(forward_text, 'None'),
192
        ]))
193
194
    def start(self, default=None):
195
        """Start prompt with ``default`` text and return value.
196
197
        Args:
198
            default (None or str): A default text of the prompt. If omitted, a
199
                text in the context specified in the constructor is used.
200
201
        Returns:
202
            int: The status of the prompt.
203
        """
204
        status = self.on_init(default) or STATUS_PROGRESS
205
        if self.nvim.options['timeout']:
206
            timeoutlen = timedelta(
207
                milliseconds=int(self.nvim.options['timeoutlen'])
208
            )
209
        else:
210
            timeoutlen = None
211
        try:
212
            status = self.on_update(status) or STATUS_PROGRESS
213
            while status is STATUS_PROGRESS:
214
                self.on_redraw()
215
                status = self.on_keypress(
216
                    self.keymap.harvest(self.nvim, timeoutlen)
217
                ) or STATUS_PROGRESS
218
                status = self.on_update(status) or STATUS_PROGRESS
219
        except KeyboardInterrupt:
220
            status = STATUS_CANCEL
221
        except Exception as e:
222
            self.nvim.command('|'.join([
223
                'echoerr "%s"' % line.translate(ESCAPE_ECHO)
224
                for line in str(e).splitlines()
225
            ]))
226
            status = STATUS_ERROR
227
        self.nvim.command('redraw!')
228
        if self.text:
229
            self.nvim.call('histadd', 'input', self.text)
230
        return self.on_term(status)
231
232
    def on_init(self, default):
233
        """Initialize the prompt.
234
235
        It calls 'inputsave' function in Vim and assign ``default`` text to the
236
        ``self.text`` to initialize the prompt text in default.
237
238
        Args:
239
            default (None or str): A default text of the prompt. If omitted, a
240
                text in the context specified in the constructor is used.
241
242
        Returns:
243
            None or int: The return value will be used as a status of the
244
                prompt mainloop, indicating that if return value is not
245
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
246
                Returning None is equal to returning STATUS_PROGRESS.
247
        """
248
        self.nvim.call('inputsave')
249
        if default:
250
            self.text = default
251
252
    def on_update(self, status):
253
        """Update the prompt status and return the status.
254
255
        It is used to update the prompt status. In default, it does nothing and
256
        return the specified ``status`` directly.
257
258
        Args:
259
            status (int): A prompt status which is updated by previous
260
                on_keypress call.
261
262
        Returns:
263
            None or int: The return value will be used as a status of the
264
                prompt mainloop, indicating that if return value is not
265
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
266
                Returning None is equal to returning STATUS_PROGRESS.
267
        """
268
        return status
269
270
    def on_redraw(self):
271
        """Redraw the prompt.
272
273
        It is used to redraw the prompt. In default, it echos specified prefix
274
        the caret, and input text.
275
        """
276
        self.redraw_prompt()
277
278
    def on_keypress(self, keystroke):
279
        """Handle a pressed keystroke and return the status.
280
281
        It is used to handle a pressed keystroke. Note that subclass should NOT
282
        override this method to perform actions. Register a new custom action
283
        instead. In default, it call action and return the result if the
284
        keystroke is <xxx:xxx>or call Vim function XXX and return the result
285
        if the keystroke is <call:XXX>.
286
287
        Args:
288
            keystroke (Keystroke): A pressed keystroke instance. Note that this
289
                instance is a reslved keystroke instace by keymap.
290
291
        Returns:
292
            None or int: The return value will be used as a status of the
293
                prompt mainloop, indicating that if return value is not
294
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
295
                Returning None is equal to returning STATUS_PROGRESS.
296
        """
297
        m = ACTION_KEYSTROKE_PATTERN.match(str(keystroke))
298
        if m:
299
            return self.action.call(self, m.group(1))
300
        else:
301
            self.update_text(str(keystroke))
302
303
    def on_term(self, status):
304
        """Finalize the prompt.
305
306
        It calls 'inputrestore' function in Vim to finalize the prompt in
307
        default. The return value is used as a return value of the prompt.
308
309
        Args:
310
            status (int): A prompt status.
311
312
        Returns:
313
            int: A status which is used as a result value of the prompt.
314
        """
315
        self.nvim.call('inputrestore')
316
        return status
317