cpython/Lib/_pyrepl/commands.py

#   Copyright 2000-2010 Michael Hudson-Doyle <[email protected]>
#                       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