From c5aea777ea6f8b94a73c1a9da75ab291420151ce Mon Sep 17 00:00:00 2001 From: Kunall Banerjee Date: Tue, 24 Feb 2026 12:14:47 -0500 Subject: [PATCH] editor: Fix clipboard selection range for multi-line copy-and-trim (#48977) When copying multiple selections with copy-and-trim, create a single clipboard selection spanning the original buffer range rather than one selection per trimmed line. This preserves correct paste behavior in Vim mode when pasting trimmed content. Closes #48869. Release Notes: - Fixed clipboard selection range for multi-line copy-and-trim --------- Co-authored-by: dino --- crates/editor/src/editor.rs | 156 +++++++++++++++--------------- crates/editor/src/editor_tests.rs | 44 ++++++++- 2 files changed, 119 insertions(+), 81 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3e734fdf1ab8254807a65c96bb98a0f804bc4dc4..1a0a66b7b6074df549d932d4488013d48f7f3f5e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -13664,94 +13664,94 @@ impl Editor { let selections = self.selections.all::(&self.display_snapshot(cx)); let buffer = self.buffer.read(cx).read(cx); let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let max_point = buffer.max_point(); - let mut is_first = true; - let mut prev_selection_was_entire_line = false; - for selection in &selections { - let mut start = selection.start; - let mut end = selection.end; - let is_entire_line = selection.is_empty() || self.selections.line_mode(); - let mut add_trailing_newline = false; - if is_entire_line { - start = Point::new(start.row, 0); - let next_line_start = Point::new(end.row + 1, 0); - if next_line_start <= max_point { - end = next_line_start; - } else { - // We're on the last line without a trailing newline. - // Copy to the end of the line and add a newline afterwards. - end = Point::new(end.row, buffer.line_len(MultiBufferRow(end.row))); - add_trailing_newline = true; - } + + let max_point = buffer.max_point(); + let mut is_first = true; + for selection in &selections { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode(); + let mut add_trailing_newline = false; + if is_entire_line { + start = Point::new(start.row, 0); + let next_line_start = Point::new(end.row + 1, 0); + if next_line_start <= max_point { + end = next_line_start; + } else { + // We're on the last line without a trailing newline. + // Copy to the end of the line and add a newline afterwards. + end = Point::new(end.row, buffer.line_len(MultiBufferRow(end.row))); + add_trailing_newline = true; } + } - let mut trimmed_selections = Vec::new(); - if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { - let row = MultiBufferRow(start.row); - let first_indent = buffer.indent_size_for_line(row); - if first_indent.len == 0 || start.column > first_indent.len { - trimmed_selections.push(start..end); - } else { - trimmed_selections.push( - Point::new(row.0, first_indent.len) - ..Point::new(row.0, buffer.line_len(row)), - ); - for row in start.row + 1..=end.row { - let mut line_len = buffer.line_len(MultiBufferRow(row)); - if row == end.row { - line_len = end.column; - } - if line_len == 0 { - trimmed_selections - .push(Point::new(row, 0)..Point::new(row, line_len)); - continue; - } - let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); - if row_indent_size.len >= first_indent.len { - trimmed_selections.push( - Point::new(row, first_indent.len)..Point::new(row, line_len), - ); - } else { - trimmed_selections.clear(); - trimmed_selections.push(start..end); - break; - } + let mut trimmed_selections = Vec::new(); + if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { + let row = MultiBufferRow(start.row); + let first_indent = buffer.indent_size_for_line(row); + if first_indent.len == 0 || start.column > first_indent.len { + trimmed_selections.push(start..end); + } else { + trimmed_selections.push( + Point::new(row.0, first_indent.len) + ..Point::new(row.0, buffer.line_len(row)), + ); + for row in start.row + 1..=end.row { + let mut line_len = buffer.line_len(MultiBufferRow(row)); + if row == end.row { + line_len = end.column; + } + if line_len == 0 { + trimmed_selections.push(Point::new(row, 0)..Point::new(row, line_len)); + continue; + } + let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); + if row_indent_size.len >= first_indent.len { + trimmed_selections + .push(Point::new(row, first_indent.len)..Point::new(row, line_len)); + } else { + trimmed_selections.clear(); + trimmed_selections.push(start..end); + break; } } - } else { - trimmed_selections.push(start..end); } + } else { + trimmed_selections.push(start..end); + } - let is_multiline_trim = trimmed_selections.len() > 1; - for trimmed_range in trimmed_selections { - if is_first { - is_first = false; - } else if is_multiline_trim || !prev_selection_was_entire_line { - text += "\n"; - } - prev_selection_was_entire_line = is_entire_line && !is_multiline_trim; - let mut len = 0; - for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { - text.push_str(chunk); - len += chunk.len(); - } - if add_trailing_newline { - text.push('\n'); - len += 1; + let is_multiline_trim = trimmed_selections.len() > 1; + let mut selection_len: usize = 0; + let prev_selection_was_entire_line = is_entire_line && !is_multiline_trim; + + for trimmed_range in trimmed_selections { + if is_first { + is_first = false; + } else if is_multiline_trim || !prev_selection_was_entire_line { + text.push('\n'); + if is_multiline_trim { + selection_len += 1; } - clipboard_selections.push(ClipboardSelection::for_buffer( - len, - is_entire_line, - trimmed_range, - &buffer, - self.project.as_ref(), - cx, - )); + } + for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { + text.push_str(chunk); + selection_len += chunk.len(); + } + if add_trailing_newline { + text.push('\n'); + selection_len += 1; } } + + clipboard_selections.push(ClipboardSelection::for_buffer( + selection_len, + is_entire_line, + start..end, + &buffer, + self.project.as_ref(), + cx, + )); } cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 7f5a84ebd326603e1c239bbbb4062b115b17d095..d1090b5e0eb676c916ab98c7750ba80237f8e087 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -8030,16 +8030,54 @@ async fn test_copy_trim_line_mode(cx: &mut TestAppContext) { let mut cx = EditorTestContext::new(cx).await; cx.set_state(indoc! {" - « a - bˇ» + « fn main() { + dbg!(1) + }ˇ» + "}); + cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true)); + cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx)); + + assert_eq!( + cx.read_from_clipboard().and_then(|item| item.text()), + Some("fn main() {\n dbg!(1)\n}\n".to_string()) + ); + + let clipboard_selections: Vec = cx + .read_from_clipboard() + .and_then(|item| item.entries().first().cloned()) + .and_then(|entry| match entry { + gpui::ClipboardEntry::String(text) => text.metadata_json(), + _ => None, + }) + .expect("should have clipboard selections"); + + assert_eq!(clipboard_selections.len(), 1); + assert!(clipboard_selections[0].is_entire_line); + + cx.set_state(indoc! {" + «fn main() { + dbg!(1) + }ˇ» "}); cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true)); cx.update_editor(|editor, window, cx| editor.copy_and_trim(&CopyAndTrim, window, cx)); assert_eq!( cx.read_from_clipboard().and_then(|item| item.text()), - Some("a\nb\n".to_string()) + Some("fn main() {\n dbg!(1)\n}\n".to_string()) ); + + let clipboard_selections: Vec = cx + .read_from_clipboard() + .and_then(|item| item.entries().first().cloned()) + .and_then(|entry| match entry { + gpui::ClipboardEntry::String(text) => text.metadata_json(), + _ => None, + }) + .expect("should have clipboard selections"); + + assert_eq!(clipboard_selections.len(), 1); + assert!(clipboard_selections[0].is_entire_line); } #[gpui::test]