From 056da0a0f1e5388acacba0d8eff2a5bb7c56bdef Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 23 Jan 2024 09:37:44 -0700 Subject: [PATCH] Add keymatch modes so terminal can have cmd-k This isn't my favorite idea of a fix, but it does work for now, and it seems likely the terminal will need to configure other aspects of action dispatch in the future. In the future we should explore making it possible to do this via the keymap, either by making disabling bindings more robust; or by having a way to indicate immediate mode per binding. --- crates/gpui/src/key_dispatch.rs | 13 ++++++++++++ crates/gpui/src/window.rs | 22 ++++++++++++++------ crates/gpui/src/window/element_cx.rs | 19 ++++++++++++----- crates/terminal_view/src/terminal_element.rs | 1 + 4 files changed, 44 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index dd5a7ab84e..59c75abd5e 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -63,6 +63,16 @@ use std::{ sync::Arc, }; +/// KeymatchMode controls how keybindings are resolved in the case of conflicting pending keystrokes. +/// When `Sequenced`, gpui will wait for 1s for sequences to complete. +/// When `Immediate`, gpui will immediately resolve the keybinding. +#[derive(Default, PartialEq)] +pub enum KeymatchMode { + #[default] + Sequenced, + Immediate, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)] pub(crate) struct DispatchNodeId(usize); @@ -75,6 +85,7 @@ pub(crate) struct DispatchTree { keystroke_matchers: FxHashMap, KeystrokeMatcher>, keymap: Arc>, action_registry: Rc, + pub(crate) keymatch_mode: KeymatchMode, } #[derive(Default)] @@ -106,6 +117,7 @@ impl DispatchTree { keystroke_matchers: FxHashMap::default(), keymap, action_registry, + keymatch_mode: KeymatchMode::Sequenced, } } @@ -116,6 +128,7 @@ impl DispatchTree { self.focusable_node_ids.clear(); self.view_node_ids.clear(); self.keystroke_matchers.clear(); + self.keymatch_mode = KeymatchMode::Sequenced; } pub fn push_node( diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 5796d139f9..e026c71185 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2,11 +2,12 @@ use crate::{ px, size, transparent_black, Action, AnyDrag, AnyView, AppContext, Arena, AsyncWindowContext, AvailableSpace, Bounds, Context, Corners, CursorStyle, DispatchActionListener, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, EventEmitter, FileDropEvent, Flatten, - GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchResult, - Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, MouseMoveEvent, - MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, PlatformWindow, Point, - PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, Subscription, - TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, WindowOptions, + GlobalElementId, Hsla, KeyBinding, KeyContext, KeyDownEvent, KeyMatch, KeymatchMode, + KeymatchResult, Keystroke, KeystrokeEvent, Model, ModelContext, Modifiers, MouseButton, + MouseMoveEvent, MouseUpEvent, Pixels, PlatformAtlas, PlatformDisplay, PlatformInput, + PlatformWindow, Point, PromptLevel, Render, ScaledPixels, SharedString, Size, SubscriberSet, + Subscription, TaffyLayoutEngine, Task, View, VisualContext, WeakView, WindowBounds, + WindowOptions, }; use anyhow::{anyhow, Context as _, Result}; use collections::FxHashSet; @@ -1214,12 +1215,21 @@ impl<'a> WindowContext<'a> { .dispatch_path(node_id); if let Some(key_down_event) = event.downcast_ref::() { - let KeymatchResult { bindings, pending } = self + let KeymatchResult { + bindings, + mut pending, + } = self .window .rendered_frame .dispatch_tree .dispatch_key(&key_down_event.keystroke, &dispatch_path); + if self.window.rendered_frame.dispatch_tree.keymatch_mode == KeymatchMode::Immediate + && !bindings.is_empty() + { + pending = false; + } + 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 diff --git a/crates/gpui/src/window/element_cx.rs b/crates/gpui/src/window/element_cx.rs index 132cc72a5c..14ed818d25 100644 --- a/crates/gpui/src/window/element_cx.rs +++ b/crates/gpui/src/window/element_cx.rs @@ -17,11 +17,11 @@ use crate::{ prelude::*, size, AnyTooltip, AppContext, AvailableSpace, Bounds, BoxShadow, ContentMask, Corners, CursorStyle, DevicePixels, DispatchPhase, DispatchTree, ElementId, ElementStateBox, EntityId, FocusHandle, FocusId, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputHandler, IsZero, KeyContext, KeyEvent, LayoutId, MonochromeSprite, MouseEvent, PaintQuad, - Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, RenderGlyphParams, - RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, StackingContext, - StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, Window, - WindowContext, SUBPIXEL_VARIANTS, + InputHandler, IsZero, KeyContext, KeyEvent, KeymatchMode, LayoutId, MonochromeSprite, + MouseEvent, PaintQuad, Path, Pixels, PlatformInputHandler, Point, PolychromeSprite, Quad, + RenderGlyphParams, RenderImageParams, RenderSvgParams, Scene, Shadow, SharedString, Size, + StackingContext, StackingOrder, Style, Surface, TextStyleRefinement, Underline, UnderlineStyle, + Window, WindowContext, SUBPIXEL_VARIANTS, }; type AnyMouseListener = Box; @@ -1090,6 +1090,15 @@ impl<'a> ElementContext<'a> { } } + /// keymatch mode immediate instructs GPUI to prefer shorter action bindings. + /// In the case that you have a keybinding of `"cmd-k": "terminal::Clear"` and + /// `"cmd-k left": "workspace::MoveLeft"`, GPUI will by default wait for 1s after + /// you type cmd-k to see if you're going to type left. + /// This is problematic in the terminal + pub fn keymatch_mode_immediate(&mut self) { + self.window.next_frame.dispatch_tree.keymatch_mode = KeymatchMode::Immediate; + } + /// Register a mouse event listener on the window for the next frame. The type of event /// is determined by the first parameter of the given listener. When the next frame is rendered /// the listener will be cleared. diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index c67fbfc4d0..fc9e4bb21e 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -762,6 +762,7 @@ impl Element for TerminalElement { self.interactivity .paint(bounds, bounds.size, state, cx, |_, _, cx| { cx.handle_input(&self.focus, terminal_input_handler); + cx.keymatch_mode_immediate(); cx.on_key_event({ let this = self.terminal.clone();