From 784e074795100b6bd1a79c1c22feefddb1a0893f Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Wed, 10 Mar 2021 21:12:23 +0100 Subject: [PATCH] Improve the RollingMatcher implementation --- espanso-match/src/event.rs | 2 +- espanso-match/src/lib.rs | 6 +- espanso-match/src/rolling/matcher.rs | 123 +++++++++++++++---- espanso-match/src/rolling/mod.rs | 170 +++++++++++++++++++++++++-- espanso-match/src/rolling/tree.rs | 37 ++++-- 5 files changed, 289 insertions(+), 49 deletions(-) diff --git a/espanso-match/src/event.rs b/espanso-match/src/event.rs index 9c457ae..1d3b87a 100644 --- a/espanso-match/src/event.rs +++ b/espanso-match/src/event.rs @@ -21,7 +21,7 @@ pub enum Event { Key { key: Key, - char: Option + chars: Option }, VirtualSeparator, } diff --git a/espanso-match/src/lib.rs b/espanso-match/src/lib.rs index dc9c1a2..185a04c 100644 --- a/espanso-match/src/lib.rs +++ b/espanso-match/src/lib.rs @@ -18,6 +18,7 @@ */ use std::any::Any; +use event::Event; #[macro_use] extern crate lazy_static; @@ -25,7 +26,6 @@ extern crate lazy_static; pub mod event; pub mod rolling; -pub trait Matcher { - // TODO: create suitable event type - fn process(&self, prev_state: Option<&dyn Any>, event: Option) -> (Box, Vec); +pub trait Matcher<'a, State, Id> where Id: Clone { + fn process(&'a self, prev_state: Option<&'a State>, event: Event) -> (State, Vec); } \ No newline at end of file diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs index 3a22eee..f9a4bf9 100644 --- a/espanso-match/src/rolling/matcher.rs +++ b/espanso-match/src/rolling/matcher.rs @@ -18,44 +18,90 @@ */ use unicase::UniCase; - +use crate::Matcher; use crate::event::{Event, Key}; +use super::{RollingItem, RollingMatch, tree::{MatcherTreeNode, MatcherTreeRef}}; -use super::tree::{MatcherTreeNode, MatcherTreeRef}; +pub struct RollingMatcherState<'a, Id> { + nodes: Vec<&'a MatcherTreeNode>, +} -pub struct RollingMatcher { +impl <'a, Id> Default for RollingMatcherState<'a, Id> { + fn default() -> Self { + Self { + nodes: Vec::new(), + } + } +} + +pub struct RollingMatcher { char_word_separators: Vec, key_word_separators: Vec, - root: MatcherTreeNode, + root: MatcherTreeNode, } -// impl Matcher for RollingMatcher { -// fn process( -// &self, -// prev_state: &dyn std::any::Any, -// event: Option, -// ) -> (Box, Vec) { -// todo!() -// } -// } -impl RollingMatcher { - // TODO: to find the matches, we first call the `find_refs` to get the list of matching nodes - // then we scan them and if any of those references is of variant `Matches`, then we return those - // match ids, otherwise None +impl <'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher where Id: Clone { + fn process( + &'a self, + prev_state: Option<&'a RollingMatcherState<'a, Id>>, + event: Event, + ) -> (RollingMatcherState<'a, Id>, Vec) { + let mut next_refs = Vec::new(); + + // First compute the old refs + if let Some(prev_state) = prev_state { + for node_ref in prev_state.nodes.iter() { + next_refs.extend(self.find_refs(node_ref, &event)); + } + } + + // Calculate new ones + let root_refs = self.find_refs(&self.root, &event); + next_refs.extend(root_refs); + + let mut next_nodes = Vec::new(); + + for node_ref in next_refs { + match node_ref { + MatcherTreeRef::Matches(matches) => { + // Reset the state and return the matches + return (RollingMatcherState::default(), matches.clone()); + }, + MatcherTreeRef::Node(node) => { + next_nodes.push(node.as_ref()); + } + } + } + + let current_state = RollingMatcherState { + nodes: next_nodes, + }; + + (current_state, Vec::new()) + } +} + +impl RollingMatcher { + pub fn new(matches: &[RollingMatch]) -> Self { + todo!() + // Self { + + // } + } // TODO: test - fn find_refs<'a>(&'a self, node: &'a MatcherTreeNode, event: &Event) -> Vec<&'a MatcherTreeRef> { + fn find_refs<'a>(&'a self, node: &'a MatcherTreeNode, event: &Event) -> Vec<&'a MatcherTreeRef> { let mut refs = Vec::new(); - if let Event::Key { key, char } = event { + if let Event::Key { key, chars } = event { // Key matching if let Some((_, node_ref)) = node.keys.iter().find(|(_key, _)| _key == key) { refs.push(node_ref); } - if let Some(char) = char { + if let Some(char) = chars { // Char matching if let Some((_, node_ref)) = node.chars.iter().find(|(_char, _)| _char == char) { refs.push(node_ref); @@ -84,10 +130,10 @@ impl RollingMatcher { fn is_word_separator(&self, event: &Event) -> bool { match event { - Event::Key { key, char } => { + Event::Key { key, chars } => { if self.key_word_separators.contains(&key) { true - } else if let Some(char) = char { + } else if let Some(char) = chars { self.char_word_separators.contains(&char) } else { false @@ -104,7 +150,36 @@ mod tests { use super::*; #[test] - fn test() { - + fn test() { // TODO: convert to actual test + let root = MatcherTreeNode { + chars: vec![ + ("h".to_string(), MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![ + ("i".to_string(), MatcherTreeRef::Matches(vec![1])), + ], + ..Default::default() + })) + ) + ], + ..Default::default() + }; + + let matcher = RollingMatcher { + char_word_separators: vec![".".to_owned()], + key_word_separators: vec![Key::ArrowUp], + root, + }; + + let (state, matches) = matcher.process(None, Event::Key { + key: Key::Other, + chars: Some("h".to_string()), + }); + assert_eq!(matches.len(), 0); + + let (state, matches) = matcher.process(Some(&state), Event::Key { + key: Key::Other, + chars: Some("i".to_string()), + }); + assert_eq!(matches, vec![1]); } } diff --git a/espanso-match/src/rolling/mod.rs b/espanso-match/src/rolling/mod.rs index add1d83..8322a82 100644 --- a/espanso-match/src/rolling/mod.rs +++ b/espanso-match/src/rolling/mod.rs @@ -1,7 +1,7 @@ /* * This file is part of espanso. * - * Copyright (C) 2019-2021 Federico Terzi + * Copyright (C) 2019-202 case_insensitive: (), preserve_case_markers: (), left_word: (), right_word: ()1 Federico Terzi * * espanso is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -17,19 +17,173 @@ * along with espanso. If not, see . */ -use self::item::RollingItem; +use crate::event::Key; -mod item; -mod tree; mod matcher; +mod tree; -pub struct RollingMatch { - id: i32, +#[derive(Debug, PartialEq)] +pub enum RollingItem { + WordSeparator, + Key(Key), + Char(String), + CharInsensitive(String), +} + +#[derive(Debug, PartialEq)] +pub struct RollingMatch { + id: Id, items: Vec, } -impl RollingMatch { - pub fn new(id: i32, items: Vec) -> Self { +impl RollingMatch { + pub fn new(id: Id, items: Vec) -> Self { + Self { id, items } + } + + pub fn from_string(id: Id, string: &str, opt: &StringMatchOptions) -> Self { + let mut items = Vec::new(); + + if opt.left_word { + items.push(RollingItem::WordSeparator); + } + + for (index, c) in string.chars().enumerate() { + if opt.case_insensitive { + // If preserve case markers is true, we want to keep the first two chars + // as case-sensitive. This is needed to implement the "propagate_case" option. + if opt.preserve_case_markers && index < 2 { + items.push(RollingItem::Char(c.to_string())) + } else { + items.push(RollingItem::CharInsensitive(c.to_string())) + } + } else { + items.push(RollingItem::Char(c.to_string())) + } + } + + if opt.right_word { + items.push(RollingItem::WordSeparator); + } + Self { id, items } } } + +pub struct StringMatchOptions { + case_insensitive: bool, + preserve_case_markers: bool, + left_word: bool, + right_word: bool, +} + +impl Default for StringMatchOptions { + fn default() -> Self { + Self { + case_insensitive: false, + preserve_case_markers: false, + left_word: false, + right_word: false, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_match_from_string_base_case() { + assert_eq!( + RollingMatch::from_string(1, "test", &StringMatchOptions::default()), + RollingMatch { + id: 1, + items: vec![ + RollingItem::Char("t".to_string()), + RollingItem::Char("e".to_string()), + RollingItem::Char("s".to_string()), + RollingItem::Char("t".to_string()), + ] + } + ) + } + + #[test] + fn test_match_from_string_left_word() { + assert_eq!( + RollingMatch::from_string(1, "test", &StringMatchOptions { + left_word: true, + ..Default::default() + }), + RollingMatch { + id: 1, + items: vec![ + RollingItem::WordSeparator, + RollingItem::Char("t".to_string()), + RollingItem::Char("e".to_string()), + RollingItem::Char("s".to_string()), + RollingItem::Char("t".to_string()), + ] + } + ) + } + + #[test] + fn test_match_from_string_right_word() { + assert_eq!( + RollingMatch::from_string(1, "test", &StringMatchOptions { + right_word: true, + ..Default::default() + }), + RollingMatch { + id: 1, + items: vec![ + RollingItem::Char("t".to_string()), + RollingItem::Char("e".to_string()), + RollingItem::Char("s".to_string()), + RollingItem::Char("t".to_string()), + RollingItem::WordSeparator, + ] + } + ) + } + + #[test] + fn test_match_from_string_case_insensitive() { + assert_eq!( + RollingMatch::from_string(1, "test", &StringMatchOptions { + case_insensitive: true, + ..Default::default() + }), + RollingMatch { + id: 1, + items: vec![ + RollingItem::CharInsensitive("t".to_string()), + RollingItem::CharInsensitive("e".to_string()), + RollingItem::CharInsensitive("s".to_string()), + RollingItem::CharInsensitive("t".to_string()), + ] + } + ) + } + + #[test] + fn test_match_from_string_preserve_case_markers() { + assert_eq!( + RollingMatch::from_string(1, "test", &StringMatchOptions { + case_insensitive: true, + preserve_case_markers: true, + ..Default::default() + }), + RollingMatch { + id: 1, + items: vec![ + RollingItem::Char("t".to_string()), + RollingItem::Char("e".to_string()), + RollingItem::CharInsensitive("s".to_string()), + RollingItem::CharInsensitive("t".to_string()), + ] + } + ) + } +} diff --git a/espanso-match/src/rolling/tree.rs b/espanso-match/src/rolling/tree.rs index 032135d..e8f3dcf 100644 --- a/espanso-match/src/rolling/tree.rs +++ b/espanso-match/src/rolling/tree.rs @@ -21,26 +21,37 @@ use unicase::UniCase; use crate::event::Key; -use super::item::RollingItem; +use super::RollingItem; #[derive(Debug)] -pub(crate) enum MatcherTreeRef { - Matches(Vec), - Node(Box), +pub(crate) enum MatcherTreeRef { + Matches(Vec), + Node(Box>), } #[derive(Debug)] -pub(crate) struct MatcherTreeNode { - pub word_separators: Option, - pub keys: Vec<(Key, MatcherTreeRef)>, - pub chars: Vec<(String, MatcherTreeRef)>, - pub chars_insensitive: Vec<(UniCase, MatcherTreeRef)>, +pub(crate) struct MatcherTreeNode { + pub word_separators: Option>, + pub keys: Vec<(Key, MatcherTreeRef)>, + pub chars: Vec<(String, MatcherTreeRef)>, + pub chars_insensitive: Vec<(UniCase, MatcherTreeRef)>, } -impl MatcherTreeNode { - // TODO: test - pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode { - todo!() +impl Default for MatcherTreeNode { + fn default() -> Self { + Self { + word_separators: None, + keys: Vec::new(), + chars: Vec::new(), + chars_insensitive: Vec::new(), + } } } +impl MatcherTreeNode { + // TODO: test + pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode { + // TODO: implement the tree building algorithm + todo!() + } +}