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.
This commit is contained in:
Tim Vermeulen authored and GitHub committed 2026-09-08 20:13:56 +00:00
1 parent a6b3c5b33d
commit bcf8c7bee7
2 files changed
+42 -7

No files matched your search

+8 -7
View File
@@ -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<usize> = 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
+34
View File
@@ -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, |_| {});