Add a way to copy with the selections trimmed (#27206)
No default binding currently, `cmd/ctr-shift-c` seem somewhat natural but those are occupied by the collab panel. https://github.com/user-attachments/assets/702cc52a-a4b7-4f2c-bb7f-12ca0c66faeb Release Notes: - Added a way to copy with the selections trimmed --------- Co-authored-by: Cole Miller <m@cole-miller.net>
This commit is contained in:
parent
a74f2bb18b
commit
9609e04bb2
4 changed files with 231 additions and 13 deletions
|
@ -275,6 +275,7 @@ actions!(
|
|||
ConvertToUpperCamelCase,
|
||||
ConvertToUpperCase,
|
||||
Copy,
|
||||
CopyAndTrim,
|
||||
CopyFileLocation,
|
||||
CopyHighlightJson,
|
||||
CopyFileName,
|
||||
|
|
|
@ -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>) {
|
||||
self.do_copy(true, cx);
|
||||
}
|
||||
|
||||
pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
|
||||
self.do_copy(false, cx);
|
||||
}
|
||||
|
||||
fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context<Self>) {
|
||||
let selections = self.selections.all::<Point>(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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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, |_| {});
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue