jk (#4189)
Add support for mapping `jk` to escape in vim mode. This changes the behaviour of the keymatches when there are pending matches. Before: Even if there was a pending match, any complete matches would be triggered and the pending state lost. After: If there is a pending match, any complete matches are delayed by 1s, or until more keys are typed. Release Notes: - Added support for mapping `jk` in vim mode ([#2378](https://github.com/zed-industries/community/issues/2378)), ([#176](https://github.com/zed-industries/community/issues/176))
This commit is contained in:
commit
72cb865108
17 changed files with 575 additions and 607 deletions
|
@ -1,6 +1,57 @@
|
|||
/// KeyDispatch is where GPUI deals with binding actions to key events.
|
||||
///
|
||||
/// The key pieces to making a key binding work are to define an action,
|
||||
/// implement a method that takes that action as a type parameter,
|
||||
/// and then to register the action during render on a focused node
|
||||
/// with a keymap context:
|
||||
///
|
||||
/// ```rust
|
||||
/// actions!(editor,[Undo, Redo]);;
|
||||
///
|
||||
/// impl Editor {
|
||||
/// fn undo(&mut self, _: &Undo, _cx: &mut ViewContext<Self>) { ... }
|
||||
/// fn redo(&mut self, _: &Redo, _cx: &mut ViewContext<Self>) { ... }
|
||||
/// }
|
||||
///
|
||||
/// impl Render for Editor {
|
||||
/// fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
/// div()
|
||||
/// .track_focus(&self.focus_handle)
|
||||
/// .keymap_context("Editor")
|
||||
/// .on_action(cx.listener(Editor::undo))
|
||||
/// .on_action(cx.listener(Editor::redo))
|
||||
/// ...
|
||||
/// }
|
||||
/// }
|
||||
///```
|
||||
///
|
||||
/// The keybindings themselves are managed independently by calling cx.bind_keys().
|
||||
/// (Though mostly when developing Zed itself, you just need to add a new line to
|
||||
/// assets/keymaps/default.json).
|
||||
///
|
||||
/// ```rust
|
||||
/// cx.bind_keys([
|
||||
/// KeyBinding::new("cmd-z", Editor::undo, Some("Editor")),
|
||||
/// KeyBinding::new("cmd-shift-z", Editor::redo, Some("Editor")),
|
||||
/// ])
|
||||
/// ```
|
||||
///
|
||||
/// With all of this in place, GPUI will ensure that if you have an Editor that contains
|
||||
/// the focus, hitting cmd-z will Undo.
|
||||
///
|
||||
/// In real apps, it is a little more complicated than this, because typically you have
|
||||
/// several nested views that each register keyboard handlers. In this case action matching
|
||||
/// bubbles up from the bottom. For example in Zed, the Workspace is the top-level view, which contains Pane's, which contain Editors. If there are conflicting keybindings defined
|
||||
/// then the Editor's bindings take precedence over the Pane's bindings, which take precedence over the Workspace.
|
||||
///
|
||||
/// In GPUI, keybindings are not limited to just single keystrokes, you can define
|
||||
/// sequences by separating the keys with a space:
|
||||
///
|
||||
/// KeyBinding::new("cmd-k left", pane::SplitLeft, Some("Pane"))
|
||||
///
|
||||
use crate::{
|
||||
Action, ActionRegistry, DispatchPhase, ElementContext, EntityId, FocusId, KeyBinding,
|
||||
KeyContext, KeyMatch, Keymap, Keystroke, KeystrokeMatcher, WindowContext,
|
||||
KeyContext, Keymap, KeymatchResult, Keystroke, KeystrokeMatcher, WindowContext,
|
||||
};
|
||||
use collections::FxHashMap;
|
||||
use parking_lot::Mutex;
|
||||
|
@ -272,30 +323,51 @@ impl DispatchTree {
|
|||
.collect()
|
||||
}
|
||||
|
||||
// dispatch_key pushses the next keystroke into any key binding matchers.
|
||||
// any matching bindings are returned in the order that they should be dispatched:
|
||||
// * First by length of binding (so if you have a binding for "b" and "ab", the "ab" binding fires first)
|
||||
// * Secondly by depth in the tree (so if Editor has a binding for "b" and workspace a
|
||||
// binding for "b", the Editor action fires first).
|
||||
pub fn dispatch_key(
|
||||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
context: &[KeyContext],
|
||||
) -> Vec<Box<dyn Action>> {
|
||||
if !self.keystroke_matchers.contains_key(context) {
|
||||
let keystroke_contexts = context.iter().cloned().collect();
|
||||
self.keystroke_matchers.insert(
|
||||
keystroke_contexts,
|
||||
KeystrokeMatcher::new(self.keymap.clone()),
|
||||
);
|
||||
}
|
||||
dispatch_path: &SmallVec<[DispatchNodeId; 32]>,
|
||||
) -> KeymatchResult {
|
||||
let mut bindings = SmallVec::<[KeyBinding; 1]>::new();
|
||||
let mut pending = false;
|
||||
|
||||
let keystroke_matcher = self.keystroke_matchers.get_mut(context).unwrap();
|
||||
if let KeyMatch::Some(actions) = keystroke_matcher.match_keystroke(keystroke, context) {
|
||||
// Clear all pending keystrokes when an action has been found.
|
||||
for keystroke_matcher in self.keystroke_matchers.values_mut() {
|
||||
keystroke_matcher.clear_pending();
|
||||
let mut context_stack: SmallVec<[KeyContext; 4]> = SmallVec::new();
|
||||
for node_id in dispatch_path {
|
||||
let node = self.node(*node_id);
|
||||
|
||||
if let Some(context) = node.context.clone() {
|
||||
context_stack.push(context);
|
||||
}
|
||||
|
||||
actions
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
|
||||
while !context_stack.is_empty() {
|
||||
let keystroke_matcher = self
|
||||
.keystroke_matchers
|
||||
.entry(context_stack.clone())
|
||||
.or_insert_with(|| KeystrokeMatcher::new(self.keymap.clone()));
|
||||
|
||||
let result = keystroke_matcher.match_keystroke(keystroke, &context_stack);
|
||||
pending = result.pending || pending;
|
||||
for new_binding in result.bindings {
|
||||
match bindings
|
||||
.iter()
|
||||
.position(|el| el.keystrokes.len() < new_binding.keystrokes.len())
|
||||
{
|
||||
Some(idx) => {
|
||||
bindings.insert(idx, new_binding);
|
||||
}
|
||||
None => bindings.push(new_binding),
|
||||
}
|
||||
}
|
||||
context_stack.pop();
|
||||
}
|
||||
|
||||
KeymatchResult { bindings, pending }
|
||||
}
|
||||
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
|
|
|
@ -50,7 +50,7 @@ impl KeyBinding {
|
|||
if self.keystrokes.as_ref().starts_with(pending_keystrokes) {
|
||||
// If the binding is completed, push it onto the matches list
|
||||
if self.keystrokes.as_ref().len() == pending_keystrokes.len() {
|
||||
KeyMatch::Some(vec![self.action.boxed_clone()])
|
||||
KeyMatch::Matched
|
||||
} else {
|
||||
KeyMatch::Pending
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::{Action, KeyContext, Keymap, KeymapVersion, Keystroke};
|
||||
use crate::{KeyBinding, KeyContext, Keymap, KeymapVersion, Keystroke};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) struct KeystrokeMatcher {
|
||||
|
@ -8,6 +9,11 @@ pub(crate) struct KeystrokeMatcher {
|
|||
keymap_version: KeymapVersion,
|
||||
}
|
||||
|
||||
pub struct KeymatchResult {
|
||||
pub bindings: SmallVec<[KeyBinding; 1]>,
|
||||
pub pending: bool,
|
||||
}
|
||||
|
||||
impl KeystrokeMatcher {
|
||||
pub fn new(keymap: Arc<Mutex<Keymap>>) -> Self {
|
||||
let keymap_version = keymap.lock().version();
|
||||
|
@ -18,10 +24,6 @@ impl KeystrokeMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn clear_pending(&mut self) {
|
||||
self.pending_keystrokes.clear();
|
||||
}
|
||||
|
||||
pub fn has_pending_keystrokes(&self) -> bool {
|
||||
!self.pending_keystrokes.is_empty()
|
||||
}
|
||||
|
@ -39,7 +41,7 @@ impl KeystrokeMatcher {
|
|||
&mut self,
|
||||
keystroke: &Keystroke,
|
||||
context_stack: &[KeyContext],
|
||||
) -> KeyMatch {
|
||||
) -> 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 {
|
||||
|
@ -48,7 +50,7 @@ impl KeystrokeMatcher {
|
|||
}
|
||||
|
||||
let mut pending_key = None;
|
||||
let mut found_actions = Vec::new();
|
||||
let mut bindings = SmallVec::new();
|
||||
|
||||
for binding in keymap.bindings().rev() {
|
||||
if !keymap.binding_enabled(binding, context_stack) {
|
||||
|
@ -58,8 +60,8 @@ impl KeystrokeMatcher {
|
|||
for candidate in keystroke.match_candidates() {
|
||||
self.pending_keystrokes.push(candidate.clone());
|
||||
match binding.match_keystrokes(&self.pending_keystrokes) {
|
||||
KeyMatch::Some(mut actions) => {
|
||||
found_actions.append(&mut actions);
|
||||
KeyMatch::Matched => {
|
||||
bindings.push(binding.clone());
|
||||
}
|
||||
KeyMatch::Pending => {
|
||||
pending_key.get_or_insert(candidate);
|
||||
|
@ -70,16 +72,21 @@ impl KeystrokeMatcher {
|
|||
}
|
||||
}
|
||||
|
||||
if !found_actions.is_empty() {
|
||||
self.pending_keystrokes.clear();
|
||||
return KeyMatch::Some(found_actions);
|
||||
} else if let Some(pending_key) = pending_key {
|
||||
if bindings.len() == 0 && pending_key.is_none() && self.pending_keystrokes.len() > 0 {
|
||||
drop(keymap);
|
||||
self.pending_keystrokes.remove(0);
|
||||
return self.match_keystroke(keystroke, context_stack);
|
||||
}
|
||||
|
||||
let pending = if let Some(pending_key) = pending_key {
|
||||
self.pending_keystrokes.push(pending_key);
|
||||
KeyMatch::Pending
|
||||
true
|
||||
} else {
|
||||
self.pending_keystrokes.clear();
|
||||
KeyMatch::None
|
||||
}
|
||||
false
|
||||
};
|
||||
|
||||
KeymatchResult { bindings, pending }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -87,386 +94,9 @@ impl KeystrokeMatcher {
|
|||
/// - KeyMatch::None => No match is valid for this key given any pending keystrokes.
|
||||
/// - KeyMatch::Pending => There exist bindings that is still waiting for more keys.
|
||||
/// - KeyMatch::Some(matches) => One or more bindings have received the necessary key presses.
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum KeyMatch {
|
||||
None,
|
||||
Pending,
|
||||
Some(Vec<Box<dyn Action>>),
|
||||
}
|
||||
|
||||
impl KeyMatch {
|
||||
/// Returns true if the match is complete.
|
||||
pub fn is_some(&self) -> bool {
|
||||
matches!(self, KeyMatch::Some(_))
|
||||
}
|
||||
|
||||
/// Get the matches if the match is complete.
|
||||
pub fn matches(self) -> Option<Vec<Box<dyn Action>>> {
|
||||
match self {
|
||||
KeyMatch::Some(matches) => Some(matches),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for KeyMatch {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(KeyMatch::None, KeyMatch::None) => true,
|
||||
(KeyMatch::Pending, KeyMatch::Pending) => true,
|
||||
(KeyMatch::Some(a), KeyMatch::Some(b)) => {
|
||||
if a.len() != b.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (a, b) in a.iter().zip(b.iter()) {
|
||||
if !a.partial_eq(b.as_ref()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use serde_derive::Deserialize;
|
||||
|
||||
use super::*;
|
||||
use crate::{self as gpui, KeyBindingContextPredicate, Modifiers};
|
||||
use crate::{actions, KeyBinding};
|
||||
|
||||
#[test]
|
||||
fn test_keymap_and_view_ordering() {
|
||||
actions!(test, [EditorAction, ProjectPanelAction]);
|
||||
|
||||
let mut editor = KeyContext::default();
|
||||
editor.add("Editor");
|
||||
|
||||
let mut project_panel = KeyContext::default();
|
||||
project_panel.add("ProjectPanel");
|
||||
|
||||
// Editor 'deeper' in than project panel
|
||||
let dispatch_path = vec![project_panel, editor];
|
||||
|
||||
// But editor actions 'higher' up in keymap
|
||||
let keymap = Keymap::new(vec![
|
||||
KeyBinding::new("left", EditorAction, Some("Editor")),
|
||||
KeyBinding::new("left", ProjectPanelAction, Some("ProjectPanel")),
|
||||
]);
|
||||
|
||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
||||
|
||||
let matches = matcher
|
||||
.match_keystroke(&Keystroke::parse("left").unwrap(), &dispatch_path)
|
||||
.matches()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches[0].partial_eq(&EditorAction));
|
||||
assert!(matches.get(1).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multi_keystroke_match() {
|
||||
actions!(test, [B, AB, C, D, DA, E, EF]);
|
||||
|
||||
let mut context1 = KeyContext::default();
|
||||
context1.add("1");
|
||||
|
||||
let mut context2 = KeyContext::default();
|
||||
context2.add("2");
|
||||
|
||||
let dispatch_path = vec![context2, context1];
|
||||
|
||||
let keymap = Keymap::new(vec![
|
||||
KeyBinding::new("a b", AB, Some("1")),
|
||||
KeyBinding::new("b", B, Some("2")),
|
||||
KeyBinding::new("c", C, Some("2")),
|
||||
KeyBinding::new("d", D, Some("1")),
|
||||
KeyBinding::new("d", D, Some("2")),
|
||||
KeyBinding::new("d a", DA, Some("2")),
|
||||
]);
|
||||
|
||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
||||
|
||||
// Binding with pending prefix always takes precedence
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
|
||||
KeyMatch::Pending,
|
||||
);
|
||||
// B alone doesn't match because a was pending, so AB is returned instead
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path),
|
||||
KeyMatch::Some(vec![Box::new(AB)]),
|
||||
);
|
||||
assert!(!matcher.has_pending_keystrokes());
|
||||
|
||||
// Without an a prefix, B is dispatched like expected
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &dispatch_path[0..1]),
|
||||
KeyMatch::Some(vec![Box::new(B)]),
|
||||
);
|
||||
assert!(!matcher.has_pending_keystrokes());
|
||||
|
||||
// If a is prefixed, C will not be dispatched because there
|
||||
// was a pending binding for it
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &dispatch_path),
|
||||
KeyMatch::Pending,
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("c").unwrap(), &dispatch_path),
|
||||
KeyMatch::None,
|
||||
);
|
||||
assert!(!matcher.has_pending_keystrokes());
|
||||
|
||||
// If a single keystroke matches multiple bindings in the tree
|
||||
// only one of them is returned.
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("d").unwrap(), &dispatch_path),
|
||||
KeyMatch::Some(vec![Box::new(D)]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_keystroke_parsing() {
|
||||
assert_eq!(
|
||||
Keystroke::parse("ctrl-p").unwrap(),
|
||||
Keystroke {
|
||||
key: "p".into(),
|
||||
modifiers: Modifiers {
|
||||
control: true,
|
||||
alt: false,
|
||||
shift: false,
|
||||
command: false,
|
||||
function: false,
|
||||
},
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Keystroke::parse("alt-shift-down").unwrap(),
|
||||
Keystroke {
|
||||
key: "down".into(),
|
||||
modifiers: Modifiers {
|
||||
control: false,
|
||||
alt: true,
|
||||
shift: true,
|
||||
command: false,
|
||||
function: false,
|
||||
},
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Keystroke::parse("shift-cmd--").unwrap(),
|
||||
Keystroke {
|
||||
key: "-".into(),
|
||||
modifiers: Modifiers {
|
||||
control: false,
|
||||
alt: false,
|
||||
shift: true,
|
||||
command: true,
|
||||
function: false,
|
||||
},
|
||||
ime_key: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_predicate_parsing() {
|
||||
use KeyBindingContextPredicate::*;
|
||||
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("a && (b == c || d != e)").unwrap(),
|
||||
And(
|
||||
Box::new(Identifier("a".into())),
|
||||
Box::new(Or(
|
||||
Box::new(Equal("b".into(), "c".into())),
|
||||
Box::new(NotEqual("d".into(), "e".into())),
|
||||
))
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
KeyBindingContextPredicate::parse("!a").unwrap(),
|
||||
Not(Box::new(Identifier("a".into())),)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_predicate_eval() {
|
||||
let predicate = KeyBindingContextPredicate::parse("a && b || c == d").unwrap();
|
||||
|
||||
let mut context = KeyContext::default();
|
||||
context.add("a");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeyContext::default();
|
||||
context.add("a");
|
||||
context.add("b");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeyContext::default();
|
||||
context.add("a");
|
||||
context.set("c", "x");
|
||||
assert!(!predicate.eval(&[context]));
|
||||
|
||||
let mut context = KeyContext::default();
|
||||
context.add("a");
|
||||
context.set("c", "d");
|
||||
assert!(predicate.eval(&[context]));
|
||||
|
||||
let predicate = KeyBindingContextPredicate::parse("!a").unwrap();
|
||||
assert!(predicate.eval(&[KeyContext::default()]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_context_child_predicate_eval() {
|
||||
let predicate = KeyBindingContextPredicate::parse("a && b > c").unwrap();
|
||||
let contexts = [
|
||||
context_set(&["a", "b"]),
|
||||
context_set(&["c", "d"]), // match this context
|
||||
context_set(&["e", "f"]),
|
||||
];
|
||||
|
||||
assert!(!predicate.eval(&contexts[..=0]));
|
||||
assert!(predicate.eval(&contexts[..=1]));
|
||||
assert!(!predicate.eval(&contexts[..=2]));
|
||||
|
||||
let predicate = KeyBindingContextPredicate::parse("a && b > c && !d > e").unwrap();
|
||||
let contexts = [
|
||||
context_set(&["a", "b"]),
|
||||
context_set(&["c", "d"]),
|
||||
context_set(&["e"]),
|
||||
context_set(&["a", "b"]),
|
||||
context_set(&["c"]),
|
||||
context_set(&["e"]), // only match this context
|
||||
context_set(&["f"]),
|
||||
];
|
||||
|
||||
assert!(!predicate.eval(&contexts[..=0]));
|
||||
assert!(!predicate.eval(&contexts[..=1]));
|
||||
assert!(!predicate.eval(&contexts[..=2]));
|
||||
assert!(!predicate.eval(&contexts[..=3]));
|
||||
assert!(!predicate.eval(&contexts[..=4]));
|
||||
assert!(predicate.eval(&contexts[..=5]));
|
||||
assert!(!predicate.eval(&contexts[..=6]));
|
||||
|
||||
fn context_set(names: &[&str]) -> KeyContext {
|
||||
let mut keymap = KeyContext::default();
|
||||
names.iter().for_each(|name| keymap.add(name.to_string()));
|
||||
keymap
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matcher() {
|
||||
#[derive(Clone, Deserialize, PartialEq, Eq, Debug)]
|
||||
pub struct A(pub String);
|
||||
impl_actions!(test, [A]);
|
||||
actions!(test, [B, Ab, Dollar, Quote, Ess, Backtick]);
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
struct ActionArg {
|
||||
a: &'static str,
|
||||
}
|
||||
|
||||
let keymap = Keymap::new(vec![
|
||||
KeyBinding::new("a", A("x".to_string()), Some("a")),
|
||||
KeyBinding::new("b", B, Some("a")),
|
||||
KeyBinding::new("a b", Ab, Some("a || b")),
|
||||
KeyBinding::new("$", Dollar, Some("a")),
|
||||
KeyBinding::new("\"", Quote, Some("a")),
|
||||
KeyBinding::new("alt-s", Ess, Some("a")),
|
||||
KeyBinding::new("ctrl-`", Backtick, Some("a")),
|
||||
]);
|
||||
|
||||
let mut context_a = KeyContext::default();
|
||||
context_a.add("a");
|
||||
|
||||
let mut context_b = KeyContext::default();
|
||||
context_b.add("b");
|
||||
|
||||
let mut matcher = KeystrokeMatcher::new(Arc::new(Mutex::new(keymap)));
|
||||
|
||||
// Basic match
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(A("x".to_string()))])
|
||||
);
|
||||
matcher.clear_pending();
|
||||
|
||||
// Multi-keystroke match
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_b.clone()]),
|
||||
KeyMatch::Pending
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(Ab)])
|
||||
);
|
||||
matcher.clear_pending();
|
||||
|
||||
// Failed matches don't interfere with matching subsequent keys
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("x").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::None
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("a").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(A("x".to_string()))])
|
||||
);
|
||||
matcher.clear_pending();
|
||||
|
||||
let mut context_c = KeyContext::default();
|
||||
context_c.add("c");
|
||||
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(
|
||||
&Keystroke::parse("a").unwrap(),
|
||||
&[context_c.clone(), context_b.clone()]
|
||||
),
|
||||
KeyMatch::Pending
|
||||
);
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("b").unwrap(), &[context_b.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(Ab)])
|
||||
);
|
||||
|
||||
// handle Czech $ (option + 4 key)
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("alt-ç->$").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(Dollar)])
|
||||
);
|
||||
|
||||
// handle Brazilian quote (quote key then space key)
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(
|
||||
&Keystroke::parse("space->\"").unwrap(),
|
||||
&[context_a.clone()]
|
||||
),
|
||||
KeyMatch::Some(vec![Box::new(Quote)])
|
||||
);
|
||||
|
||||
// handle ctrl+` on a brazilian keyboard
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("ctrl-->`").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(Backtick)])
|
||||
);
|
||||
|
||||
// handle alt-s on a US keyboard
|
||||
assert_eq!(
|
||||
matcher.match_keystroke(&Keystroke::parse("alt-s->ß").unwrap(), &[context_a.clone()]),
|
||||
KeyMatch::Some(vec![Box::new(Ess)])
|
||||
);
|
||||
}
|
||||
Matched,
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -96,7 +96,19 @@ impl TestWindow {
|
|||
result
|
||||
}
|
||||
|
||||
pub fn simulate_keystroke(&mut self, keystroke: Keystroke, is_held: bool) {
|
||||
pub fn simulate_keystroke(&mut self, mut keystroke: Keystroke, is_held: bool) {
|
||||
if keystroke.ime_key.is_none()
|
||||
&& !keystroke.modifiers.command
|
||||
&& !keystroke.modifiers.control
|
||||
&& !keystroke.modifiers.function
|
||||
{
|
||||
keystroke.ime_key = Some(if keystroke.modifiers.shift {
|
||||
keystroke.key.to_ascii_uppercase().clone()
|
||||
} else {
|
||||
keystroke.key.clone()
|
||||
})
|
||||
}
|
||||
|
||||
if self.simulate_input(PlatformInput::KeyDown(KeyDownEvent {
|
||||
keystroke: keystroke.clone(),
|
||||
is_held,
|
||||
|
@ -112,8 +124,9 @@ impl TestWindow {
|
|||
);
|
||||
};
|
||||
drop(lock);
|
||||
let text = keystroke.ime_key.unwrap_or(keystroke.key);
|
||||
input_handler.replace_text_in_range(None, &text);
|
||||
if let Some(text) = keystroke.ime_key.as_ref() {
|
||||
input_handler.replace_text_in_range(None, &text);
|
||||
}
|
||||
|
||||
self.0.lock().input_handler = Some(input_handler);
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@ 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, 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, 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;
|
||||
|
@ -33,6 +33,7 @@ use std::{
|
|||
atomic::{AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use util::{measure, ResultExt};
|
||||
|
||||
|
@ -273,11 +274,47 @@ 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, Debug)]
|
||||
struct PendingInput {
|
||||
keystrokes: SmallVec<[Keystroke; 1]>,
|
||||
bindings: SmallVec<[KeyBinding; 1]>,
|
||||
focus: Option<FocusId>,
|
||||
timer: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl PendingInput {
|
||||
fn is_noop(&self) -> bool {
|
||||
self.bindings.is_empty() && (self.keystrokes.iter().all(|k| k.ime_key.is_none()))
|
||||
}
|
||||
|
||||
fn input(&self) -> String {
|
||||
self.keystrokes
|
||||
.iter()
|
||||
.flat_map(|k| k.ime_key.clone())
|
||||
.collect::<Vec<String>>()
|
||||
.join("")
|
||||
}
|
||||
|
||||
fn used_by_binding(&self, binding: &KeyBinding) -> bool {
|
||||
if self.keystrokes.is_empty() {
|
||||
return true;
|
||||
}
|
||||
let keystroke = &self.keystrokes[0];
|
||||
for candidate in keystroke.match_candidates() {
|
||||
if binding.match_keystrokes(&[candidate]) == KeyMatch::Pending {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ElementStateBox {
|
||||
pub(crate) inner: Box<dyn Any>,
|
||||
pub(crate) parent_view_id: EntityId,
|
||||
|
@ -379,6 +416,7 @@ impl Window {
|
|||
activation_observers: SubscriberSet::new(),
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
pending_input: None,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
focus_invalidated: false,
|
||||
|
@ -1175,44 +1213,67 @@ impl<'a> WindowContext<'a> {
|
|||
.dispatch_tree
|
||||
.dispatch_path(node_id);
|
||||
|
||||
let mut actions: Vec<Box<dyn Action>> = Vec::new();
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
let KeymatchResult { bindings, pending } = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &dispatch_path);
|
||||
|
||||
let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new();
|
||||
for node_id in &dispatch_path {
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
|
||||
if let Some(context) = node.context.clone() {
|
||||
context_stack.push(context);
|
||||
}
|
||||
}
|
||||
|
||||
for node_id in dispatch_path.iter().rev() {
|
||||
// Match keystrokes
|
||||
let node = self.window.rendered_frame.dispatch_tree.node(*node_id);
|
||||
if node.context.is_some() {
|
||||
if let Some(key_down_event) = event.downcast_ref::<KeyDownEvent>() {
|
||||
let mut new_actions = self
|
||||
.window
|
||||
.rendered_frame
|
||||
.dispatch_tree
|
||||
.dispatch_key(&key_down_event.keystroke, &context_stack);
|
||||
actions.append(&mut new_actions);
|
||||
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;
|
||||
currently_pending
|
||||
.keystrokes
|
||||
.push(key_down_event.keystroke.clone());
|
||||
for binding in bindings {
|
||||
currently_pending.bindings.push(binding);
|
||||
}
|
||||
|
||||
context_stack.pop();
|
||||
}
|
||||
}
|
||||
// for vim compatibility, we also should check "is input handler enabled"
|
||||
if !currently_pending.is_noop() {
|
||||
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();
|
||||
}));
|
||||
} else {
|
||||
currently_pending.timer = None;
|
||||
}
|
||||
self.window.pending_input = Some(currently_pending);
|
||||
|
||||
if !actions.is_empty() {
|
||||
self.clear_pending_keystrokes();
|
||||
}
|
||||
|
||||
self.propagate_event = true;
|
||||
for action in actions {
|
||||
self.dispatch_action_on_node(node_id, action.boxed_clone());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(action));
|
||||
self.propagate_event = false;
|
||||
return;
|
||||
} else if let Some(currently_pending) = self.window.pending_input.take() {
|
||||
if bindings
|
||||
.iter()
|
||||
.all(|binding| !currently_pending.used_by_binding(&binding))
|
||||
{
|
||||
self.replay_pending_input(currently_pending)
|
||||
}
|
||||
}
|
||||
|
||||
if !bindings.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());
|
||||
if !self.propagate_event {
|
||||
self.dispatch_keystroke_observers(event, Some(binding.action));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1255,6 +1316,40 @@ 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;
|
||||
}
|
||||
|
||||
let input = currently_pending.input();
|
||||
|
||||
self.propagate_event = true;
|
||||
for binding in currently_pending.bindings {
|
||||
self.dispatch_action_on_node(node_id, binding.action.boxed_clone());
|
||||
if !self.propagate_event {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if !input.is_empty() {
|
||||
if let Some(mut input_handler) = self.window.platform_window.take_input_handler() {
|
||||
input_handler.flush_pending_input(&input, 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…
Add table
Add a link
Reference in a new issue