From 82bf8923ccd97f9eeec06d5e38fd1feb7f21a5c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 7 Apr 2026 09:31:17 +0000 Subject: [PATCH] Fix triple-click line selection not extending wrapped lines beyond viewport bottom Similar to commit 625e984 which fixed extending into scrollback (above viewport), this fix extends line selection below the viewport when a wrapped line continues past the bottom edge. Adds continue_line_downwards_offscreen() and applies it in both the initial selection and extending selection code paths. Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/e548ce84-fdb7-4fd1-b3df-e1166b45f5bd Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com> --- docs/changelog.rst | 2 ++ kitty/screen.c | 31 +++++++++++++++++++++++++++++++ kitty_tests/mouse.py | 27 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index 1b930e61a..fc9477c75 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -217,6 +217,8 @@ Detailed list of changes - Fix dragging of splits layout borders sometimes moving in the wrong direction or having no effect (:pull:`9447`) +- Fix triple-click line selection not extending wrapped lines beyond the bottom edge of the viewport + - Password input in kittens: hide the cursor and display a blinking 🔒 at the end of typed characters to make it visually clear the user is entering a password - edit-in-kitty: Ignore environment variables as some editors execute code present in env vars diff --git a/kitty/screen.c b/kitty/screen.c index bedeaf6c4..b83a6a1e8 100644 --- a/kitty/screen.c +++ b/kitty/screen.c @@ -5395,6 +5395,17 @@ continue_line_downwards(Screen *self, index_type bottom_line, SelectionBoundary return bottom_line; } +static index_type +continue_line_downwards_offscreen(Screen *self, int bottom_line, SelectionBoundary *start, SelectionBoundary *end) { + index_type num_offscreen = 0; + Line *line = NULL; + while ((line = checked_range_line(self, bottom_line + 1)) && range_line_is_continued(self, bottom_line + 1)) { + screen_selection_range_for_line_(line, &start->x, &end->x); + bottom_line++; num_offscreen++; + } + return num_offscreen; +} + static int clamp_selection_input_to_multicell(Screen *self, const Selection *s, index_type x, index_type y, bool in_left_half_of_cell) { int delta = 0; @@ -5538,6 +5549,16 @@ do_update_selection(Screen *self, Selection *s, index_type x, index_type y, bool s->start.x = up_start.x; } } + // extend below viewport if needed + if (bottom_line >= self->lines - 1 && self->scrolled_by > 0 && self->linebuf == self->main_linebuf) { + int range_bottom = (int)bottom_line - (int)self->scrolled_by; + index_type num_below_viewport = continue_line_downwards_offscreen( + self, range_bottom, &down_start, &down_end); + if (num_below_viewport) { + s->end_scrolled_by -= num_below_viewport; + s->end.x = down_end.x; + } + } } } #undef S @@ -5562,6 +5583,16 @@ do_update_selection(Screen *self, Selection *s, index_type x, index_type y, bool } } else { a->in_left_half_of_cell = false; a->x = down_end.x; a->y = bottom_line; + // extend below viewport if needed + if (bottom_line >= self->lines - 1 && self->scrolled_by > 0 && self->linebuf == self->main_linebuf) { + int range_bottom = (int)bottom_line - (int)self->scrolled_by; + index_type num_below_viewport = continue_line_downwards_offscreen( + self, range_bottom, &down_start, &down_end); + if (num_below_viewport) { + s->end_scrolled_by -= num_below_viewport; + s->end.x = down_end.x; + } + } } // allow selecting whitespace at the start of the top line if (a->y == top_line && s->input_current.y == top_line && s->input_current.x < a->x && adjusted_boundary_is_before) a->x = s->input_current.x; diff --git a/kitty_tests/mouse.py b/kitty_tests/mouse.py index 6b4ec5078..d9f270e10 100644 --- a/kitty_tests/mouse.py +++ b/kitty_tests/mouse.py @@ -255,6 +255,33 @@ class TestMouse(BaseTest): release(x=2, button=GLFW_MOUSE_BUTTON_RIGHT) self.ae(sel(), 'ABCDE12345\n678') + # line select for wrapped lines below viewport + s.reset() + s.scroll(100, False) + # draw enough content to enable scrolling: fill lines-1 visual lines, then draw a long wrapped line + s.draw(('X' * s.columns) * (s.lines - 1)) + s.linefeed(), s.carriage_return() + s.draw('ABCDE12345') # wraps to 2 visual lines, second one pushed below viewport + # scroll up by 1 so top visual line of the wrapped text is visible but bottom wraps off-screen + s.scroll(1, True) + multi_click(x=1, y=s.lines - 1, count=3) + self.ae(sel(), 'ABCDE12345') + # extending selection to a line that wraps below viewport + s.reset() + s.scroll(100, False) + s.draw(('X' * s.columns) * (s.lines - 2)) + s.linefeed(), s.carriage_return() + s.draw('678') + s.linefeed(), s.carriage_return() + s.draw('ABCDE12345') + s.scroll(1, True) + multi_click(x=1, y=3, count=3) + self.ae(sel(), '678') + press(x=2, y=s.lines - 1, button=GLFW_MOUSE_BUTTON_RIGHT) + release(x=2, y=s.lines - 1, button=GLFW_MOUSE_BUTTON_RIGHT) + self.ae(sel(), '678\nABCDE12345') + s.scroll(100, False) + # Rectangle select init() press(x=1, y=1, modifiers=GLFW_MOD_ALT | GLFW_MOD_CONTROL)