Rebrand to Galaxy, major improvements to Bedrock support, still needs some TLC though
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
#[derive(Clone, Debug, Default, Eq, PartialEq)]
|
||||
pub struct Context {
|
||||
pub set: HashSet<&'static str>,
|
||||
pub map: HashMap<&'static str, &'static str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub enum ContextPredicate {
|
||||
Identifier(&'static str),
|
||||
Equal(&'static str, &'static str),
|
||||
NotEqual(&'static str, &'static str),
|
||||
Not(Box<ContextPredicate>),
|
||||
And(Box<ContextPredicate>, Box<ContextPredicate>),
|
||||
Or(Box<ContextPredicate>, Box<ContextPredicate>),
|
||||
Just(bool),
|
||||
}
|
||||
|
||||
pub mod macros {
|
||||
/// Returns a context predicate identifier.
|
||||
#[macro_export]
|
||||
macro_rules! id {
|
||||
($val:literal) => {
|
||||
$crate::keymap::ContextPredicate::Identifier($val)
|
||||
};
|
||||
($val:expr) => {
|
||||
$crate::keymap::ContextPredicate::Identifier($val)
|
||||
};
|
||||
}
|
||||
pub use id;
|
||||
|
||||
/// Returns a context predicate which checks whether the given context
|
||||
/// key has a particular value.
|
||||
#[macro_export]
|
||||
macro_rules! eq {
|
||||
($a:literal, $b:literal) => {
|
||||
$crate::keymap::ContextPredicate::Equal($a, $b)
|
||||
};
|
||||
}
|
||||
pub use eq;
|
||||
|
||||
/// Returns a context predicate which checks whether the given context
|
||||
/// key does _not_ have a particular value.
|
||||
#[macro_export]
|
||||
macro_rules! ne {
|
||||
($a:literal, $b:literal) => {
|
||||
$crate::keymap::ContextPredicate::NotEqual($a, $b)
|
||||
};
|
||||
}
|
||||
pub use ne;
|
||||
|
||||
impl std::ops::Not for ContextPredicate {
|
||||
type Output = ContextPredicate;
|
||||
|
||||
fn not(self) -> Self::Output {
|
||||
ContextPredicate::Not(Box::new(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitAnd for ContextPredicate {
|
||||
type Output = ContextPredicate;
|
||||
|
||||
fn bitand(self, rhs: Self) -> Self::Output {
|
||||
ContextPredicate::And(Box::new(self), Box::new(rhs))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::BitOr for ContextPredicate {
|
||||
type Output = ContextPredicate;
|
||||
|
||||
fn bitor(self, rhs: Self) -> Self::Output {
|
||||
ContextPredicate::Or(Box::new(self), Box::new(rhs))
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a context predicate which is always matched.
|
||||
#[macro_export]
|
||||
macro_rules! always {
|
||||
() => {
|
||||
$crate::keymap::ContextPredicate::Just(true)
|
||||
};
|
||||
}
|
||||
pub use always;
|
||||
|
||||
use super::ContextPredicate;
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn extend(&mut self, other: Context) {
|
||||
for v in other.set {
|
||||
self.set.insert(v);
|
||||
}
|
||||
for (k, v) in other.map {
|
||||
self.map.insert(k, v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ContextPredicate {
|
||||
pub fn eval(&self, ctx: &Context) -> bool {
|
||||
match self {
|
||||
Self::Identifier(name) => ctx.set.contains(*name),
|
||||
Self::Equal(left, right) => ctx
|
||||
.map
|
||||
.get(left)
|
||||
.map(|value| value == right)
|
||||
.unwrap_or(false),
|
||||
Self::NotEqual(left, right) => ctx
|
||||
.map
|
||||
.get(left)
|
||||
.map(|value| value != right)
|
||||
.unwrap_or(true),
|
||||
Self::Not(pred) => !pred.eval(ctx),
|
||||
Self::And(left, right) => left.eval(ctx) && right.eval(ctx),
|
||||
Self::Or(left, right) => left.eval(ctx) || right.eval(ctx),
|
||||
Self::Just(val) => *val,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "context_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,25 @@
|
||||
use super::{macros::*, *};
|
||||
|
||||
#[test]
|
||||
fn test_context_predicate_eval() -> anyhow::Result<()> {
|
||||
let predicate = id!("a") & id!("b") | eq!("c", "d");
|
||||
|
||||
let mut context = Context::default();
|
||||
context.set.insert("a");
|
||||
assert!(!predicate.eval(&context));
|
||||
|
||||
context.set.insert("b");
|
||||
assert!(predicate.eval(&context));
|
||||
|
||||
context.set.remove("b");
|
||||
context.map.insert("c", "x");
|
||||
assert!(!predicate.eval(&context));
|
||||
|
||||
context.map.insert("c", "d");
|
||||
assert!(predicate.eval(&context));
|
||||
|
||||
let predicate = !id!("a");
|
||||
assert!(predicate.eval(&Context::default()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,378 @@
|
||||
use super::{
|
||||
BindingLens, Context, CustomTag, EditableBinding, EditableBindingLens, FixedBinding, Keymap,
|
||||
Keystroke, Trigger,
|
||||
};
|
||||
use crate::{actions::StandardAction, Action, EntityId};
|
||||
use itertools::Either;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Matcher {
|
||||
pending: HashMap<EntityId, Pending>,
|
||||
keymap: Keymap,
|
||||
/// Default binding validator that should run on every binding (irrespective of the [`Context`]
|
||||
/// the binding was registered against).
|
||||
default_binding_validator: Option<BindingValidatorFn>,
|
||||
/// List of validators to be used during binding validation. Each binding validator validates
|
||||
/// all of the bindings that match the [`Context`] it is paired with.
|
||||
binding_validators: Vec<(Context, BindingValidatorFn)>,
|
||||
/// Function to convert bindings that have a [`CustomTag`] trigger to one that has a
|
||||
/// [`Keystroke`]-based trigger instead. If `None`, bindings are not converted.
|
||||
custom_trigger_to_keystroke_fn: Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
|
||||
/// Function to lookup the default keystroke for a given custom action. Used when converting
|
||||
/// custom actions to key events during keybinding editing.
|
||||
default_keystroke_trigger_for_custom_action:
|
||||
Option<Box<dyn Fn(CustomTag) -> Option<Keystroke> + 'static>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct Pending {
|
||||
keystrokes: Vec<Keystroke>,
|
||||
context: Option<Context>,
|
||||
}
|
||||
|
||||
type BindingValidatorFn = Box<dyn Fn(BindingLens) -> IsBindingValid>;
|
||||
|
||||
/// Enum indicating the results of validating a binding.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum IsBindingValid {
|
||||
/// The binding is valid.
|
||||
Yes,
|
||||
/// The binding is invalid.
|
||||
No,
|
||||
}
|
||||
|
||||
pub enum MatchResult {
|
||||
None,
|
||||
Pending,
|
||||
Action(Arc<dyn Action>),
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
pub fn new(keymap: Keymap) -> Self {
|
||||
Self {
|
||||
pending: HashMap::new(),
|
||||
keymap,
|
||||
default_binding_validator: None,
|
||||
binding_validators: vec![],
|
||||
custom_trigger_to_keystroke_fn: None,
|
||||
default_keystroke_trigger_for_custom_action: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_keymap(&mut self, keymap: Keymap) {
|
||||
self.pending.clear();
|
||||
self.keymap = keymap;
|
||||
}
|
||||
|
||||
/// Helper function to that returns [`Trigger`] with any [`Trigger::Custom`]s replaced by a
|
||||
/// [`Trigger::Keystrokes`].
|
||||
fn convert_custom_trigger_to_keystroke_trigger(
|
||||
trigger: Trigger,
|
||||
custom_tag_to_keystroke: &dyn Fn(CustomTag) -> Option<Keystroke>,
|
||||
) -> Trigger {
|
||||
let Trigger::Custom(custom_tag) = trigger else {
|
||||
return trigger;
|
||||
};
|
||||
|
||||
let Some(new_keystroke) = custom_tag_to_keystroke(custom_tag) else {
|
||||
return trigger;
|
||||
};
|
||||
|
||||
Trigger::Keystrokes(vec![new_keystroke])
|
||||
}
|
||||
|
||||
pub fn register_fixed_bindings<T: IntoIterator<Item = FixedBinding>>(&mut self, bindings: T) {
|
||||
self.pending.clear();
|
||||
|
||||
let bindings = match &self.custom_trigger_to_keystroke_fn {
|
||||
None => Either::Left(bindings),
|
||||
Some(custom_tag_to_keystroke) => {
|
||||
let bindings = bindings.into_iter().map(|mut fixed_binding| {
|
||||
fixed_binding.trigger = Self::convert_custom_trigger_to_keystroke_trigger(
|
||||
fixed_binding.trigger,
|
||||
custom_tag_to_keystroke,
|
||||
);
|
||||
fixed_binding
|
||||
});
|
||||
Either::Right(bindings)
|
||||
}
|
||||
};
|
||||
self.keymap.register_fixed_bindings(bindings.into_iter());
|
||||
}
|
||||
|
||||
/// Register new actions with the key matcher
|
||||
///
|
||||
/// Editable Bindings have a name identifier which can be used to override their key bindings
|
||||
/// via the `set_custom_trigger` method.
|
||||
pub fn register_editable_bindings<A: IntoIterator<Item = EditableBinding>>(
|
||||
&mut self,
|
||||
actions: A,
|
||||
) {
|
||||
self.pending.clear();
|
||||
|
||||
let actions = match &self.custom_trigger_to_keystroke_fn {
|
||||
None => Either::Left(actions),
|
||||
Some(custom_tag_to_keystroke) => {
|
||||
let bindings = actions.into_iter().map(|mut editable_binding| {
|
||||
editable_binding.trigger = Self::convert_custom_trigger_to_keystroke_trigger(
|
||||
editable_binding.trigger,
|
||||
custom_tag_to_keystroke,
|
||||
);
|
||||
editable_binding
|
||||
});
|
||||
Either::Right(bindings)
|
||||
}
|
||||
};
|
||||
self.keymap.register_editable_bindings(actions.into_iter());
|
||||
}
|
||||
|
||||
/// Set a custom trigger for a given editable binding name.
|
||||
///
|
||||
/// This will override the default trigger for that action.
|
||||
pub fn set_custom_trigger(&mut self, name: String, trigger: Trigger) {
|
||||
self.pending.clear();
|
||||
self.keymap
|
||||
.update_custom_trigger(name.as_str(), Some(trigger));
|
||||
}
|
||||
|
||||
/// Remove any custom trigger associated with a given action.
|
||||
///
|
||||
/// This will return the trigger to its default state.
|
||||
pub fn remove_custom_trigger<N>(&mut self, name: N)
|
||||
where
|
||||
N: AsRef<str>,
|
||||
{
|
||||
self.pending.clear();
|
||||
self.keymap.update_custom_trigger(name.as_ref(), None);
|
||||
}
|
||||
|
||||
/// Registers a validator that validates every binding that matches the given view's default
|
||||
/// [`Context`].
|
||||
/// After the app is initialized, the provided `binding_validator` function is called for every
|
||||
/// binding that matches the View's default context. If the binding is invalid (indicated by
|
||||
/// [`IsBindingValid::No`]), the app will panic if `debug_assertions` are enabled.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn register_binding_validator<F: Fn(BindingLens) -> IsBindingValid + 'static>(
|
||||
&mut self,
|
||||
context: Context,
|
||||
binding_validator: F,
|
||||
) {
|
||||
self.binding_validators
|
||||
.push((context, Box::new(binding_validator)));
|
||||
}
|
||||
|
||||
/// Sets a default binding validator that runs on _every_ binding that is registered by the
|
||||
/// application.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn set_default_binding_validator<F: Fn(BindingLens) -> IsBindingValid + 'static>(
|
||||
&mut self,
|
||||
binding_validator: F,
|
||||
) {
|
||||
self.default_binding_validator = Some(Box::new(binding_validator));
|
||||
}
|
||||
|
||||
/// Runs through each registered binding validator, asserting that each matching binding is
|
||||
/// valid.
|
||||
#[cfg(debug_assertions)]
|
||||
pub(crate) fn validate_bindings(&mut self) {
|
||||
let mut all_failed_bindings = vec![];
|
||||
for (context, validator) in &self.binding_validators {
|
||||
for binding in self.bindings_for_context(context.clone()) {
|
||||
if let IsBindingValid::No = validator(binding) {
|
||||
all_failed_bindings.push(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(default_validator) = &self.default_binding_validator {
|
||||
for binding in self.get_bindings() {
|
||||
if let IsBindingValid::No = default_validator(binding) {
|
||||
all_failed_bindings.push(binding);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !all_failed_bindings.is_empty() {
|
||||
panic!("Bindings failed validation {all_failed_bindings:#?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Overrides any registered binding that has a [`Trigger::Custom`] to one that is keystroke
|
||||
/// based ([`Trigger::Keystrokes`]) using the provided `custom_to_keystroke` fn.
|
||||
pub(crate) fn convert_custom_triggers_to_keystroke_triggers(
|
||||
&mut self,
|
||||
custom_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
|
||||
) {
|
||||
self.custom_trigger_to_keystroke_fn = Some(Box::new(custom_to_keystroke));
|
||||
}
|
||||
|
||||
/// Registers a lookup function that returns the default keystroke for a given custom action.
|
||||
/// Used when converting custom actions to key events during keybinding editing.
|
||||
pub(crate) fn register_default_keystroke_triggers_for_custom_actions(
|
||||
&mut self,
|
||||
custom_to_keystroke: impl Fn(CustomTag) -> Option<Keystroke> + 'static,
|
||||
) {
|
||||
self.default_keystroke_trigger_for_custom_action = Some(Box::new(custom_to_keystroke));
|
||||
}
|
||||
|
||||
pub(crate) fn custom_action_bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
|
||||
self.keymap.custom_action_bindings()
|
||||
}
|
||||
|
||||
/// Returns the first matching binding for the given custom action (not taking)
|
||||
/// into account the current context
|
||||
pub fn default_binding_for_custom_action(
|
||||
&self,
|
||||
custom_tag: CustomTag,
|
||||
) -> Option<BindingLens<'_>> {
|
||||
self.keymap
|
||||
.bindings()
|
||||
// Filter out just the matching custom binding or action.
|
||||
// We look for matches against either the current or original trigger.
|
||||
.find(|binding| {
|
||||
matches!(
|
||||
(binding.trigger, binding.original_trigger),
|
||||
(Trigger::Custom(tag), _) | (_, Some(Trigger::Custom(tag))) if *tag == custom_tag
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns any matching binding for the given custom tag and context
|
||||
pub fn binding_for_custom_action_in_context(
|
||||
&self,
|
||||
custom_tag: CustomTag,
|
||||
context: &Context,
|
||||
) -> Option<BindingLens<'_>> {
|
||||
self.keymap
|
||||
.custom_action_bindings()
|
||||
// First filter out just the matching custom binding or action
|
||||
// We look for matches against either the current or original trigger.
|
||||
.filter(|binding| {
|
||||
matches!(
|
||||
(binding.trigger, binding.original_trigger),
|
||||
(Trigger::Custom(tag), _) | (_, Some(Trigger::Custom(tag))) if *tag == custom_tag
|
||||
)
|
||||
})
|
||||
// And then filter against the current context and return the first match
|
||||
.find(move |binding| binding.context_predicate.eval(context))
|
||||
}
|
||||
|
||||
pub fn default_keystroke_trigger_for_custom_action(
|
||||
&self,
|
||||
custom_tag: CustomTag,
|
||||
) -> Option<Keystroke> {
|
||||
self.default_keystroke_trigger_for_custom_action
|
||||
.as_ref()
|
||||
.and_then(|f| f(custom_tag))
|
||||
}
|
||||
|
||||
pub fn get_binding_by_name(&self, name: &str) -> Option<BindingLens<'_>> {
|
||||
self.keymap.get_binding_by_name(name)
|
||||
}
|
||||
|
||||
/// Returns an iterator of lenses to key bindings that apply to the given context.
|
||||
///
|
||||
/// Key bindings are returned in precedence order, so the highest precedence key binding is
|
||||
/// returned first.
|
||||
pub fn bindings_for_context(&self, context: Context) -> impl Iterator<Item = BindingLens<'_>> {
|
||||
self.keymap
|
||||
.bindings()
|
||||
.filter(move |binding| binding.context_predicate.eval(&context))
|
||||
}
|
||||
|
||||
/// Fetch an iterator of editable bindings
|
||||
///
|
||||
/// The triggers for those actions will be overwritten by any custom triggers
|
||||
///
|
||||
/// Items will be returned in the reverse order they were registered, the most recently
|
||||
/// registered editable binding will have the highest precedence
|
||||
pub fn editable_bindings(&self) -> impl Iterator<Item = EditableBindingLens<'_>> {
|
||||
self.keymap.editable_bindings()
|
||||
}
|
||||
|
||||
/// Fetch an iterator of `BindingLens` objects, with the editable key bindings
|
||||
/// modified by the custom bindings, where appropriate.
|
||||
///
|
||||
/// Editable bindings will be returned first, followed by any fixed bindings in the reverse
|
||||
/// order they were added.
|
||||
pub fn get_bindings(&self) -> impl Iterator<Item = BindingLens<'_>> {
|
||||
self.keymap.bindings()
|
||||
}
|
||||
|
||||
pub fn push_keystroke(
|
||||
&mut self,
|
||||
keystroke: Keystroke,
|
||||
view_id: EntityId,
|
||||
ctx: &Context,
|
||||
) -> MatchResult {
|
||||
let pending = self.pending.entry(view_id).or_default();
|
||||
|
||||
if let Some(pending_ctx) = pending.context.as_ref() {
|
||||
if pending_ctx != ctx {
|
||||
pending.keystrokes.clear();
|
||||
}
|
||||
}
|
||||
|
||||
pending.keystrokes.push(keystroke);
|
||||
|
||||
let mut retain_pending = false;
|
||||
for binding in self.keymap.bindings() {
|
||||
if let Trigger::Keystrokes(keystrokes) = &binding.trigger {
|
||||
if keystrokes.starts_with(&pending.keystrokes)
|
||||
&& binding.context_predicate.eval(ctx)
|
||||
{
|
||||
if keystrokes.len() == pending.keystrokes.len() {
|
||||
self.pending.remove(&view_id);
|
||||
return MatchResult::Action(binding.action.clone());
|
||||
} else {
|
||||
retain_pending = true;
|
||||
pending.context = Some(ctx.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if retain_pending {
|
||||
MatchResult::Pending
|
||||
} else {
|
||||
self.pending.remove(&view_id);
|
||||
MatchResult::None
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to match with a StandardAction.
|
||||
// This returns None or Action, never Pending.
|
||||
pub fn match_standard(&self, action: StandardAction, ctx: &Context) -> MatchResult {
|
||||
for binding in self.keymap.bindings() {
|
||||
if let Trigger::Standard(triggeract) = binding.trigger {
|
||||
if *triggeract == action && binding.context_predicate.eval(ctx) {
|
||||
return MatchResult::Action(binding.action.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
MatchResult::None
|
||||
}
|
||||
|
||||
// Attempt to match with a CustomAction.
|
||||
// This returns None or Action, never Pending.
|
||||
pub fn match_custom(&self, action: CustomTag, ctx: &Context) -> MatchResult {
|
||||
for binding in self.keymap.bindings() {
|
||||
if let Trigger::Custom(tag) = binding.trigger {
|
||||
if *tag == action && binding.context_predicate.eval(ctx) {
|
||||
return MatchResult::Action(binding.action.clone());
|
||||
}
|
||||
}
|
||||
if let Some(Trigger::Custom(tag)) = binding.original_trigger {
|
||||
if *tag == action && binding.context_predicate.eval(ctx) {
|
||||
return MatchResult::Action(binding.action.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
MatchResult::None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "matcher_test.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,252 @@
|
||||
use crate::keymap::macros::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_matcher() -> anyhow::Result<()> {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Action {
|
||||
A(String),
|
||||
B,
|
||||
AB,
|
||||
}
|
||||
|
||||
let keymap = Keymap::new(vec![
|
||||
FixedBinding::new("a", Action::A("b".into()), id!("a")),
|
||||
FixedBinding::new("b", Action::B, id!("a")),
|
||||
FixedBinding::new("a b", Action::AB, id!("a") | id!("b")),
|
||||
]);
|
||||
|
||||
let mut ctx_a = Context::default();
|
||||
ctx_a.set.insert("a");
|
||||
|
||||
let mut ctx_b = Context::default();
|
||||
ctx_b.set.insert("b");
|
||||
|
||||
let mut matcher = Matcher::new(keymap);
|
||||
|
||||
let view_id = EntityId::new();
|
||||
|
||||
// Basic match
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("a", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::A("b".into())
|
||||
);
|
||||
|
||||
// Multi-keystroke match
|
||||
assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id, &ctx_b)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::AB
|
||||
);
|
||||
|
||||
// Failed matches don't interfere with matching subsequent keys
|
||||
assert!(matcher.test_keystroke("x", view_id, &ctx_a).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("a", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::A("b".into())
|
||||
);
|
||||
|
||||
// Pending keystrokes are cleared when the context changes
|
||||
assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::B
|
||||
);
|
||||
|
||||
let mut ctx_c = Context::default();
|
||||
ctx_c.set.insert("c");
|
||||
|
||||
// Pending keystrokes are maintained per-view
|
||||
let view_id1 = EntityId::new();
|
||||
let view_id2 = EntityId::new();
|
||||
assert_ne!(view_id1, view_id2);
|
||||
assert!(matcher.test_keystroke("a", view_id1, &ctx_b).is_none());
|
||||
assert!(matcher.test_keystroke("a", view_id2, &ctx_c).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id1, &ctx_b)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::AB
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_editable_binding_matching() {
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum Action {
|
||||
A(&'static str),
|
||||
B,
|
||||
AOrB,
|
||||
}
|
||||
|
||||
let mut keymap = Keymap::default();
|
||||
use crate::keymap::macros::*;
|
||||
keymap.register_editable_bindings([
|
||||
EditableBinding::new("a", "Action for A", Action::A("b"))
|
||||
.with_key_binding("a")
|
||||
.with_context_predicate(id!("a")),
|
||||
EditableBinding::new("b", "Action for B", Action::B)
|
||||
.with_key_binding("b")
|
||||
.with_context_predicate(id!("a")),
|
||||
EditableBinding::new("a_or_b", "Action for A or B", Action::AOrB)
|
||||
.with_key_binding("a b")
|
||||
.with_context_predicate(id!("a") | id!("b")),
|
||||
]);
|
||||
|
||||
let mut ctx_a = Context::default();
|
||||
ctx_a.set.insert("a");
|
||||
|
||||
let mut ctx_b = Context::default();
|
||||
ctx_b.set.insert("b");
|
||||
|
||||
let mut matcher = Matcher::new(keymap);
|
||||
|
||||
let view_id = EntityId::new();
|
||||
|
||||
// Basic match
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("a", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::A("b"),
|
||||
);
|
||||
|
||||
// Multi-keystroke match
|
||||
assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id, &ctx_b)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::AOrB
|
||||
);
|
||||
|
||||
// Failed matches don't interfere with matching subsequent keys
|
||||
assert!(matcher.test_keystroke("x", view_id, &ctx_a).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("a", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::A("b")
|
||||
);
|
||||
|
||||
// Pending keystrokes are cleared when the context changes
|
||||
assert!(matcher.test_keystroke("a", view_id, &ctx_b).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id, &ctx_a)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::B
|
||||
);
|
||||
|
||||
let mut ctx_c = Context::default();
|
||||
ctx_c.set.insert("c");
|
||||
|
||||
// Pending keystrokes are maintained per-view
|
||||
let view_id1 = EntityId::new();
|
||||
let view_id2 = EntityId::new();
|
||||
assert_ne!(view_id1, view_id2);
|
||||
assert!(matcher.test_keystroke("a", view_id1, &ctx_b).is_none());
|
||||
assert!(matcher.test_keystroke("a", view_id2, &ctx_c).is_none());
|
||||
assert_eq!(
|
||||
matcher
|
||||
.test_keystroke("b", view_id1, &ctx_b)
|
||||
.unwrap()
|
||||
.as_action::<Action>(),
|
||||
&Action::AOrB
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bindings_for_context() {
|
||||
#[derive(Debug)]
|
||||
enum Action {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
}
|
||||
let keymap = Keymap::new(vec![
|
||||
FixedBinding::new("a", Action::A, id!("a")),
|
||||
FixedBinding::new("b", Action::B, id!("b")),
|
||||
FixedBinding::new("c", Action::C, id!("b")),
|
||||
]);
|
||||
let matcher = Matcher::new(keymap);
|
||||
|
||||
let mut ctx_a = Context::default();
|
||||
ctx_a.set.insert("a");
|
||||
|
||||
let mut ctx_b = Context::default();
|
||||
ctx_b.set.insert("b");
|
||||
|
||||
// Getting bindings for the 'a' context returns a single result
|
||||
let ctx_a_bindings = matcher
|
||||
.bindings_for_context(ctx_a)
|
||||
.filter_map(|bind| match bind.trigger {
|
||||
Trigger::Keystrokes(keys) => {
|
||||
assert_eq!(keys.len(), 1);
|
||||
Some(keys[0].normalized())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(ctx_a_bindings.len(), 1);
|
||||
assert_eq!(ctx_a_bindings, vec!["a"]);
|
||||
|
||||
// Getting bindings for the 'b' context returns two results, in the reverse order they
|
||||
// added, so the "c" binding first followed by the "b" binding
|
||||
let ctx_b_bindings = matcher
|
||||
.bindings_for_context(ctx_b)
|
||||
.filter_map(|bind| match bind.trigger {
|
||||
Trigger::Keystrokes(keys) => {
|
||||
assert_eq!(keys.len(), 1);
|
||||
Some(keys[0].normalized())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(ctx_b_bindings, vec!["c", "b"]);
|
||||
}
|
||||
|
||||
impl Matcher {
|
||||
fn test_keystroke(
|
||||
&mut self,
|
||||
keystroke: &str,
|
||||
view_id: EntityId,
|
||||
ctx: &Context,
|
||||
) -> Option<Arc<dyn Action>> {
|
||||
match self.push_keystroke(Keystroke::parse(keystroke).unwrap(), view_id, ctx) {
|
||||
MatchResult::Action(action) => Some(action),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
trait AsAction {
|
||||
fn as_action<A: Action>(&self) -> &A;
|
||||
}
|
||||
|
||||
impl AsAction for Arc<dyn Action> {
|
||||
fn as_action<A: Action>(&self) -> &A {
|
||||
self.as_ref().as_any().downcast_ref::<A>().unwrap()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user