vim: Add :norm support (#33232)

Closes #21198

Release Notes:

- Adds support for `:norm`
- Allows for vim and zed style modified keys specified in issue
  - Vim style <C-w> and zed style <ctrl-w>
- Differs from vim in how multi-line is handled 
  - vim is sequential
  - zed is combinational (with multi-cursor)
This commit is contained in:
AidanV 2025-07-23 22:06:05 -07:00 committed by GitHub
parent c08851a85e
commit 8b0ec287a5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 328 additions and 48 deletions

View file

@ -32,7 +32,7 @@ use futures::{
mpsc::{self, UnboundedReceiver, UnboundedSender},
oneshot,
},
future::try_join_all,
future::{Shared, try_join_all},
};
use gpui::{
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
@ -87,7 +87,7 @@ use std::{
borrow::Cow,
cell::RefCell,
cmp,
collections::hash_map::DefaultHasher,
collections::{VecDeque, hash_map::DefaultHasher},
env,
hash::{Hash, Hasher},
path::{Path, PathBuf},
@ -1043,6 +1043,13 @@ type PromptForOpenPath = Box<
) -> oneshot::Receiver<Option<Vec<PathBuf>>>,
>;
#[derive(Default)]
struct DispatchingKeystrokes {
dispatched: HashSet<Vec<Keystroke>>,
queue: VecDeque<Keystroke>,
task: Option<Shared<Task<()>>>,
}
/// Collects everything project-related for a certain window opened.
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
///
@ -1080,7 +1087,7 @@ pub struct Workspace {
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: Option<WorkspaceId>,
app_state: Arc<AppState>,
dispatching_keystrokes: Rc<RefCell<(HashSet<String>, Vec<Keystroke>)>>,
dispatching_keystrokes: Rc<RefCell<DispatchingKeystrokes>>,
_subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
@ -2311,49 +2318,65 @@ impl Workspace {
window: &mut Window,
cx: &mut Context<Self>,
) {
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.0.insert(action.0.clone()) {
cx.propagate();
return;
}
let mut keystrokes: Vec<Keystroke> = action
let keystrokes: Vec<Keystroke> = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.collect();
keystrokes.reverse();
let _ = self.send_keystrokes_impl(keystrokes, window, cx);
}
state.1.append(&mut keystrokes);
drop(state);
pub fn send_keystrokes_impl(
&mut self,
keystrokes: Vec<Keystroke>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Shared<Task<()>> {
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.dispatched.insert(keystrokes.clone()) {
cx.propagate();
return state.task.clone().unwrap();
}
state.queue.extend(keystrokes);
let keystrokes = self.dispatching_keystrokes.clone();
window
.spawn(cx, async move |cx| {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
keystrokes.borrow_mut().0.clear();
return Ok(());
};
cx.update(|window, cx| {
let focused = window.focused(cx);
window.dispatch_keystroke(keystroke.clone(), cx);
if window.focused(cx) != focused {
// dispatch_keystroke may cause the focus to change.
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
// And we need that to happen before the next keystroke to keep vim mode happy...
// (Note that the tests always do this implicitly, so you must manually test with something like:
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
// )
window.draw(cx).clear();
if state.task.is_none() {
state.task = Some(
window
.spawn(cx, async move |cx| {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let mut state = keystrokes.borrow_mut();
let Some(keystroke) = state.queue.pop_front() else {
state.dispatched.clear();
state.task.take();
return;
};
drop(state);
cx.update(|window, cx| {
let focused = window.focused(cx);
window.dispatch_keystroke(keystroke.clone(), cx);
if window.focused(cx) != focused {
// dispatch_keystroke may cause the focus to change.
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
// And we need that to happen before the next keystroke to keep vim mode happy...
// (Note that the tests always do this implicitly, so you must manually test with something like:
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j <enter> u"]}
// )
window.draw(cx).clear();
}
})
.ok();
}
})?;
}
*keystrokes.borrow_mut() = Default::default();
anyhow::bail!("over 100 keystrokes passed to send_keystrokes");
})
.detach_and_log_err(cx);
*keystrokes.borrow_mut() = Default::default();
log::error!("over 100 keystrokes passed to send_keystrokes");
})
.shared(),
);
}
state.task.clone().unwrap()
}
fn save_all_internal(