• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#   Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com>
2#                       Antonio Cuni
3#                       Armin Rigo
4#
5#                        All Rights Reserved
6#
7#
8# Permission to use, copy, modify, and distribute this software and
9# its documentation for any purpose is hereby granted without fee,
10# provided that the above copyright notice appear in all copies and
11# that both that copyright notice and this permission notice appear in
12# supporting documentation.
13#
14# THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO
15# THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
16# AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
17# INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER
18# RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF
19# CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
20# CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
21
22from __future__ import annotations
23import os
24
25# Categories of actions:
26#  killing
27#  yanking
28#  motion
29#  editing
30#  history
31#  finishing
32# [completion]
33
34
35# types
36if False:
37    from .historical_reader import HistoricalReader
38
39
40class Command:
41    finish: bool = False
42    kills_digit_arg: bool = True
43
44    def __init__(
45        self, reader: HistoricalReader, event_name: str, event: list[str]
46    ) -> None:
47        # Reader should really be "any reader" but there's too much usage of
48        # HistoricalReader methods and fields in the code below for us to
49        # refactor at the moment.
50
51        self.reader = reader
52        self.event = event
53        self.event_name = event_name
54
55    def do(self) -> None:
56        pass
57
58
59class KillCommand(Command):
60    def kill_range(self, start: int, end: int) -> None:
61        if start == end:
62            return
63        r = self.reader
64        b = r.buffer
65        text = b[start:end]
66        del b[start:end]
67        if is_kill(r.last_command):
68            if start < r.pos:
69                r.kill_ring[-1] = text + r.kill_ring[-1]
70            else:
71                r.kill_ring[-1] = r.kill_ring[-1] + text
72        else:
73            r.kill_ring.append(text)
74        r.pos = start
75        r.dirty = True
76
77
78class YankCommand(Command):
79    pass
80
81
82class MotionCommand(Command):
83    pass
84
85
86class EditCommand(Command):
87    pass
88
89
90class FinishCommand(Command):
91    finish = True
92    pass
93
94
95def is_kill(command: type[Command] | None) -> bool:
96    return command is not None and issubclass(command, KillCommand)
97
98
99def is_yank(command: type[Command] | None) -> bool:
100    return command is not None and issubclass(command, YankCommand)
101
102
103# etc
104
105
106class digit_arg(Command):
107    kills_digit_arg = False
108
109    def do(self) -> None:
110        r = self.reader
111        c = self.event[-1]
112        if c == "-":
113            if r.arg is not None:
114                r.arg = -r.arg
115            else:
116                r.arg = -1
117        else:
118            d = int(c)
119            if r.arg is None:
120                r.arg = d
121            else:
122                if r.arg < 0:
123                    r.arg = 10 * r.arg - d
124                else:
125                    r.arg = 10 * r.arg + d
126        r.dirty = True
127
128
129class clear_screen(Command):
130    def do(self) -> None:
131        r = self.reader
132        r.console.clear()
133        r.dirty = True
134
135
136class refresh(Command):
137    def do(self) -> None:
138        self.reader.dirty = True
139
140
141class repaint(Command):
142    def do(self) -> None:
143        self.reader.dirty = True
144        self.reader.console.repaint()
145
146
147class kill_line(KillCommand):
148    def do(self) -> None:
149        r = self.reader
150        b = r.buffer
151        eol = r.eol()
152        for c in b[r.pos : eol]:
153            if not c.isspace():
154                self.kill_range(r.pos, eol)
155                return
156        else:
157            self.kill_range(r.pos, eol + 1)
158
159
160class unix_line_discard(KillCommand):
161    def do(self) -> None:
162        r = self.reader
163        self.kill_range(r.bol(), r.pos)
164
165
166class unix_word_rubout(KillCommand):
167    def do(self) -> None:
168        r = self.reader
169        for i in range(r.get_arg()):
170            self.kill_range(r.bow(), r.pos)
171
172
173class kill_word(KillCommand):
174    def do(self) -> None:
175        r = self.reader
176        for i in range(r.get_arg()):
177            self.kill_range(r.pos, r.eow())
178
179
180class backward_kill_word(KillCommand):
181    def do(self) -> None:
182        r = self.reader
183        for i in range(r.get_arg()):
184            self.kill_range(r.bow(), r.pos)
185
186
187class yank(YankCommand):
188    def do(self) -> None:
189        r = self.reader
190        if not r.kill_ring:
191            r.error("nothing to yank")
192            return
193        r.insert(r.kill_ring[-1])
194
195
196class yank_pop(YankCommand):
197    def do(self) -> None:
198        r = self.reader
199        b = r.buffer
200        if not r.kill_ring:
201            r.error("nothing to yank")
202            return
203        if not is_yank(r.last_command):
204            r.error("previous command was not a yank")
205            return
206        repl = len(r.kill_ring[-1])
207        r.kill_ring.insert(0, r.kill_ring.pop())
208        t = r.kill_ring[-1]
209        b[r.pos - repl : r.pos] = t
210        r.pos = r.pos - repl + len(t)
211        r.dirty = True
212
213
214class interrupt(FinishCommand):
215    def do(self) -> None:
216        import signal
217
218        self.reader.console.finish()
219        self.reader.finish()
220        os.kill(os.getpid(), signal.SIGINT)
221
222
223class ctrl_c(Command):
224    def do(self) -> None:
225        self.reader.console.finish()
226        self.reader.finish()
227        raise KeyboardInterrupt
228
229
230class suspend(Command):
231    def do(self) -> None:
232        import signal
233
234        r = self.reader
235        p = r.pos
236        r.console.finish()
237        os.kill(os.getpid(), signal.SIGSTOP)
238        ## this should probably be done
239        ## in a handler for SIGCONT?
240        r.console.prepare()
241        r.pos = p
242        # r.posxy = 0, 0  # XXX this is invalid
243        r.dirty = True
244        r.console.screen = []
245
246
247class up(MotionCommand):
248    def do(self) -> None:
249        r = self.reader
250        for _ in range(r.get_arg()):
251            x, y = r.pos2xy()
252            new_y = y - 1
253
254            if r.bol() == 0:
255                if r.historyi > 0:
256                    r.select_item(r.historyi - 1)
257                    return
258                r.pos = 0
259                r.error("start of buffer")
260                return
261
262            if (
263                x
264                > (
265                    new_x := r.max_column(new_y)
266                )  # we're past the end of the previous line
267                or x == r.max_column(y)
268                and any(
269                    not i.isspace() for i in r.buffer[r.bol() :]
270                )  # move between eols
271            ):
272                x = new_x
273
274            r.setpos_from_xy(x, new_y)
275
276
277class down(MotionCommand):
278    def do(self) -> None:
279        r = self.reader
280        b = r.buffer
281        for _ in range(r.get_arg()):
282            x, y = r.pos2xy()
283            new_y = y + 1
284
285            if new_y > r.max_row():
286                if r.historyi < len(r.history):
287                    r.select_item(r.historyi + 1)
288                    r.pos = r.eol(0)
289                    return
290                r.pos = len(b)
291                r.error("end of buffer")
292                return
293
294            if (
295                x
296                > (
297                    new_x := r.max_column(new_y)
298                )  # we're past the end of the previous line
299                or x == r.max_column(y)
300                and any(
301                    not i.isspace() for i in r.buffer[r.bol() :]
302                )  # move between eols
303            ):
304                x = new_x
305
306            r.setpos_from_xy(x, new_y)
307
308
309class left(MotionCommand):
310    def do(self) -> None:
311        r = self.reader
312        for i in range(r.get_arg()):
313            p = r.pos - 1
314            if p >= 0:
315                r.pos = p
316            else:
317                self.reader.error("start of buffer")
318
319
320class right(MotionCommand):
321    def do(self) -> None:
322        r = self.reader
323        b = r.buffer
324        for i in range(r.get_arg()):
325            p = r.pos + 1
326            if p <= len(b):
327                r.pos = p
328            else:
329                self.reader.error("end of buffer")
330
331
332class beginning_of_line(MotionCommand):
333    def do(self) -> None:
334        self.reader.pos = self.reader.bol()
335
336
337class end_of_line(MotionCommand):
338    def do(self) -> None:
339        self.reader.pos = self.reader.eol()
340
341
342class home(MotionCommand):
343    def do(self) -> None:
344        self.reader.pos = 0
345
346
347class end(MotionCommand):
348    def do(self) -> None:
349        self.reader.pos = len(self.reader.buffer)
350
351
352class forward_word(MotionCommand):
353    def do(self) -> None:
354        r = self.reader
355        for i in range(r.get_arg()):
356            r.pos = r.eow()
357
358
359class backward_word(MotionCommand):
360    def do(self) -> None:
361        r = self.reader
362        for i in range(r.get_arg()):
363            r.pos = r.bow()
364
365
366class self_insert(EditCommand):
367    def do(self) -> None:
368        r = self.reader
369        text = self.event * r.get_arg()
370        r.insert(text)
371
372
373class insert_nl(EditCommand):
374    def do(self) -> None:
375        r = self.reader
376        r.insert("\n" * r.get_arg())
377
378
379class transpose_characters(EditCommand):
380    def do(self) -> None:
381        r = self.reader
382        b = r.buffer
383        s = r.pos - 1
384        if s < 0:
385            r.error("cannot transpose at start of buffer")
386        else:
387            if s == len(b):
388                s -= 1
389            t = min(s + r.get_arg(), len(b) - 1)
390            c = b[s]
391            del b[s]
392            b.insert(t, c)
393            r.pos = t
394            r.dirty = True
395
396
397class backspace(EditCommand):
398    def do(self) -> None:
399        r = self.reader
400        b = r.buffer
401        for i in range(r.get_arg()):
402            if r.pos > 0:
403                r.pos -= 1
404                del b[r.pos]
405                r.dirty = True
406            else:
407                self.reader.error("can't backspace at start")
408
409
410class delete(EditCommand):
411    def do(self) -> None:
412        r = self.reader
413        b = r.buffer
414        if (
415            r.pos == 0
416            and len(b) == 0  # this is something of a hack
417            and self.event[-1] == "\004"
418        ):
419            r.update_screen()
420            r.console.finish()
421            raise EOFError
422        for i in range(r.get_arg()):
423            if r.pos != len(b):
424                del b[r.pos]
425                r.dirty = True
426            else:
427                self.reader.error("end of buffer")
428
429
430class accept(FinishCommand):
431    def do(self) -> None:
432        pass
433
434
435class help(Command):
436    def do(self) -> None:
437        import _sitebuiltins
438
439        with self.reader.suspend():
440            self.reader.msg = _sitebuiltins._Helper()()  # type: ignore[assignment, call-arg]
441
442
443class invalid_key(Command):
444    def do(self) -> None:
445        pending = self.reader.console.getpending()
446        s = "".join(self.event) + pending.data
447        self.reader.error("`%r' not bound" % s)
448
449
450class invalid_command(Command):
451    def do(self) -> None:
452        s = self.event_name
453        self.reader.error("command `%s' not known" % s)
454
455
456class show_history(Command):
457    def do(self) -> None:
458        from .pager import get_pager
459        from site import gethistoryfile  # type: ignore[attr-defined]
460
461        history = os.linesep.join(self.reader.history[:])
462        with self.reader.suspend():
463            pager = get_pager()
464            pager(history, gethistoryfile())
465
466
467class paste_mode(Command):
468
469    def do(self) -> None:
470        self.reader.paste_mode = not self.reader.paste_mode
471        self.reader.dirty = True
472
473
474class enable_bracketed_paste(Command):
475    def do(self) -> None:
476        self.reader.paste_mode = True
477        self.reader.in_bracketed_paste = True
478
479class disable_bracketed_paste(Command):
480    def do(self) -> None:
481        self.reader.paste_mode = False
482        self.reader.in_bracketed_paste = False
483        self.reader.dirty = True
484