From bcf8c7bee7e5aa5eb11eeadb6462e41537bc2607 Mon Sep 17 00:00:00 2001 From: Tim Vermeulen Date: Tue, 8 Sep 2026 20:13:56 +0000 Subject: [PATCH] Fix crash in `Editor::rotate_selections` using cursors on nonconsecutive lines (#63937) Crash repro: ``` A 1234567890 B C ``` 1. place cursors before `A`, `B`, and `C` 2. run `editor: rotate selections forward` Caused by the current `Editor::rotate_selections` logic assuming that the text between the end of one of the lines being rotated and the start of the next one is just the newline: ```rs let newline_len = line_ranges[1].start.0 - line_ranges[0].end.0; ``` which fails when those lines aren't consecutive in the editor. ## 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 adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable --- Release Notes: - Fixed crash in `editor: rotate selections forward` and `editor: rotate selections backward` using cursors on nonconsecutive lines. --- crates/editor/src/editor.rs | 15 +++++++------- crates/editor/src/editor_tests.rs | 34 +++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b457e7fb2d..2691bc22ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6826,13 +6826,14 @@ impl Editor { .map(|(i, &row)| (row, i)) .collect(); - // Compute new line start offsets after rotation (handles CRLF) - let newline_len = line_ranges[1].start.0 - line_ranges[0].end.0; - let first_line_start = line_ranges[0].start.0; - let mut new_line_starts: Vec = vec![first_line_start]; - for text in line_texts.iter().take(num_rows - 1) { - let prev_start = *new_line_starts.last().unwrap(); - new_line_starts.push(prev_start + text.len() + newline_len); + let mut old_line_end = 0; + let mut new_line_end = 0; + let mut new_line_starts = Vec::new(); + for (range, text) in line_ranges.iter().zip(&line_texts) { + let line_start = new_line_end + (range.start.0 - old_line_end); + new_line_starts.push(line_start); + old_line_end = range.end.0; + new_line_end = line_start + text.len(); } let new_selections = selections diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bb6c2c372f..1f1d483a5c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8623,6 +8623,40 @@ async fn test_rotate_selections(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_rotate_selections_nonconsecutive_lines(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + ˇline1 + line2 + liˇne3 + line4ˇ + "}); + + cx.update_editor(|e, window, cx| { + e.rotate_selections_forward(&RotateSelectionsForward, window, cx) + }); + cx.assert_editor_state(indoc! {" + line4ˇ + line2 + ˇline1 + liˇne3 + "}); + + cx.update_editor(|e, window, cx| { + e.rotate_selections_backward(&RotateSelectionsBackward, window, cx) + }); + cx.assert_editor_state(indoc! {" + ˇline1 + line2 + liˇne3 + line4ˇ + "}); +} + #[gpui::test] fn test_move_line_up_down(cx: &mut TestAppContext) { init_test(cx, |_| {});