Prompt   A
last analyzed

Complexity

Total Complexity 23

Size/Duplication

Total Lines 277
Duplicated Lines 0 %

Importance

Changes 21
Bugs 0 Features 0
Metric Value
c 21
b 0
f 0
dl 0
loc 277
rs 10
wmc 23

14 Methods

Rating   Name   Duplication   Size   Complexity  
A restore() 0 4 1
A replace_text() 0 24 1
A store() 0 5 1
B start() 0 37 7
A on_update() 0 17 1
A on_keypress() 0 23 2
A on_harvest() 0 8 1
A on_term() 0 14 1
A on_redraw() 0 7 1
A redraw_prompt() 0 19 2
A __init__() 0 20 1
A on_init() 0 12 1
A insert_text() 0 25 1
A update_text() 0 26 2
1
"""Prompt module."""
2
import copy
3
import re
4
import weakref
5
from collections import namedtuple
6
from datetime import timedelta
7
from .action import ACTION_PATTERN
8
from .util import build_echon_expr
9
10
11
ACTION_KEYSTROKE_PATTERN = re.compile(
12
    r'<(?P<action>%s)>' % ACTION_PATTERN.pattern
13
)
14
15
16
STATUS_PROGRESS = 0
17
STATUS_ACCEPT = 1
18
STATUS_CANCEL = 2
19
STATUS_INTERRUPT = 3
20
21
INSERT_MODE_INSERT = 1
22
INSERT_MODE_REPLACE = 2
23
24
DEFAULT_HARVEST_INTERVAL = 0.033
25
26
Condition = namedtuple('Condition', ['text', 'caret_locus'])
27
28
29
class Prompt:
30
    """Prompt class.
31
32
    Attributes:
33
        prefix: Prompt prefix
34
        highlight_prefix: Highlight group name for the prefix
35
        highlight_text: Highlight group name for the text
36
        highlight_caret: Highlight group name for the caret
37
        harvest_interval: Harvest interval in second
38
    """
39
40
    prefix = ''
41
42
    highlight_prefix = 'Question'
43
44
    highlight_text = 'None'
45
46
    highlight_caret = 'IncSearch'
47
48
    harvest_interval = DEFAULT_HARVEST_INTERVAL
49
50
    def __init__(self, nvim):
51
        """Constructor.
52
53
        Args:
54
            nvim (neovim.Nvim): A ``neovim.Nvim`` instance.
55
        """
56
        from .caret import Caret
57
        from .history import History
58
        from .keymap import DEFAULT_KEYMAP_RULES, Keymap
59
        from .action import DEFAULT_ACTION
60
        self.text = ''
61
        self.nvim = nvim
62
        self.insert_mode = INSERT_MODE_INSERT
63
        self.caret = Caret(weakref.proxy(self))
64
        self.history = History(weakref.proxy(self))
65
        self.action = copy.copy(DEFAULT_ACTION)
66
        self.keymap = Keymap.from_rules(nvim, DEFAULT_KEYMAP_RULES)
67
        # MacVim (GUI) has a problem on 'redraw'
68
        self.is_macvim = (
69
            nvim.call('has', 'gui_running') and nvim.call('has', 'mac')
70
        )
71
72
    def insert_text(self, text):
73
        """Insert text after the caret.
74
75
        Args:
76
            text (str): A text which will be inserted after the caret.
77
78
        Example:
79
            >>> from unittest.mock import MagicMock
80
            >>> nvim = MagicMock()
81
            >>> nvim.options = {'encoding': 'utf-8'}
82
            >>> prompt = Prompt(nvim)
83
            >>> prompt.text = "Hello Goodbye"
84
            >>> prompt.caret.locus = 3
85
            >>> prompt.insert_text('AA')
86
            >>> prompt.text
87
            'HelAAlo Goodbye'
88
        """
89
        locus = self.caret.locus
90
        self.text = ''.join([
91
            self.caret.get_backward_text(),
92
            text,
93
            self.caret.get_selected_text(),
94
            self.caret.get_forward_text(),
95
        ])
96
        self.caret.locus = locus + len(text)
97
98
    def replace_text(self, text):
99
        """Replace text after the caret.
100
101
        Args:
102
            text (str): A text which will be replaced after the caret.
103
104
        Example:
105
            >>> from unittest.mock import MagicMock
106
            >>> nvim = MagicMock()
107
            >>> nvim.options = {'encoding': 'utf-8'}
108
            >>> prompt = Prompt(nvim)
109
            >>> prompt.text = "Hello Goodbye"
110
            >>> prompt.caret.locus = 3
111
            >>> prompt.replace_text('AA')
112
            >>> prompt.text
113
            'HelAA Goodbye'
114
        """
115
        locus = self.caret.locus
116
        self.text = ''.join([
117
            self.caret.get_backward_text(),
118
            text,
119
            self.caret.get_forward_text()[len(text) - 1:],
120
        ])
121
        self.caret.locus = locus + len(text)
122
123
    def update_text(self, text):
124
        """Insert or replace text after the caret.
125
126
        Args:
127
            text (str): A text which will be replaced after the caret.
128
129
        Example:
130
            >>> from unittest.mock import MagicMock
131
            >>> nvim = MagicMock()
132
            >>> nvim.options = {'encoding': 'utf-8'}
133
            >>> prompt = Prompt(nvim)
134
            >>> prompt.text = "Hello Goodbye"
135
            >>> prompt.caret.locus = 3
136
            >>> prompt.insert_mode = INSERT_MODE_INSERT
137
            >>> prompt.update_text('AA')
138
            >>> prompt.text
139
            'HelAAlo Goodbye'
140
            >>> prompt.insert_mode = INSERT_MODE_REPLACE
141
            >>> prompt.update_text('BB')
142
            >>> prompt.text
143
            'HelAABB Goodbye'
144
        """
145
        if self.insert_mode == INSERT_MODE_INSERT:
146
            self.insert_text(text)
147
        else:
148
            self.replace_text(text)
149
150
    def redraw_prompt(self):
151
        """Redraw prompt."""
152
        # NOTE:
153
        # There is a highlight name 'Cursor' but some sometime the visibility
154
        # is quite low (e.g. tender) so use 'IncSearch' instead while the
155
        # visibility is quite good and most recent colorscheme care about it.
156
        backward_text = self.caret.get_backward_text()
157
        selected_text = self.caret.get_selected_text()
158
        forward_text = self.caret.get_forward_text()
159
        self.nvim.command('|'.join([
160
            'redraw',
161
            build_echon_expr(self.prefix, self.highlight_prefix),
162
            build_echon_expr(backward_text, self.highlight_text),
163
            build_echon_expr(selected_text, self.highlight_caret),
164
            build_echon_expr(forward_text, self.highlight_text),
165
        ]))
166
        if self.is_macvim:
167
            # MacVim requires extra 'redraw'
168
            self.nvim.command('redraw')
169
170
    def start(self):
171
        """Start prompt and return value.
172
173
        Returns:
174
            int: The status of the prompt.
175
        """
176
        status = self.on_init() or STATUS_PROGRESS
177
        if self.nvim.options['timeout']:
178
            timeoutlen = timedelta(
179
                milliseconds=int(self.nvim.options['timeoutlen'])
180
            )
181
        else:
182
            timeoutlen = None
183
        try:
184
            status = self.on_update(status) or STATUS_PROGRESS
185
            while status is STATUS_PROGRESS:
186
                self.on_redraw()
187
                status = self.on_keypress(self.keymap.harvest(
188
                    self.nvim,
189
                    timeoutlen=timeoutlen,
190
                    callback=self.on_harvest,
191
                    interval=self.harvest_interval,
192
                )) or STATUS_PROGRESS
193
                status = self.on_update(status) or status
194
        except self.nvim.error as e:
195
            # NOTE:
196
            # neovim raise nvim.error instead of KeyboardInterrupt when Ctrl-C
197
            # has pressed so treat it as a real KeyboardInterrupt exception.
198
            if str(e) == "b'Keyboard interrupt'":
199
                status = STATUS_INTERRUPT
200
            else:
201
                raise e
202
        except KeyboardInterrupt:
203
            status = STATUS_INTERRUPT
204
        if self.text:
205
            self.nvim.call('histadd', 'input', self.text)
206
        return self.on_term(status)
207
208
    def on_init(self):
209
        """Initialize the prompt.
210
211
        It calls 'inputsave' function in Vim in default.
212
213
        Returns:
214
            None or int: The return value will be used as a status of the
215
                prompt mainloop, indicating that if return value is not
216
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
217
                Returning None is equal to returning STATUS_PROGRESS.
218
        """
219
        self.nvim.call('inputsave')
220
221
    def on_update(self, status):
222
        """Update the prompt status and return the status.
223
224
        It is used to update the prompt status. In default, it does nothing and
225
        return the specified ``status`` directly.
226
227
        Args:
228
            status (int): A prompt status which is updated by previous
229
                on_keypress call.
230
231
        Returns:
232
            None or int: The return value will be used as a status of the
233
                prompt mainloop, indicating that if return value is not
234
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
235
                Returning None is equal to returning STATUS_PROGRESS.
236
        """
237
        pass
238
239
    def on_redraw(self):
240
        """Redraw the prompt.
241
242
        It is used to redraw the prompt. In default, it echos specified prefix
243
        the caret, and input text.
244
        """
245
        self.redraw_prompt()
246
247
    def on_harvest(self):
248
        """Callback which is called during a keycode harvest.
249
250
        This callback is called most often. Developers should not call heavy
251
        procession on this callback.
252
253
        """
254
        pass
255
256
    def on_keypress(self, keystroke):
257
        """Handle a pressed keystroke and return the status.
258
259
        It is used to handle a pressed keystroke. Note that subclass should NOT
260
        override this method to perform actions. Register a new custom action
261
        instead. In default, it call action and return the result if the
262
        keystroke looks like <xxx:xxx>.
263
264
        Args:
265
            keystroke (Keystroke): A pressed keystroke instance. Note that this
266
                instance is a reslved keystroke instace by keymap.
267
268
        Returns:
269
            None or int: The return value will be used as a status of the
270
                prompt mainloop, indicating that if return value is not
271
                STATUS_PROGRESS, the prompt mainloop immediately terminated.
272
                Returning None is equal to returning STATUS_PROGRESS.
273
        """
274
        m = ACTION_KEYSTROKE_PATTERN.match(str(keystroke))
275
        if m:
276
            return self.action.call(self, m.group('action'))
277
        else:
278
            self.update_text(str(keystroke))
279
280
    def on_term(self, status):
281
        """Finalize the prompt.
282
283
        It calls 'inputrestore' function in Vim to finalize the prompt in
284
        default. The return value is used as a return value of the prompt.
285
286
        Args:
287
            status (int): A prompt status.
288
289
        Returns:
290
            int: A status which is used as a result value of the prompt.
291
        """
292
        self.nvim.call('inputrestore')
293
        return status
294
295
    def store(self):
296
        """Save current prompt condition into a Condition instance."""
297
        return Condition(
298
            text=self.text,
299
            caret_locus=self.caret.locus,
300
        )
301
302
    def restore(self, condition):
303
        """Load current prompt condition from a Condition instance."""
304
        self.text = condition.text
305
        self.caret.locus = condition.caret_locus
306