| # Copyright 2000-2010 Michael Hudson-Doyle <micahel@gmail.com> | |
| # Antonio Cuni | |
| # Armin Rigo | |
| # | |
| # All Rights Reserved | |
| # | |
| # | |
| # Permission to use, copy, modify, and distribute this software and | |
| # its documentation for any purpose is hereby granted without fee, | |
| # provided that the above copyright notice appear in all copies and | |
| # that both that copyright notice and this permission notice appear in | |
| # supporting documentation. | |
| # | |
| # THE AUTHOR MICHAEL HUDSON DISCLAIMS ALL WARRANTIES WITH REGARD TO | |
| # THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY | |
| # AND FITNESS, IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, | |
| # INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER | |
| # RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF | |
| # CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN | |
| # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | |
| from __future__ import annotations | |
| import os | |
| # Categories of actions: | |
| # killing | |
| # yanking | |
| # motion | |
| # editing | |
| # history | |
| # finishing | |
| # [completion] | |
| # types | |
| if False: | |
| from .historical_reader import HistoricalReader | |
| class Command: | |
| finish: bool = False | |
| kills_digit_arg: bool = True | |
| def __init__( | |
| self, reader: HistoricalReader, event_name: str, event: list[str] | |
| ) -> None: | |
| # Reader should really be "any reader" but there's too much usage of | |
| # HistoricalReader methods and fields in the code below for us to | |
| # refactor at the moment. | |
| self.reader = reader | |
| self.event = event | |
| self.event_name = event_name | |
| def do(self) -> None: | |
| pass | |
| class KillCommand(Command): | |
| def kill_range(self, start: int, end: int) -> None: | |
| if start == end: | |
| return | |
| r = self.reader | |
| b = r.buffer | |
| text = b[start:end] | |
| del b[start:end] | |
| if is_kill(r.last_command): | |
| if start < r.pos: | |
| r.kill_ring[-1] = text + r.kill_ring[-1] | |
| else: | |
| r.kill_ring[-1] = r.kill_ring[-1] + text | |
| else: | |
| r.kill_ring.append(text) | |
| r.pos = start | |
| r.dirty = True | |
| class YankCommand(Command): | |
| pass | |
| class MotionCommand(Command): | |
| pass | |
| class EditCommand(Command): | |
| pass | |
| class FinishCommand(Command): | |
| finish = True | |
| pass | |
| def is_kill(command: type[Command] | None) -> bool: | |
| return command is not None and issubclass(command, KillCommand) | |
| def is_yank(command: type[Command] | None) -> bool: | |
| return command is not None and issubclass(command, YankCommand) | |
| # etc | |
| class digit_arg(Command): | |
| kills_digit_arg = False | |
| def do(self) -> None: | |
| r = self.reader | |
| c = self.event[-1] | |
| if c == "-": | |
| if r.arg is not None: | |
| r.arg = -r.arg | |
| else: | |
| r.arg = -1 | |
| else: | |
| d = int(c) | |
| if r.arg is None: | |
| r.arg = d | |
| else: | |
| if r.arg < 0: | |
| r.arg = 10 * r.arg - d | |
| else: | |
| r.arg = 10 * r.arg + d | |
| r.dirty = True | |
| class clear_screen(Command): | |
| def do(self) -> None: | |
| r = self.reader | |
| r.console.clear() | |
| r.dirty = True | |
| class refresh(Command): | |
| def do(self) -> None: | |
| self.reader.dirty = True | |
| class repaint(Command): | |
| def do(self) -> None: | |
| self.reader.dirty = True | |
| self.reader.console.repaint() | |
| class kill_line(KillCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| eol = r.eol() | |
| for c in b[r.pos : eol]: | |
| if not c.isspace(): | |
| self.kill_range(r.pos, eol) | |
| return | |
| else: | |
| self.kill_range(r.pos, eol + 1) | |
| class unix_line_discard(KillCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| self.kill_range(r.bol(), r.pos) | |
| class unix_word_rubout(KillCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| self.kill_range(r.bow(), r.pos) | |
| class kill_word(KillCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| self.kill_range(r.pos, r.eow()) | |
| class backward_kill_word(KillCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| self.kill_range(r.bow(), r.pos) | |
| class yank(YankCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| if not r.kill_ring: | |
| r.error("nothing to yank") | |
| return | |
| r.insert(r.kill_ring[-1]) | |
| class yank_pop(YankCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| if not r.kill_ring: | |
| r.error("nothing to yank") | |
| return | |
| if not is_yank(r.last_command): | |
| r.error("previous command was not a yank") | |
| return | |
| repl = len(r.kill_ring[-1]) | |
| r.kill_ring.insert(0, r.kill_ring.pop()) | |
| t = r.kill_ring[-1] | |
| b[r.pos - repl : r.pos] = t | |
| r.pos = r.pos - repl + len(t) | |
| r.dirty = True | |
| class interrupt(FinishCommand): | |
| def do(self) -> None: | |
| import signal | |
| self.reader.console.finish() | |
| self.reader.finish() | |
| os.kill(os.getpid(), signal.SIGINT) | |
| class ctrl_c(Command): | |
| def do(self) -> None: | |
| self.reader.console.finish() | |
| self.reader.finish() | |
| raise KeyboardInterrupt | |
| class suspend(Command): | |
| def do(self) -> None: | |
| import signal | |
| r = self.reader | |
| p = r.pos | |
| r.console.finish() | |
| os.kill(os.getpid(), signal.SIGSTOP) | |
| ## this should probably be done | |
| ## in a handler for SIGCONT? | |
| r.console.prepare() | |
| r.pos = p | |
| # r.posxy = 0, 0 # XXX this is invalid | |
| r.dirty = True | |
| r.console.screen = [] | |
| class up(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for _ in range(r.get_arg()): | |
| x, y = r.pos2xy() | |
| new_y = y - 1 | |
| if r.bol() == 0: | |
| if r.historyi > 0: | |
| r.select_item(r.historyi - 1) | |
| return | |
| r.pos = 0 | |
| r.error("start of buffer") | |
| return | |
| if ( | |
| x | |
| > ( | |
| new_x := r.max_column(new_y) | |
| ) # we're past the end of the previous line | |
| or x == r.max_column(y) | |
| and any( | |
| not i.isspace() for i in r.buffer[r.bol() :] | |
| ) # move between eols | |
| ): | |
| x = new_x | |
| r.setpos_from_xy(x, new_y) | |
| class down(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| for _ in range(r.get_arg()): | |
| x, y = r.pos2xy() | |
| new_y = y + 1 | |
| if new_y > r.max_row(): | |
| if r.historyi < len(r.history): | |
| r.select_item(r.historyi + 1) | |
| r.pos = r.eol(0) | |
| return | |
| r.pos = len(b) | |
| r.error("end of buffer") | |
| return | |
| if ( | |
| x | |
| > ( | |
| new_x := r.max_column(new_y) | |
| ) # we're past the end of the previous line | |
| or x == r.max_column(y) | |
| and any( | |
| not i.isspace() for i in r.buffer[r.bol() :] | |
| ) # move between eols | |
| ): | |
| x = new_x | |
| r.setpos_from_xy(x, new_y) | |
| class left(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| p = r.pos - 1 | |
| if p >= 0: | |
| r.pos = p | |
| else: | |
| self.reader.error("start of buffer") | |
| class right(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| for i in range(r.get_arg()): | |
| p = r.pos + 1 | |
| if p <= len(b): | |
| r.pos = p | |
| else: | |
| self.reader.error("end of buffer") | |
| class beginning_of_line(MotionCommand): | |
| def do(self) -> None: | |
| self.reader.pos = self.reader.bol() | |
| class end_of_line(MotionCommand): | |
| def do(self) -> None: | |
| self.reader.pos = self.reader.eol() | |
| class home(MotionCommand): | |
| def do(self) -> None: | |
| self.reader.pos = 0 | |
| class end(MotionCommand): | |
| def do(self) -> None: | |
| self.reader.pos = len(self.reader.buffer) | |
| class forward_word(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| r.pos = r.eow() | |
| class backward_word(MotionCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| for i in range(r.get_arg()): | |
| r.pos = r.bow() | |
| class self_insert(EditCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| text = self.event * r.get_arg() | |
| r.insert(text) | |
| class insert_nl(EditCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| r.insert("\n" * r.get_arg()) | |
| class transpose_characters(EditCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| s = r.pos - 1 | |
| if s < 0: | |
| r.error("cannot transpose at start of buffer") | |
| else: | |
| if s == len(b): | |
| s -= 1 | |
| t = min(s + r.get_arg(), len(b) - 1) | |
| c = b[s] | |
| del b[s] | |
| b.insert(t, c) | |
| r.pos = t | |
| r.dirty = True | |
| class backspace(EditCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| for i in range(r.get_arg()): | |
| if r.pos > 0: | |
| r.pos -= 1 | |
| del b[r.pos] | |
| r.dirty = True | |
| else: | |
| self.reader.error("can't backspace at start") | |
| class delete(EditCommand): | |
| def do(self) -> None: | |
| r = self.reader | |
| b = r.buffer | |
| if ( | |
| r.pos == 0 | |
| and len(b) == 0 # this is something of a hack | |
| and self.event[-1] == "\004" | |
| ): | |
| r.update_screen() | |
| r.console.finish() | |
| raise EOFError | |
| for i in range(r.get_arg()): | |
| if r.pos != len(b): | |
| del b[r.pos] | |
| r.dirty = True | |
| else: | |
| self.reader.error("end of buffer") | |
| class accept(FinishCommand): | |
| def do(self) -> None: | |
| pass | |
| class help(Command): | |
| def do(self) -> None: | |
| import _sitebuiltins | |
| with self.reader.suspend(): | |
| self.reader.msg = _sitebuiltins._Helper()() # type: ignore[assignment, call-arg] | |
| class invalid_key(Command): | |
| def do(self) -> None: | |
| pending = self.reader.console.getpending() | |
| s = "".join(self.event) + pending.data | |
| self.reader.error("`%r' not bound" % s) | |
| class invalid_command(Command): | |
| def do(self) -> None: | |
| s = self.event_name | |
| self.reader.error("command `%s' not known" % s) | |
| class show_history(Command): | |
| def do(self) -> None: | |
| from .pager import get_pager | |
| from site import gethistoryfile # type: ignore[attr-defined] | |
| history = os.linesep.join(self.reader.history[:]) | |
| with self.reader.suspend(): | |
| pager = get_pager() | |
| pager(history, gethistoryfile()) | |
| class paste_mode(Command): | |
| def do(self) -> None: | |
| self.reader.paste_mode = not self.reader.paste_mode | |
| self.reader.dirty = True | |
| class enable_bracketed_paste(Command): | |
| def do(self) -> None: | |
| self.reader.paste_mode = True | |
| self.reader.in_bracketed_paste = True | |
| class disable_bracketed_paste(Command): | |
| def do(self) -> None: | |
| self.reader.paste_mode = False | |
| self.reader.in_bracketed_paste = False | |
| self.reader.dirty = True |