From beab299aa0cbcd8c21209afc8d079d27687490a2 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 13 Mar 2021 13:45:37 +0100 Subject: [PATCH] Improve Rolling matcher implementation and add Regex matcher --- Cargo.lock | 1 - espanso-match/Cargo.toml | 1 - espanso-match/src/event.rs | 2 +- espanso-match/src/lib.rs | 35 +++++++--- espanso-match/src/regex/mod.rs | 77 ++++++++++++++++++++-- espanso-match/src/rolling/matcher.rs | 95 ++++++++++++++++------------ espanso-match/src/rolling/mod.rs | 3 +- espanso-match/src/rolling/util.rs | 73 +++++++++++++++++++++ espanso-match/src/util.rs | 43 +++++++++++++ 9 files changed, 273 insertions(+), 57 deletions(-) create mode 100644 espanso-match/src/rolling/util.rs create mode 100644 espanso-match/src/util.rs diff --git a/Cargo.lock b/Cargo.lock index 0758999..46c544a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -301,7 +301,6 @@ name = "espanso-match" version = "0.1.0" dependencies = [ "anyhow", - "lazy_static", "log", "regex", "thiserror", diff --git a/espanso-match/Cargo.toml b/espanso-match/Cargo.toml index 336eb24..5cbf6b6 100644 --- a/espanso-match/Cargo.toml +++ b/espanso-match/Cargo.toml @@ -9,5 +9,4 @@ log = "0.4.14" anyhow = "1.0.38" thiserror = "1.0.23" regex = "1.4.3" -lazy_static = "1.4.0" unicase = "2.6.0" \ No newline at end of file diff --git a/espanso-match/src/event.rs b/espanso-match/src/event.rs index d6f5b34..85aa4af 100644 --- a/espanso-match/src/event.rs +++ b/espanso-match/src/event.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -#[derive(Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq)] pub enum Event { Key { key: Key, diff --git a/espanso-match/src/lib.rs b/espanso-match/src/lib.rs index 8480dcb..81415c5 100644 --- a/espanso-match/src/lib.rs +++ b/espanso-match/src/lib.rs @@ -17,16 +17,35 @@ * along with espanso. If not, see . */ +use std::collections::HashMap; + use event::Event; -#[macro_use] -extern crate lazy_static; - pub mod event; -pub mod rolling; pub mod regex; +pub mod rolling; +mod util; -// TODO: instead of returning Vec, return a Vec of structs which contain Id, trigger string and variables(if any) -pub trait Matcher<'a, State, Id> where Id: Clone { - fn process(&'a self, prev_state: Option<&State>, event: Event) -> (State, Vec); -} \ No newline at end of file +#[derive(Debug, Clone, PartialEq)] +pub struct MatchResult { + id: Id, + trigger: String, + vars: HashMap, +} + +impl Default for MatchResult { + fn default() -> Self { + Self { + id: Id::default(), + trigger: "".to_string(), + vars: HashMap::new(), + } + } +} + +pub trait Matcher<'a, State, Id> +where + Id: Clone, +{ + fn process(&'a self, prev_state: Option<&State>, event: Event) -> (State, Vec>); +} diff --git a/espanso-match/src/regex/mod.rs b/espanso-match/src/regex/mod.rs index dc5a166..1ee223f 100644 --- a/espanso-match/src/regex/mod.rs +++ b/espanso-match/src/regex/mod.rs @@ -17,10 +17,12 @@ * along with espanso. If not, see . */ +use std::collections::HashMap; + use log::error; use regex::{Regex, RegexSet}; -use crate::event::Event; +use crate::{MatchResult, event::Event}; use crate::Matcher; #[derive(Debug)] @@ -68,7 +70,7 @@ where &'a self, prev_state: Option<&RegexMatcherState>, event: Event, - ) -> (RegexMatcherState, Vec) { + ) -> (RegexMatcherState, Vec>) { let mut buffer = if let Some(prev_state) = prev_state { prev_state.buffer.clone() } else { @@ -83,13 +85,38 @@ where // Find matches if self.regex_set.is_match(&buffer) { + let mut matches = Vec::new(); + for index in self.regex_set.matches(&buffer) { if let (Some(id), Some(regex)) = (self.ids.get(index), self.regexes.get(index)) { - // TODO: find complete match and captures + if let Some(captures) = regex.captures(&buffer) { + let full_match = captures.get(0).map_or("", |m| m.as_str()); + if !full_match.is_empty() { + + // Now extract the captured names as variables + let variables: HashMap = regex + .capture_names() + .flatten() + .filter_map(|n| Some((n.to_string(), captures.name(n)?.as_str().to_string()))) + .collect(); + + let result = MatchResult { + id: (*id).clone(), + trigger: full_match.to_string(), + vars: variables, + }; + + matches.push(result); + } + } } else { error!("received inconsistent index from regex set with index: {}", index); } } + + if !matches.is_empty() { + return (RegexMatcherState::default(), matches) + } } let current_state = RegexMatcherState { buffer }; @@ -122,4 +149,46 @@ impl RegexMatcher { } } -// TODO: test \ No newline at end of file +#[cfg(test)] +mod tests { + use super::*; + use crate::util::tests::get_matches_after_str; + + fn match_result(id: Id, trigger: &str, vars: &[(&str, &str)]) -> MatchResult { + let vars: HashMap = vars.iter().map(|(key, value)| { + (key.to_string(), value.to_string()) + }).collect(); + + MatchResult { + id, + trigger: trigger.to_string(), + vars, + } + } + + #[test] + fn matcher_simple_matches() { + let matcher = RegexMatcher::new(&[ + RegexMatch::new(1, "hello"), + RegexMatch::new(2, "num\\d{1,3}s"), + ]); + assert_eq!(get_matches_after_str("hi", &matcher), vec![]); + assert_eq!(get_matches_after_str("hello", &matcher), vec![match_result(1, "hello", &[])]); + assert_eq!(get_matches_after_str("say hello", &matcher), vec![match_result(1, "hello", &[])]); + assert_eq!(get_matches_after_str("num1s", &matcher), vec![match_result(2, "num1s", &[])]); + assert_eq!(get_matches_after_str("num134s", &matcher), vec![match_result(2, "num134s", &[])]); + assert_eq!(get_matches_after_str("nums", &matcher), vec![]); + } + + #[test] + fn matcher_with_variables() { + let matcher = RegexMatcher::new(&[ + RegexMatch::new(1, "hello\\((?P.*?)\\)"), + RegexMatch::new(2, "multi\\((?P.*?),(?P.*?)\\)"), + ]); + assert_eq!(get_matches_after_str("hi", &matcher), vec![]); + assert_eq!(get_matches_after_str("say hello(mary)", &matcher), vec![match_result(1, "hello(mary)", &[("name", "mary")])]); + assert_eq!(get_matches_after_str("hello(mary", &matcher), vec![]); + assert_eq!(get_matches_after_str("multi(mary,jane)", &matcher), vec![match_result(2, "multi(mary,jane)", &[("name1", "mary"), ("name2", "jane")])]); + } +} \ No newline at end of file diff --git a/espanso-match/src/rolling/matcher.rs b/espanso-match/src/rolling/matcher.rs index c65e113..1c45a90 100644 --- a/espanso-match/src/rolling/matcher.rs +++ b/espanso-match/src/rolling/matcher.rs @@ -17,25 +17,30 @@ * along with espanso. If not, see . */ -use super::{ - tree::{MatcherTreeNode, MatcherTreeRef}, - RollingMatch, -}; -use crate::event::{Event, Key}; +use std::collections::HashMap; + +use super::{RollingMatch, tree::{MatcherTreeNode, MatcherTreeRef}, util::extract_string_from_events}; +use crate::{MatchResult, event::{Event, Key}}; use crate::Matcher; use unicase::UniCase; #[derive(Clone)] pub struct RollingMatcherState<'a, Id> { - nodes: Vec<&'a MatcherTreeNode>, + paths: Vec>, } impl<'a, Id> Default for RollingMatcherState<'a, Id> { fn default() -> Self { - Self { nodes: Vec::new() } + Self { paths: Vec::new() } } } +#[derive(Clone)] +struct RollingMatcherStatePath<'a, Id> { + node: &'a MatcherTreeNode, + events: Vec, +} + pub struct RollingMatcherOptions { char_word_separators: Vec, key_word_separators: Vec, @@ -65,35 +70,53 @@ where &'a self, prev_state: Option<&RollingMatcherState<'a, Id>>, event: Event, - ) -> (RollingMatcherState<'a, Id>, Vec) { + ) -> (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)); + for node_path in prev_state.paths.iter() { + next_refs.extend(self.find_refs(node_path.node, &event).into_iter().map(|node_ref| { + let mut new_events = node_path.events.clone(); + new_events.push(event.clone()); + (node_ref, new_events) + })); } } // Calculate new ones let root_refs = self.find_refs(&self.root, &event); - next_refs.extend(root_refs); + next_refs.extend(root_refs.into_iter().map(|node_ref| { + (node_ref, vec![event.clone()]) + })); - let mut next_nodes = Vec::new(); + let mut next_paths = Vec::new(); - for node_ref in next_refs { + for (node_ref, events) in next_refs { match node_ref { MatcherTreeRef::Matches(matches) => { + let trigger = extract_string_from_events(&events); + let results = matches.iter().map(|id| { + MatchResult { + id: id.clone(), + trigger: trigger.clone(), + vars: HashMap::new(), + } + }).collect(); + // Reset the state and return the matches - return (RollingMatcherState::default(), matches.clone()); + return (RollingMatcherState::default(), results); } MatcherTreeRef::Node(node) => { - next_nodes.push(node.as_ref()); + next_paths.push(RollingMatcherStatePath { + node: node.as_ref(), + events, + }); } } } - let current_state = RollingMatcherState { nodes: next_nodes }; + let current_state = RollingMatcherState { paths: next_paths }; (current_state, Vec::new()) } @@ -169,25 +192,14 @@ impl RollingMatcher { mod tests { use super::*; use crate::rolling::{StringMatchOptions}; + use crate::util::tests::get_matches_after_str; - 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; + fn match_result(id: Id, trigger: &str) -> MatchResult { + MatchResult { + id, + trigger: trigger.to_string(), + ..Default::default() } - - matches } #[test] @@ -205,8 +217,9 @@ mod tests { }, ); - 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("hi", &matcher), vec![match_result(1, "hi"), match_result(5, "hi")]); + assert_eq!(get_matches_after_str("my", &matcher), vec![match_result(3, "my")]); + assert_eq!(get_matches_after_str("mmy", &matcher), vec![match_result(3, "my")]); assert_eq!(get_matches_after_str("invalid", &matcher), vec![]); } @@ -232,7 +245,7 @@ mod tests { ); assert_eq!(get_matches_after_str("hi", &matcher), vec![]); - assert_eq!(get_matches_after_str(".hi,", &matcher), vec![1]); + assert_eq!(get_matches_after_str(".hi,", &matcher), vec![match_result(1, ".hi,")]); } #[test] @@ -264,11 +277,11 @@ mod tests { }, ); - 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("hi", &matcher), vec![match_result(1, "hi")]); + assert_eq!(get_matches_after_str("Hi", &matcher), vec![match_result(1, "Hi")]); + assert_eq!(get_matches_after_str("HI", &matcher), vec![match_result(1, "HI")]); + assert_eq!(get_matches_after_str("arty", &matcher), vec![match_result(3, "arty")]); + assert_eq!(get_matches_after_str("arTY", &matcher), vec![match_result(3, "arTY")]); 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 9a8d92e..71abf33 100644 --- a/espanso-match/src/rolling/mod.rs +++ b/espanso-match/src/rolling/mod.rs @@ -19,8 +19,9 @@ use crate::event::Key; -mod matcher; +pub mod matcher; mod tree; +mod util; #[derive(Debug, Clone, PartialEq)] pub enum RollingItem { diff --git a/espanso-match/src/rolling/util.rs b/espanso-match/src/rolling/util.rs new file mode 100644 index 0000000..f8c86c6 --- /dev/null +++ b/espanso-match/src/rolling/util.rs @@ -0,0 +1,73 @@ +/* + * 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 . + */ + +use crate::Event; + +// TODO: test +pub(crate) fn extract_string_from_events(events: &[Event]) -> String { + let mut string = String::new(); + + for event in events { + if let Event::Key { key: _, chars } = event { + if let Some(chars) = chars { + string.push_str(chars); + } + } + } + + string +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::event::Key; + + #[test] + fn extract_string_from_events_all_chars() { + assert_eq!(extract_string_from_events(&[ + Event::Key { key: Key::Other, chars: Some("h".to_string()) }, + Event::Key { key: Key::Other, chars: Some("e".to_string()) }, + Event::Key { key: Key::Other, chars: Some("l".to_string()) }, + Event::Key { key: Key::Other, chars: Some("l".to_string()) }, + Event::Key { key: Key::Other, chars: Some("o".to_string()) }, + ]), "hello"); + } + + #[test] + fn extract_string_from_events_no_chars() { + assert_eq!(extract_string_from_events(&[ + Event::Key { key: Key::ArrowUp, chars: None }, + Event::Key { key: Key::ArrowUp, chars: None }, + Event::Key { key: Key::ArrowUp, chars: None }, + ]), ""); + } + + #[test] + fn extract_string_from_events_mixed() { + assert_eq!(extract_string_from_events(&[ + Event::Key { key: Key::Other, chars: Some("h".to_string()) }, + Event::Key { key: Key::Other, chars: Some("e".to_string()) }, + Event::Key { key: Key::Other, chars: Some("l".to_string()) }, + Event::Key { key: Key::Other, chars: Some("l".to_string()) }, + Event::Key { key: Key::Other, chars: Some("o".to_string()) }, + Event::Key { key: Key::ArrowUp, chars: None }, + ]), "hello"); + } +} \ No newline at end of file diff --git a/espanso-match/src/util.rs b/espanso-match/src/util.rs new file mode 100644 index 0000000..08ee612 --- /dev/null +++ b/espanso-match/src/util.rs @@ -0,0 +1,43 @@ +/* + * 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 . + */ + +#[cfg(test)] +pub(crate) mod tests { + use crate::{MatchResult, Matcher, event::{Event, Key}}; + + pub(crate) fn get_matches_after_str<'a, Id: Clone, S, M: Matcher<'a, S, Id>>(string: &str, matcher: &'a M) -> 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 + } +} \ No newline at end of file