aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-02-05 14:58:45 +0100
committerGitHub <noreply@github.com>2026-02-05 14:58:45 +0100
commitc8942cfbb45411c22b978229fbadfe760a98d751 (patch)
treef0dd4aa4e8f644c5504091440b94521443a4fec6
parentfix: don't trim empty characters with background (#4680) (diff)
downloadsl0ppij-c8942cfbb45411c22b978229fbadfe760a98d751.tar.gz
sl0ppij-c8942cfbb45411c22b978229fbadfe760a98d751.tar.xz
sl0ppij-c8942cfbb45411c22b978229fbadfe760a98d751.zip
fix: scrolling with double/triple-click selection (#4681)
* fix: set double/triple-click selection inactive after its end * add pr
-rw-r--r--CHANGELOG.md1
-rw-r--r--zellij-server/src/panes/grid.rs6
-rw-r--r--zellij-server/src/panes/selection.rs4
-rw-r--r--zellij-server/src/panes/unit/grid_tests.rs143
-rw-r--r--zellij-server/src/panes/unit/selection_tests.rs77
5 files changed, 231 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf5d3d77..af82125a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -42,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
* fix: proper error handling in route (https://github.com/zellij-org/zellij/pull/4655)
* feat: allow resizing panes with the mouse (https://github.com/zellij-org/zellij/pull/4678)
* fix: don't trim empty colored cells when resizing terminal grid (https://github.com/zellij-org/zellij/pull/4680)
+* fix: prevent selection from scrolling up after double/triple-click (https://github.com/zellij-org/zellij/pull/4681)
## [0.43.1] - 2025-08-08
diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs
index ee88e40c..8a7c380e 100644
--- a/zellij-server/src/panes/grid.rs
+++ b/zellij-server/src/panes/grid.rs
@@ -1850,6 +1850,12 @@ impl Grid {
let old_selection = self.selection;
self.selection.end(*end);
self.update_selected_lines(&old_selection, &self.selection.clone());
+ } else {
+ // we do this rather than using .end() so that the selection will be marked as inactive
+ // (so we won't keep changing its start/end points as we scroll) but so we won't update
+ // its end position to the above "end" position (which is incorrect behavior for
+ // double/triple click - it will mean we won't mark until the end of the word/line)
+ self.selection.finalize();
}
self.mark_for_rerender();
}
diff --git a/zellij-server/src/panes/selection.rs b/zellij-server/src/panes/selection.rs
index 343ebfde..22c10e18 100644
--- a/zellij-server/src/panes/selection.rs
+++ b/zellij-server/src/panes/selection.rs
@@ -41,6 +41,10 @@ impl Selection {
self.end = end;
}
+ pub fn finalize(&mut self) {
+ self.active = false;
+ }
+
pub fn set_start_and_end_positions(&mut self, start: Position, end: Position) {
self.active = true;
self.start = start;
diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs
index c584e021..80b413f1 100644
--- a/zellij-server/src/panes/unit/grid_tests.rs
+++ b/zellij-server/src/panes/unit/grid_tests.rs
@@ -4219,3 +4219,146 @@ fn preserve_background_color_on_resize() {
trailing_spaces
);
}
+
+fn create_grid_with_content(content: &str) -> Grid {
+ let mut vte_parser = vte::Parser::new();
+ let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
+ let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
+ let mut grid = Grid::new(
+ 20,
+ 80,
+ Rc::new(RefCell::new(Palette::default())),
+ terminal_emulator_color_codes,
+ Rc::new(RefCell::new(LinkHandler::new())),
+ Rc::new(RefCell::new(None)),
+ sixel_image_store,
+ Style::default(),
+ false,
+ true,
+ true,
+ true,
+ false,
+ );
+ for byte in content.as_bytes() {
+ vte_parser.advance(&mut grid, *byte);
+ }
+ grid
+}
+
+#[test]
+fn double_click_selection_preserved_after_scroll() {
+ let content = "line 0\nline 1\nline 2\nline 3\nline 4\nthis is a word test\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\n";
+ let mut grid = create_grid_with_content(content);
+
+ for _ in 0..20 {
+ grid.add_canonical_line();
+ }
+
+ let word_position = Position::new(5, 10);
+ grid.start_selection(&word_position);
+
+ let selection_before = grid.get_selected_text();
+ let word_start = grid.selection.start;
+ let word_end = grid.selection.end;
+
+ grid.end_selection(&word_position);
+
+ grid.scroll_up_one_line();
+
+ let selection_after_start = grid.selection.start;
+ let selection_after_end = grid.selection.end;
+
+ assert_eq!(selection_after_start.line.0, word_start.line.0 + 1);
+ assert_eq!(selection_after_end.line.0, word_end.line.0 + 1);
+ assert_eq!(selection_after_start.column, word_start.column);
+ assert_eq!(selection_after_end.column, word_end.column);
+
+ let text_after = grid.get_selected_text();
+ assert_eq!(selection_before, text_after);
+}
+
+#[test]
+fn triple_click_selection_preserved_after_scroll() {
+ let content = "line 0\nline 1\nline 2\nline 3\nline 4\nthis is line five with some text\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\n";
+ let mut grid = create_grid_with_content(content);
+
+ for _ in 0..20 {
+ grid.add_canonical_line();
+ }
+
+ let line_position = Position::new(5, 15);
+ grid.start_selection(&line_position);
+ grid.start_selection(&line_position);
+ grid.start_selection(&line_position);
+
+ let selection_before = grid.get_selected_text();
+ let line_start = grid.selection.start;
+ let line_end = grid.selection.end;
+
+ grid.end_selection(&line_position);
+
+ grid.scroll_up_one_line();
+
+ let selection_after_start = grid.selection.start;
+ let selection_after_end = grid.selection.end;
+
+ assert_eq!(selection_after_start.line.0, line_start.line.0 + 1);
+ assert_eq!(selection_after_end.line.0, line_end.line.0 + 1);
+ assert_eq!(selection_after_start.column, line_start.column);
+ assert_eq!(selection_after_end.column, line_end.column);
+
+ let text_after = grid.get_selected_text();
+ assert_eq!(selection_before, text_after);
+}
+
+#[test]
+fn double_click_selection_moves_with_multiple_scrolls() {
+ let content = "line 0\nline 1\nline 2\nline 3\nline 4\nthis is a word test\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\n";
+ let mut grid = create_grid_with_content(content);
+
+ for _ in 0..20 {
+ grid.add_canonical_line();
+ }
+
+ let word_position = Position::new(5, 10);
+ grid.start_selection(&word_position);
+
+ let initial_start = grid.selection.start;
+ let initial_end = grid.selection.end;
+
+ grid.end_selection(&word_position);
+
+ for _ in 0..5 {
+ grid.scroll_up_one_line();
+ }
+
+ assert_eq!(grid.selection.start.line.0, initial_start.line.0 + 5);
+ assert_eq!(grid.selection.end.line.0, initial_end.line.0 + 5);
+ assert_eq!(grid.selection.start.column, initial_start.column);
+ assert_eq!(grid.selection.end.column, initial_end.column);
+}
+
+#[test]
+fn single_click_drag_selection_preserved_after_scroll() {
+ let content = "line 0\nline 1\nline 2\nline 3\nline 4\nsome text here\nline 6\nline 7\nline 8\nline 9\nline 10\nline 11\nline 12\nline 13\nline 14\nline 15\nline 16\nline 17\nline 18\nline 19\n";
+ let mut grid = create_grid_with_content(content);
+
+ for _ in 0..20 {
+ grid.add_canonical_line();
+ }
+
+ grid.start_selection(&Position::new(5, 5));
+ grid.update_selection(&Position::new(5, 10));
+
+ let start_before = grid.selection.start;
+ let end_before = grid.selection.end;
+
+ grid.end_selection(&Position::new(5, 10));
+
+ grid.scroll_up_one_line();
+
+ assert_eq!(grid.selection.start.line.0, start_before.line.0 + 1);
+ assert_eq!(grid.selection.end.line.0, end_before.line.0 + 1);
+ assert_eq!(grid.selection.start.column, start_before.column);
+ assert_eq!(grid.selection.end.column, end_before.column);
+}
diff --git a/zellij-server/src/panes/unit/selection_tests.rs b/zellij-server/src/panes/unit/selection_tests.rs
index 778172f2..4a0b346f 100644
--- a/zellij-server/src/panes/unit/selection_tests.rs
+++ b/zellij-server/src/panes/unit/selection_tests.rs
@@ -454,3 +454,80 @@ fn add_line_to_position_reduce_from_above() {
assert_eq!(selection.start, Position::new(line_index_to_add as i32, 0));
assert_eq!(selection.end, selection_end);
}
+
+#[test]
+fn finalize_sets_active_to_false() {
+ let mut selection = Selection::default();
+ selection.start(Position::new(10, 10));
+ assert!(selection.active);
+
+ selection.finalize();
+ assert!(!selection.active);
+}
+
+#[test]
+fn finalize_preserves_positions() {
+ let start = Position::new(5, 10);
+ let end = Position::new(15, 20);
+ let mut selection = Selection {
+ start,
+ end,
+ active: true,
+ last_added_word_position: Some((start, end)),
+ last_added_line_index: None,
+ };
+
+ selection.finalize();
+
+ assert_eq!(selection.start, start);
+ assert_eq!(selection.end, end);
+ assert!(!selection.active);
+}
+
+#[test]
+fn double_click_then_scroll_moves_both_positions() {
+ let word_start = Position::new(10, 5);
+ let word_end = Position::new(10, 15);
+ let mut selection = Selection::default();
+
+ selection.set_start_and_end_positions(word_start, word_end);
+ assert!(selection.active);
+
+ selection.finalize();
+ assert!(!selection.active);
+
+ selection.move_down(3);
+
+ assert_eq!(selection.start, Position::new(13, 5));
+ assert_eq!(selection.end, Position::new(13, 15));
+}
+
+#[test]
+fn triple_click_then_scroll_moves_both_positions() {
+ let line_start = Position::new(10, 0);
+ let line_end = Position::new(10, 80);
+ let mut selection = Selection::default();
+
+ selection.set_start_and_end_positions(line_start, line_end);
+ selection.finalize();
+
+ selection.move_up(5);
+
+ assert_eq!(selection.start, Position::new(5, 0));
+ assert_eq!(selection.end, Position::new(5, 80));
+}
+
+#[test]
+fn set_start_and_end_without_finalize_only_moves_start() {
+ let word_start = Position::new(10, 5);
+ let word_end = Position::new(10, 15);
+ let mut selection = Selection::default();
+
+ selection.set_start_and_end_positions(word_start, word_end);
+ assert!(selection.active);
+
+ selection.move_down(3);
+
+ assert_eq!(selection.start, Position::new(13, 5));
+ assert_eq!(selection.end, word_end);
+}