Vim: Regression with edit repetition (.) after completions
Summary
Repeating the edit action via . that contained completion results in completed text added after the completion prefix, duplicating it.
Description
Here's a screencast of the reproduction in a new document:
Screencast From 2025-05-29 22-08-02.webm
When typing hello again, I'm using the sequence i + he + Tab + Esc. After that, using . to repeat the action types hehello, as it repeats he and adds the completed text.
Expected behavior is that typing . should just insert hello.
I've bisected this to https://github.com/zed-industries/zed/pull/28586 - before that PR, the sequence above works as expected.
Zed Version and System Specs
Zed: v0.188.4 (Zed) OS: Linux Wayland ubuntu 25.04 Memory: 183.7 GiB Architecture: x86_64 GPU: NVIDIA GeForce RTX 5070 || NVIDIA || 570.133.07
I just bisected this myself since I distinctly remembered it working before, and then I saw your comment on #28586.
I think it's the same bug as reported in: https://github.com/zed-industries/zed/issues/30758
Yup, that's definitely the same bug.
I think I see the bug. The changes in #28586 modify the EditorEvent::InputHandled event to set utf16_range_to_replace to None, but the Vim implementation was relying on that field to remove the prefix upon repeat:
- Vim state records the range to replace here.
- Upon repeat, it passes that range to replace back to the editor when repeating the insertion.
- I'm not 100% sure, but it seems like the editor then selects that passed in range so that it gets overwritten when it later repeats the input event.
So now that the range is not populated, the Vim recording outputs the full completion without first removing the text that is supposed to be replaced by that completion.
One issue with using range_to_replace is that it results in odd behavior if the completion is a standalone action instead of a continuation of prefix input. E.g. if he is already typed, and you do something like a + ctrl+p and select hello, the text in the completion is recorded as hello, but the only text that's added is llo. Using the replace range then results in deleting a few characters before the repetition.
One possible alternative is something like this, which was part of the code before the PR. This seems to function correctly and is probably safe?
diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs
index 8276992734..b6b42ef79b 100644
--- a/crates/editor/src/editor.rs
+++ b/crates/editor/src/editor.rs
@@ -5475,9 +5475,18 @@ impl Editor {
}
}
+ let mut common_prefix_len = 0;
+ for (a, b) in old_text.chars().zip(new_text.chars()) {
+ if a == b {
+ common_prefix_len += a.len_utf8();
+ } else {
+ break;
+ }
+ }
+
cx.emit(EditorEvent::InputHandled {
utf16_range_to_replace: None,
- text: new_text.clone().into(),
+ text: new_text[common_prefix_len..].into(),
});
self.transact(window, cx, |this, window, cx| {
This does not account for behavior for multi-cursor completions, where the completion at every cursor might be of a different length, but this just seems broken (?) right now anyway, e.g.
https://github.com/user-attachments/assets/d8367c09-d634-43e6-8664-1f565f955109
Not sure what the expected behavior is of course. With my patch repeating the edit would just add lo, so undo + repeat doesn't result in the same edit applied. To be honest I don't fully understand the problem the linked PR was designed to solve, as I don't use multiple cursors.
In a less adversarial context (if multiple cursors are placed after the same text, so the completion is the same length), the above works fine. I'm going to submit this patch and we'll see if others have better ideas, as this issue is fairly problematic for me.