From c6f6b5449fb275ef5d78a8a3e8f7f800fbc1a644 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Mon, 20 May 2024 16:01:27 -0400 Subject: [PATCH 1/4] gh-111201: Add append to screen method to avoid recalculation --- Lib/_pyrepl/commands.py | 1 + Lib/_pyrepl/reader.py | 34 +++++++++++++++++++++++++++++----- Lib/_pyrepl/types.py | 1 + Lib/_pyrepl/unix_console.py | 4 ++-- 4 files changed, 33 insertions(+), 7 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 51c7afebede5a8..436f49be08278f 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -359,6 +359,7 @@ class self_insert(EditCommand): def do(self) -> None: r = self.reader r.insert(self.event * r.get_arg()) + r.calc_screen = r.append_to_screen class insert_nl(EditCommand): diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index d15a150180811d..c7beb0e77b4512 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -35,7 +35,7 @@ # types Command = commands.Command if False: - from .types import Callback, SimpleContextManager, KeySpec, CommandName + from .types import Callback, SimpleContextManager, KeySpec, CommandName, Callable, Self def disp_str(buffer: str) -> tuple[str, list[int]]: @@ -229,9 +229,11 @@ class Reader: keymap: tuple[tuple[str, str], ...] = () input_trans: input.KeymapTranslator = field(init=False) input_trans_stack: list[input.KeymapTranslator] = field(default_factory=list) + screen: list[str] = field(default_factory=list) screeninfo: list[tuple[int, list[int]]] = field(init=False) cxy: tuple[int, int] = field(init=False) lxy: tuple[int, int] = field(init=False) + calc_screen: Callable[[Self], list[str]] = field(init=False) def __post_init__(self) -> None: # Enable the use of `insert` without a `prepare` call - necessary to @@ -241,14 +243,36 @@ def __post_init__(self) -> None: self.input_trans = input.KeymapTranslator( self.keymap, invalid_cls="invalid-key", character_cls="self-insert" ) - self.screeninfo = [(0, [0])] + self.screeninfo = [(0, [])] self.cxy = self.pos2xy() self.lxy = (self.pos, 0) + self.calc_screen = self.calc_complete_screen def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: return default_keymap - def calc_screen(self) -> list[str]: + def append_to_screen(self) -> list[str]: + new_screen = self.screen.copy() or [''] + + new_character = self.buffer[-1] + new_character_len = wlen(new_character) + + last_line_len = wlen(new_screen[-1]) + if last_line_len + new_character_len >= self.console.width: # We need to wrap here + new_screen[-1] += '\\' + self.screeninfo[-1][1].append(1) + new_screen.append(self.buffer[-1]) + self.screeninfo.append((0, [new_character_len])) + else: + new_screen[-1] += self.buffer[-1] + self.screeninfo[-1][1].append(new_character_len) + self.cxy = self.pos2xy() + + # Reset the function that is used for completing the screen + self.calc_screen = self.calc_complete_screen + return new_screen + + def calc_complete_screen(self) -> list[str]: """The purpose of this method is to translate changes in self.buffer into changes in self.screen. Currently it rips everything down and starts from scratch, which whilst not @@ -561,8 +585,8 @@ def update_screen(self) -> None: def refresh(self) -> None: """Recalculate and refresh the screen.""" # this call sets up self.cxy, so call it first. - screen = self.calc_screen() - self.console.refresh(screen, self.cxy) + self.screen = self.calc_screen() + self.console.refresh(self.screen, self.cxy) self.dirty = False def do_cmd(self, cmd: tuple[str, list[str]]) -> None: diff --git a/Lib/_pyrepl/types.py b/Lib/_pyrepl/types.py index f9d48b828c720b..94ba07110b1644 100644 --- a/Lib/_pyrepl/types.py +++ b/Lib/_pyrepl/types.py @@ -1,3 +1,4 @@ +from typing import Self from collections.abc import Callable, Iterator Callback = Callable[[], object] diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 7c59f48df406e6..3614cfbbe1b96c 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -214,7 +214,7 @@ def change_encoding(self, encoding: str) -> None: """ self.encoding = encoding - def refresh(self, screen, c_xy): + def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: """ Refresh the console screen. @@ -293,7 +293,7 @@ def refresh(self, screen, c_xy): self.__show_cursor() - self.screen = screen + self.screen = screen.copy() self.move_cursor(cx, cy) self.flushoutput() From 34712d0a3a2f30c76e32c9b40e49f9fa0b5a6d63 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 21 May 2024 10:28:14 -0400 Subject: [PATCH 2/4] Rework method to choose function --- Lib/_pyrepl/commands.py | 4 +++- Lib/_pyrepl/reader.py | 18 +++++++++++++----- Lib/_pyrepl/types.py | 1 - Lib/_pyrepl/unix_console.py | 2 +- Lib/_pyrepl/utils.py | 6 ++++++ 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index 436f49be08278f..dfc081e69f56e4 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -22,6 +22,8 @@ from __future__ import annotations import os +from .utils import CalcScreen + # Categories of actions: # killing # yanking @@ -359,7 +361,7 @@ class self_insert(EditCommand): def do(self) -> None: r = self.reader r.insert(self.event * r.get_arg()) - r.calc_screen = r.append_to_screen + r.calc_screen_method = CalcScreen.CALC_APPEND_SCREEN class insert_nl(EditCommand): diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index c7beb0e77b4512..83f6d8fa38924a 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -28,14 +28,14 @@ from . import commands, console, input -from .utils import ANSI_ESCAPE_SEQUENCE, wlen +from .utils import ANSI_ESCAPE_SEQUENCE, wlen, CalcScreen from .trace import trace # types Command = commands.Command if False: - from .types import Callback, SimpleContextManager, KeySpec, CommandName, Callable, Self + from .types import Callback, SimpleContextManager, KeySpec, CommandName def disp_str(buffer: str) -> tuple[str, list[int]]: @@ -233,7 +233,7 @@ class Reader: screeninfo: list[tuple[int, list[int]]] = field(init=False) cxy: tuple[int, int] = field(init=False) lxy: tuple[int, int] = field(init=False) - calc_screen: Callable[[Self], list[str]] = field(init=False) + calc_screen_method: CalcScreen = CalcScreen.CALC_COMPLETE_SCREEN def __post_init__(self) -> None: # Enable the use of `insert` without a `prepare` call - necessary to @@ -246,7 +246,6 @@ def __post_init__(self) -> None: self.screeninfo = [(0, [])] self.cxy = self.pos2xy() self.lxy = (self.pos, 0) - self.calc_screen = self.calc_complete_screen def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: return default_keymap @@ -269,7 +268,7 @@ def append_to_screen(self) -> list[str]: self.cxy = self.pos2xy() # Reset the function that is used for completing the screen - self.calc_screen = self.calc_complete_screen + self.calc_screen_method = CalcScreen.CALC_COMPLETE_SCREEN return new_screen def calc_complete_screen(self) -> list[str]: @@ -327,6 +326,15 @@ def calc_complete_screen(self) -> list[str]: screeninfo.append((0, [])) return screen + def calc_screen(self) -> list[str]: + match self.calc_screen_method: + case CalcScreen.CALC_COMPLETE_SCREEN: + return self.calc_complete_screen() + case CalcScreen.CALC_APPEND_SCREEN: + return self.append_to_screen() + case _: + assert False, "No valid calc_screen" + def process_prompt(self, prompt: str) -> tuple[str, int]: """Process the prompt. diff --git a/Lib/_pyrepl/types.py b/Lib/_pyrepl/types.py index 94ba07110b1644..f9d48b828c720b 100644 --- a/Lib/_pyrepl/types.py +++ b/Lib/_pyrepl/types.py @@ -1,4 +1,3 @@ -from typing import Self from collections.abc import Callable, Iterator Callback = Callable[[], object] diff --git a/Lib/_pyrepl/unix_console.py b/Lib/_pyrepl/unix_console.py index 3614cfbbe1b96c..ec7d0636b9aeb3 100644 --- a/Lib/_pyrepl/unix_console.py +++ b/Lib/_pyrepl/unix_console.py @@ -214,7 +214,7 @@ def change_encoding(self, encoding: str) -> None: """ self.encoding = encoding - def refresh(self, screen: list[str], c_xy: tuple[int, int]) -> None: + def refresh(self, screen, c_xy): """ Refresh the console screen. diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index cd1df7c49a216d..f864b475e27a98 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -1,9 +1,15 @@ import re import unicodedata +from enum import Enum ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") +class CalcScreen(Enum): + CALC_COMPLETE_SCREEN = 1 + CALC_APPEND_SCREEN = 2 + + def str_width(c: str) -> int: w = unicodedata.east_asian_width(c) if w in ('N', 'Na', 'H', 'A'): From e516be31d5237c96c8b19e3adc143741e676c4a6 Mon Sep 17 00:00:00 2001 From: Lysandros Nikolaou Date: Tue, 21 May 2024 10:47:53 -0400 Subject: [PATCH 3/4] Only use new method when a single character is added to the end --- Lib/_pyrepl/commands.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index dfc081e69f56e4..fce66a0aa5a77c 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -360,8 +360,10 @@ def do(self) -> None: class self_insert(EditCommand): def do(self) -> None: r = self.reader - r.insert(self.event * r.get_arg()) - r.calc_screen_method = CalcScreen.CALC_APPEND_SCREEN + text = self.event * r.get_arg() + r.insert(text) + if len(text) == 1 and r.pos == len(r.buffer): + r.calc_screen_method = CalcScreen.CALC_APPEND_SCREEN class insert_nl(EditCommand): From ca97bd55e74845cb2aa632468c1e40985b81b609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Langa?= Date: Tue, 21 May 2024 21:48:37 +0200 Subject: [PATCH 4/4] Remove enum --- Lib/_pyrepl/commands.py | 4 +--- Lib/_pyrepl/completing_reader.py | 16 ++++++++-------- Lib/_pyrepl/reader.py | 18 ++++++------------ Lib/_pyrepl/utils.py | 6 ------ 4 files changed, 15 insertions(+), 29 deletions(-) diff --git a/Lib/_pyrepl/commands.py b/Lib/_pyrepl/commands.py index fce66a0aa5a77c..3d9722d1586c2a 100644 --- a/Lib/_pyrepl/commands.py +++ b/Lib/_pyrepl/commands.py @@ -22,8 +22,6 @@ from __future__ import annotations import os -from .utils import CalcScreen - # Categories of actions: # killing # yanking @@ -363,7 +361,7 @@ def do(self) -> None: text = self.event * r.get_arg() r.insert(text) if len(text) == 1 and r.pos == len(r.buffer): - r.calc_screen_method = CalcScreen.CALC_APPEND_SCREEN + r.calc_screen = r.append_to_screen class insert_nl(EditCommand): diff --git a/Lib/_pyrepl/completing_reader.py b/Lib/_pyrepl/completing_reader.py index 19fc06feaf3ced..3f8506bfe8f8f8 100644 --- a/Lib/_pyrepl/completing_reader.py +++ b/Lib/_pyrepl/completing_reader.py @@ -187,8 +187,8 @@ def do(self) -> None: if p: r.insert(p) if last_is_completer: - if not r.cmpltn_menu_vis: - r.cmpltn_menu_vis = 1 + if not r.cmpltn_menu_visible: + r.cmpltn_menu_visible = True r.cmpltn_menu, r.cmpltn_menu_end = build_menu( r.console, completions, r.cmpltn_menu_end, r.use_brackets, r.sort_in_column) @@ -208,7 +208,7 @@ def do(self) -> None: commands.self_insert.do(self) - if r.cmpltn_menu_vis: + if r.cmpltn_menu_visible: stem = r.get_stem() if len(stem) < 1: r.cmpltn_reset() @@ -235,7 +235,7 @@ class CompletingReader(Reader): ### Instance variables cmpltn_menu: list[str] = field(init=False) - cmpltn_menu_vis: int = field(init=False) + cmpltn_menu_visible: bool = field(init=False) cmpltn_menu_end: int = field(init=False) cmpltn_menu_choices: list[str] = field(init=False) @@ -255,9 +255,9 @@ def after_command(self, cmd: Command) -> None: if not isinstance(cmd, (complete, self_insert)): self.cmpltn_reset() - def calc_screen(self) -> list[str]: - screen = super().calc_screen() - if self.cmpltn_menu_vis: + def calc_complete_screen(self) -> list[str]: + screen = super().calc_complete_screen() + if self.cmpltn_menu_visible: ly = self.lxy[1] screen[ly:ly] = self.cmpltn_menu self.screeninfo[ly:ly] = [(0, [])]*len(self.cmpltn_menu) @@ -270,7 +270,7 @@ def finish(self) -> None: def cmpltn_reset(self) -> None: self.cmpltn_menu = [] - self.cmpltn_menu_vis = 0 + self.cmpltn_menu_visible = False self.cmpltn_menu_end = 0 self.cmpltn_menu_choices = [] diff --git a/Lib/_pyrepl/reader.py b/Lib/_pyrepl/reader.py index 83f6d8fa38924a..d29fe9ac4c8995 100644 --- a/Lib/_pyrepl/reader.py +++ b/Lib/_pyrepl/reader.py @@ -28,14 +28,16 @@ from . import commands, console, input -from .utils import ANSI_ESCAPE_SEQUENCE, wlen, CalcScreen +from .utils import ANSI_ESCAPE_SEQUENCE, wlen from .trace import trace # types Command = commands.Command if False: + from typing import Callable from .types import Callback, SimpleContextManager, KeySpec, CommandName + CalcScreen = Callable[[], list[str]] def disp_str(buffer: str) -> tuple[str, list[int]]: @@ -233,7 +235,7 @@ class Reader: screeninfo: list[tuple[int, list[int]]] = field(init=False) cxy: tuple[int, int] = field(init=False) lxy: tuple[int, int] = field(init=False) - calc_screen_method: CalcScreen = CalcScreen.CALC_COMPLETE_SCREEN + calc_screen: CalcScreen = field(init=False) def __post_init__(self) -> None: # Enable the use of `insert` without a `prepare` call - necessary to @@ -246,6 +248,7 @@ def __post_init__(self) -> None: self.screeninfo = [(0, [])] self.cxy = self.pos2xy() self.lxy = (self.pos, 0) + self.calc_screen = self.calc_complete_screen def collect_keymap(self) -> tuple[tuple[KeySpec, CommandName], ...]: return default_keymap @@ -268,7 +271,7 @@ def append_to_screen(self) -> list[str]: self.cxy = self.pos2xy() # Reset the function that is used for completing the screen - self.calc_screen_method = CalcScreen.CALC_COMPLETE_SCREEN + self.calc_screen = self.calc_complete_screen return new_screen def calc_complete_screen(self) -> list[str]: @@ -326,15 +329,6 @@ def calc_complete_screen(self) -> list[str]: screeninfo.append((0, [])) return screen - def calc_screen(self) -> list[str]: - match self.calc_screen_method: - case CalcScreen.CALC_COMPLETE_SCREEN: - return self.calc_complete_screen() - case CalcScreen.CALC_APPEND_SCREEN: - return self.append_to_screen() - case _: - assert False, "No valid calc_screen" - def process_prompt(self, prompt: str) -> tuple[str, int]: """Process the prompt. diff --git a/Lib/_pyrepl/utils.py b/Lib/_pyrepl/utils.py index f864b475e27a98..cd1df7c49a216d 100644 --- a/Lib/_pyrepl/utils.py +++ b/Lib/_pyrepl/utils.py @@ -1,15 +1,9 @@ import re import unicodedata -from enum import Enum ANSI_ESCAPE_SEQUENCE = re.compile(r"\x1b\[[ -@]*[A-~]") -class CalcScreen(Enum): - CALC_COMPLETE_SCREEN = 1 - CALC_APPEND_SCREEN = 2 - - def str_width(c: str) -> int: w = unicodedata.east_asian_width(c) if w in ('N', 'Na', 'H', 'A'):