vim: Improve pasting while in replace mode (#41549)

- Update `vim::normal::Vim.normal_replace` to work with more than one
  character
- Add `vim::replace::Vim.paste_replace` to handle pasting the 
  clipboard's contents while in replace mode
- Update vim's handling of the `editor::actions::Paste` action so that
  the `paste_replace` method is called when vim is in replace mode,
  otherwise it'll just call the regular `editor::Editor.paste` method

Closes #41378 

Release Notes:

- Improved pasting while in Vim's Replace mode, ensuring that the Zed
replaces the same number of characters as the length of the contents
being pasted
This commit is contained in:
Dino
2025-10-30 20:33:03 +00:00
committed by GitHub
parent 5ae0768ce4
commit 8aa2158418
3 changed files with 60 additions and 4 deletions
+11 -2
View File
@@ -965,8 +965,17 @@ impl Vim {
window: &mut Window,
cx: &mut Context<Self>,
) {
// We need to use `text.chars().count()` instead of `text.len()` here as
// `len()` counts bytes, not characters.
let char_count = text.chars().count();
let count = Vim::take_count(cx).unwrap_or(char_count);
let is_return_char = text == "\n".into() || text == "\r".into();
let count = Vim::take_count(cx).unwrap_or(1);
let repeat_count = match (is_return_char, char_count) {
(true, _) => 0,
(_, 1) => count,
(_, _) => 1,
};
Vim::take_forced_motion(cx);
self.stop_recording(cx);
self.update_editor(cx, |_, editor, cx| {
@@ -989,7 +998,7 @@ impl Vim {
edits.push((
range.start.to_offset(&display_map, Bias::Left)
..range.end.to_offset(&display_map, Bias::Left),
text.repeat(if is_return_char { 0 } else { count }),
text.repeat(repeat_count),
));
}
+37 -2
View File
@@ -1,5 +1,5 @@
use crate::{
Vim,
Operator, Vim,
motion::{self, Motion},
object::Object,
state::Mode,
@@ -8,7 +8,7 @@ use editor::{
Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint,
display_map::ToDisplayPoint,
};
use gpui::{Context, Window, actions};
use gpui::{ClipboardEntry, Context, Window, actions};
use language::{Point, SelectionGoal};
use std::ops::Range;
use std::sync::Arc;
@@ -278,10 +278,27 @@ impl Vim {
);
}
}
/// Pastes the clipboard contents, replacing the same number of characters
/// as the clipboard's contents.
pub fn paste_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let clipboard_text =
cx.read_from_clipboard()
.and_then(|item| match item.entries().first() {
Some(ClipboardEntry::String(text)) => Some(text.text().to_string()),
_ => None,
});
if let Some(text) = clipboard_text {
self.push_operator(Operator::Replace, window, cx);
self.normal_replace(Arc::from(text), window, cx);
}
}
}
#[cfg(test)]
mod test {
use gpui::ClipboardItem;
use indoc::indoc;
use crate::{
@@ -521,4 +538,22 @@ mod test {
assert_eq!(0, highlights.len());
});
}
#[gpui::test]
async fn test_paste_replace(cx: &mut gpui::TestAppContext) {
let mut cx = VimTestContext::new(cx, true).await;
cx.set_state(indoc! {"ˇ123"}, Mode::Replace);
cx.write_to_clipboard(ClipboardItem::new_string("456".to_string()));
cx.dispatch_action(editor::actions::Paste);
cx.assert_state(indoc! {"45ˇ6"}, Mode::Replace);
// If the clipboard's contents length is greater than the remaining text
// length, nothing sould be replace and cursor should remain in the same
// position.
cx.set_state(indoc! {"ˇ123"}, Mode::Replace);
cx.write_to_clipboard(ClipboardItem::new_string("4567".to_string()));
cx.dispatch_action(editor::actions::Paste);
cx.assert_state(indoc! {"ˇ123"}, Mode::Replace);
}
}
+12
View File
@@ -23,6 +23,7 @@ use collections::HashMap;
use editor::{
Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, SelectionEffects,
ToPoint,
actions::Paste,
movement::{self, FindRange},
};
use gpui::{
@@ -919,6 +920,17 @@ impl Vim {
);
});
Vim::action(
editor,
cx,
|vim, _: &editor::actions::Paste, window, cx| match vim.mode {
Mode::Replace => vim.paste_replace(window, cx),
_ => {
vim.update_editor(cx, |_, editor, cx| editor.paste(&Paste, window, cx));
}
},
);
normal::register(editor, cx);
insert::register(editor, cx);
helix::register(editor, cx);