From b975bd10fdcd91c3ccd2cba4dce4dfe3393a5032 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2026 13:09:57 +0000 Subject: [PATCH] Fix mouse multi cursor placement for wrapped lines (#61590) (cherry-pick to preview) (#61778) Cherry-pick of #61590 to preview ---- Closes https://github.com/zed-industries/zed/issues/25237 https://github.com/user-attachments/assets/0f1d54a0-c8f1-4b49-8327-73375a2e1f4e Release Notes: - Fixed mouse multi cursor placement for wrapped lines --------- Co-authored-by: Finn Evers Co-authored-by: Kirill Bulatov Co-authored-by: Finn Evers --- crates/editor/src/editor_tests.rs | 81 +++++++++++++++++++++++++++++++ crates/editor/src/selection.rs | 7 +++ 2 files changed, 88 insertions(+) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index ff3e9bb162b..273b8bd2eeb 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -41812,6 +41812,87 @@ async fn test_columnar_selection_past_end_of_line(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_columnar_selection_with_soft_wrap(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + cx.set_state(indoc! {" + ˇ1. Very long line to show how a wrapped line would look + 2. Very long line to show how a wrapped line would look + "}); + + let soft_wrap_second_line_row = |editor: &mut Editor, cx: &mut Context| { + editor.set_wrap_width(Some(100.0.into()), cx); + let snapshot = editor.display_snapshot(cx); + let second_line_row = Point::new(1, 0).to_display_point(&snapshot).row(); + assert!( + second_line_row.0 > 1, + "expected the first line to be soft wrapped" + ); + second_line_row + }; + + // Dragging a columnar selection across the soft-wrapped rows of the first + // line must produce one selection per buffer line, not one per display row. + cx.update_editor(|editor, window, cx| { + let second_line_row = soft_wrap_second_line_row(editor, cx); + editor.select( + SelectPhase::BeginColumnar { + position: DisplayPoint::new(DisplayRow(0), 0), + goal_column: 0, + reset: true, + mode: ColumnarMode::FromMouse, + }, + window, + cx, + ); + editor.select( + SelectPhase::Update { + position: DisplayPoint::new(second_line_row, 1), + goal_column: 1, + scroll_delta: gpui::Point::default(), + }, + window, + cx, + ); + }); + + cx.assert_editor_state(indoc! {" + «1ˇ». Very long line to show how a wrapped line would look + «2ˇ». Very long line to show how a wrapped line would look + "}); + + cx.update_editor(|editor, window, cx| { + let second_line_row = soft_wrap_second_line_row(editor, cx); + editor.select( + SelectPhase::BeginColumnar { + position: DisplayPoint::new(DisplayRow(0), 0), + goal_column: 0, + reset: true, + mode: ColumnarMode::FromMouse, + }, + window, + cx, + ); + editor.select( + SelectPhase::Update { + position: DisplayPoint::new(second_line_row, 0), + goal_column: 0, + scroll_delta: gpui::Point::default(), + }, + window, + cx, + ); + }); + + cx.assert_editor_state(indoc! {" + ˇ1. Very long line to show how a wrapped line would look + ˇ2. Very long line to show how a wrapped line would look + "}); +} + #[gpui::test] async fn test_toggle_markdown_block_quote(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/selection.rs b/crates/editor/src/selection.rs index 3f9b177326b..c8b56d16825 100644 --- a/crates/editor/src/selection.rs +++ b/crates/editor/src/selection.rs @@ -1809,6 +1809,7 @@ impl Editor { let end_x = tail_x.max(head_x); let reversed = head_x < tail_x; + let mut last_buffer_row = None; let selection_ranges = (start_row.0..=end_row.0) .map(DisplayRow) .filter_map(|row| { @@ -1816,6 +1817,12 @@ impl Editor { return None; } + let buffer_row = row.as_display_point().to_point(display_map).row; + if last_buffer_row == Some(buffer_row) { + return None; + } + last_buffer_row = Some(buffer_row); + let layout = display_map.layout_row(row, &text_layout_details); if matches!(columnar_state, ColumnarSelectionState::FromSelection { .. }) && start_x > layout.width