From 80d7da090231ad864eb3381d4a461bf104358e1f Mon Sep 17 00:00:00 2001 From: Arvin Verain Date: Tue, 25 Feb 2025 19:01:17 +0800 Subject: [PATCH 1/4] fix: Edge case when paging from a line within prompt When PROMPT_START is already past scrollback and can't be found upwards, set the proper values depending on if the search is over or deferred downwards. --- kitty/screen.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kitty/screen.c b/kitty/screen.c index e4a44da5e..8363b2f8f 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4122,7 +4122,8 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig } if (y1 < upward_limit) { oo->reached_upper_limit = true; - found_output = true; start = upward_limit; + found_output = direction != 0; start = upward_limit; + found_prompt = direction != 0; } } From 59f134d511541e4299a8eb721b93b0ab9ae5e45f Mon Sep 17 00:00:00 2001 From: Arvin Verain Date: Tue, 25 Feb 2025 19:09:02 +0800 Subject: [PATCH 2/4] fix: Edge case when paging from a line within output When OUTPUT_START is already past scrollback and can't be found upwards, properly handle the first prompt encountered to capture the correct command output to be paged. --- kitty/screen.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/kitty/screen.c b/kitty/screen.c index 8363b2f8f..3b860788f 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4133,8 +4133,13 @@ find_cmd_output(Screen *self, OutputOffset *oo, index_type start_screen_y, unsig if (on_screen_only && !found_output && y2 > screen_limit) break; line = checked_range_line(self, y2); if (line && line->attrs.prompt_kind == PROMPT_START) { - if (!found_prompt) found_prompt = true; - else if (found_prompt && !found_output) { + if (!found_prompt) { + if (direction == 0) { + found_next_prompt = true; end = y2; + break; + } + found_prompt = true; + } else if (found_prompt && !found_output) { // skip fetching wrapped prompt lines while (range_line_is_continued(self, y2)) { y2++; From 07f30122e65fa9b46eb6fcb6664bfbd918f9246c Mon Sep 17 00:00:00 2001 From: Arvin Verain Date: Tue, 25 Feb 2025 19:30:29 +0800 Subject: [PATCH 3/4] test: Add more tests for cmd paging edge cases --- kitty_tests/screen.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index 60e6ad95d..e2ce3228e 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1331,6 +1331,30 @@ def lvco(): s.scroll_to_prompt(1) self.ae(lvco(), '0wrapout\n0y') + # test: losing markers past scrollback + s = self.create_screen(lines=10, scrollback=0) + draw_prompt('a' * (s.columns * 3)) + draw_output(1, 'v' * (s.columns * 2)), draw_output(1, 'w', False) + draw_prompt('b') + draw_output(1, 'x') + # remove prompt start above, set last visited to within prompt + s.clear_scrollback() + s.scroll_to_prompt(1) + self.ae(lvco(), '0vvvvvvvvvv\n0w') + # remove output start above, set last visited to within output + draw_output(3, 'y', False), draw_output(1, 'z', False) + s.clear_scrollback() + s.scroll_to_prompt(1) + self.ae(lvco(), 'vvvvvv\n0w') + draw_output(1, 'end', False) + s.clear_scrollback() + s.scroll_to_prompt(1) + self.ae(lvco(), 'v\n0w') + # clear last visited line without setting new one + draw_output(1, 'end', False) + s.clear_scrollback() + self.ae(lvco(), '') + # test that post rewrap prompt lines have correct attributes s = self.create_screen(cols=5, lines=5, scrollback=15) draw_prompt('P' * (s.columns - 2)) From 6d75f7cff47c7e75defdddbbede6e1a2b3b1e289 Mon Sep 17 00:00:00 2001 From: Arvin Verain Date: Fri, 28 Mar 2025 21:35:01 +0800 Subject: [PATCH 4/4] test: Add method to easily set last_visited attributes --- kitty/fast_data_types.pyi | 3 +++ kitty/screen.c | 8 ++++++++ kitty_tests/screen.py | 34 ++++++++++++++-------------------- 3 files changed, 25 insertions(+), 20 deletions(-) diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 02cdef05c..57a28f4dc 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1302,6 +1302,9 @@ class Screen: def scroll_to_prompt(self, num_of_prompts: int = -1) -> bool: pass + def set_last_visited_prompt(self, visual_y: int = 0) -> bool: + pass + def reverse_scroll(self, amt: int, fill_from_scrollback: bool = False) -> bool: pass diff --git a/kitty/screen.c b/kitty/screen.c index 3b860788f..5c57140b6 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -4702,6 +4702,13 @@ scroll_to_prompt(Screen *self, PyObject *args) { Py_RETURN_FALSE; } +static PyObject* +set_last_visited_prompt(Screen *self, PyObject *args) { + index_type visual_y = 0; + if (!PyArg_ParseTuple(args, "|I", &visual_y)) return NULL; + if (screen_set_last_visited_prompt(self, visual_y)) { Py_RETURN_TRUE; } + Py_RETURN_FALSE; +} bool screen_is_selection_dirty(Screen *self) { @@ -5550,6 +5557,7 @@ static PyMethodDef methods[] = { MND(is_rectangle_select, METH_NOARGS) MND(scroll, METH_VARARGS) MND(scroll_to_prompt, METH_VARARGS) + MND(set_last_visited_prompt, METH_VARARGS) MND(send_escape_code_to_child, METH_VARARGS) MND(pause_rendering, METH_VARARGS) MND(hyperlink_at, METH_VARARGS) diff --git a/kitty_tests/screen.py b/kitty_tests/screen.py index e2ce3228e..e6ffc76cf 100644 --- a/kitty_tests/screen.py +++ b/kitty_tests/screen.py @@ -1294,41 +1294,35 @@ def lvco(): self.ae(lvco(), '0x\n1x') # test: obscure prompt s.scroll(2, False) + s.set_last_visited_prompt() self.ae(lvco(), '0x\n1x') # test: prompts without output s.scroll(s.scrolled_by, False) + s.resize(5, s.columns + 5) draw_prompt('4') - # resizing resets last visited, rely on scroll_to_prompt - s.resize(2, s.columns + 4) - s.scroll_to_prompt() - s.scroll_to_prompt(1) - self.ae(str(s.visual_line(0)), '$ 4') + s.set_last_visited_prompt(2) self.ae(lvco(), '') - s.resize(5, s.columns) draw_prompt('wrapcmd') - s.scroll_to_prompt(1) self.ae(lvco(), '') draw_output(1, 'wrapout'), draw_output(1, 'y', False) + s.set_last_visited_prompt(0) self.ae(lvco(), '0wrapout\n0y\n') # wrap long prompt with long output - s.resize(5, s.columns - 4) - self.ae(str(s.visual_line(0)), 'pcmd') + s.resize(5, s.columns - 5) # test: set last visited to previous empty prompt s.scroll_to_prompt(-2) self.ae(str(s.visual_line(0)), '$ 4') self.ae(lvco(), '0wrapout\n0y\n') - mark_prompt(), s.draw('$ end') + draw_prompt('end') + s.scroll_to_prompt(-1) + self.ae(lvco(), '0wrapout\n0y') + s.scroll_to_prompt(1) self.ae(lvco(), '0wrapout\n0y') # test: set last visited to continued line of long prompt - s.scroll_to_prompt(2) - self.ae(str(s.visual_line(0)), 'pcmd') - self.ae(lvco(), '0wrapout\n0y') - s.scroll_to_prompt() + s.set_last_visited_prompt(1) self.ae(lvco(), '0wrapout\n0y') # test: set last visited to continued line of output - s.carriage_return(), s.index() - draw_output(1, 'z') - s.scroll_to_prompt(1) + s.set_last_visited_prompt(3) self.ae(lvco(), '0wrapout\n0y') # test: losing markers past scrollback @@ -1339,16 +1333,16 @@ def lvco(): draw_output(1, 'x') # remove prompt start above, set last visited to within prompt s.clear_scrollback() - s.scroll_to_prompt(1) + s.set_last_visited_prompt(0) self.ae(lvco(), '0vvvvvvvvvv\n0w') # remove output start above, set last visited to within output draw_output(3, 'y', False), draw_output(1, 'z', False) s.clear_scrollback() - s.scroll_to_prompt(1) + s.set_last_visited_prompt(0) self.ae(lvco(), 'vvvvvv\n0w') draw_output(1, 'end', False) s.clear_scrollback() - s.scroll_to_prompt(1) + s.set_last_visited_prompt(0) self.ae(lvco(), 'v\n0w') # clear last visited line without setting new one draw_output(1, 'end', False)