Merge branch 'case-propagation' into dev

This commit is contained in:
Federico Terzi 2020-03-02 23:51:45 +01:00
commit 9183e1f1b9
6 changed files with 387 additions and 93 deletions

View File

@ -253,10 +253,10 @@ impl Configs {
let mut merged_matches = new_config.matches; let mut merged_matches = new_config.matches;
let mut match_trigger_set = HashSet::new(); let mut match_trigger_set = HashSet::new();
merged_matches.iter().for_each(|m| { merged_matches.iter().for_each(|m| {
match_trigger_set.insert(m.trigger.clone()); match_trigger_set.extend(m.triggers.clone());
}); });
let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| { let parent_matches : Vec<Match> = self.matches.iter().filter(|&m| {
!match_trigger_set.contains(&m.trigger) !m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
}).cloned().collect(); }).cloned().collect();
merged_matches.extend(parent_matches); merged_matches.extend(parent_matches);
@ -280,10 +280,10 @@ impl Configs {
// Merge matches // Merge matches
let mut match_trigger_set = HashSet::new(); let mut match_trigger_set = HashSet::new();
self.matches.iter().for_each(|m| { self.matches.iter().for_each(|m| {
match_trigger_set.insert(m.trigger.clone()); match_trigger_set.extend(m.triggers.clone());
}); });
let default_matches : Vec<Match> = default.matches.iter().filter(|&m| { let default_matches : Vec<Match> = default.matches.iter().filter(|&m| {
!match_trigger_set.contains(&m.trigger) !m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
}).cloned().collect(); }).cloned().collect();
self.matches.extend(default_matches); self.matches.extend(default_matches);
@ -465,16 +465,16 @@ impl ConfigSet {
} }
fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool { fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool {
let mut sorted_triggers : Vec<String> = default.matches.iter().map(|t| { let mut sorted_triggers : Vec<String> = default.matches.iter().flat_map(|t| {
t.trigger.clone() t.triggers.clone()
}).collect(); }).collect();
sorted_triggers.sort(); sorted_triggers.sort();
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers); let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
for s in specific.iter() { for s in specific.iter() {
let mut specific_triggers : Vec<String> = s.matches.iter().map(|t| { let mut specific_triggers : Vec<String> = s.matches.iter().flat_map(|t| {
t.trigger.clone() t.triggers.clone()
}).collect(); }).collect();
specific_triggers.sort(); specific_triggers.sort();
has_conflicts |= Self::list_has_conflicts(&specific_triggers); has_conflicts |= Self::list_has_conflicts(&specific_triggers);
@ -831,9 +831,9 @@ mod tests {
assert_eq!(config_set.default.matches.len(), 2); assert_eq!(config_set.default.matches.len(), 2);
assert_eq!(config_set.specific[0].matches.len(), 3); assert_eq!(config_set.specific[0].matches.len(), 3);
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == "hello").is_some()); assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == "hello").is_some());
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":lol").is_some()); assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":lol").is_some());
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some()); assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":yess").is_some());
} }
#[test] #[test]
@ -860,12 +860,12 @@ mod tests {
assert!(config_set.specific[0].matches.iter().find(|x| { assert!(config_set.specific[0].matches.iter().find(|x| {
if let MatchContentType::Text(content) = &x.content { if let MatchContentType::Text(content) = &x.content {
x.trigger == ":lol" && content.replace == "newstring" x.triggers[0] == ":lol" && content.replace == "newstring"
}else{ }else{
false false
} }
}).is_some()); }).is_some());
assert!(config_set.specific[0].matches.iter().find(|x| x.trigger == ":yess").is_some()); assert!(config_set.specific[0].matches.iter().find(|x| x.triggers[0] == ":yess").is_some());
} }
#[test] #[test]
@ -894,7 +894,7 @@ mod tests {
assert!(config_set.specific[0].matches.iter().find(|x| { assert!(config_set.specific[0].matches.iter().find(|x| {
if let MatchContentType::Text(content) = &x.content { if let MatchContentType::Text(content) = &x.content {
x.trigger == "hello" && content.replace == "newstring" x.triggers[0] == "hello" && content.replace == "newstring"
}else{ }else{
false false
} }
@ -962,8 +962,8 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 0); assert_eq!(config_set.specific.len(), 0);
assert_eq!(config_set.default.matches.len(), 2); assert_eq!(config_set.default.matches.len(), 2);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
} }
#[test] #[test]
@ -983,9 +983,9 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 1); assert_eq!(config_set.specific.len(), 1);
assert_eq!(config_set.default.matches.len(), 1); assert_eq!(config_set.default.matches.len(), 1);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(!config_set.default.matches.iter().any(|m| m.trigger == "hello")); assert!(!config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "hello")); assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "hello"));
} }
#[test] #[test]
@ -1016,9 +1016,9 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 0); assert_eq!(config_set.specific.len(), 0);
assert_eq!(config_set.default.matches.len(), 3); assert_eq!(config_set.default.matches.len(), 3);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hello")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
assert!(config_set.default.matches.iter().any(|m| m.trigger == "super")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "super"));
} }
#[test] #[test]
@ -1042,7 +1042,7 @@ mod tests {
assert_eq!(config_set.default.matches.len(), 1); assert_eq!(config_set.default.matches.len(), 1);
assert!(config_set.default.matches.iter().any(|m| { assert!(config_set.default.matches.iter().any(|m| {
if let MatchContentType::Text(content) = &m.content { if let MatchContentType::Text(content) = &m.content {
m.trigger == "hasta" && content.replace == "world" m.triggers[0] == "hasta" && content.replace == "world"
}else{ }else{
false false
} }
@ -1068,8 +1068,8 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 0); assert_eq!(config_set.specific.len(), 0);
assert_eq!(config_set.default.matches.len(), 2); assert_eq!(config_set.default.matches.len(), 2);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(config_set.default.matches.iter().any(|m| m.trigger == "harry")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "harry"));
} }
#[test] #[test]
@ -1089,8 +1089,8 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 1); assert_eq!(config_set.specific.len(), 1);
assert_eq!(config_set.default.matches.len(), 1); assert_eq!(config_set.default.matches.len(), 1);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry")); assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
} }
#[test] #[test]
@ -1120,9 +1120,9 @@ mod tests {
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
assert_eq!(config_set.specific.len(), 1); assert_eq!(config_set.specific.len(), 1);
assert_eq!(config_set.default.matches.len(), 1); assert_eq!(config_set.default.matches.len(), 1);
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta")); assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry")); assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "ron")); assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "ron"));
} }
#[test] #[test]

View File

@ -22,7 +22,7 @@ use crate::keyboard::KeyboardManager;
use crate::config::ConfigManager; use crate::config::ConfigManager;
use crate::config::BackendType; use crate::config::BackendType;
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use log::{info, warn, error}; use log::{info, warn, debug, error};
use crate::ui::{UIManager, MenuItem, MenuItemType}; use crate::ui::{UIManager, MenuItem, MenuItemType};
use crate::event::{ActionEventReceiver, ActionType}; use crate::event::{ActionEventReceiver, ActionType};
use crate::extension::Extension; use crate::extension::Extension;
@ -132,7 +132,7 @@ lazy_static! {
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer> impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
MatchReceiver for Engine<'a, S, C, M, U, R>{ MatchReceiver for Engine<'a, S, C, M, U, R>{
fn on_match(&self, m: &Match, trailing_separator: Option<char>) { fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) {
let config = self.config_manager.active_config(); let config = self.config_manager.active_config();
if !config.enable_active { if !config.enable_active {
@ -141,20 +141,21 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
// avoid espanso reinterpreting its own actions // avoid espanso reinterpreting its own actions
if self.check_last_action_and_set(self.action_noop_interval) { if self.check_last_action_and_set(self.action_noop_interval) {
debug!("Last action was too near, nooping the action.");
return; return;
} }
let char_count = if trailing_separator.is_none() { let char_count = if trailing_separator.is_none() {
m.trigger.chars().count() as i32 m.triggers[trigger_offset].chars().count() as i32
}else{ }else{
m.trigger.chars().count() as i32 + 1 // Count also the separator m.triggers[trigger_offset].chars().count() as i32 + 1 // Count also the separator
}; };
self.keyboard_manager.delete_string(char_count); self.keyboard_manager.delete_string(char_count);
let mut previous_clipboard_content : Option<String> = None; let mut previous_clipboard_content : Option<String> = None;
let rendered = self.renderer.render_match(m, config, vec![]); let rendered = self.renderer.render_match(m, trigger_offset, config, vec![]);
match rendered { match rendered {
RenderResult::Text(mut target_string) => { RenderResult::Text(mut target_string) => {
@ -233,7 +234,7 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
self.keyboard_manager.trigger_paste(&config.paste_shortcut); self.keyboard_manager.trigger_paste(&config.paste_shortcut);
}, },
RenderResult::Error => { RenderResult::Error => {
error!("Could not render match: {}", m.trigger); error!("Could not render match: {}", m.triggers[trigger_offset]);
}, },
} }

View File

@ -29,14 +29,15 @@ pub(crate) mod scrolling;
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
pub struct Match { pub struct Match {
pub trigger: String, pub triggers: Vec<String>,
pub content: MatchContentType, pub content: MatchContentType,
pub word: bool, pub word: bool,
pub passive_only: bool, pub passive_only: bool,
pub propagate_case: bool,
// Automatically calculated from the trigger, used by the matcher to check for correspondences. // Automatically calculated from the triggers, used by the matcher to check for correspondences.
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub _trigger_sequence: Vec<TriggerEntry>, pub _trigger_sequences: Vec<Vec<TriggerEntry>>,
} }
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
@ -74,18 +75,49 @@ impl<'a> From<&'a AutoMatch> for Match{
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap(); static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)\\s*\\}\\}").unwrap();
}; };
// TODO: may need to replace windows newline (\r\n) with newline only (\n) let mut triggers = if !other.triggers.is_empty() {
other.triggers.clone()
}else if !other.trigger.is_empty() {
vec!(other.trigger.clone())
}else{
panic!("Match does not have any trigger defined: {:?}", other)
};
// Calculate the trigger sequence // If propagate_case is true, we need to generate all the possible triggers
let mut trigger_sequence = Vec::new(); // For example, specifying "hello" as a trigger, we need to have:
let trigger_chars : Vec<char> = other.trigger.chars().collect(); // "hello", "Hello", "HELLO"
trigger_sequence.extend(trigger_chars.into_iter().map(|c| { if other.propagate_case {
TriggerEntry::Char(c) // List with first letter capitalized
})); let first_capitalized : Vec<String> = triggers.iter().map(|trigger| {
if other.word { // If it's a word match, end with a word separator let mut capitalized = trigger.clone();
trigger_sequence.push(TriggerEntry::WordSeparator); let mut v: Vec<char> = capitalized.chars().collect();
v[0] = v[0].to_uppercase().nth(0).unwrap();
v.into_iter().collect()
}).collect();
let all_capitalized : Vec<String> = triggers.iter().map(|trigger| {
trigger.to_uppercase()
}).collect();
triggers.extend(first_capitalized);
triggers.extend(all_capitalized);
} }
let trigger_sequences = triggers.iter().map(|trigger| {
// Calculate the trigger sequence
let mut trigger_sequence = Vec::new();
let trigger_chars : Vec<char> = 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);
}
trigger_sequence
}).collect();
let content = if let Some(replace) = &other.replace { // Text match let content = if let Some(replace) = &other.replace { // Text match
let new_replace = replace.clone(); let new_replace = replace.clone();
@ -132,11 +164,12 @@ impl<'a> From<&'a AutoMatch> for Match{
}; };
Self { Self {
trigger: other.trigger.clone(), triggers,
content, content,
word: other.word, word: other.word,
passive_only: other.passive_only, passive_only: other.passive_only,
_trigger_sequence: trigger_sequence, _trigger_sequences: trigger_sequences,
propagate_case: other.propagate_case,
} }
} }
} }
@ -144,8 +177,12 @@ impl<'a> From<&'a AutoMatch> for Match{
/// Used to deserialize the Match struct before applying some custom elaboration. /// Used to deserialize the Match struct before applying some custom elaboration.
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
struct AutoMatch { struct AutoMatch {
#[serde(default = "default_trigger")]
pub trigger: String, pub trigger: String,
#[serde(default = "default_triggers")]
pub triggers: Vec<String>,
#[serde(default = "default_replace")] #[serde(default = "default_replace")]
pub replace: Option<String>, pub replace: Option<String>,
@ -160,13 +197,19 @@ struct AutoMatch {
#[serde(default = "default_passive_only")] #[serde(default = "default_passive_only")]
pub passive_only: bool, pub passive_only: bool,
#[serde(default = "default_propagate_case")]
pub propagate_case: bool,
} }
fn default_trigger() -> String {"".to_owned()}
fn default_triggers() -> Vec<String> {Vec::new()}
fn default_vars() -> Vec<MatchVariable> {Vec::new()} fn default_vars() -> Vec<MatchVariable> {Vec::new()}
fn default_word() -> bool {false} fn default_word() -> bool {false}
fn default_passive_only() -> bool {false} fn default_passive_only() -> bool {false}
fn default_replace() -> Option<String> {None} fn default_replace() -> Option<String> {None}
fn default_image_path() -> Option<String> {None} fn default_image_path() -> Option<String> {None}
fn default_propagate_case() -> bool {false}
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct MatchVariable { pub struct MatchVariable {
@ -185,7 +228,7 @@ pub enum TriggerEntry {
} }
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>, trigger_offset: usize);
fn on_enable_update(&self, status: bool); fn on_enable_update(&self, status: bool);
fn on_passive(&self); fn on_passive(&self);
} }
@ -281,10 +324,10 @@ mod tests {
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e')); assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s')); assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
} }
#[test] #[test]
@ -297,11 +340,11 @@ mod tests {
let _match : Match = serde_yaml::from_str(match_str).unwrap(); let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match._trigger_sequence[0], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[1], TriggerEntry::Char('e')); assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequence[2], TriggerEntry::Char('s')); assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequence[3], TriggerEntry::Char('t')); assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequence[4], TriggerEntry::WordSeparator); assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
} }
#[test] #[test]
@ -322,4 +365,108 @@ mod tests {
}, },
} }
} }
#[test]
fn test_match_trigger_populates_triggers_vector() {
let match_str = r###"
trigger: ":test"
replace: "This is a test"
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test"])
}
#[test]
fn test_match_triggers_are_correctly_parsed() {
let match_str = r###"
triggers:
- ":test1"
- :test2
replace: "This is a test"
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test1", ":test2"])
}
#[test]
fn test_match_triggers_are_correctly_parsed_square_brackets() {
let match_str = r###"
triggers: [":test1", ":test2"]
replace: "This is a test"
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec![":test1", ":test2"])
}
#[test]
fn test_match_propagate_case() {
let match_str = r###"
trigger: "hello"
replace: "This is a test"
propagate_case: true
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec!["hello", "Hello", "HELLO"])
}
#[test]
fn test_match_propagate_case_multi_trigger() {
let match_str = r###"
triggers: ["hello", "hi"]
replace: "This is a test"
propagate_case: true
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match.triggers, vec!["hello", "hi", "Hello", "Hi", "HELLO", "HI"])
}
#[test]
fn test_match_trigger_sequence_with_word_propagate_case() {
let match_str = r###"
trigger: "test"
replace: "This is a test"
word: true
propagate_case: true
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
assert_eq!(_match._trigger_sequences[0][0], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[0][1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequences[0][2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequences[0][3], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[0][4], TriggerEntry::WordSeparator);
assert_eq!(_match._trigger_sequences[1][0], TriggerEntry::Char('T'));
assert_eq!(_match._trigger_sequences[1][1], TriggerEntry::Char('e'));
assert_eq!(_match._trigger_sequences[1][2], TriggerEntry::Char('s'));
assert_eq!(_match._trigger_sequences[1][3], TriggerEntry::Char('t'));
assert_eq!(_match._trigger_sequences[1][4], TriggerEntry::WordSeparator);
assert_eq!(_match._trigger_sequences[2][0], TriggerEntry::Char('T'));
assert_eq!(_match._trigger_sequences[2][1], TriggerEntry::Char('E'));
assert_eq!(_match._trigger_sequences[2][2], TriggerEntry::Char('S'));
assert_eq!(_match._trigger_sequences[2][3], TriggerEntry::Char('T'));
assert_eq!(_match._trigger_sequences[2][4], TriggerEntry::WordSeparator);
}
#[test]
fn test_match_empty_replace_doesnt_crash() {
let match_str = r###"
trigger: "hello"
replace: ""
"###;
let _match : Match = serde_yaml::from_str(match_str).unwrap();
}
} }

View File

@ -39,6 +39,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
struct MatchEntry<'a> { struct MatchEntry<'a> {
start: usize, start: usize,
count: usize, count: usize,
trigger_offset: usize, // The index of the trigger in the Match that matched
_match: &'a Match _match: &'a Match
} }
@ -73,8 +74,8 @@ 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 { fn is_matching(mtc: &Match, current_char: &str, start: usize, trigger_offset: usize, is_current_word_separator: bool) -> bool {
match mtc._trigger_sequence[start] { match mtc._trigger_sequences[trigger_offset][start] {
TriggerEntry::Char(c) => { TriggerEntry::Char(c) => {
current_char.starts_with(c) current_char.starts_with(c)
}, },
@ -112,38 +113,43 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
let mut current_set_queue = self.current_set_queue.borrow_mut(); let mut current_set_queue = self.current_set_queue.borrow_mut();
let new_matches: Vec<MatchEntry> = active_config.matches.iter() let mut new_matches: Vec<MatchEntry> = Vec::new();
.filter(|&x| {
// only active-enabled matches are considered
if x.passive_only {
return false;
}
let mut result = Self::is_matching(x, c, 0, is_current_word_separator); for m in active_config.matches.iter() {
// only active-enabled matches are considered
if m.passive_only {
continue
}
if x.word { for trigger_offset in 0..m._trigger_sequences.len() {
let mut result = Self::is_matching(m, c, 0, trigger_offset, is_current_word_separator);
if m.word {
result = result && *was_previous_word_separator result = result && *was_previous_word_separator
} }
result if result {
}) new_matches.push(MatchEntry{
.map(|x | MatchEntry{ start: 1,
start: 1, count: m._trigger_sequences[trigger_offset].len(),
count: x._trigger_sequence.len(), trigger_offset,
_match: &x _match: &m
}) });
.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 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| {
Self::is_matching(x._match, c, x.start, is_current_word_separator) Self::is_matching(x._match, c, x.start, x.trigger_offset, is_current_word_separator)
}) })
.map(|x | MatchEntry{ .map(|x | MatchEntry{
start: x.start+1, start: x.start+1,
count: x.count, count: x.count,
trigger_offset: x.trigger_offset,
_match: &x._match _match: &x._match
}) })
.collect(); .collect();
@ -154,11 +160,11 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
None => {new_matches}, None => {new_matches},
}; };
let mut found_match = None; let mut found_entry = None;
for entry in combined_matches.iter() { for entry in combined_matches.iter() {
if entry.start == entry.count { if entry.start == entry.count {
found_match = Some(entry._match); found_entry = Some(entry.clone());
break; break;
} }
} }
@ -171,7 +177,9 @@ 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(mtc) = found_match { if let Some(entry) = found_entry {
let mtc = entry._match;
if let Some(last) = current_set_queue.back_mut() { if let Some(last) = current_set_queue.back_mut() {
last.clear(); last.clear();
} }
@ -194,7 +202,7 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
// Force espanso to consider the last char as a separator // Force espanso to consider the last char as a separator
*was_previous_word_separator = true; *was_previous_word_separator = true;
self.receiver.on_match(mtc, trailing_separator); self.receiver.on_match(mtc, trailing_separator, entry.trigger_offset);
} }
} }

View File

@ -59,15 +59,18 @@ impl DefaultRenderer {
} }
} }
fn find_match(config: &Configs, trigger: &str) -> Option<Match> { fn find_match(config: &Configs, trigger: &str) -> Option<(Match, usize)> {
let mut result = None; let mut result = None;
// TODO: if performances become a problem, implement a more efficient lookup // TODO: if performances become a problem, implement a more efficient lookup
for m in config.matches.iter() { for m in config.matches.iter() {
if m.trigger == trigger { for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
result = Some(m.clone()); if m_trigger == trigger {
break; result = Some((m.clone(), trigger_offset));
break;
}
} }
} }
result result
@ -75,7 +78,7 @@ impl DefaultRenderer {
} }
impl super::Renderer for DefaultRenderer { impl super::Renderer for DefaultRenderer {
fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult { fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult {
// Manage the different types of matches // Manage the different types of matches
match &m.content { match &m.content {
// Text Match // Text Match
@ -104,11 +107,11 @@ impl super::Renderer for DefaultRenderer {
continue continue
} }
let inner_match = inner_match.unwrap(); let (inner_match, trigger_offset) = inner_match.unwrap();
// Render the inner match // Render the inner match
// TODO: inner arguments // TODO: inner arguments
let result = self.render_match(&inner_match, config, vec![]); let result = self.render_match(&inner_match, trigger_offset, config, vec![]);
// Inner matches are only supported for text-expansions, warn the user otherwise // Inner matches are only supported for text-expansions, warn the user otherwise
match result { match result {
@ -155,6 +158,47 @@ impl super::Renderer for DefaultRenderer {
// Render any argument that may be present // Render any argument that may be present
let target_string = utils::render_args(&target_string, &args); let target_string = utils::render_args(&target_string, &args);
// Handle case propagation
let target_string = if m.propagate_case {
let trigger = &m.triggers[trigger_offset];
let first_char = trigger.chars().nth(0);
let second_char = trigger.chars().nth(1);
let mode: i32 = if let Some(first_char) = first_char {
if first_char.is_uppercase() {
if let Some(second_char) = second_char {
if second_char.is_uppercase() {
2 // Full CAPITALIZATION
}else{
1 // Only first letter capitalized: Capitalization
}
}else{
2 // Single char, defaults to full CAPITALIZATION
}
}else{
0 // Lowercase, no action
}
}else{
0
};
match mode {
1 => {
// Capitalize the first letter
let mut v: Vec<char> = target_string.chars().collect();
v[0] = v[0].to_uppercase().nth(0).unwrap();
v.into_iter().collect()
},
2 => { // Full capitalization
target_string.to_uppercase()
},
_ => { // Noop
target_string
}
}
}else{
target_string
};
RenderResult::Text(target_string) RenderResult::Text(target_string)
}, },
@ -202,9 +246,9 @@ impl super::Renderer for DefaultRenderer {
config.passive_arg_delimiter, config.passive_arg_delimiter,
config.passive_arg_escape); config.passive_arg_escape);
let m = m.unwrap(); let (m, trigger_offset) = m.unwrap();
// Render the actual match // Render the actual match
let result = self.render_match(&m, &config, args); let result = self.render_match(&m, trigger_offset, &config, args);
match result { match result {
RenderResult::Text(out) => { RenderResult::Text(out) => {
@ -521,4 +565,98 @@ mod tests {
verify_render(rendered, "this is my {{unknown}}"); verify_render(rendered, "this is my {{unknown}}");
} }
#[test]
fn test_render_passive_simple_match_multi_trigger_no_args() {
let text = "this is a :yolo and :test";
let config = get_config_for(r###"
matches:
- triggers: [':test', ':yolo']
replace: result
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "this is a result and result");
}
#[test]
fn test_render_passive_simple_match_multi_trigger_with_args() {
let text = ":yolo/Jon/";
let config = get_config_for(r###"
matches:
- triggers: [':greet', ':yolo']
replace: "Hi $0$"
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "Hi Jon");
}
#[test]
fn test_render_match_case_propagation_no_case() {
let config = get_config_for(r###"
matches:
- trigger: 'test'
replace: result
propagate_case: true
"###);
let renderer = get_renderer(config.clone());
let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "test").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
verify_render(rendered, "result");
}
#[test]
fn test_render_match_case_propagation_first_capital() {
let config = get_config_for(r###"
matches:
- trigger: 'test'
replace: result
propagate_case: true
"###);
let renderer = get_renderer(config.clone());
let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "Test").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
verify_render(rendered, "Result");
}
#[test]
fn test_render_match_case_propagation_all_capital() {
let config = get_config_for(r###"
matches:
- trigger: 'test'
replace: result
propagate_case: true
"###);
let renderer = get_renderer(config.clone());
let m = config.matches[0].clone();
let trigger_offset = m.triggers.iter().position(|x| x== "TEST").unwrap();
let rendered = renderer.render_match(&m, trigger_offset, &config, vec![]);
verify_render(rendered, "RESULT");
}
} }

View File

@ -26,7 +26,7 @@ pub(crate) mod utils;
pub trait Renderer { pub trait Renderer {
// Render a match output // Render a match output
fn render_match(&self, m: &Match, config: &Configs, args: Vec<String>) -> RenderResult; fn render_match(&self, m: &Match, trigger_offset: usize, config: &Configs, args: Vec<String>) -> RenderResult;
// Render a passive expansion text // Render a passive expansion text
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult; fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;