From 6c1977f48ad422f951d68927c67b05d10e157a30 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 19 Oct 2019 23:31:05 +0200 Subject: [PATCH 1/3] First draft of the word separation feature, proposed in issue #82 --- src/config/mod.rs | 4 ++ src/engine.rs | 17 +++++- src/matcher/mod.rs | 8 ++- src/matcher/scrolling.rs | 115 ++++++++++++++++++++++++++++++++------- 4 files changed, 121 insertions(+), 23 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index a8bf6cc..009526b 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -50,6 +50,7 @@ fn default_log_level() -> i32 { 0 } fn default_ipc_server_port() -> i32 { 34982 } fn default_use_system_agent() -> bool { true } fn default_config_caching_interval() -> i32 { 800 } +fn default_word_separators() -> Vec { vec![' ', ',', '.', '\r', '\n'] } fn default_toggle_interval() -> u32 { 230 } fn default_backspace_limit() -> i32 { 3 } fn default_exclude_default_matches() -> bool {false} @@ -87,6 +88,9 @@ pub struct Configs { #[serde(default = "default_config_caching_interval")] pub config_caching_interval: i32, + #[serde(default = "default_word_separators")] + pub word_separators: Vec, // TODO: add parsing test + #[serde(default)] pub toggle_key: KeyModifier, diff --git a/src/engine.rs b/src/engine.rs index d848b27..72ad643 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -103,16 +103,22 @@ lazy_static! { impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager> MatchReceiver for Engine<'a, S, C, M, U>{ - fn on_match(&self, m: &Match) { + fn on_match(&self, m: &Match, trailing_separator: Option) { let config = self.config_manager.active_config(); if config.disabled { return; } - self.keyboard_manager.delete_string(m.trigger.chars().count() as i32); + let char_count = if trailing_separator.is_none() { + m.trigger.chars().count() as i32 + }else{ + m.trigger.chars().count() as i32 + 1 // Count also the separator + }; - let target_string = if m._has_vars { + self.keyboard_manager.delete_string(char_count); + + let mut target_string = if m._has_vars { let mut output_map = HashMap::new(); for variable in m.vars.iter() { @@ -142,6 +148,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa m.replace.clone() }; + // If a trailing separator was counted in the match, add it back to the target string + if let Some(trailing_separator) = trailing_separator { + target_string.push(trailing_separator); + } + match config.backend { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 59b9ca7..a806edc 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -30,6 +30,7 @@ pub struct Match { pub trigger: String, pub replace: String, pub vars: Vec, + pub word: bool, #[serde(skip_serializing)] pub _has_vars: bool, @@ -57,6 +58,7 @@ impl<'a> From<&'a AutoMatch> for Match{ trigger: other.trigger.clone(), replace: other.replace.clone(), vars: other.vars.clone(), + word: other.word.clone(), _has_vars: has_vars, } } @@ -70,9 +72,13 @@ struct AutoMatch { #[serde(default = "default_vars")] pub vars: Vec, + + #[serde(default = "default_word")] + pub word: bool, } fn default_vars() -> Vec {Vec::new()} +fn default_word() -> bool {false} #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MatchVariable { @@ -85,7 +91,7 @@ pub struct MatchVariable { } pub trait MatchReceiver { - fn on_match(&self, m: &Match); + fn on_match(&self, m: &Match, trailing_separator: Option); fn on_enable_update(&self, status: bool); } diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 3d919db..b45b09c 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -31,12 +31,19 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { current_set_queue: RefCell>>>, toggle_press_time: RefCell, is_enabled: RefCell, + was_previous_char_word_separator: RefCell, } +#[derive(Clone)] struct MatchEntry<'a> { start: 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> { @@ -49,7 +56,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { receiver, current_set_queue, toggle_press_time, - is_enabled: RefCell::new(true) + is_enabled: RefCell::new(true), + was_previous_char_word_separator: RefCell::new(true), } } @@ -75,33 +83,72 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa return; } + // Obtain the configuration for the active application if present, + // otherwise get the default one + let active_config = self.config_manager.active_config(); + + // Check if the current char is a word separator + let is_current_word_separator = active_config.word_separators.contains( + &c.chars().nth(0).unwrap_or_default() + ); + if is_current_word_separator { + + } + + let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); + let mut current_set_queue = self.current_set_queue.borrow_mut(); - let new_matches: Vec = self.config_manager.matches().iter() - .filter(|&x| x.trigger.starts_with(c)) + let new_matches: Vec = active_config.matches.iter() + .filter(|&x| { + if !x.trigger.starts_with(c) { + false + }else{ + if x.word { + *was_previous_word_separator + }else{ + true + } + } + }) .map(|x | MatchEntry{ start: 1, count: x.trigger.chars().count(), - _match: &x + _match: &x, + waiting_for_separator: false }) .collect(); // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. - let combined_matches: Vec = match current_set_queue.back() { + let mut combined_matches: Vec = match current_set_queue.back_mut() { Some(last_matches) => { let mut updated: Vec = last_matches.iter() .filter(|&x| { - let nchar = x._match.trigger.chars().nth(x.start); - if let Some(nchar) = nchar { - c.starts_with(nchar) + if x.waiting_for_separator { + // The match is only waiting for a separator to call the replacement + is_current_word_separator }else{ - false + let nchar = x._match.trigger.chars().nth(x.start); + if let Some(nchar) = nchar { + c.starts_with(nchar) + }else{ + false + } } }) - .map(|x | MatchEntry{ - start: x.start+1, - count: x.count, - _match: &x._match + .map(|x | { + let new_start = if x.waiting_for_separator { + x.start // Avoid incrementing, we are only waiting for a separator + }else{ + 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(); @@ -113,9 +160,24 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let mut found_match = None; - for entry in combined_matches.iter() { - if entry.start == entry.count { - found_match = Some(entry._match); + for entry in combined_matches.iter_mut() { + let is_found_match = if entry.start == entry.count { + if !entry._match.word { + 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; } } @@ -126,12 +188,27 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa current_set_queue.pop_front(); } - if let Some(_match) = found_match { + if let Some(match_entry) = found_match { if let Some(last) = current_set_queue.back_mut() { last.clear(); } - self.receiver.on_match(_match); + + let trailing_separator = if !match_entry.waiting_for_separator { + None + }else{ + let as_char = c.chars().nth(0); + match as_char { + Some(c) => { + Some(c) // Current char is the trailing separator + }, + None => {None}, + } + }; + + self.receiver.on_match(match_entry._match, trailing_separator); } + + *was_previous_word_separator = is_current_word_separator; } fn handle_modifier(&self, m: KeyModifier) { From 2e60042b2bcd42d0461bc9202725972883cf0cb5 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Mon, 21 Oct 2019 23:52:03 +0200 Subject: [PATCH 2/3] Improve newline support on windows for Word matches --- src/engine.rs | 11 +++++++++-- src/matcher/scrolling.rs | 12 +++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/engine.rs b/src/engine.rs index 72ad643..4376c52 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -150,9 +150,16 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa // If a trailing separator was counted in the match, add it back to the target string if let Some(trailing_separator) = trailing_separator { - target_string.push(trailing_separator); + if trailing_separator == '\r' { // If the trailing separator is a carriage return, + target_string.push('\n'); // convert it to new line + }else{ + target_string.push(trailing_separator); + } } + // Convert Windows style newlines into unix styles + target_string = target_string.replace("\r\n", "\n"); + match config.backend { BackendType::Inject => { // Send the expected string. On linux, newlines are managed automatically @@ -162,7 +169,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa self.keyboard_manager.send_string(&target_string); }else{ // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.lines(); + let splits = target_string.split('\n'); for (i, split) in splits.enumerate() { if i > 0 { diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index b45b09c..e3f7a69 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -91,9 +91,6 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let is_current_word_separator = active_config.word_separators.contains( &c.chars().nth(0).unwrap_or_default() ); - if is_current_word_separator { - - } let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut(); @@ -104,6 +101,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa if !x.trigger.starts_with(c) { false }else{ + // If word option is true, a match can only be started if the previous + // char was a word separator if x.word { *was_previous_word_separator }else{ @@ -188,6 +187,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa current_set_queue.pop_front(); } + *was_previous_word_separator = is_current_word_separator; + if let Some(match_entry) = found_match { if let Some(last) = current_set_queue.back_mut() { last.clear(); @@ -196,6 +197,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let trailing_separator = if !match_entry.waiting_for_separator { None }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); match as_char { Some(c) => { @@ -207,8 +211,6 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa self.receiver.on_match(match_entry._match, trailing_separator); } - - *was_previous_word_separator = is_current_word_separator; } fn handle_modifier(&self, m: KeyModifier) { From 34ce0ee9b14c77e94c218916a62143b398067e5e Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Tue, 22 Oct 2019 20:41:33 +0200 Subject: [PATCH 3/3] Refactor matcher to support word separators --- src/matcher/mod.rs | 53 ++++++++++++++++++++ src/matcher/scrolling.rs | 105 ++++++++++++++------------------------- 2 files changed, 89 insertions(+), 69 deletions(-) diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index a806edc..772f732 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -34,6 +34,10 @@ pub struct Match { #[serde(skip_serializing)] pub _has_vars: bool, + + // Automatically calculated from the trigger, used by the matcher to check for correspondences. + #[serde(skip_serializing)] + pub _trigger_sequence: Vec, } impl <'de> serde::Deserialize<'de> for Match { @@ -54,12 +58,23 @@ impl<'a> From<&'a AutoMatch> for Match{ // Check if the match contains variables let has_vars = VAR_REGEX.is_match(&other.replace); + // Calculate the trigger sequence + let mut trigger_sequence = Vec::new(); + let trigger_chars : Vec = 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 { trigger: other.trigger.clone(), replace: other.replace.clone(), vars: other.vars.clone(), word: other.word.clone(), _has_vars: has_vars, + _trigger_sequence: trigger_sequence, } } } @@ -90,6 +105,12 @@ pub struct MatchVariable { pub params: Mapping, } +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub enum TriggerEntry { + Char(char), + WordSeparator +} + pub trait MatchReceiver { fn on_match(&self, m: &Match, trailing_separator: Option); fn on_enable_update(&self, status: bool); @@ -155,4 +176,36 @@ mod tests { 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); + } } \ No newline at end of file diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index e3f7a69..dad77c1 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -17,7 +17,7 @@ * along with espanso. If not, see . */ -use crate::matcher::{Match, MatchReceiver}; +use crate::matcher::{Match, MatchReceiver, TriggerEntry}; use std::cell::RefCell; use crate::event::{KeyModifier, ActionEventReceiver, ActionType}; use crate::config::ConfigManager; @@ -38,12 +38,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { struct MatchEntry<'a> { start: usize, count: usize, - _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, + _match: &'a Match } 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); } + + 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> { @@ -98,56 +104,32 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let new_matches: Vec = active_config.matches.iter() .filter(|&x| { - if !x.trigger.starts_with(c) { - false - }else{ - // If word option is true, a match can only be started if the previous - // char was a word separator - if x.word { - *was_previous_word_separator - }else{ - true - } + let mut result = Self::is_matching(x, c, 0, is_current_word_separator); + + if x.word { + result = result && *was_previous_word_separator } + + result }) .map(|x | MatchEntry{ start: 1, - count: x.trigger.chars().count(), - _match: &x, - waiting_for_separator: false + count: x._trigger_sequence.len(), + _match: &x }) .collect(); // TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup. - let mut combined_matches: Vec = match current_set_queue.back_mut() { + let combined_matches: Vec = match current_set_queue.back_mut() { Some(last_matches) => { let mut updated: Vec = last_matches.iter() .filter(|&x| { - if x.waiting_for_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 - } - } + Self::is_matching(x._match, c, x.start, is_current_word_separator) }) - .map(|x | { - let new_start = if x.waiting_for_separator { - x.start // Avoid incrementing, we are only waiting for a separator - }else{ - 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 - } + .map(|x | MatchEntry{ + start: x.start+1, + count: x.count, + _match: &x._match }) .collect(); @@ -159,24 +141,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa let mut found_match = None; - for entry in combined_matches.iter_mut() { - let is_found_match = if entry.start == entry.count { - if !entry._match.word { - 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()); + for entry in combined_matches.iter() { + if entry.start == entry.count { + found_match = Some(entry._match); break; } } @@ -189,17 +156,14 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa *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() { last.clear(); } - let trailing_separator = if !match_entry.waiting_for_separator { + let trailing_separator = if !is_current_word_separator { None }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); match as_char { 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); } }