From dcef83e413c8aecde6b929a172ff185d04fe2142 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Wed, 1 Apr 2026 16:29:27 +0200 Subject: [PATCH] editor: Clear previous select mode when clicking on a sticky header (#52636) Clicking on a sticky header causes `selections.select_ranges([anchor..anchor])` to be called, but this does not clear the editor's `selections.select_mode()`, resulting in possible incorrect selections if this is followed up by a shift-click. This PR fixes that with ```diff - selections.select_ranges([anchor..anchor]); + selections.clear_disjoint(); + selections.set_pending_anchor_range(anchor..anchor, SelectMode::Character); ``` which is essentially what `editor.select(SelectPhase::Begin { ... }, ...)` (i.e. a regular single click in the editor) does as well. Before: https://github.com/user-attachments/assets/bcf2647e-a22a-4866-8975-d29e135df148 After: https://github.com/user-attachments/assets/fb82db51-fef1-4b7c-9954-6e076ae0b176 Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed bug that caused clicking on a sticky header to not always properly clear the previous selection. --- crates/editor/src/editor_tests.rs | 85 +++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 8 ++- 2 files changed, 92 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 65a872e6035565bb01fdd78e00d6cf0f35d35ef8..7e397507eda0d800ee9ed6b204ed95e71d50234b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -32309,6 +32309,91 @@ async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) { assert_eq!(selections, vec![empty_range(4, 5)]); } +#[gpui::test] +async fn test_clicking_sticky_header_sets_character_select_mode(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + cx.update(|cx| { + SettingsStore::update_global(cx, |store, cx| { + store.update_user_settings(cx, |settings| { + settings.editor.sticky_scroll = Some(settings::StickyScrollContent { + enabled: Some(true), + }) + }); + }); + }); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, window, cx| { + editor + .style(cx) + .text + .line_height_in_pixels(window.rem_size()) + }); + + let buffer = indoc! {" + fn foo() { + let abc = 123; + } + ˇstruct Bar; + "}; + cx.set_state(&buffer); + + cx.update_editor(|editor, _, cx| { + editor + .buffer() + .read(cx) + .as_singleton() + .unwrap() + .update(cx, |buffer, cx| { + buffer.set_language(Some(rust_lang()), cx); + }) + }); + + let text_origin_x = cx.update_editor(|editor, _, _| { + editor + .last_position_map + .as_ref() + .unwrap() + .text_hitbox + .bounds + .origin + .x + }); + + cx.update_editor(|editor, window, cx| { + // Double click on `struct` to select it + editor.begin_selection(DisplayPoint::new(DisplayRow(3), 1), false, 2, window, cx); + editor.end_selection(window, cx); + + // Scroll down one row to make `fn foo() {` a sticky header + editor.scroll(gpui::Point { x: 0., y: 1. }, None, window, cx); + }); + cx.run_until_parked(); + + // Click at the start of the `fn foo() {` sticky header + cx.simulate_click( + gpui::Point { + x: text_origin_x, + y: 0.5 * line_height, + }, + Modifiers::none(), + ); + cx.run_until_parked(); + + // Shift-click at the end of `fn foo() {` to select the whole row + cx.update_editor(|editor, window, cx| { + editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx); + editor.end_selection(window, cx); + }); + cx.run_until_parked(); + + let selections = cx.update_editor(|editor, _, cx| display_ranges(editor, cx)); + assert_eq!( + selections, + vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 10)] + ); +} + #[gpui::test] async fn test_next_prev_reference(cx: &mut TestAppContext) { const CYCLE_POSITIONS: &[&'static str] = &[ diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9ce080c87bf82ec1098e2a4b1db6bc6a65d22828..2cb159546b426b4abae8c201cee5b75aca46f0e4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6732,7 +6732,13 @@ impl EditorElement { SelectionEffects::scroll(Autoscroll::top_relative(line_index)), window, cx, - |selections| selections.select_ranges([anchor..anchor]), + |selections| { + selections.clear_disjoint(); + selections.set_pending_anchor_range( + anchor..anchor, + crate::SelectMode::Character, + ); + }, ); cx.stop_propagation(); });