Add argument rendering in passive matches

This commit is contained in:
Federico Terzi 2020-01-18 22:55:50 +01:00
parent 0eb58704a9
commit 6378aa3bcc
9 changed files with 255 additions and 33 deletions

View File

@ -55,7 +55,8 @@ fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8
fn default_toggle_interval() -> u32 { 230 } fn default_toggle_interval() -> u32 { 230 }
fn default_preserve_clipboard() -> bool {false} fn default_preserve_clipboard() -> bool {false}
fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() } fn default_passive_match_regex() -> String{ "(?P<name>:\\p{L}+)(/(?P<args>.*)/)?".to_owned() }
fn default_passive_arg_regex() -> String{ "((\\\\/)|[^/])+".to_owned() } fn default_passive_arg_delimiter() -> char { '/' }
fn default_passive_arg_escape() -> char { '\\' }
fn default_backspace_limit() -> i32 { 3 } fn default_backspace_limit() -> i32 { 3 }
fn default_exclude_default_matches() -> bool {false} fn default_exclude_default_matches() -> bool {false}
fn default_matches() -> Vec<Match> { Vec::new() } fn default_matches() -> Vec<Match> { Vec::new() }
@ -107,8 +108,11 @@ pub struct Configs {
#[serde(default = "default_passive_match_regex")] #[serde(default = "default_passive_match_regex")]
pub passive_match_regex: String, pub passive_match_regex: String,
#[serde(default = "default_passive_arg_regex")] #[serde(default = "default_passive_arg_delimiter")]
pub passive_arg_regex: String, pub passive_arg_delimiter: char,
#[serde(default = "default_passive_arg_escape")]
pub passive_arg_escape: char,
#[serde(default)] #[serde(default)]
pub paste_shortcut: PasteShortcut, pub paste_shortcut: PasteShortcut,
@ -159,7 +163,8 @@ impl Configs {
validate_field!(result, self.use_system_agent, default_use_system_agent()); validate_field!(result, self.use_system_agent, default_use_system_agent());
validate_field!(result, self.preserve_clipboard, default_preserve_clipboard()); validate_field!(result, self.preserve_clipboard, default_preserve_clipboard());
validate_field!(result, self.passive_match_regex, default_passive_match_regex()); validate_field!(result, self.passive_match_regex, default_passive_match_regex());
validate_field!(result, self.passive_arg_regex, default_passive_arg_regex()); validate_field!(result, self.passive_arg_delimiter, default_passive_arg_delimiter());
validate_field!(result, self.passive_arg_escape, default_passive_arg_escape());
result result
} }

View File

@ -33,7 +33,7 @@ impl super::Extension for DateExtension {
String::from("date") String::from("date")
} }
fn calculate(&self, params: &Mapping) -> Option<String> { fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> {
let now: DateTime<Local> = Local::now(); let now: DateTime<Local> = Local::now();
let format = params.get(&Value::from("format")); let format = params.get(&Value::from("format"));

View File

@ -26,7 +26,7 @@ mod random;
pub trait Extension { pub trait Extension {
fn name(&self) -> String; fn name(&self) -> String;
fn calculate(&self, params: &Mapping) -> Option<String>; fn calculate(&self, params: &Mapping, args: &Vec<String>) -> Option<String>;
} }
pub fn get_extensions() -> Vec<Box<dyn Extension>> { pub fn get_extensions() -> Vec<Box<dyn Extension>> {

View File

@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
String::from("random") String::from("random")
} }
fn calculate(&self, params: &Mapping) -> Option<String> { fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> { // TODO: add argument handling
let choices = params.get(&Value::from("choices")); let choices = params.get(&Value::from("choices"));
if choices.is_none() { if choices.is_none() {
warn!("No 'choices' parameter specified for random variable"); warn!("No 'choices' parameter specified for random variable");
@ -82,7 +82,7 @@ mod tests {
params.insert(Value::from("choices"), Value::from(choices.clone())); params.insert(Value::from("choices"), Value::from(choices.clone()));
let extension = RandomExtension::new(); let extension = RandomExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
@ -90,4 +90,6 @@ mod tests {
assert!(choices.iter().any(|x| x == &output)); assert!(choices.iter().any(|x| x == &output));
} }
// TODO: add test with arguments
} }

View File

@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension {
String::from("script") String::from("script")
} }
fn calculate(&self, params: &Mapping) -> Option<String> { fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> { // TODO: add argument handling
let args = params.get(&Value::from("args")); let args = params.get(&Value::from("args"));
if args.is_none() { if args.is_none() {
warn!("No 'args' parameter specified for script variable"); warn!("No 'args' parameter specified for script variable");

View File

@ -34,7 +34,7 @@ impl super::Extension for ShellExtension {
String::from("shell") String::from("shell")
} }
fn calculate(&self, params: &Mapping) -> Option<String> { fn calculate(&self, params: &Mapping, _: &Vec<String>) -> Option<String> { // TODO: add argument handling
let cmd = params.get(&Value::from("cmd")); let cmd = params.get(&Value::from("cmd"));
if cmd.is_none() { if cmd.is_none() {
warn!("No 'cmd' parameter specified for shell variable"); warn!("No 'cmd' parameter specified for shell variable");
@ -90,7 +90,7 @@ mod tests {
params.insert(Value::from("cmd"), Value::from("echo hello world")); params.insert(Value::from("cmd"), Value::from("echo hello world"));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
@ -108,7 +108,7 @@ mod tests {
params.insert(Value::from("trim"), Value::from(true)); params.insert(Value::from("trim"), Value::from(true));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
assert_eq!(output.unwrap(), "hello world"); assert_eq!(output.unwrap(), "hello world");
@ -126,7 +126,7 @@ mod tests {
params.insert(Value::from("trim"), Value::from(true)); params.insert(Value::from("trim"), Value::from(true));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
assert_eq!(output.unwrap(), "hello world"); assert_eq!(output.unwrap(), "hello world");
@ -139,7 +139,7 @@ mod tests {
params.insert(Value::from("trim"), Value::from("error")); params.insert(Value::from("trim"), Value::from("error"));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
if cfg!(target_os = "windows") { if cfg!(target_os = "windows") {
@ -157,9 +157,11 @@ mod tests {
params.insert(Value::from("trim"), Value::from(true)); params.insert(Value::from("trim"), Value::from(true));
let extension = ShellExtension::new(); let extension = ShellExtension::new();
let output = extension.calculate(&params); let output = extension.calculate(&params, &vec![]);
assert!(output.is_some()); assert!(output.is_some());
assert_eq!(output.unwrap(), "hello world"); assert_eq!(output.unwrap(), "hello world");
} }
// TODO: add tests with arguments
} }

View File

@ -36,9 +36,6 @@ pub struct DefaultRenderer {
// Regex used to identify matches (and arguments) in passive expansions // Regex used to identify matches (and arguments) in passive expansions
passive_match_regex: Regex, passive_match_regex: Regex,
// Regex used to separate arguments in passive expansions
passive_arg_regex: Regex,
} }
impl DefaultRenderer { impl DefaultRenderer {
@ -54,15 +51,10 @@ impl DefaultRenderer {
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
panic!("Invalid passive match regex"); panic!("Invalid passive match regex");
}); });
let passive_arg_regex = Regex::new(&config.passive_arg_regex)
.unwrap_or_else(|e| {
panic!("Invalid passive arg regex");
});
DefaultRenderer{ DefaultRenderer{
extension_map, extension_map,
passive_match_regex, passive_match_regex,
passive_arg_regex
} }
} }
@ -129,7 +121,7 @@ impl super::Renderer for DefaultRenderer {
// TODO: pass the arguments to the extension // TODO: pass the arguments to the extension
let extension = self.extension_map.get(&variable.var_type); let extension = self.extension_map.get(&variable.var_type);
if let Some(extension) = extension { if let Some(extension) = extension {
let ext_out = extension.calculate(&variable.params); let ext_out = extension.calculate(&variable.params, &args);
if let Some(output) = ext_out { if let Some(output) = ext_out {
output_map.insert(variable.name.clone(), output); output_map.insert(variable.name.clone(), output);
}else{ }else{
@ -142,11 +134,6 @@ impl super::Renderer for DefaultRenderer {
} }
} }
// TODO: replace the arguments
// the idea is that every param placeholder, such as $1$, is replaced with
// an ArgExtension when loading a match, which renders the argument as output
// this is only used as syntactic sugar
// Replace the variables // Replace the variables
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| { let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
let var_name = caps.name("name").unwrap().as_str(); let var_name = caps.name("name").unwrap().as_str();
@ -159,6 +146,9 @@ impl super::Renderer for DefaultRenderer {
content.replace.clone() content.replace.clone()
}; };
// Render any argument that may be present
let target_string = utils::render_args(&target_string, &args);
RenderResult::Text(target_string) RenderResult::Text(target_string)
}, },
@ -202,9 +192,9 @@ impl super::Renderer for DefaultRenderer {
}else{ }else{
"" ""
}; };
let args : Vec<String> = self.passive_arg_regex.split(match_args).into_iter().map( let args : Vec<String> = utils::split_args(match_args,
|arg| arg.to_owned() config.passive_arg_delimiter,
).collect(); config.passive_arg_escape);
let m = m.unwrap(); let m = m.unwrap();
// Render the actual match // Render the actual match
@ -242,7 +232,6 @@ mod tests {
fn verify_render(rendered: RenderResult, target: &str) { fn verify_render(rendered: RenderResult, target: &str) {
match rendered { match rendered {
RenderResult::Text(rendered) => { RenderResult::Text(rendered) => {
println!("{}", rendered);
assert_eq!(rendered, target); assert_eq!(rendered, target);
}, },
_ => { _ => {
@ -326,4 +315,97 @@ mod tests {
verify_render(rendered, result); verify_render(rendered, result);
} }
#[test]
fn test_render_passive_nested_matches_no_args() {
let text = ":greet";
let config = get_config_for(r###"
matches:
- trigger: ':greet'
replace: "hi {{name}}"
vars:
- name: name
type: match
params:
trigger: ":name"
- trigger: ':name'
replace: john
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "hi john");
}
#[test]
fn test_render_passive_simple_match_with_args() {
let text = ":greet/Jon/";
let config = get_config_for(r###"
matches:
- trigger: ':greet'
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_passive_simple_match_with_multiple_args() {
let text = ":greet/Jon/Snow/";
let config = get_config_for(r###"
matches:
- trigger: ':greet'
replace: "Hi $0$, there is $1$ outside"
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "Hi Jon, there is Snow outside");
}
#[test]
fn test_render_passive_simple_match_with_escaped_args() {
let text = ":greet/Jon/10\\/12/";
let config = get_config_for(r###"
matches:
- trigger: ':greet'
replace: "Hi $0$, today is $1$"
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "Hi Jon, today is 10/12");
}
#[test]
fn test_render_passive_simple_match_with_args_not_closed() {
let text = ":greet/Jon/Snow";
let config = get_config_for(r###"
matches:
- trigger: ':greet'
replace: "Hi $0$"
"###);
let renderer = get_renderer(config.clone());
let rendered = renderer.render_passive(text, &config);
verify_render(rendered, "Hi JonSnow");
}
} }

View File

@ -22,6 +22,7 @@ use crate::matcher::{Match};
use crate::config::Configs; use crate::config::Configs;
pub(crate) mod default; pub(crate) mod default;
pub(crate) mod utils;
pub trait Renderer { pub trait Renderer {
// Render a match output // Render a match output

130
src/render/utils.rs Normal file
View File

@ -0,0 +1,130 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2020 Federico Terzi
*
* espanso is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* espanso is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use regex::{Regex, Captures};
lazy_static! {
static ref ARG_REGEX: Regex = Regex::new("\\$(?P<pos>\\d+)\\$").unwrap();
}
pub fn render_args(text: &str, args: &Vec<String>) -> String {
let result = ARG_REGEX.replace_all(text, |caps: &Captures| {
let position_str = caps.name("pos").unwrap().as_str();
let position = position_str.parse::<i32>().unwrap_or(-1);
if position >= 0 && position < args.len() as i32 {
args[position as usize].to_owned()
}else{
"".to_owned()
}
});
result.to_string()
}
pub fn split_args(text: &str, delimiter: char, escape: char) -> Vec<String> {
let mut output = vec![];
// Make sure the text is not empty
if text.is_empty() {
return output
}
let mut last = String::from("");
let mut previous : char = char::from(0);
text.chars().into_iter().for_each(|c| {
if c == delimiter {
if previous != escape {
output.push(last.clone());
last = String::from("");
}else{
last.push(c);
}
}else if c == escape {
if previous == escape {
last.push(c);
}
}else{
last.push(c);
}
previous = c;
});
// Add the last one
output.push(last);
output
}
// TESTS
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_args_no_args() {
let args = vec!("hello".to_owned());
assert_eq!(render_args("no args", &args), "no args")
}
#[test]
fn test_render_args_one_arg() {
let args = vec!("jon".to_owned());
assert_eq!(render_args("hello $0$", &args), "hello jon")
}
#[test]
fn test_render_args_one_multiple_args() {
let args = vec!("jon".to_owned(), "snow".to_owned());
assert_eq!(render_args("hello $0$, the $1$ is white", &args), "hello jon, the snow is white")
}
#[test]
fn test_render_args_out_of_range() {
let args = vec!("jon".to_owned());
assert_eq!(render_args("hello $10$", &args), "hello ")
}
#[test]
fn test_split_args_one_arg() {
assert_eq!(split_args("jon", '/', '\\'), vec!["jon"])
}
#[test]
fn test_split_args_two_args() {
assert_eq!(split_args("jon/snow", '/', '\\'), vec!["jon", "snow"])
}
#[test]
fn test_split_args_escaping() {
assert_eq!(split_args("jon\\/snow", '/', '\\'), vec!["jon/snow"])
}
#[test]
fn test_split_args_escaping_escape() {
assert_eq!(split_args("jon\\\\snow", '/', '\\'), vec!["jon\\snow"])
}
#[test]
fn test_split_args_empty() {
let empty_vec : Vec<String> = vec![];
assert_eq!(split_args("", '/', '\\'), empty_vec)
}
}