mirror of
https://github.com/zed-industries/zed.git
synced 2025-02-04 10:12:47 +00:00
Update handling of 'pending' keys
Before this change if you had a matching binding and a pending key, the matching binding happened unconditionally. Now we will wait a second before triggering that binding to give you time to complete the action.
This commit is contained in:
parent
b8ed83a452
commit
b06e2eb6af
7 changed files with 137 additions and 44 deletions
|
@ -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 {
|
||||
|
|
|
@ -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<dyn Action>; 1]>,
|
||||
pub pending: bool,
|
||||
}
|
||||
|
||||
impl KeystrokeMatcher {
|
||||
pub fn new(keymap: Arc<Mutex<Keymap>>) -> 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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<FocusId>,
|
||||
focus_enabled: bool,
|
||||
pending_input: Option<PendingInput>,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) focus_invalidated: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct PendingInput {
|
||||
text: String,
|
||||
actions: SmallVec<[Box<dyn Action>; 1]>,
|
||||
focus: Option<FocusId>,
|
||||
timer: Option<Task<()>>,
|
||||
}
|
||||
|
||||
pub(crate) struct ElementStateBox {
|
||||
inner: Box<dyn Any>,
|
||||
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::<KeyDownEvent>() {
|
||||
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<dyn Action>) {
|
||||
let dispatch_path = self
|
||||
.window
|
||||
|
|
Loading…
Reference in a new issue