fix: yank whole lines with yy and preserve paste shape
This commit is contained in:
+162
-6
@@ -524,6 +524,7 @@ core_actions![
|
||||
("core.change", Action::Change, &[Mode::Visual]),
|
||||
("core.delete_line", Action::DeleteLine, &[Mode::Normal]),
|
||||
("core.change_line", Action::ChangeLine, &[Mode::Normal]),
|
||||
("core.copy_line", Action::CopyLine, &[Mode::Normal]),
|
||||
(
|
||||
"core.change_to_line_end",
|
||||
Action::ChangeToLineEnd,
|
||||
@@ -5885,10 +5886,10 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
let action = match input {
|
||||
Input::Char(ch) if ch == operator => Some(if operator == 'd' {
|
||||
Action::DeleteLine
|
||||
} else {
|
||||
Action::ChangeLine
|
||||
Input::Char(ch) if ch == operator => Some(match operator {
|
||||
'd' => Action::DeleteLine,
|
||||
'y' => Action::CopyLine,
|
||||
_ => Action::ChangeLine,
|
||||
}),
|
||||
Input::Char('w') if operator == 'd' => Some(Action::DeleteWordForward),
|
||||
Input::Char('b') if operator == 'd' => Some(Action::DeleteWordBackward),
|
||||
@@ -5967,7 +5968,7 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
if self.active.view.mode == Mode::Normal
|
||||
&& let Input::Char(operator @ ('c' | 'd')) = input
|
||||
&& let Input::Char(operator @ ('c' | 'd' | 'y')) = input
|
||||
{
|
||||
self.line_pending = Some((operator, self.count.take().unwrap_or(1)));
|
||||
return None;
|
||||
@@ -6197,6 +6198,7 @@ impl Editor {
|
||||
Action::DeleteLine | Action::ChangeLine => {
|
||||
self.edit_lines(matches!(action, Action::ChangeLine), count);
|
||||
}
|
||||
Action::CopyLine => self.copy_lines(count),
|
||||
Action::ReplaceCharacter => {
|
||||
self.line_pending = Some(('r', count));
|
||||
}
|
||||
@@ -7912,6 +7914,29 @@ impl Editor {
|
||||
self.selections()
|
||||
}
|
||||
|
||||
fn copy_lines(&mut self, count: usize) {
|
||||
let targets = self.active.view.targets.clone();
|
||||
let primary = self.active.view.primary;
|
||||
let mode = self.active.view.mode;
|
||||
let linewise = self.active.view.linewise;
|
||||
let visual_motions = self.active.view.visual_motions.take();
|
||||
let ranges = self.line_selections(count);
|
||||
let fragments = self.selection_fragments(&ranges);
|
||||
self.active.view.targets = targets;
|
||||
self.active.view.primary = primary;
|
||||
self.active.view.mode = mode;
|
||||
self.active.view.linewise = linewise;
|
||||
self.active.view.visual_motions = visual_motions;
|
||||
match self.store_clipboard(
|
||||
fragments,
|
||||
self.clipboard_mode == ClipboardMode::System,
|
||||
true,
|
||||
) {
|
||||
Ok(()) => self.set_message("Copied lines"),
|
||||
Err(error) => self.set_message(error),
|
||||
}
|
||||
}
|
||||
|
||||
fn edit_lines(&mut self, change: bool, count: usize) {
|
||||
let targets = self.active.view.targets.clone();
|
||||
let primary = self.active.view.primary;
|
||||
@@ -10284,6 +10309,93 @@ mod tests {
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
editor
|
||||
}
|
||||
#[test]
|
||||
fn copy_line_action_and_y_prefix_respect_bindings_and_cancellation() {
|
||||
let mut editor = editor();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("one\ntwo".into())])
|
||||
.unwrap();
|
||||
editor
|
||||
.bind("normal", "q", Some("core.copy_line".into()))
|
||||
.unwrap();
|
||||
assert!(
|
||||
editor
|
||||
.bind("visual", "q", Some("core.copy_line".into()))
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
editor
|
||||
.bind("insert", "q", Some("core.copy_line".into()))
|
||||
.is_err()
|
||||
);
|
||||
type_text(&mut editor, "q");
|
||||
assert_eq!(editor.clipboard, ["one\n"]);
|
||||
type_text(&mut editor, "2y3");
|
||||
assert_eq!(editor.line_operator_pending(), Some('y'));
|
||||
assert_eq!(editor.count(), Some(6));
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.line_operator_pending(), None);
|
||||
assert_eq!(editor.count(), None);
|
||||
editor
|
||||
.bind("normal", "y", Some("core.down".into()))
|
||||
.unwrap();
|
||||
type_text(&mut editor, "y");
|
||||
assert_eq!(editor.cursor().row, 1);
|
||||
assert_eq!(editor.line_operator_pending(), None);
|
||||
editor.bind("normal", "y", None).unwrap();
|
||||
type_text(&mut editor, "yy");
|
||||
assert_eq!(editor.clipboard, ["one\n"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_line_restores_overlapping_multicarets_and_preserves_redo_and_repeat() {
|
||||
let mut editor = editor();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("one\ntwo\nthree".into())])
|
||||
.unwrap();
|
||||
type_text(&mut editor, "iX");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('u'));
|
||||
editor.active.view.targets = vec![
|
||||
Target {
|
||||
anchor: Cursor { row: 0, column: 1 },
|
||||
active: Cursor { row: 0, column: 1 },
|
||||
preferred_columns: Some((1, 1)),
|
||||
},
|
||||
Target {
|
||||
anchor: Cursor { row: 1, column: 2 },
|
||||
active: Cursor { row: 1, column: 2 },
|
||||
preferred_columns: Some((2, 2)),
|
||||
},
|
||||
];
|
||||
editor.active.view.primary = 1;
|
||||
let targets = editor.active.view.targets.clone();
|
||||
let history = format!(
|
||||
"{:?}",
|
||||
(&editor.active.undo, &editor.active.redo, &editor.last_edit)
|
||||
);
|
||||
let revision = editor.text_revision();
|
||||
type_text(&mut editor, "2yy");
|
||||
assert_eq!(editor.clipboard, ["one\ntwo\nthree\n"]);
|
||||
assert!(editor.clipboard_linewise);
|
||||
assert_eq!(editor.active.view.targets, targets);
|
||||
assert_eq!(editor.active.view.primary, 1);
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
assert!(!editor.active.view.linewise);
|
||||
assert_eq!(editor.text_revision(), revision);
|
||||
assert_eq!(
|
||||
format!(
|
||||
"{:?}",
|
||||
(&editor.active.undo, &editor.active.redo, &editor.last_edit)
|
||||
),
|
||||
history
|
||||
);
|
||||
editor.handle(Input::Char('U'));
|
||||
assert_eq!(editor.context().text, "Xone\ntwo\nthree");
|
||||
editor.handle(Input::Char('.'));
|
||||
assert_eq!(editor.context().text, "XXone\ntwo\nthree");
|
||||
}
|
||||
|
||||
fn type_text(editor: &mut Editor, text: &str) {
|
||||
for ch in text.chars() {
|
||||
editor.handle(if ch == '\n' {
|
||||
@@ -12895,6 +13007,16 @@ mod tests {
|
||||
let id = editor.buffer_id();
|
||||
type_text(&mut editor, "2w");
|
||||
assert_eq!(editor.cursor().column, 11);
|
||||
let targets = editor.active.view.targets.clone();
|
||||
let revision = editor.text_revision();
|
||||
type_text(&mut editor, "yy");
|
||||
assert_eq!(editor.clipboard, ["alpha beta alpha\n"]);
|
||||
assert!(editor.clipboard_linewise);
|
||||
assert_eq!(editor.active.view.targets, targets);
|
||||
assert_eq!(editor.text_revision(), revision);
|
||||
editor.handle(Input::Char('p'));
|
||||
assert_eq!(editor.active.view.targets, targets);
|
||||
assert_eq!(editor.text_revision(), revision);
|
||||
type_text(&mut editor, "0v4ly");
|
||||
assert_eq!(editor.clipboard, ["alpha"]);
|
||||
assert_eq!(editor.mode(), Mode::Normal);
|
||||
@@ -15207,7 +15329,7 @@ mod tests {
|
||||
let carets = editor.carets();
|
||||
let revision = editor.text_revision();
|
||||
fixture.fail("fail-write", true);
|
||||
for keys in ["2dd", "cc", "C", "D", "Y", "s", "d2w", "d2b"] {
|
||||
for keys in ["yy", "2y3y", "2dd", "cc", "C", "D", "Y", "s", "d2w", "d2b"] {
|
||||
type_text(&mut editor, keys);
|
||||
assert_eq!(editor.context().text, "Xone\ntwo\nthree");
|
||||
assert_eq!(editor.carets(), carets);
|
||||
@@ -15221,6 +15343,40 @@ mod tests {
|
||||
assert_eq!(editor.context().text, "XXone\ntXwo\nthree");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copy_line_publication_failure_keeps_owned_shape_register_and_full_state() {
|
||||
let fixture = Fixture::new();
|
||||
let mut editor = fixture.editor();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("one\ntwo\n".into())])
|
||||
.unwrap();
|
||||
fixture.fail("fail-write", true);
|
||||
let initial = state(&editor);
|
||||
type_text(&mut editor, "yy");
|
||||
assert_eq!(state(&editor), initial);
|
||||
assert!(editor.owned_system_clipboard.is_none());
|
||||
fixture.fail("fail-write", false);
|
||||
type_text(&mut editor, "yy");
|
||||
assert_eq!(fixture.bytes("state"), b"one\n");
|
||||
assert_eq!(
|
||||
editor.owned_system_clipboard,
|
||||
Some(("one\n".into(), vec!["one\n".into()], true))
|
||||
);
|
||||
editor.handle(Input::Char('j'));
|
||||
fixture.fail("fail-write", true);
|
||||
let before = state(&editor);
|
||||
type_text(&mut editor, "2yy");
|
||||
assert_eq!(state(&editor), before);
|
||||
assert_eq!(fixture.bytes("state"), b"one\n");
|
||||
fixture.fail("fail-write", false);
|
||||
type_text(&mut editor, "jyy");
|
||||
assert_eq!(fixture.bytes("state"), b"\n");
|
||||
assert_eq!(
|
||||
editor.owned_system_clipboard,
|
||||
Some(("\n".into(), vec!["\n".into()], true))
|
||||
);
|
||||
}
|
||||
|
||||
// Includes every replay-owned state, not the intentionally changed error message.
|
||||
fn state(editor: &Editor) -> String {
|
||||
format!(
|
||||
|
||||
+2
-1
@@ -28,7 +28,8 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||
Ctrl+Option/Alt+h/l previous/next tab; :tab.new opens current buffer in another tab
|
||||
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
||||
u undo, U redo, : command, / search, n/N next/previous match
|
||||
dd deletes lines; cc changes lines and enters Insert; counts: 3dd, 2cc; Esc cancels prefix
|
||||
yy copies whole lines; dd deletes them; cc changes them and enters Insert; counts: 3yy, 2dd, 2cc
|
||||
Linewise p/P paste below/above the current row; characterwise p/P paste after/before the grapheme
|
||||
Normal/Visual: . repeats last native edit at current targets (one undo group)
|
||||
v characterwise select, V linewise select, p/P paste fragments, Ctrl+k/Ctrl+e clone above/below
|
||||
y/d/c/p/P use the system clipboard by default; config.clipboard_mode = \"separate\" isolates it
|
||||
|
||||
Reference in New Issue
Block a user