diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 1bb7feafeb730b4daf8e31f95ca7fada08972c11..fb14067f881c080a625ab1ad7683414e3c63de26 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -1,6 +1,6 @@ use crate::{ - Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, KeyMatch, - Keymap, Keystroke, KeystrokeMatcher, WindowContext, + Action, ActionRegistry, DispatchPhase, EntityId, FocusId, KeyBinding, KeyContext, Keymap, + KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext, }; use collections::FxHashMap; use parking_lot::Mutex; @@ -276,8 +276,9 @@ impl DispatchTree { &mut self, keystroke: &Keystroke, dispatch_path: &SmallVec<[DispatchNodeId; 32]>, - ) -> SmallVec<[KeyBinding; 1]> { + ) -> KeymatchResult { let mut actions = SmallVec::new(); + let mut pending = false; let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new(); for node_id in dispatch_path { @@ -294,12 +295,13 @@ impl DispatchTree { .entry(context_stack.clone()) .or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone())); - let mut matches = keystroke_matcher.match_keystroke(keystroke, &context_stack); - actions.append(&mut matches); + let mut result = keystroke_matcher.match_keystroke(keystroke, &context_stack); + pending = result.pending || pending; + actions.append(&mut result.actions); context_stack.pop(); } - actions + KeymatchResult { actions, pending } } pub fn has_pending_keystrokes(&self) -> bool { diff --git a/crates/gpui/src/keymap/matcher.rs b/crates/gpui/src/keymap/matcher.rs index 91eab4d3526601868c6d89a5fb9401f19db92c24..f81ce028711ad028176595cc85615b51ed1794ab 100644 --- a/crates/gpui/src/keymap/matcher.rs +++ b/crates/gpui/src/keymap/matcher.rs @@ -1,4 +1,4 @@ -use crate::{Action, KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke}; +use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke}; use parking_lot::Mutex; use smallvec::SmallVec; use std::sync::Arc; @@ -9,6 +9,11 @@ pub struct KeystrokeMatcher { keymap_version: KeymapVersion, } +pub struct KeymatchResult { + pub actions: SmallVec<[Box; 1]>, + pub pending: bool, +} + impl KeystrokeMatcher { pub fn new(keymap: Arc>) -> Self { let keymap_version = keymap.lock().version(); @@ -40,7 +45,7 @@ impl KeystrokeMatcher { &mut self, keystroke: &Keystroke, context_stack: &[KeyContext], - ) -> SmallVec<[KeyBinding; 1]> { + ) -> KeymatchResult { let keymap = self.keymap.lock(); // Clear pending keystrokes if the keymap has changed since the last matched keystroke. if keymap.version() != self.keymap_version { @@ -49,7 +54,7 @@ impl KeystrokeMatcher { } let mut pending_key = None; - let mut found = SmallVec::new(); + let mut actions = SmallVec::new(); for binding in keymap.bindings().rev() { if !keymap.binding_enabled(binding, context_stack) { @@ -60,7 +65,7 @@ impl KeystrokeMatcher { self.pending_keystrokes.push(candidate.clone()); match binding.match_keystrokes(&self.pending_keystrokes) { KeyMatch::Matched => { - found.push(binding.clone()); + actions.push(binding.action.boxed_clone()); } KeyMatch::Pending => { pending_key.get_or_insert(candidate); @@ -71,15 +76,15 @@ impl KeystrokeMatcher { } } - if !found.is_empty() { - self.pending_keystrokes.clear(); - } else if let Some(pending_key) = pending_key { + let pending = if let Some(pending_key) = pending_key { self.pending_keystrokes.push(pending_key); + true } else { self.pending_keystrokes.clear(); + false }; - found + KeymatchResult { actions, pending } } } diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index f7ddd232ecf02eeee3c25e231f70dc6af01b85e5..e623742740259f62db547b6b2c4791cec15051ca 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -359,7 +359,7 @@ impl PlatformInputHandler { self.cx .update(|cx| { self.handler - .replace_text_in_range(replacement_range, text, cx) + .replace_text_in_range(replacement_range, text, cx); }) .ok(); } @@ -392,6 +392,13 @@ impl PlatformInputHandler { .ok() .flatten() } + + pub(crate) fn flush_pending_input(&mut self, input: &str, cx: &mut WindowContext) { + let Some(range) = self.handler.selected_text_range(cx) else { + return; + }; + self.handler.replace_text_in_range(Some(range), &input, cx); + } } /// Zed's interface for handling text input from the platform's IME system diff --git a/crates/gpui/src/platform/keystroke.rs b/crates/gpui/src/platform/keystroke.rs index 0db8d0744dbff82ab7844c8a0f9c5677e4c1c919..2e1acfa630627da23d939ae8eb1c1e11eebce425 100644 --- a/crates/gpui/src/platform/keystroke.rs +++ b/crates/gpui/src/platform/keystroke.rs @@ -30,24 +30,26 @@ impl Keystroke { pub(crate) fn match_candidates(&self) -> SmallVec<[Keystroke; 2]> { let mut possibilities = SmallVec::new(); match self.ime_key.as_ref() { - None => possibilities.push(self.clone()), Some(ime_key) => { - possibilities.push(Keystroke { - modifiers: Modifiers { - control: self.modifiers.control, - alt: false, - shift: false, - command: false, - function: false, - }, - key: ime_key.to_string(), - ime_key: None, - }); + if ime_key != &self.key { + possibilities.push(Keystroke { + modifiers: Modifiers { + control: self.modifiers.control, + alt: false, + shift: false, + command: false, + function: false, + }, + key: ime_key.to_string(), + ime_key: None, + }); + } possibilities.push(Keystroke { ime_key: None, ..self.clone() }); } + None => possibilities.push(self.clone()), } possibilities } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 1ef5d346cc379c0b025f8a148189082731b564c8..99009dcf16894f30d09fc375ec8c3916f0de16bc 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1542,9 +1542,7 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS replacement_range, text: text.to_string(), }); - if text.to_string().to_ascii_lowercase() != pending_key_down.0.keystroke.key { - pending_key_down.0.keystroke.ime_key = Some(text.to_string()); - } + pending_key_down.0.keystroke.ime_key = Some(text.to_string()); window_state.lock().pending_key_down = Some(pending_key_down); } } diff --git a/crates/gpui/src/platform/test/window.rs b/crates/gpui/src/platform/test/window.rs index 095af9b094a9f93f8d3e7660e8088ee6cf220f49..173a64b37b01b841f25b769017214ed0f379457e 100644 --- a/crates/gpui/src/platform/test/window.rs +++ b/crates/gpui/src/platform/test/window.rs @@ -1,7 +1,8 @@ use crate::{ px, AnyWindowHandle, AtlasKey, AtlasTextureId, AtlasTile, Bounds, KeyDownEvent, Keystroke, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, - Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, WindowOptions, + Pixels, Platform, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformInputHandler, + PlatformWindow, Point, Size, TestPlatform, TileId, WindowAppearance, WindowBounds, + WindowOptions, }; use collections::HashMap; use parking_lot::Mutex; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 10be2a5741962109f4ef42e075fd5cff4f2474d4..aa0df7d75701db5d82c5e03e72147abaed79358d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4,13 +4,13 @@ use crate::{ DevicePixels, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, FontId, GlobalElementId, GlyphId, Hsla, ImageData, InputHandler, IsZero, KeyBinding, KeyContext, KeyDownEvent, KeyEvent, - KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, MouseButton, - MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, - PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, PromptLevel, - Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, Scene, - Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface, TaffyLayoutEngine, - Task, Underline, UnderlineStyle, View, VisualContext, WeakView, WindowBounds, WindowOptions, - SUBPIXEL_VARIANTS, + KeymatchResult, KeystrokeEvent, LayoutId, Model, ModelContext, Modifiers, MonochromeSprite, + MouseButton, MouseEvent, MouseMoveEvent, MouseUpEvent, Path, Pixels, PlatformAtlas, + PlatformDisplay, PlatformInput, PlatformInputHandler, PlatformWindow, Point, PolychromeSprite, + PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, RenderSvgParams, ScaledPixels, + Scene, Shadow, SharedString, Size, Style, SubscriberSet, Subscription, Surface, + TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, WeakView, + WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::{FxHashMap, FxHashSet}; @@ -38,6 +38,7 @@ use std::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, + time::Duration, }; use util::{post_inc, ResultExt}; @@ -282,11 +283,20 @@ pub struct Window { activation_observers: SubscriberSet<(), AnyObserver>, pub(crate) focus: Option, focus_enabled: bool, + pending_input: Option, #[cfg(any(test, feature = "test-support"))] pub(crate) focus_invalidated: bool, } +#[derive(Default)] +struct PendingInput { + text: String, + actions: SmallVec<[Box; 1]>, + focus: Option, + timer: Option>, +} + pub(crate) struct ElementStateBox { inner: Box, parent_view_id: EntityId, @@ -506,6 +516,7 @@ impl Window { activation_observers: SubscriberSet::new(), focus: None, focus_enabled: true, + pending_input: None, #[cfg(any(test, feature = "test-support"))] focus_invalidated: false, @@ -1785,21 +1796,56 @@ impl<'a> WindowContext<'a> { .dispatch_path(node_id); if let Some(key_down_event) = event.downcast_ref::() { - let bindings = self + let KeymatchResult { actions, pending } = self .window .rendered_frame .dispatch_tree .dispatch_key(&key_down_event.keystroke, &dispatch_path); - if !bindings.is_empty() { + if pending { + let mut currently_pending = self.window.pending_input.take().unwrap_or_default(); + if currently_pending.focus.is_some() && currently_pending.focus != self.window.focus + { + currently_pending = PendingInput::default(); + } + currently_pending.focus = self.window.focus; + if let Some(new_text) = &key_down_event.keystroke.ime_key.as_ref() { + currently_pending.text += new_text + } + for action in actions { + currently_pending.actions.push(action); + } + + currently_pending.timer = Some(self.spawn(|mut cx| async move { + cx.background_executor.timer(Duration::from_secs(1)).await; + cx.update(move |cx| { + cx.clear_pending_keystrokes(); + let Some(currently_pending) = cx.window.pending_input.take() else { + return; + }; + cx.replay_pending_input(currently_pending) + }) + .log_err(); + })); + self.window.pending_input = Some(currently_pending); + + self.propagate_event = false; + return; + } else if let Some(currently_pending) = self.window.pending_input.take() { + if actions.is_empty() { + self.replay_pending_input(currently_pending) + } + } + + if !actions.is_empty() { self.clear_pending_keystrokes(); } self.propagate_event = true; - for binding in bindings { - self.dispatch_action_on_node(node_id, binding.action.boxed_clone()); + for action in actions { + self.dispatch_action_on_node(node_id, action.boxed_clone()); if !self.propagate_event { - self.dispatch_keystroke_observers(event, Some(binding.action)); + self.dispatch_keystroke_observers(event, Some(action)); return; } } @@ -1840,6 +1886,38 @@ impl<'a> WindowContext<'a> { .has_pending_keystrokes() } + fn replay_pending_input(&mut self, currently_pending: PendingInput) { + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .rendered_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.rendered_frame.dispatch_tree.root_node_id()); + + if self.window.focus != currently_pending.focus { + return; + } + + self.propagate_event = true; + for action in currently_pending.actions { + self.dispatch_action_on_node(node_id, action); + if !self.propagate_event { + return; + } + } + + if !currently_pending.text.is_empty() { + if let Some(mut input_handler) = self.window.platform_window.take_input_handler() { + input_handler.flush_pending_input(¤tly_pending.text, self); + self.window.platform_window.set_input_handler(input_handler) + } + } + } + fn dispatch_action_on_node(&mut self, node_id: DispatchNodeId, action: Box) { let dispatch_path = self .window