Merge branch 'case-propagation' into dev
This commit is contained in:
commit
9183e1f1b9
|
@ -253,10 +253,10 @@ impl Configs {
|
|||
let mut merged_matches = new_config.matches;
|
||||
let mut match_trigger_set = HashSet::new();
|
||||
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| {
|
||||
!match_trigger_set.contains(&m.trigger)
|
||||
!m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
|
||||
}).cloned().collect();
|
||||
|
||||
merged_matches.extend(parent_matches);
|
||||
|
@ -280,10 +280,10 @@ impl Configs {
|
|||
// Merge matches
|
||||
let mut match_trigger_set = HashSet::new();
|
||||
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| {
|
||||
!match_trigger_set.contains(&m.trigger)
|
||||
!m.triggers.iter().any(|trigger| match_trigger_set.contains(trigger))
|
||||
}).cloned().collect();
|
||||
|
||||
self.matches.extend(default_matches);
|
||||
|
@ -465,16 +465,16 @@ impl ConfigSet {
|
|||
}
|
||||
|
||||
fn has_conflicts(default: &Configs, specific: &Vec<Configs>) -> bool {
|
||||
let mut sorted_triggers : Vec<String> = default.matches.iter().map(|t| {
|
||||
t.trigger.clone()
|
||||
let mut sorted_triggers : Vec<String> = default.matches.iter().flat_map(|t| {
|
||||
t.triggers.clone()
|
||||
}).collect();
|
||||
sorted_triggers.sort();
|
||||
|
||||
let mut has_conflicts = Self::list_has_conflicts(&sorted_triggers);
|
||||
|
||||
for s in specific.iter() {
|
||||
let mut specific_triggers : Vec<String> = s.matches.iter().map(|t| {
|
||||
t.trigger.clone()
|
||||
let mut specific_triggers : Vec<String> = s.matches.iter().flat_map(|t| {
|
||||
t.triggers.clone()
|
||||
}).collect();
|
||||
specific_triggers.sort();
|
||||
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.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.trigger == ":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] == "hello").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.triggers[0] == ":yess").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -860,12 +860,12 @@ mod tests {
|
|||
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||
if let MatchContentType::Text(content) = &x.content {
|
||||
x.trigger == ":lol" && content.replace == "newstring"
|
||||
x.triggers[0] == ":lol" && content.replace == "newstring"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
}).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]
|
||||
|
@ -894,7 +894,7 @@ mod tests {
|
|||
|
||||
assert!(config_set.specific[0].matches.iter().find(|x| {
|
||||
if let MatchContentType::Text(content) = &x.content {
|
||||
x.trigger == "hello" && content.replace == "newstring"
|
||||
x.triggers[0] == "hello" && content.replace == "newstring"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
|
@ -962,8 +962,8 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 0);
|
||||
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.trigger == "hello"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -983,9 +983,9 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.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.trigger == "hello"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "hello"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(!config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1016,9 +1016,9 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 0);
|
||||
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.trigger == "hello"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "super"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hello"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "super"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1042,7 +1042,7 @@ mod tests {
|
|||
assert_eq!(config_set.default.matches.len(), 1);
|
||||
assert!(config_set.default.matches.iter().any(|m| {
|
||||
if let MatchContentType::Text(content) = &m.content {
|
||||
m.trigger == "hasta" && content.replace == "world"
|
||||
m.triggers[0] == "hasta" && content.replace == "world"
|
||||
}else{
|
||||
false
|
||||
}
|
||||
|
@ -1068,8 +1068,8 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 0);
|
||||
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.trigger == "harry"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1089,8 +1089,8 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 1);
|
||||
assert_eq!(config_set.default.matches.len(), 1);
|
||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -1120,9 +1120,9 @@ mod tests {
|
|||
let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap();
|
||||
assert_eq!(config_set.specific.len(), 1);
|
||||
assert_eq!(config_set.default.matches.len(), 1);
|
||||
assert!(config_set.default.matches.iter().any(|m| m.trigger == "hasta"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "harry"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.trigger == "ron"));
|
||||
assert!(config_set.default.matches.iter().any(|m| m.triggers[0] == "hasta"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "harry"));
|
||||
assert!(config_set.specific[0].matches.iter().any(|m| m.triggers[0] == "ron"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
@ -22,7 +22,7 @@ use crate::keyboard::KeyboardManager;
|
|||
use crate::config::ConfigManager;
|
||||
use crate::config::BackendType;
|
||||
use crate::clipboard::ClipboardManager;
|
||||
use log::{info, warn, error};
|
||||
use log::{info, warn, debug, error};
|
||||
use crate::ui::{UIManager, MenuItem, MenuItemType};
|
||||
use crate::event::{ActionEventReceiver, ActionType};
|
||||
use crate::extension::Extension;
|
||||
|
@ -132,7 +132,7 @@ lazy_static! {
|
|||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager, R: Renderer>
|
||||
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();
|
||||
|
||||
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
|
||||
if self.check_last_action_and_set(self.action_noop_interval) {
|
||||
debug!("Last action was too near, nooping the action.");
|
||||
return;
|
||||
}
|
||||
|
||||
let char_count = if trailing_separator.is_none() {
|
||||
m.trigger.chars().count() as i32
|
||||
m.triggers[trigger_offset].chars().count() as i32
|
||||
}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);
|
||||
|
||||
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 {
|
||||
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);
|
||||
},
|
||||
RenderResult::Error => {
|
||||
error!("Could not render match: {}", m.trigger);
|
||||
error!("Could not render match: {}", m.triggers[trigger_offset]);
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -29,14 +29,15 @@ pub(crate) mod scrolling;
|
|||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
pub struct Match {
|
||||
pub trigger: String,
|
||||
pub triggers: Vec<String>,
|
||||
pub content: MatchContentType,
|
||||
pub word: 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)]
|
||||
pub _trigger_sequence: Vec<TriggerEntry>,
|
||||
pub _trigger_sequences: Vec<Vec<TriggerEntry>>,
|
||||
}
|
||||
|
||||
#[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();
|
||||
};
|
||||
|
||||
// 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
|
||||
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);
|
||||
// If propagate_case is true, we need to generate all the possible triggers
|
||||
// For example, specifying "hello" as a trigger, we need to have:
|
||||
// "hello", "Hello", "HELLO"
|
||||
if other.propagate_case {
|
||||
// List with first letter capitalized
|
||||
let first_capitalized : Vec<String> = triggers.iter().map(|trigger| {
|
||||
let mut capitalized = trigger.clone();
|
||||
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 new_replace = replace.clone();
|
||||
|
||||
|
@ -132,11 +164,12 @@ impl<'a> From<&'a AutoMatch> for Match{
|
|||
};
|
||||
|
||||
Self {
|
||||
trigger: other.trigger.clone(),
|
||||
triggers,
|
||||
content,
|
||||
word: other.word,
|
||||
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.
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
struct AutoMatch {
|
||||
#[serde(default = "default_trigger")]
|
||||
pub trigger: String,
|
||||
|
||||
#[serde(default = "default_triggers")]
|
||||
pub triggers: Vec<String>,
|
||||
|
||||
#[serde(default = "default_replace")]
|
||||
pub replace: Option<String>,
|
||||
|
||||
|
@ -160,13 +197,19 @@ struct AutoMatch {
|
|||
|
||||
#[serde(default = "default_passive_only")]
|
||||
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_word() -> bool {false}
|
||||
fn default_passive_only() -> bool {false}
|
||||
fn default_replace() -> Option<String> {None}
|
||||
fn default_image_path() -> Option<String> {None}
|
||||
fn default_propagate_case() -> bool {false}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MatchVariable {
|
||||
|
@ -185,7 +228,7 @@ pub enum TriggerEntry {
|
|||
}
|
||||
|
||||
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_passive(&self);
|
||||
}
|
||||
|
@ -281,10 +324,10 @@ mod tests {
|
|||
|
||||
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_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'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -297,11 +340,11 @@ mod tests {
|
|||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
#[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();
|
||||
}
|
||||
}
|
|
@ -39,6 +39,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
|||
struct MatchEntry<'a> {
|
||||
start: usize,
|
||||
count: usize,
|
||||
trigger_offset: usize, // The index of the trigger in the Match that matched
|
||||
_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);
|
||||
}
|
||||
|
||||
fn is_matching(mtc: &Match, current_char: &str, start: usize, is_current_word_separator: bool) -> bool {
|
||||
match mtc._trigger_sequence[start] {
|
||||
fn is_matching(mtc: &Match, current_char: &str, start: usize, trigger_offset: usize, is_current_word_separator: bool) -> bool {
|
||||
match mtc._trigger_sequences[trigger_offset][start] {
|
||||
TriggerEntry::Char(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 new_matches: Vec<MatchEntry> = active_config.matches.iter()
|
||||
.filter(|&x| {
|
||||
// only active-enabled matches are considered
|
||||
if x.passive_only {
|
||||
return false;
|
||||
}
|
||||
let mut new_matches: Vec<MatchEntry> = Vec::new();
|
||||
|
||||
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
|
||||
})
|
||||
.map(|x | MatchEntry{
|
||||
start: 1,
|
||||
count: x._trigger_sequence.len(),
|
||||
_match: &x
|
||||
})
|
||||
.collect();
|
||||
if result {
|
||||
new_matches.push(MatchEntry{
|
||||
start: 1,
|
||||
count: m._trigger_sequences[trigger_offset].len(),
|
||||
trigger_offset,
|
||||
_match: &m
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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() {
|
||||
Some(last_matches) => {
|
||||
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
||||
.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{
|
||||
start: x.start+1,
|
||||
count: x.count,
|
||||
trigger_offset: x.trigger_offset,
|
||||
_match: &x._match
|
||||
})
|
||||
.collect();
|
||||
|
@ -154,11 +160,11 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
None => {new_matches},
|
||||
};
|
||||
|
||||
let mut found_match = None;
|
||||
let mut found_entry = None;
|
||||
|
||||
for entry in combined_matches.iter() {
|
||||
if entry.start == entry.count {
|
||||
found_match = Some(entry._match);
|
||||
found_entry = Some(entry.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -171,7 +177,9 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
|||
|
||||
*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() {
|
||||
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
|
||||
*was_previous_word_separator = true;
|
||||
|
||||
self.receiver.on_match(mtc, trailing_separator);
|
||||
self.receiver.on_match(mtc, trailing_separator, entry.trigger_offset);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
// TODO: if performances become a problem, implement a more efficient lookup
|
||||
for m in config.matches.iter() {
|
||||
if m.trigger == trigger {
|
||||
result = Some(m.clone());
|
||||
break;
|
||||
for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
|
||||
if m_trigger == trigger {
|
||||
result = Some((m.clone(), trigger_offset));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
result
|
||||
|
@ -75,7 +78,7 @@ impl 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
|
||||
match &m.content {
|
||||
// Text Match
|
||||
|
@ -104,11 +107,11 @@ impl super::Renderer for DefaultRenderer {
|
|||
continue
|
||||
}
|
||||
|
||||
let inner_match = inner_match.unwrap();
|
||||
let (inner_match, trigger_offset) = inner_match.unwrap();
|
||||
|
||||
// Render the inner match
|
||||
// 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
|
||||
match result {
|
||||
|
@ -155,6 +158,47 @@ impl super::Renderer for DefaultRenderer {
|
|||
// Render any argument that may be present
|
||||
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)
|
||||
},
|
||||
|
||||
|
@ -202,9 +246,9 @@ impl super::Renderer for DefaultRenderer {
|
|||
config.passive_arg_delimiter,
|
||||
config.passive_arg_escape);
|
||||
|
||||
let m = m.unwrap();
|
||||
let (m, trigger_offset) = m.unwrap();
|
||||
// Render the actual match
|
||||
let result = self.render_match(&m, &config, args);
|
||||
let result = self.render_match(&m, trigger_offset, &config, args);
|
||||
|
||||
match result {
|
||||
RenderResult::Text(out) => {
|
||||
|
@ -521,4 +565,98 @@ mod tests {
|
|||
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -26,7 +26,7 @@ pub(crate) mod utils;
|
|||
|
||||
pub trait Renderer {
|
||||
// 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
|
||||
fn render_passive(&self, text: &str, config: &Configs) -> RenderResult;
|
||||
|
|
Loading…
Reference in New Issue
Block a user