Fix panic involving growing and shrinking selections while buffer size changes (#48898)

Repro steps:

1. Make a Rust file and check it in to git
2. Delete a lot of the file
3. Expand all the diffs (cmd+")
4. Select larger syntax node (ctrl+cmd+right)
5. Collapse all the diffs
6. Select smaller syntax node (ctrl+cmd+left)
7. Panic!

The problem was we were using byte offsets instead of anchors so after
we shrank the buffer by collapsing diffs we'd be pointing past the end
of it.

Release notes:
- N/A
This commit is contained in:
Eric Holk authored and GitHub committed 2026-02-19 19:04:54 +00:00
1 parent b2af9984c5
commit b945adf5fa
2 files changed
+67 -2

No files matched your search

+12 -2
View File
@@ -1790,7 +1790,7 @@ impl ClipboardSelection {
// selections, scroll behavior, was newest selection reversed
type SelectSyntaxNodeHistoryState = (
Box<[Selection<MultiBufferOffset>]>,
Box<[Selection<Anchor>]>,
SelectSyntaxNodeScrollBehavior,
bool,
);
@@ -16590,6 +16590,10 @@ impl Editor {
SelectSyntaxNodeScrollBehavior::CursorBottom
};
let old_selections: Box<[Selection<Anchor>]> = old_selections
.iter()
.map(|s| s.map(|offset| buffer.anchor_before(offset)))
.collect();
self.select_syntax_node_history.push((
old_selections,
scroll_behavior,
@@ -16612,9 +16616,15 @@ impl Editor {
selection.reversed = is_selection_reversed;
}
let snapshot = self.buffer.read(cx).snapshot(cx);
let selections: Vec<Selection<MultiBufferOffset>> = selections
.iter()
.map(|s| s.map(|anchor| anchor.to_offset(&snapshot)))
.collect();
self.select_syntax_node_history.disable_clearing = true;
self.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
s.select(selections.to_vec());
s.select(selections);
});
self.select_syntax_node_history.disable_clearing = false;
+55
View File
@@ -23113,6 +23113,61 @@ async fn test_toggle_deletion_hunk_at_start_of_file(
cx.assert_state_with_diff(hunk_expanded);
}
#[gpui::test]
async fn test_select_smaller_syntax_node_after_diff_hunk_collapse(
executor: BackgroundExecutor,
cx: &mut TestAppContext,
) {
init_test(cx, |_| {});
let mut cx = EditorTestContext::new(cx).await;
cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
cx.set_state(
&r#"
fn main() {
let x = ˇ1;
}
"#
.unindent(),
);
let diff_base = r#"
fn removed_one() {
println!("this function was deleted");
}
fn removed_two() {
println!("this function was also deleted");
}
fn main() {
let x = 1;
}
"#
.unindent();
cx.set_head_text(&diff_base);
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
});
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
});
cx.update_editor(|editor, window, cx| {
editor.collapse_all_diff_hunks(&CollapseAllDiffHunks, window, cx);
});
executor.run_until_parked();
cx.update_editor(|editor, window, cx| {
editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
});
}
#[gpui::test]
async fn test_expand_first_line_diff_hunk_keeps_deleted_lines_visible(
executor: BackgroundExecutor,