Merge branch 'feature-word-separators' into dev
This commit is contained in:
commit
fc9f34632f
|
@ -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<char> { 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<char>, // TODO: add parsing test
|
||||
|
||||
#[serde(default)]
|
||||
pub toggle_key: KeyModifier,
|
||||
|
||||
|
|
|
@ -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<char>) {
|
||||
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,18 @@ 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 {
|
||||
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
|
||||
|
@ -151,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 {
|
||||
|
|
|
@ -30,9 +30,14 @@ pub struct Match {
|
|||
pub trigger: String,
|
||||
pub replace: String,
|
||||
pub vars: Vec<MatchVariable>,
|
||||
pub word: bool,
|
||||
|
||||
#[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<TriggerEntry>,
|
||||
}
|
||||
|
||||
impl <'de> serde::Deserialize<'de> for Match {
|
||||
|
@ -53,11 +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<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 {
|
||||
trigger: other.trigger.clone(),
|
||||
replace: other.replace.clone(),
|
||||
vars: other.vars.clone(),
|
||||
word: other.word.clone(),
|
||||
_has_vars: has_vars,
|
||||
_trigger_sequence: trigger_sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,9 +87,13 @@ struct AutoMatch {
|
|||
|
||||
#[serde(default = "default_vars")]
|
||||
pub vars: Vec<MatchVariable>,
|
||||
|
||||
#[serde(default = "default_word")]
|
||||
pub word: bool,
|
||||
}
|
||||
|
||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||
fn default_word() -> bool {false}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MatchVariable {
|
||||
|
@ -84,8 +105,14 @@ 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);
|
||||
fn on_match(&self, m: &Match, trailing_separator: Option<char>);
|
||||
fn on_enable_update(&self, status: bool);
|
||||
}
|
||||
|
||||
|
@ -149,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);
|
||||
}
|
||||
}
|
|
@ -17,7 +17,7 @@
|
|||
* 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 crate::event::{KeyModifier, ActionEventReceiver, ActionType};
|
||||
use crate::config::ConfigManager;
|
||||
|
@ -31,8 +31,10 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
|||
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
||||
toggle_press_time: RefCell<SystemTime>,
|
||||
is_enabled: RefCell<bool>,
|
||||
was_previous_char_word_separator: RefCell<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct MatchEntry<'a> {
|
||||
start: usize,
|
||||
count: usize,
|
||||
|
@ -49,7 +51,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),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,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> {
|
||||
|
@ -75,28 +89,42 @@ 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()
|
||||
);
|
||||
|
||||
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<MatchEntry> = self.config_manager.matches().iter()
|
||||
.filter(|&x| x.trigger.starts_with(c))
|
||||
let new_matches: Vec<MatchEntry> = active_config.matches.iter()
|
||||
.filter(|&x| {
|
||||
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(),
|
||||
count: x._trigger_sequence.len(),
|
||||
_match: &x
|
||||
})
|
||||
.collect();
|
||||
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
|
||||
|
||||
let combined_matches: Vec<MatchEntry> = match current_set_queue.back() {
|
||||
let combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
|
||||
Some(last_matches) => {
|
||||
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
||||
.filter(|&x| {
|
||||
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 | MatchEntry{
|
||||
start: x.start+1,
|
||||
|
@ -126,11 +154,29 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
current_set_queue.pop_front();
|
||||
}
|
||||
|
||||
if let Some(_match) = found_match {
|
||||
*was_previous_word_separator = is_current_word_separator;
|
||||
|
||||
if let Some(mtc) = found_match {
|
||||
if let Some(last) = current_set_queue.back_mut() {
|
||||
last.clear();
|
||||
}
|
||||
self.receiver.on_match(_match);
|
||||
|
||||
let trailing_separator = if !is_current_word_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},
|
||||
}
|
||||
};
|
||||
|
||||
// Force espanso to consider the last char as a separator
|
||||
*was_previous_word_separator = true;
|
||||
|
||||
self.receiver.on_match(mtc, trailing_separator);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user