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