diff --git a/espanso-match/src/event.rs b/espanso-match/src/event.rs index 1d3b87a..d6f5b34 100644 --- a/espanso-match/src/event.rs +++ b/espanso-match/src/event.rs @@ -26,7 +26,7 @@ pub enum Event { VirtualSeparator, } -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Key { // Modifiers Alt, diff --git a/espanso-match/src/lib.rs b/espanso-match/src/lib.rs index 185a04c..86851e4 100644 --- a/espanso-match/src/lib.rs +++ b/espanso-match/src/lib.rs @@ -17,7 +17,6 @@ * along with espanso. If not, see . */ -use std::any::Any; use event::Event; #[macro_use] @@ -27,5 +26,5 @@ pub mod event; pub mod rolling; pub trait Matcher<'a, State, Id> where Id: Clone { - fn process(&'a self, prev_state: Option<&'a State>, event: Event) -> (State, Vec); + fn process(&'a self, prev_state: Option<&State>, event: Event) -> (State, Vec); } \ No newline at end of file diff --git a/espanso-match/src/rolling/item.rs b/espanso-match/src/rolling/item.rs deleted file mode 100644 index fce6f07..0000000 --- a/espanso-match/src/rolling/item.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::event::Key; - -/* - * This file is part of espanso. - * - * Copyright (C) 2019-2021 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 - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * espanso is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with espanso. If not, see . - */ - - -pub enum RollingItem { - WordSeparator, - Key(Key), - Char(String), - CharInsensitive(String), -} \ No newline at end of file diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs index f9a4bf9..c65e113 100644 --- a/espanso-match/src/rolling/matcher.rs +++ b/espanso-match/src/rolling/matcher.rs @@ -17,19 +17,35 @@ * along with espanso. If not, see . */ -use unicase::UniCase; -use crate::Matcher; +use super::{ + tree::{MatcherTreeNode, MatcherTreeRef}, + RollingMatch, +}; use crate::event::{Event, Key}; -use super::{RollingItem, RollingMatch, tree::{MatcherTreeNode, MatcherTreeRef}}; +use crate::Matcher; +use unicase::UniCase; +#[derive(Clone)] pub struct RollingMatcherState<'a, Id> { nodes: Vec<&'a MatcherTreeNode>, } -impl <'a, Id> Default for RollingMatcherState<'a, Id> { +impl<'a, Id> Default for RollingMatcherState<'a, Id> { + fn default() -> Self { + Self { nodes: Vec::new() } + } +} + +pub struct RollingMatcherOptions { + char_word_separators: Vec, + key_word_separators: Vec, +} + +impl Default for RollingMatcherOptions { fn default() -> Self { Self { - nodes: Vec::new(), + char_word_separators: Vec::new(), + key_word_separators: Vec::new(), } } } @@ -41,11 +57,13 @@ pub struct RollingMatcher { root: MatcherTreeNode, } - -impl <'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher where Id: Clone { +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>>, + prev_state: Option<&RollingMatcherState<'a, Id>>, event: Event, ) -> (RollingMatcherState<'a, Id>, Vec) { let mut next_refs = Vec::new(); @@ -68,31 +86,34 @@ impl <'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher { // 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, - }; + let current_state = RollingMatcherState { nodes: next_nodes }; (current_state, Vec::new()) } } -impl RollingMatcher { - pub fn new(matches: &[RollingMatch]) -> Self { - todo!() - // Self { - - // } +impl RollingMatcher { + pub fn new(matches: &[RollingMatch], opt: RollingMatcherOptions) -> Self { + let root = MatcherTreeNode::from_matches(matches); + Self { + root, + char_word_separators: opt.char_word_separators, + key_word_separators: opt.key_word_separators, + } } - // 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, chars } = event { @@ -140,7 +161,6 @@ impl RollingMatcher { } } Event::VirtualSeparator => true, - _ => false, } } } @@ -148,38 +168,107 @@ impl RollingMatcher { #[cfg(test)] mod tests { use super::*; + use crate::rolling::{StringMatchOptions}; + + fn get_matches_after_str(string: &str, matcher: &RollingMatcher) -> Vec { + let mut prev_state = None; + let mut matches = Vec::new(); + + for c in string.chars() { + let (state, _matches) = matcher.process( + prev_state.as_ref(), + Event::Key { + key: Key::Other, + chars: Some(c.to_string()), + }, + ); + + prev_state = Some(state); + matches = _matches; + } + + matches + } #[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() - })) - ) + fn matcher_process_simple_strings() { + let matcher = RollingMatcher::new( + &[ + RollingMatch::from_string(1, "hi", &StringMatchOptions::default()), + RollingMatch::from_string(2, "hey", &StringMatchOptions::default()), + RollingMatch::from_string(3, "my", &StringMatchOptions::default()), + RollingMatch::from_string(4, "myself", &StringMatchOptions::default()), + RollingMatch::from_string(5, "hi", &StringMatchOptions::default()), ], - ..Default::default() - }; + RollingMatcherOptions { + ..Default::default() + }, + ); - let matcher = RollingMatcher { - char_word_separators: vec![".".to_owned()], - key_word_separators: vec![Key::ArrowUp], - root, - }; + assert_eq!(get_matches_after_str("hi", &matcher), vec![1, 5]); + assert_eq!(get_matches_after_str("my", &matcher), vec![3]); + assert_eq!(get_matches_after_str("invalid", &matcher), vec![]); + } - let (state, matches) = matcher.process(None, Event::Key { - key: Key::Other, - chars: Some("h".to_string()), - }); - assert_eq!(matches.len(), 0); + #[test] + fn matcher_process_word_matches() { + let matcher = RollingMatcher::new( + &[ + RollingMatch::from_string( + 1, + "hi", + &StringMatchOptions { + left_word: true, + right_word: true, + ..Default::default() + }, + ), + RollingMatch::from_string(2, "hey", &StringMatchOptions::default()), + ], + RollingMatcherOptions { + char_word_separators: vec![".".to_string(), ",".to_string()], + ..Default::default() + }, + ); - let (state, matches) = matcher.process(Some(&state), Event::Key { - key: Key::Other, - chars: Some("i".to_string()), - }); - assert_eq!(matches, vec![1]); + assert_eq!(get_matches_after_str("hi", &matcher), vec![]); + assert_eq!(get_matches_after_str(".hi,", &matcher), vec![1]); + } + + #[test] + fn matcher_process_case_insensitive() { + let matcher = RollingMatcher::new( + &[ + RollingMatch::from_string( + 1, + "hi", + &StringMatchOptions { + case_insensitive: true, + ..Default::default() + }, + ), + RollingMatch::from_string(2, "hey", &StringMatchOptions::default()), + RollingMatch::from_string( + 3, + "arty", + &StringMatchOptions { + case_insensitive: true, + preserve_case_markers: true, + ..Default::default() + }, + ), + ], + RollingMatcherOptions { + char_word_separators: vec![".".to_string(), ",".to_string()], + ..Default::default() + }, + ); + + assert_eq!(get_matches_after_str("hi", &matcher), vec![1]); + assert_eq!(get_matches_after_str("Hi", &matcher), vec![1]); + assert_eq!(get_matches_after_str("HI", &matcher), vec![1]); + assert_eq!(get_matches_after_str("arty", &matcher), vec![3]); + assert_eq!(get_matches_after_str("arTY", &matcher), vec![3]); + assert_eq!(get_matches_after_str("ARTY", &matcher), vec![]); } } diff --git a/espanso-match/src/rolling/mod.rs b/espanso-match/src/rolling/mod.rs index 8322a82..e69450c 100644 --- a/espanso-match/src/rolling/mod.rs +++ b/espanso-match/src/rolling/mod.rs @@ -22,7 +22,7 @@ use crate::event::Key; mod matcher; mod tree; -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum RollingItem { WordSeparator, Key(Key), @@ -68,6 +68,13 @@ impl RollingMatch { Self { id, items } } + + pub fn from_items(id: Id, items: &[RollingItem]) -> Self { + Self { + id, + items: items.to_vec(), + } + } } pub struct StringMatchOptions { diff --git a/espanso-match/src/rolling/tree.rs b/espanso-match/src/rolling/tree.rs index e8f3dcf..b42bb0d 100644 --- a/espanso-match/src/rolling/tree.rs +++ b/espanso-match/src/rolling/tree.rs @@ -21,15 +21,15 @@ use unicase::UniCase; use crate::event::Key; -use super::RollingItem; +use super::{RollingItem, RollingMatch}; -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub(crate) enum MatcherTreeRef { Matches(Vec), Node(Box>), } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub(crate) struct MatcherTreeNode { pub word_separators: Option>, pub keys: Vec<(Key, MatcherTreeRef)>, @@ -37,7 +37,7 @@ pub(crate) struct MatcherTreeNode { pub chars_insensitive: Vec<(UniCase, MatcherTreeRef)>, } -impl Default for MatcherTreeNode { +impl Default for MatcherTreeNode { fn default() -> Self { Self { word_separators: None, @@ -48,10 +48,312 @@ impl Default for MatcherTreeNode { } } -impl MatcherTreeNode { - // TODO: test - pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode { - // TODO: implement the tree building algorithm - todo!() +impl MatcherTreeNode +where + Id: Clone, +{ + pub fn from_matches(matches: &[RollingMatch]) -> MatcherTreeNode { + let mut root = MatcherTreeNode::default(); + for m in matches { + insert_items_recursively(m.id.clone(), &mut root, &m.items); + } + root + } +} + +fn insert_items_recursively(id: Id, node: &mut MatcherTreeNode, items: &[RollingItem]) { + if items.is_empty() { + return; + } + + if items.len() == 1 { + let item = items.get(0).unwrap(); + match item { + RollingItem::WordSeparator => { + let mut new_matches = Vec::new(); + if let Some(node_ref) = node.word_separators.take() { + if let MatcherTreeRef::Matches(matches) = node_ref { + new_matches.extend(matches); + } + } + new_matches.push(id); + node.word_separators = Some(MatcherTreeRef::Matches(new_matches)) + } + RollingItem::Key(key) => { + if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) { + if let MatcherTreeRef::Matches(matches) = &mut entry.1 { + matches.push(id) + } else { + entry.1 = MatcherTreeRef::Matches(vec![id]) + }; + } else { + node + .keys + .push((key.clone(), MatcherTreeRef::Matches(vec![id]))); + } + } + RollingItem::Char(c) => { + if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) { + if let MatcherTreeRef::Matches(matches) = &mut entry.1 { + matches.push(id) + } else { + entry.1 = MatcherTreeRef::Matches(vec![id]) + }; + } else { + node + .chars + .push((c.clone(), MatcherTreeRef::Matches(vec![id]))); + } + } + RollingItem::CharInsensitive(c) => { + let uni_char = UniCase::new(c.clone()); + if let Some(entry) = node + .chars_insensitive + .iter_mut() + .find(|(_c, _)| _c == &uni_char) + { + if let MatcherTreeRef::Matches(matches) = &mut entry.1 { + matches.push(id) + } else { + entry.1 = MatcherTreeRef::Matches(vec![id]) + }; + } else { + node + .chars_insensitive + .push((uni_char, MatcherTreeRef::Matches(vec![id]))); + } + } + } + } else { + let item = items.get(0).unwrap(); + match item { + RollingItem::WordSeparator => match node.word_separators.as_mut() { + Some(MatcherTreeRef::Node(next_node)) => { + insert_items_recursively(id, next_node.as_mut(), &items[1..]) + } + None => { + let mut next_node = Box::new(MatcherTreeNode::default()); + insert_items_recursively(id, next_node.as_mut(), &items[1..]); + node.word_separators = Some(MatcherTreeRef::Node(next_node)); + } + _ => {} + }, + RollingItem::Key(key) => { + if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) { + if let MatcherTreeRef::Node(next_node) = &mut entry.1 { + insert_items_recursively(id, next_node, &items[1..]) + } + } else { + let mut next_node = Box::new(MatcherTreeNode::default()); + insert_items_recursively(id, next_node.as_mut(), &items[1..]); + node + .keys + .push((key.clone(), MatcherTreeRef::Node(next_node))); + } + } + RollingItem::Char(c) => { + if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) { + if let MatcherTreeRef::Node(next_node) = &mut entry.1 { + insert_items_recursively(id, next_node, &items[1..]) + } + } else { + let mut next_node = Box::new(MatcherTreeNode::default()); + insert_items_recursively(id, next_node.as_mut(), &items[1..]); + node + .chars + .push((c.clone(), MatcherTreeRef::Node(next_node))); + } + } + RollingItem::CharInsensitive(c) => { + let uni_char = UniCase::new(c.clone()); + if let Some(entry) = node + .chars_insensitive + .iter_mut() + .find(|(_c, _)| _c == &uni_char) + { + if let MatcherTreeRef::Node(next_node) = &mut entry.1 { + insert_items_recursively(id, next_node, &items[1..]) + } + } else { + let mut next_node = Box::new(MatcherTreeNode::default()); + insert_items_recursively(id, next_node.as_mut(), &items[1..]); + node + .chars_insensitive + .push((uni_char, MatcherTreeRef::Node(next_node))); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::rolling::StringMatchOptions; + + #[test] + fn generate_tree_from_items_simple_strings() { + let root = MatcherTreeNode::from_matches(&[ + RollingMatch::from_string(1, "hi", &StringMatchOptions::default()), + RollingMatch::from_string(2, "hey", &StringMatchOptions::default()), + RollingMatch::from_string(3, "my", &StringMatchOptions::default()), + RollingMatch::from_string(4, "myself", &StringMatchOptions::default()), + ]); + + assert_eq!( + root, + MatcherTreeNode { + chars: vec![ + ( + "h".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![ + ("i".to_string(), MatcherTreeRef::Matches(vec![1])), + ( + "e".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2]))], + ..Default::default() + })) + ), + ], + ..Default::default() + })) + ), + ( + "m".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![3])),], + ..Default::default() + })) + ) + ], + ..Default::default() + } + ) + } + + #[test] + fn generate_tree_from_items_keys() { + let root = MatcherTreeNode::from_matches(&[ + RollingMatch::from_items( + 1, + &[ + RollingItem::Key(Key::ArrowUp), + RollingItem::Key(Key::ArrowLeft), + ], + ), + RollingMatch::from_items( + 2, + &[ + RollingItem::Key(Key::ArrowUp), + RollingItem::Key(Key::ArrowRight), + ], + ), + ]); + + assert_eq!( + root, + MatcherTreeNode { + keys: vec![( + Key::ArrowUp, + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + keys: vec![ + (Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])), + (Key::ArrowRight, MatcherTreeRef::Matches(vec![2])), + ], + ..Default::default() + })) + ),], + ..Default::default() + } + ) + } + + #[test] + fn generate_tree_from_items_mixed() { + let root = MatcherTreeNode::from_matches(&[ + RollingMatch::from_items( + 1, + &[ + RollingItem::Key(Key::ArrowUp), + RollingItem::Key(Key::ArrowLeft), + ], + ), + RollingMatch::from_string( + 2, + "my", + &StringMatchOptions { + left_word: true, + ..Default::default() + }, + ), + RollingMatch::from_string( + 3, + "hi", + &StringMatchOptions { + left_word: true, + right_word: true, + ..Default::default() + }, + ), + RollingMatch::from_string( + 4, + "no", + &StringMatchOptions { + case_insensitive: true, + ..Default::default() + }, + ), + ]); + + assert_eq!( + root, + MatcherTreeNode { + keys: vec![( + Key::ArrowUp, + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + keys: vec![(Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])),], + ..Default::default() + })) + ),], + word_separators: Some(MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![ + ( + "m".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2])),], + ..Default::default() + })) + ), + ( + "h".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars: vec![( + "i".to_string(), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + word_separators: Some(MatcherTreeRef::Matches(vec![3])), + ..Default::default() + })) + ),], + ..Default::default() + })) + ), + ], + ..Default::default() + }))), + chars_insensitive: vec![( + UniCase::new("n".to_string()), + MatcherTreeRef::Node(Box::new(MatcherTreeNode { + chars_insensitive: vec![( + UniCase::new("o".to_string()), + MatcherTreeRef::Matches(vec![4]) + ),], + ..Default::default() + })) + ),], + ..Default::default() + } + ) } }