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

Prompt.on_keypress()   A

Complexity

Conditions 2

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

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