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.
This commit is contained in:
Tim Vermeulen 2026-04-01 16:29:27 +02:00 committed by GitHub
parent 06dbce41b3
commit dcef83e413
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 92 additions and 1 deletions

View file

@ -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] = &[

View file

@ -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();
});