diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index a6f2b1ca17f2829350f06adf66a7143bb1cd4a59..948638719159a0416320d7460a8eef5aa10f1f45 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -275,6 +275,7 @@ actions!( ConvertToUpperCamelCase, ConvertToUpperCase, Copy, + CopyAndTrim, CopyFileLocation, CopyHighlightJson, CopyFileName, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 90f82ba45887a456517b7e5ff4be48afcb390645..c4fe9d8c0b4ac6a2daf37869998296938cab5f7a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -9429,7 +9429,15 @@ impl Editor { self.do_paste(&text, metadata, false, window, cx); } + pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context) { + self.do_copy(true, cx); + } + pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { + self.do_copy(false, cx); + } + + fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx).read(cx); let mut text = String::new(); @@ -9438,7 +9446,7 @@ impl Editor { { let max_point = buffer.max_point(); let mut is_first = true; - for selection in selections.iter() { + 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; @@ -9446,21 +9454,55 @@ impl Editor { start = Point::new(start.row, 0); end = cmp::min(max_point, Point::new(end.row + 1, 0)); } - if is_first { - is_first = false; + + 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 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, buffer.line_len(MultiBufferRow(row))), + ); + } else { + trimmed_selections.clear(); + trimmed_selections.push(start..end); + break; + } + } + } } else { - text += "\n"; + trimmed_selections.push(start..end); } - let mut len = 0; - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - len += chunk.len(); + + for trimmed_range in trimmed_selections { + if is_first { + is_first = false; + } else { + text += "\n"; + } + let mut len = 0; + for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { + text.push_str(chunk); + len += chunk.len(); + } + clipboard_selections.push(ClipboardSelection { + len, + is_entire_line, + first_line_indent: buffer + .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) + .len, + }); } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - first_line_indent: buffer.indent_size_for_line(MultiBufferRow(start.row)).len, - }); } } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 1bf36f260a71393d5ed388048a2637d68688b391..868f53d72b920c37b00b165854f8667a554ab91c 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4918,6 +4918,180 @@ async fn test_clipboard(cx: &mut TestAppContext) { tˇhe lazy dog"}); } +#[gpui::test] +async fn test_copy_trim(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + cx.set_state( + r#" «for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);ˇ» + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + "#, + ); + cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Regular copying preserves all indentation selected", + ); + cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "for selection in selections.iter() { +let mut start = selection.start; +let mut end = selection.end; +let is_entire_line = selection.is_empty() || self.selections.line_mode; +if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Copying with stripping should strip all leading whitespaces" + ); + + cx.set_state( + r#" « for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);ˇ» + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + "#, + ); + cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + " for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Regular copying preserves all indentation selected", + ); + cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "for selection in selections.iter() { +let mut start = selection.start; +let mut end = selection.end; +let is_entire_line = selection.is_empty() || self.selections.line_mode; +if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Copying with stripping should strip all leading whitespaces, even if some of it was selected" + ); + + cx.set_state( + r#" «ˇ for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);» + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + "#, + ); + cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + " for selection in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Regular copying for reverse selection works the same", + ); + cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "for selection in selections.iter() { +let mut start = selection.start; +let mut end = selection.end; +let is_entire_line = selection.is_empty() || self.selections.line_mode; +if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "Copying with stripping for reverse selection works the same" + ); + + cx.set_state( + r#" for selection «in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);ˇ» + end = cmp::min(max_point, Point::new(end.row + 1, 0)); + } + "#, + ); + cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "When selecting past the indent, the copying works as usual", + ); + cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx)); + assert_eq!( + cx.read_from_clipboard() + .and_then(|item| item.text().as_deref().map(str::to_string)), + Some( + "in selections.iter() { + let mut start = selection.start; + let mut end = selection.end; + let is_entire_line = selection.is_empty() || self.selections.line_mode; + if is_entire_line { + start = Point::new(start.row, 0);" + .to_string() + ), + "When selecting past the indent, nothing is trimmed" + ); +} + #[gpui::test] async fn test_paste_multiline(cx: &mut TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c1fe9182dee4201522fc06c198d53dad2f9cd879..86cdbeb7be1807cf2b5907c92a3f7acdd093b4f6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -244,6 +244,7 @@ impl EditorElement { register_action(editor, window, Editor::kill_ring_cut); register_action(editor, window, Editor::kill_ring_yank); register_action(editor, window, Editor::copy); + register_action(editor, window, Editor::copy_and_trim); register_action(editor, window, Editor::paste); register_action(editor, window, Editor::undo); register_action(editor, window, Editor::redo);