vim: Show 'j' from jk pre-emptively (#32007)

Fixes: #29812
Fixes: #22538

Co-Authored-By: <corentinhenry@gmail.com>

Release Notes:

- vim: Multi-key bindings in insert mode will now show the pending
keystroke in the buffer. For example if you have `jk` mapped to escape,
pressing `j` will immediately show a `j`.
This commit is contained in:
Conrad Irwin 2025-06-06 14:11:51 -06:00 committed by GitHub
parent 35a119d573
commit 5ad51ca48e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 151 additions and 4 deletions

View file

@ -299,6 +299,7 @@ pub enum DebugStackFrameLine {}
enum DocumentHighlightRead {}
enum DocumentHighlightWrite {}
enum InputComposition {}
pub enum PendingInput {}
enum SelectedTextHighlight {}
pub enum ConflictsOuter {}
@ -1776,6 +1777,8 @@ impl Editor {
.detach();
cx.on_blur(&focus_handle, window, Self::handle_blur)
.detach();
cx.observe_pending_input(window, Self::observe_pending_input)
.detach();
let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) {
Some(false)
@ -19553,6 +19556,90 @@ impl Editor {
cx.notify();
}
pub fn observe_pending_input(&mut self, window: &mut Window, cx: &mut Context<Self>) {
let mut pending: String = window
.pending_input_keystrokes()
.into_iter()
.flatten()
.filter_map(|keystroke| {
if keystroke.modifiers.is_subset_of(&Modifiers::shift()) {
Some(keystroke.key_char.clone().unwrap_or(keystroke.key.clone()))
} else {
None
}
})
.collect();
if !self.input_enabled || self.read_only || !self.focus_handle.is_focused(window) {
pending = "".to_string();
}
let existing_pending = self
.text_highlights::<PendingInput>(cx)
.map(|(_, ranges)| ranges.iter().cloned().collect::<Vec<_>>());
if existing_pending.is_none() && pending.is_empty() {
return;
}
let transaction =
self.transact(window, cx, |this, window, cx| {
let selections = this.selections.all::<usize>(cx);
let edits = selections
.iter()
.map(|selection| (selection.end..selection.end, pending.clone()));
this.edit(edits, cx);
this.change_selections(None, window, cx, |s| {
s.select_ranges(selections.into_iter().enumerate().map(|(ix, sel)| {
sel.start + ix * pending.len()..sel.end + ix * pending.len()
}));
});
if let Some(existing_ranges) = existing_pending {
let edits = existing_ranges.iter().map(|range| (range.clone(), ""));
this.edit(edits, cx);
}
});
let snapshot = self.snapshot(window, cx);
let ranges = self
.selections
.all::<usize>(cx)
.into_iter()
.map(|selection| {
snapshot.buffer_snapshot.anchor_after(selection.end)
..snapshot
.buffer_snapshot
.anchor_before(selection.end + pending.len())
})
.collect();
if pending.is_empty() {
self.clear_highlights::<PendingInput>(cx);
} else {
self.highlight_text::<PendingInput>(
ranges,
HighlightStyle {
underline: Some(UnderlineStyle {
thickness: px(1.),
color: None,
wavy: false,
}),
..Default::default()
},
cx,
);
}
self.ime_transaction = self.ime_transaction.or(transaction);
if let Some(transaction) = self.ime_transaction {
self.buffer.update(cx, |buffer, cx| {
buffer.group_until_transaction(transaction, cx);
});
}
if self.text_highlights::<PendingInput>(cx).is_none() {
self.ime_transaction.take();
}
}
pub fn register_action<A: Action>(
&mut self,
listener: impl Fn(&A, &mut Window, &mut App) + 'static,