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 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]

View File

@ -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]);
},
}

View File

@ -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,11 +75,38 @@ 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)
};
// 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> = other.trigger.chars().collect();
let trigger_chars : Vec<char> = trigger.chars().collect();
trigger_sequence.extend(trigger_chars.into_iter().map(|c| {
TriggerEntry::Char(c)
}));
@ -86,6 +114,10 @@ impl<'a> From<&'a AutoMatch> for Match{
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();
}
}

View File

@ -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| {
let mut new_matches: Vec<MatchEntry> = Vec::new();
for m in active_config.matches.iter() {
// only active-enabled matches are considered
if x.passive_only {
return false;
if m.passive_only {
continue
}
let mut result = Self::is_matching(x, c, 0, is_current_word_separator);
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 x.word {
if m.word {
result = result && *was_previous_word_separator
}
result
})
.map(|x | MatchEntry{
if result {
new_matches.push(MatchEntry{
start: 1,
count: x._trigger_sequence.len(),
_match: &x
})
.collect();
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);
}
}

View File

@ -59,23 +59,26 @@ 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());
for (trigger_offset, m_trigger) in m.triggers.iter().enumerate() {
if m_trigger == trigger {
result = Some((m.clone(), trigger_offset));
break;
}
}
}
result
}
}
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");
}
}

View File

@ -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;