Add argument rendering in passive matches
This commit is contained in:
parent
0eb58704a9
commit
6378aa3bcc
|
@ -55,7 +55,8 @@ fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n', 22u8
|
|||
fn default_toggle_interval() -> u32 { 230 }
|
||||
fn default_preserve_clipboard() -> bool {false}
|
||||
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_exclude_default_matches() -> bool {false}
|
||||
fn default_matches() -> Vec<Match> { Vec::new() }
|
||||
|
@ -107,8 +108,11 @@ pub struct Configs {
|
|||
#[serde(default = "default_passive_match_regex")]
|
||||
pub passive_match_regex: String,
|
||||
|
||||
#[serde(default = "default_passive_arg_regex")]
|
||||
pub passive_arg_regex: String,
|
||||
#[serde(default = "default_passive_arg_delimiter")]
|
||||
pub passive_arg_delimiter: char,
|
||||
|
||||
#[serde(default = "default_passive_arg_escape")]
|
||||
pub passive_arg_escape: char,
|
||||
|
||||
#[serde(default)]
|
||||
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.preserve_clipboard, default_preserve_clipboard());
|
||||
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
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ impl super::Extension for DateExtension {
|
|||
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 format = params.get(&Value::from("format"));
|
||||
|
|
|
@ -26,7 +26,7 @@ mod random;
|
|||
|
||||
pub trait Extension {
|
||||
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>> {
|
||||
|
|
|
@ -34,7 +34,7 @@ impl super::Extension for RandomExtension {
|
|||
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"));
|
||||
if choices.is_none() {
|
||||
warn!("No 'choices' parameter specified for random variable");
|
||||
|
@ -82,7 +82,7 @@ mod tests {
|
|||
params.insert(Value::from("choices"), Value::from(choices.clone()));
|
||||
|
||||
let extension = RandomExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
|
||||
|
@ -90,4 +90,6 @@ mod tests {
|
|||
|
||||
assert!(choices.iter().any(|x| x == &output));
|
||||
}
|
||||
|
||||
// TODO: add test with arguments
|
||||
}
|
|
@ -34,7 +34,7 @@ impl super::Extension for ScriptExtension {
|
|||
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"));
|
||||
if args.is_none() {
|
||||
warn!("No 'args' parameter specified for script variable");
|
||||
|
|
|
@ -34,7 +34,7 @@ impl super::Extension for ShellExtension {
|
|||
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"));
|
||||
if cmd.is_none() {
|
||||
warn!("No 'cmd' parameter specified for shell variable");
|
||||
|
@ -90,7 +90,7 @@ mod tests {
|
|||
params.insert(Value::from("cmd"), Value::from("echo hello world"));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
|
||||
|
@ -108,7 +108,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello world");
|
||||
|
@ -126,7 +126,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello world");
|
||||
|
@ -139,7 +139,7 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from("error"));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
if cfg!(target_os = "windows") {
|
||||
|
@ -157,9 +157,11 @@ mod tests {
|
|||
params.insert(Value::from("trim"), Value::from(true));
|
||||
|
||||
let extension = ShellExtension::new();
|
||||
let output = extension.calculate(¶ms);
|
||||
let output = extension.calculate(¶ms, &vec![]);
|
||||
|
||||
assert!(output.is_some());
|
||||
assert_eq!(output.unwrap(), "hello world");
|
||||
}
|
||||
|
||||
// TODO: add tests with arguments
|
||||
}
|
|
@ -36,9 +36,6 @@ pub struct DefaultRenderer {
|
|||
|
||||
// Regex used to identify matches (and arguments) in passive expansions
|
||||
passive_match_regex: Regex,
|
||||
|
||||
// Regex used to separate arguments in passive expansions
|
||||
passive_arg_regex: Regex,
|
||||
}
|
||||
|
||||
impl DefaultRenderer {
|
||||
|
@ -54,15 +51,10 @@ impl DefaultRenderer {
|
|||
.unwrap_or_else(|e| {
|
||||
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{
|
||||
extension_map,
|
||||
passive_match_regex,
|
||||
passive_arg_regex
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,7 +121,7 @@ impl super::Renderer for DefaultRenderer {
|
|||
// TODO: pass the arguments to the extension
|
||||
let extension = self.extension_map.get(&variable.var_type);
|
||||
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 {
|
||||
output_map.insert(variable.name.clone(), output);
|
||||
}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
|
||||
let result = VAR_REGEX.replace_all(&content.replace, |caps: &Captures| {
|
||||
let var_name = caps.name("name").unwrap().as_str();
|
||||
|
@ -159,6 +146,9 @@ impl super::Renderer for DefaultRenderer {
|
|||
content.replace.clone()
|
||||
};
|
||||
|
||||
// Render any argument that may be present
|
||||
let target_string = utils::render_args(&target_string, &args);
|
||||
|
||||
RenderResult::Text(target_string)
|
||||
},
|
||||
|
||||
|
@ -202,9 +192,9 @@ impl super::Renderer for DefaultRenderer {
|
|||
}else{
|
||||
""
|
||||
};
|
||||
let args : Vec<String> = self.passive_arg_regex.split(match_args).into_iter().map(
|
||||
|arg| arg.to_owned()
|
||||
).collect();
|
||||
let args : Vec<String> = utils::split_args(match_args,
|
||||
config.passive_arg_delimiter,
|
||||
config.passive_arg_escape);
|
||||
|
||||
let m = m.unwrap();
|
||||
// Render the actual match
|
||||
|
@ -242,7 +232,6 @@ mod tests {
|
|||
fn verify_render(rendered: RenderResult, target: &str) {
|
||||
match rendered {
|
||||
RenderResult::Text(rendered) => {
|
||||
println!("{}", rendered);
|
||||
assert_eq!(rendered, target);
|
||||
},
|
||||
_ => {
|
||||
|
@ -326,4 +315,97 @@ mod tests {
|
|||
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ use crate::matcher::{Match};
|
|||
use crate::config::Configs;
|
||||
|
||||
pub(crate) mod default;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub trait Renderer {
|
||||
// Render a match output
|
||||
|
|
130
src/render/utils.rs
Normal file
130
src/render/utils.rs
Normal 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)
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user