fix: replace selected ranges instead of ignoring visual paste

This commit is contained in:
Valdemar Lorenzen
2026-10-14 12:26:01 +02:00
parent 5580d6fbd4
commit c5f80f27a6
6 changed files with 159 additions and 11 deletions
+94 -6
View File
@@ -336,6 +336,7 @@ enum EditStart {
after: bool,
fragments: Vec<String>,
linewise: bool,
selector: Option<SelectionIntent>,
},
}
@@ -591,17 +592,25 @@ core_actions![
Action::ChangeWordBackward,
&[Mode::Normal]
),
("core.paste_before", Action::PasteBefore, &[Mode::Normal]),
("core.paste_after", Action::PasteAfter, &[Mode::Normal]),
(
"core.paste_before",
Action::PasteBefore,
&[Mode::Normal, Mode::Visual]
),
(
"core.paste_after",
Action::PasteAfter,
&[Mode::Normal, Mode::Visual]
),
(
"core.paste_system_before",
Action::PasteSystemBefore,
&[Mode::Normal],
&[Mode::Normal, Mode::Visual],
),
(
"core.paste_system_after",
Action::PasteSystemAfter,
&[Mode::Normal],
&[Mode::Normal, Mode::Visual],
),
(
"core.left",
@@ -1270,6 +1279,8 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
(Input::Char('d'), Cut),
(Input::Char('c'), Change),
(Input::Char('x'), Cut),
(Input::Char('p'), PasteAfter),
(Input::Char('P'), PasteBefore),
(Input::Delete, Cut),
(Input::Char('I'), InsertFirstNonblank),
(Input::Char('A'), AppendLineEnd),
@@ -8802,6 +8813,8 @@ impl Editor {
self.set_message("Clipboard fragment count does not match caret count");
} else {
let before = self.snapshot();
let ranges = self.selections();
let selector = (!ranges.is_empty()).then(|| self.selection_intent(&ranges));
if let Err(error) = self.paste_fragments(after, &fragments, linewise) {
self.set_message(error);
if !system {
@@ -8814,6 +8827,7 @@ impl Editor {
start: EditStart::Paste {
after,
linewise,
selector,
fragments: if system {
std::mem::take(&mut fragments)
} else {
@@ -8837,6 +8851,32 @@ impl Editor {
linewise: bool,
) -> Result<(), &'static str> {
let offsets = line_offsets(&self.active.lines);
if self.active.view.mode == Mode::Visual {
let edits = self
.active
.view
.targets
.iter()
.enumerate()
.map(|(target, item)| {
let range = self.target_range(*item);
let text = &fragments[if fragments.len() == 1 { 0 } else { target }];
Edit {
target: Some(target),
start: cursor_offset(&self.active.lines, &offsets, range.start),
end: cursor_offset(&self.active.lines, &offsets, range.end),
text: Cow::Borrowed(text),
caret: text.len(),
round_up: true,
}
})
.collect();
self.active.view.mode = Mode::Insert;
self.active.view.linewise = false;
self.apply_edits(edits);
self.leave_insert();
return Ok(());
}
if linewise {
if fragments.len() != 1
&& self
@@ -9241,7 +9281,24 @@ impl Editor {
after,
fragments,
linewise,
} => self.paste_fragments(*after, fragments, *linewise)?,
selector,
} => {
if let Some(selector) = selector {
let count = match selector {
SelectionIntent::Spans(spans) => spans.len(),
SelectionIntent::Rows(rows) => rows.len(),
SelectionIntent::Motions { .. } => 1,
};
if self.active.view.mode != Mode::Visual
&& count != 1
&& count != self.active.view.targets.len()
{
return Err("Repeat span count does not match target count");
}
self.replay_ranges(selector);
}
self.paste_fragments(*after, fragments, *linewise)?;
}
}
for step in &recipe.steps {
match step {
@@ -16367,6 +16424,38 @@ mod tests {
)
}
#[test]
fn visual_system_paste_replaces_only_after_successful_read_without_publishing() {
for (key, action) in [
('p', None),
('P', None),
('z', Some("core.paste_system_before")),
('z', Some("core.paste_system_after")),
] {
let fixture = Fixture::new();
let mut editor = fixture.editor();
if let Some(action) = action {
editor.bind("visual", "z", Some(action.into())).unwrap();
}
editor.apply(vec![Effect::Replace("abc".into())]).unwrap();
fixture.external("X");
type_text(&mut editor, "vl");
fixture.fail("fail-read", true);
fixture.fail("fail-write", true);
let before = state(&editor);
editor.handle(Input::Char(key));
assert_eq!(state(&editor), before);
fixture.fail("fail-read", false);
editor.handle(Input::Char(key));
assert_eq!(editor.context().text, "Xc");
assert_eq!(editor.mode(), Mode::Normal);
assert_eq!(fixture.bytes("state"), b"X");
assert!(fixture.bytes("writes").is_empty());
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "abc");
}
}
#[test]
fn linewise_owned_system_bytes_keep_shape_external_changes_do_not_guess_it() {
let fixture = Fixture::new();
@@ -16632,7 +16721,6 @@ mod tests {
fixture.fail("fail-read", false);
fixture.fail("fail-write", false);
for action in ["core.paste_system_before", "core.paste_system_after"] {
assert!(editor.bind("visual", "z", Some(action.into())).is_err());
assert!(editor.bind("insert", "z", Some(action.into())).is_err());
editor.bind("normal", "z", Some(action.into())).unwrap();
}