Refactor matcher to support word separators

This commit is contained in:
Federico Terzi 2019-10-22 20:41:33 +02:00
parent 2e60042b2b
commit 34ce0ee9b1
2 changed files with 89 additions and 69 deletions

View File

@ -34,6 +34,10 @@ pub struct Match {
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub _has_vars: bool, pub _has_vars: bool,
// Automatically calculated from the trigger, used by the matcher to check for correspondences.
#[serde(skip_serializing)]
pub _trigger_sequence: Vec<TriggerEntry>,
} }
impl <'de> serde::Deserialize<'de> for Match { impl <'de> serde::Deserialize<'de> for Match {
@ -54,12 +58,23 @@ impl<'a> From<&'a AutoMatch> for Match{
// Check if the match contains variables // Check if the match contains variables
let has_vars = VAR_REGEX.is_match(&other.replace); let has_vars = VAR_REGEX.is_match(&other.replace);
// Calculate the trigger sequence
let mut trigger_sequence = Vec::new();
let trigger_chars : Vec<char> = other.trigger.chars().collect();
trigger_sequence.extend(trigger_chars.into_iter().map(|c| {
TriggerEntry::Char(c)
}));
if other.word { // If it's a word match, end with a word separator
trigger_sequence.push(TriggerEntry::WordSeparator);
}
Self { Self {
trigger: other.trigger.clone(), trigger: other.trigger.clone(),
replace: other.replace.clone(), replace: other.replace.clone(),
vars: other.vars.clone(), vars: other.vars.clone(),
word: other.word.clone(), word: other.word.clone(),
_has_vars: has_vars, _has_vars: has_vars,
_trigger_sequence: trigger_sequence,
} }
} }
} }
@ -90,6 +105,12 @@ pub struct MatchVariable {
pub params: Mapping, pub params: Mapping,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
pub enum TriggerEntry {
Char(char),
WordSeparator
}
pub trait MatchReceiver { pub trait MatchReceiver {
fn on_match(&self, m: &Match, trailing_separator: Option<char>); fn on_match(&self, m: &Match, trailing_separator: Option<char>);
fn on_enable_update(&self, status: bool); fn on_enable_update(&self, status: bool);
@ -155,4 +176,36 @@ mod tests {
assert_eq!(_match._has_vars, true); assert_eq!(_match._has_vars, true);
} }
#[test]
fn test_match_trigger_sequence_without_word() {
let match_str = r###"
trigger: "test"
replace: "This is a test"
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
}
#[test]
fn test_match_trigger_sequence_with_word() {
let match_str = r###"
trigger: "test"
replace: "This is a test"
word: true
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator);
}
} }

View File

@ -17,7 +17,7 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>. * along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/ */
use crate::matcher::{Match, MatchReceiver}; use crate::matcher::{Match, MatchReceiver, TriggerEntry};
use std::cell::RefCell; use std::cell::RefCell;
use crate::event::{KeyModifier, ActionEventReceiver, ActionType}; use crate::event::{KeyModifier, ActionEventReceiver, ActionType};
use crate::config::ConfigManager; use crate::config::ConfigManager;
@ -38,12 +38,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
struct MatchEntry<'a> { struct MatchEntry<'a> {
start: usize, start: usize,
count: usize, count: usize,
_match: &'a Match, _match: &'a Match
// Usually false, becomes true if the match was detected and has the "word" option.
// This is needed to trigger the replacement only if the next char is a
// word separator ( such as space ).
waiting_for_separator: bool,
} }
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
@ -74,6 +69,17 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
self.receiver.on_enable_update(*is_enabled); self.receiver.on_enable_update(*is_enabled);
} }
fn is_matching(mtc: &Match, current_char: &str, start: usize, is_current_word_separator: bool) -> bool {
match mtc._trigger_sequence[start] {
TriggerEntry::Char(c) => {
current_char.starts_with(c)
},
TriggerEntry::WordSeparator => {
is_current_word_separator
},
}
}
} }
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> { impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMatcher<'a, R, M> {
@ -98,56 +104,32 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
let new_matches: Vec<MatchEntry> = active_config.matches.iter() let new_matches: Vec<MatchEntry> = active_config.matches.iter()
.filter(|&x| { .filter(|&x| {
if !x.trigger.starts_with(c) { let mut result = Self::is_matching(x, c, 0, is_current_word_separator);
false
}else{ if x.word {
// If word option is true, a match can only be started if the previous result = result && *was_previous_word_separator
// char was a word separator
if x.word {
*was_previous_word_separator
}else{
true
}
} }
result
}) })
.map(|x | MatchEntry{ .map(|x | MatchEntry{
start: 1, start: 1,
count: x.trigger.chars().count(), count: x._trigger_sequence.len(),
_match: &x, _match: &x
waiting_for_separator: false
}) })
.collect(); .collect();
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
let mut combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() { let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
Some(last_matches) => { Some(last_matches) => {
let mut updated: Vec<MatchEntry> = last_matches.iter() let mut updated: Vec<MatchEntry> = last_matches.iter()
.filter(|&x| { .filter(|&x| {
if x.waiting_for_separator { Self::is_matching(x._match, c, x.start, is_current_word_separator)
// The match is only waiting for a separator to call the replacement
is_current_word_separator
}else{
let nchar = x._match.trigger.chars().nth(x.start);
if let Some(nchar) = nchar {
c.starts_with(nchar)
}else{
false
}
}
}) })
.map(|x | { .map(|x | MatchEntry{
let new_start = if x.waiting_for_separator { start: x.start+1,
x.start // Avoid incrementing, we are only waiting for a separator count: x.count,
}else{ _match: &x._match
x.start+1 // Increment, we want to check if the next char matches
};
MatchEntry{
start: new_start,
count: x.count,
_match: &x._match,
waiting_for_separator: x.waiting_for_separator
}
}) })
.collect(); .collect();
@ -159,24 +141,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
let mut found_match = None; let mut found_match = None;
for entry in combined_matches.iter_mut() { for entry in combined_matches.iter() {
let is_found_match = if entry.start == entry.count { if entry.start == entry.count {
if !entry._match.word { found_match = Some(entry._match);
true
}else{
if entry.waiting_for_separator {
true
}else{
entry.waiting_for_separator = true;
false
}
}
}else{
false
};
if is_found_match {
found_match = Some(entry.clone());
break; break;
} }
} }
@ -189,17 +156,14 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
*was_previous_word_separator = is_current_word_separator; *was_previous_word_separator = is_current_word_separator;
if let Some(match_entry) = found_match { if let Some(mtc) = found_match {
if let Some(last) = current_set_queue.back_mut() { if let Some(last) = current_set_queue.back_mut() {
last.clear(); last.clear();
} }
let trailing_separator = if !match_entry.waiting_for_separator { let trailing_separator = if !is_current_word_separator {
None None
}else{ }else{
// Force espanso to consider the previous char a word separator after a match
*was_previous_word_separator = true;
let as_char = c.chars().nth(0); let as_char = c.chars().nth(0);
match as_char { match as_char {
Some(c) => { Some(c) => {
@ -209,7 +173,10 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
} }
}; };
self.receiver.on_match(match_entry._match, trailing_separator); // Force espanso to consider the last char as a separator
*was_previous_word_separator = true;
self.receiver.on_match(mtc, trailing_separator);
} }
} }