/*
* 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 std::collections::HashMap;
use super::{
tree::{MatcherTreeNode, MatcherTreeRef},
util::extract_string_from_events,
RollingMatch,
};
use crate::Matcher;
use crate::{
event::{Event, Key},
MatchResult,
};
use unicase::UniCase;
pub(crate) type IsWordSeparator = bool;
#[derive(Clone)]
pub struct RollingMatcherState<'a, Id> {
paths: Vec>,
}
impl<'a, Id> Default for RollingMatcherState<'a, Id> {
fn default() -> Self {
Self { paths: Vec::new() }
}
}
#[derive(Clone)]
struct RollingMatcherStatePath<'a, Id> {
node: &'a MatcherTreeNode,
events: Vec<(Event, IsWordSeparator)>,
}
#[derive(Default)]
pub struct RollingMatcherOptions {
pub char_word_separators: Vec,
pub key_word_separators: Vec,
}
pub struct RollingMatcher {
char_word_separators: Vec,
key_word_separators: Vec,
root: MatcherTreeNode,
}
impl<'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher
where
Id: Clone,
{
fn process(
&'a self,
prev_state: Option<&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_path in prev_state.paths.iter() {
next_refs.extend(
self
.find_refs(node_path.node, &event, true)
.into_iter()
.map(|(node_ref, is_word_separator)| {
let mut new_events = node_path.events.clone();
new_events.push((event.clone(), is_word_separator));
(node_ref, new_events)
}),
);
}
}
// Calculate new ones
let root_refs = self.find_refs(&self.root, &event, prev_state.is_some());
next_refs.extend(
root_refs
.into_iter()
.map(|(node_ref, is_word_separator)| (node_ref, vec![(event.clone(), is_word_separator)])),
);
let mut next_paths = Vec::new();
for (node_ref, events) in next_refs {
match node_ref {
MatcherTreeRef::Matches(matches) => {
let (trigger, left_separator, right_separator) = extract_string_from_events(&events);
let results = matches
.iter()
.map(|id| MatchResult {
id: id.clone(),
trigger: trigger.clone(),
left_separator: left_separator.clone(),
right_separator: right_separator.clone(),
vars: HashMap::new(),
})
.collect();
// Reset the state and return the matches
return (RollingMatcherState::default(), results);
}
MatcherTreeRef::Node(node) => {
next_paths.push(RollingMatcherStatePath {
node: node.as_ref(),
events,
});
}
}
}
let current_state = RollingMatcherState { paths: next_paths };
(current_state, Vec::new())
}
}
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,
}
}
fn find_refs<'a>(
&'a self,
node: &'a MatcherTreeNode,
event: &Event,
has_previous_state: bool,
) -> Vec<(&'a MatcherTreeRef, IsWordSeparator)> {
let mut refs = Vec::new();
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, false));
}
if let Some(char) = chars {
// Char matching
if let Some((_, node_ref)) = node.chars.iter().find(|(_char, _)| _char == char) {
refs.push((node_ref, false));
}
// Char case-insensitive
let insensitive_char = UniCase::new(char);
if let Some((_, node_ref)) = node
.chars_insensitive
.iter()
.find(|(_char, _)| *_char == insensitive_char)
{
refs.push((node_ref, false));
}
}
}
if self.is_word_separator(event) {
if let Some(node_ref) = node.word_separators.as_ref() {
refs.push((node_ref, true))
}
}
// If there is no previous state, we handle it as a word separator, exploring a step forward
// in the state.
if !has_previous_state {
if let Some(MatcherTreeRef::Node(node)) = node.word_separators.as_ref() {
refs.extend(self.find_refs(node, event, true));
}
}
refs
}
fn is_word_separator(&self, event: &Event) -> bool {
match event {
Event::Key { key, chars } => {
if self.key_word_separators.contains(key) {
true
} else if let Some(char) = chars {
self.char_word_separators.contains(char)
} else {
false
}
}
Event::VirtualSeparator => true,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rolling::StringMatchOptions;
use crate::util::tests::get_matches_after_str;
fn match_result(id: Id, trigger: &str) -> MatchResult {
MatchResult {
id,
trigger: trigger.to_string(),
..Default::default()
}
}
fn match_result_with_sep(
id: Id,
trigger: &str,
left: Option<&str>,
right: Option<&str>,
) -> MatchResult {
MatchResult {
id,
trigger: trigger.to_string(),
left_separator: left.map(str::to_owned),
right_separator: right.map(str::to_owned),
..Default::default()
}
}
#[test]
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()),
],
RollingMatcherOptions {
..Default::default()
},
);
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![]);
}
#[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()
},
);
assert_eq!(get_matches_after_str("hi", &matcher), vec![]);
// Word matches are also triggered when there is no left separator but it's a new state
assert_eq!(
get_matches_after_str("hi,", &matcher),
vec![match_result_with_sep(1, "hi,", None, Some(","))]
);
assert_eq!(
get_matches_after_str(".hi,", &matcher),
vec![match_result_with_sep(1, ".hi,", Some("."), Some(","))]
);
}
#[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,
..Default::default()
},
),
],
RollingMatcherOptions {
char_word_separators: vec![".".to_string(), ",".to_string()],
..Default::default()
},
);
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![match_result(3, "ARTY")]
);
}
}