From 0dfcb9cef725613ac4ffec6df05f9981df02c57b Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sun, 26 Jan 2020 23:56:50 +0100 Subject: [PATCH] First draft of global variables. Fix #162 --- src/config/mod.rs | 143 ++++++++++++++++++++++++++++++++++++----- src/extension/dummy.rs | 44 +++++++++++++ src/extension/mod.rs | 2 + src/render/default.rs | 79 ++++++++++++++++++++++- 4 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 src/extension/dummy.rs diff --git a/src/config/mod.rs b/src/config/mod.rs index 8eb2c2e..9dec411 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -21,7 +21,7 @@ extern crate dirs; use std::path::{Path, PathBuf}; use std::{fs}; -use crate::matcher::Match; +use crate::matcher::{Match, MatchVariable}; use std::fs::{File, create_dir_all}; use std::io::Read; use serde::{Serialize, Deserialize}; @@ -64,12 +64,13 @@ fn default_enable_active() -> bool { true } fn default_action_noop_interval() -> u128 { 500 } fn default_backspace_limit() -> i32 { 3 } fn default_restore_clipboard_delay() -> i32 { 300 } -fn default_exclude_default_matches() -> bool {false} +fn default_exclude_default_entries() -> bool {false} fn default_matches() -> Vec { Vec::new() } +fn default_global_vars() -> Vec { Vec::new() } #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Configs { - #[serde(default = "default_name")] +#[serde(default = "default_name")] pub name: String, #[serde(default = "default_parent")] @@ -144,11 +145,15 @@ pub struct Configs { #[serde(default)] pub backend: BackendType, - #[serde(default = "default_exclude_default_matches")] - pub exclude_default_matches: bool, + #[serde(default = "default_exclude_default_entries")] + pub exclude_default_entries: bool, #[serde(default = "default_matches")] - pub matches: Vec + pub matches: Vec, + + #[serde(default = "default_global_vars")] + pub global_vars: Vec + } // Macro used to validate config fields @@ -244,29 +249,56 @@ impl Configs { } fn merge_config(&mut self, new_config: Configs) { + // Merge matches let mut merged_matches = new_config.matches; - let mut trigger_set = HashSet::new(); + let mut match_trigger_set = HashSet::new(); merged_matches.iter().for_each(|m| { - trigger_set.insert(m.trigger.clone()); + match_trigger_set.insert(m.trigger.clone()); }); let parent_matches : Vec = self.matches.iter().filter(|&m| { - !trigger_set.contains(&m.trigger) + !match_trigger_set.contains(&m.trigger) }).cloned().collect(); merged_matches.extend(parent_matches); self.matches = merged_matches; + + // Merge global variables + let mut merged_global_vars = new_config.global_vars; + let mut vars_name_set = HashSet::new(); + merged_global_vars.iter().for_each(|m| { + vars_name_set.insert(m.name.clone()); + }); + let parent_vars : Vec = self.global_vars.iter().filter(|&m| { + !vars_name_set.contains(&m.name) + }).cloned().collect(); + + merged_global_vars.extend(parent_vars); + self.global_vars = merged_global_vars; } fn merge_default(&mut self, default: &Configs) { - let mut trigger_set = HashSet::new(); + // Merge matches + let mut match_trigger_set = HashSet::new(); self.matches.iter().for_each(|m| { - trigger_set.insert(m.trigger.clone()); + match_trigger_set.insert(m.trigger.clone()); }); let default_matches : Vec = default.matches.iter().filter(|&m| { - !trigger_set.contains(&m.trigger) + !match_trigger_set.contains(&m.trigger) }).cloned().collect(); self.matches.extend(default_matches); + + // Merge global variables + let mut vars_name_set = HashSet::new(); + self.global_vars.iter().for_each(|m| { + vars_name_set.insert(m.name.clone()); + }); + let default_vars : Vec = default.global_vars.iter().filter(|&m| { + !vars_name_set.contains(&m.name) + }).cloned().collect(); + + self.global_vars.extend(default_vars); + } } @@ -357,9 +389,9 @@ impl ConfigSet { let default= configs.get(0).unwrap().clone(); let mut specific = (&configs[1..]).to_vec().clone(); - // Add default matches to specific configs when needed + // Add default entries to specific configs when needed for config in specific.iter_mut() { - if !config.exclude_default_matches { + if !config.exclude_default_entries { config.merge_default(&default); } } @@ -849,7 +881,7 @@ mod tests { let user_defined_path2 = create_user_config_file(data_dir.path(), "specific2.yml", r###" name: specific1 - exclude_default_matches: true + exclude_default_entries: true matches: - trigger: "hello" @@ -884,7 +916,7 @@ mod tests { let user_defined_path2 = create_user_config_file(data_dir.path(), "specific.zzz", r###" name: specific1 - exclude_default_matches: true + exclude_default_entries: true matches: - trigger: "hello" @@ -1201,4 +1233,83 @@ mod tests { let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); assert_eq!(ConfigSet::has_conflicts(&config_set.default, &config_set.specific), false); } + + #[test] + fn test_config_set_specific_inherits_default_global_vars() { + let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" + global_vars: + - name: testvar + type: date + params: + format: "%m" + "###); + + let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" + global_vars: + - name: specificvar + type: date + params: + format: "%m" + "###); + + let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); + assert_eq!(config_set.specific.len(), 1); + assert_eq!(config_set.default.global_vars.len(), 1); + assert_eq!(config_set.specific[0].global_vars.len(), 2); + assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "testvar")); + assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar")); + } + + #[test] + fn test_config_set_default_get_variables_from_specific() { + let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" + global_vars: + - name: testvar + type: date + params: + format: "%m" + "###); + + let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" + parent: default + global_vars: + - name: specificvar + type: date + params: + format: "%m" + "###); + + let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); + assert_eq!(config_set.specific.len(), 0); + assert_eq!(config_set.default.global_vars.len(), 2); + assert!(config_set.default.global_vars.iter().any(|m| m.name == "testvar")); + assert!(config_set.default.global_vars.iter().any(|m| m.name == "specificvar")); + } + + #[test] + fn test_config_set_specific_dont_inherits_default_global_vars_when_exclude_is_on() { + let (data_dir, package_dir) = create_temp_espanso_directories_with_default_content(r###" + global_vars: + - name: testvar + type: date + params: + format: "%m" + "###); + + let user_defined_path = create_user_config_file(data_dir.path(), "specific.yml", r###" + exclude_default_entries: true + + global_vars: + - name: specificvar + type: date + params: + format: "%m" + "###); + + let config_set = ConfigSet::load(data_dir.path(), package_dir.path()).unwrap(); + assert_eq!(config_set.specific.len(), 1); + assert_eq!(config_set.default.global_vars.len(), 1); + assert_eq!(config_set.specific[0].global_vars.len(), 1); + assert!(config_set.specific[0].global_vars.iter().any(|m| m.name == "specificvar")); + } } \ No newline at end of file diff --git a/src/extension/dummy.rs b/src/extension/dummy.rs new file mode 100644 index 0000000..c9932e8 --- /dev/null +++ b/src/extension/dummy.rs @@ -0,0 +1,44 @@ +/* + * 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 . + */ + +use serde_yaml::{Mapping, Value}; + +pub struct DummyExtension {} + +impl DummyExtension { + pub fn new() -> DummyExtension { + DummyExtension{} + } +} + +impl super::Extension for DummyExtension { + fn name(&self) -> String { + String::from("dummy") + } + + fn calculate(&self, params: &Mapping, _: &Vec) -> Option { + let echo = params.get(&Value::from("echo")); + + if let Some(echo) = echo { + Some(echo.as_str().unwrap_or_default().to_owned()) + }else{ + None + } + } +} \ No newline at end of file diff --git a/src/extension/mod.rs b/src/extension/mod.rs index ccb6934..047060d 100644 --- a/src/extension/mod.rs +++ b/src/extension/mod.rs @@ -23,6 +23,7 @@ mod date; mod shell; mod script; mod random; +mod dummy; pub trait Extension { fn name(&self) -> String; @@ -35,5 +36,6 @@ pub fn get_extensions() -> Vec> { Box::new(shell::ShellExtension::new()), Box::new(script::ScriptExtension::new()), Box::new(random::RandomExtension::new()), + Box::new(dummy::DummyExtension::new()), ] } \ No newline at end of file diff --git a/src/render/default.rs b/src/render/default.rs index 2663c0b..feafac6 100644 --- a/src/render/default.rs +++ b/src/render/default.rs @@ -79,10 +79,11 @@ impl super::Renderer for DefaultRenderer { match &m.content { // Text Match MatchContentType::Text(content) => { - let target_string = if content._has_vars { + let target_string = if content._has_vars || !config.global_vars.is_empty(){ let mut output_map = HashMap::new(); - for variable in content.vars.iter() { + // Cycle through both the local and global variables + for variable in config.global_vars.iter().chain(&content.vars) { // In case of variables of type match, we need to recursively call // the render function if variable.var_type == "match" { @@ -118,7 +119,6 @@ impl super::Renderer for DefaultRenderer { }, } }else{ // Normal extension variables - // 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, &args); @@ -408,4 +408,77 @@ mod tests { verify_render(rendered, "Hi JonSnow"); } + + #[test] + fn test_render_passive_local_var() { + let text = "this is :test"; + + let config = get_config_for(r###" + matches: + - trigger: ':test' + replace: "my {{output}}" + vars: + - name: output + type: dummy + params: + echo: "result" + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "this is my result"); + } + + #[test] + fn test_render_passive_global_var() { + let text = "this is :test"; + + let config = get_config_for(r###" + global_vars: + - name: output + type: dummy + params: + echo: "result" + matches: + - trigger: ':test' + replace: "my {{output}}" + + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "this is my result"); + } + + #[test] + fn test_render_passive_global_var_is_overridden_by_local() { + let text = "this is :test"; + + let config = get_config_for(r###" + global_vars: + - name: output + type: dummy + params: + echo: "result" + matches: + - trigger: ':test' + replace: "my {{output}}" + vars: + - name: "output" + type: dummy + params: + echo: "local" + + "###); + + let renderer = get_renderer(config.clone()); + + let rendered = renderer.render_passive(text, &config); + + verify_render(rendered, "this is my local"); + } } \ No newline at end of file