commit
85f1598cf2
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -572,7 +572,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "espanso"
|
||||
version = "2.0.5-alpha"
|
||||
version = "2.1.0-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"caps",
|
||||
|
|
|
@ -89,7 +89,7 @@ fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup)
|
|||
.iter()
|
||||
.filter_map(|var| {
|
||||
let var: YAMLVariable = serde_yaml::from_value(var.clone()).ok()?;
|
||||
let (var, warnings) = try_convert_into_variable(var).ok()?;
|
||||
let (var, warnings) = try_convert_into_variable(var, true).ok()?;
|
||||
warnings.into_iter().for_each(|warning| {
|
||||
warn!("{}", warning);
|
||||
});
|
||||
|
@ -102,7 +102,7 @@ fn split_config(config: LegacyConfig) -> (LegacyInteropConfig, LegacyMatchGroup)
|
|||
.iter()
|
||||
.filter_map(|var| {
|
||||
let m: YAMLMatch = serde_yaml::from_value(var.clone()).ok()?;
|
||||
let (m, warnings) = try_convert_into_match(m).ok()?;
|
||||
let (m, warnings) = try_convert_into_match(m, true).ok()?;
|
||||
warnings.into_iter().for_each(|warning| {
|
||||
warn!("{}", warning);
|
||||
});
|
||||
|
|
|
@ -43,6 +43,8 @@ mod util;
|
|||
|
||||
lazy_static! {
|
||||
static ref VAR_REGEX: Regex = Regex::new("\\{\\{\\s*(\\w+)(\\.\\w+)?\\s*\\}\\}").unwrap();
|
||||
static ref FORM_CONTROL_REGEX: Regex =
|
||||
Regex::new("\\[\\[\\s*(\\w+)(\\.\\w+)?\\s*\\]\\]").unwrap();
|
||||
}
|
||||
|
||||
// Create an alias to make the meaning more explicit
|
||||
|
@ -72,7 +74,7 @@ impl Importer for YAMLImporter {
|
|||
|
||||
let mut global_vars = Vec::new();
|
||||
for yaml_global_var in yaml_group.global_vars.as_ref().cloned().unwrap_or_default() {
|
||||
match try_convert_into_variable(yaml_global_var) {
|
||||
match try_convert_into_variable(yaml_global_var, false) {
|
||||
Ok((var, warnings)) => {
|
||||
global_vars.push(var);
|
||||
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
||||
|
@ -85,7 +87,7 @@ impl Importer for YAMLImporter {
|
|||
|
||||
let mut matches = Vec::new();
|
||||
for yaml_match in yaml_group.matches.as_ref().cloned().unwrap_or_default() {
|
||||
match try_convert_into_match(yaml_match) {
|
||||
match try_convert_into_match(yaml_match, false) {
|
||||
Ok((m, warnings)) => {
|
||||
matches.push(m);
|
||||
non_fatal_errors.extend(warnings.into_iter().map(ErrorRecord::warn));
|
||||
|
@ -119,7 +121,10 @@ impl Importer for YAMLImporter {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warning>)> {
|
||||
pub fn try_convert_into_match(
|
||||
yaml_match: YAMLMatch,
|
||||
use_compatibility_mode: bool,
|
||||
) -> Result<(Match, Vec<Warning>)> {
|
||||
let mut warnings = Vec::new();
|
||||
|
||||
if yaml_match.uppercase_style.is_some() && yaml_match.propagate_case.is_none() {
|
||||
|
@ -203,8 +208,9 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
|||
|
||||
let mut vars: Vec<Variable> = Vec::new();
|
||||
for yaml_var in yaml_match.vars.unwrap_or_default() {
|
||||
let (var, var_warnings) = try_convert_into_variable(yaml_var.clone())
|
||||
.with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
|
||||
let (var, var_warnings) =
|
||||
try_convert_into_variable(yaml_var.clone(), use_compatibility_mode)
|
||||
.with_context(|| format!("failed to load variable: {:?}", yaml_var))?;
|
||||
warnings.extend(var_warnings);
|
||||
vars.push(var);
|
||||
}
|
||||
|
@ -216,21 +222,45 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
|||
force_mode,
|
||||
})
|
||||
} else if let Some(form_layout) = yaml_match.form {
|
||||
// TODO: test form case
|
||||
// Replace all the form fields with actual variables
|
||||
let resolved_layout = VAR_REGEX
|
||||
.replace_all(&form_layout, |caps: &Captures| {
|
||||
let var_name = caps.get(1).unwrap().as_str();
|
||||
format!("{{{{form1.{}}}}}", var_name)
|
||||
})
|
||||
.to_string();
|
||||
|
||||
// In v2.1.0-alpha the form control syntax was replaced with [[control]]
|
||||
// instead of {{control}}, so we check if compatibility mode is being used.
|
||||
// TODO: remove once compatibility mode is removed
|
||||
|
||||
let (resolved_replace, resolved_layout) = if use_compatibility_mode {
|
||||
(
|
||||
VAR_REGEX
|
||||
.replace_all(&form_layout, |caps: &Captures| {
|
||||
let var_name = caps.get(1).unwrap().as_str();
|
||||
format!("{{{{form1.{}}}}}", var_name)
|
||||
})
|
||||
.to_string(),
|
||||
VAR_REGEX
|
||||
.replace_all(&form_layout, |caps: &Captures| {
|
||||
let var_name = caps.get(1).unwrap().as_str();
|
||||
format!("[[{}]]", var_name)
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
FORM_CONTROL_REGEX
|
||||
.replace_all(&form_layout, |caps: &Captures| {
|
||||
let var_name = caps.get(1).unwrap().as_str();
|
||||
format!("{{{{form1.{}}}}}", var_name)
|
||||
})
|
||||
.to_string(),
|
||||
form_layout,
|
||||
)
|
||||
};
|
||||
|
||||
// Convert escaped brakets in forms
|
||||
let resolved_layout = resolved_layout.replace("\\{", "{ ").replace("\\}", " }");
|
||||
let resolved_replace = resolved_replace.replace("\\{", "{ ").replace("\\}", " }");
|
||||
|
||||
// Convert the form data to valid variables
|
||||
let mut params = Params::new();
|
||||
params.insert("layout".to_string(), Value::String(form_layout));
|
||||
params.insert("layout".to_string(), Value::String(resolved_layout));
|
||||
|
||||
if let Some(fields) = yaml_match.form_fields {
|
||||
params.insert("fields".to_string(), Value::Object(convert_params(fields)?));
|
||||
|
@ -241,10 +271,11 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
|||
name: "form1".to_owned(),
|
||||
var_type: "form".to_owned(),
|
||||
params,
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
MatchEffect::Text(TextEffect {
|
||||
replace: resolved_layout,
|
||||
replace: resolved_replace,
|
||||
vars,
|
||||
format: TextFormat::Plain,
|
||||
force_mode,
|
||||
|
@ -274,13 +305,18 @@ pub fn try_convert_into_match(yaml_match: YAMLMatch) -> Result<(Match, Vec<Warni
|
|||
))
|
||||
}
|
||||
|
||||
pub fn try_convert_into_variable(yaml_var: YAMLVariable) -> Result<(Variable, Vec<Warning>)> {
|
||||
pub fn try_convert_into_variable(
|
||||
yaml_var: YAMLVariable,
|
||||
use_compatibility_mode: bool,
|
||||
) -> Result<(Variable, Vec<Warning>)> {
|
||||
Ok((
|
||||
Variable {
|
||||
name: yaml_var.name,
|
||||
var_type: yaml_var.var_type,
|
||||
params: convert_params(yaml_var.params)?,
|
||||
id: next_id(),
|
||||
inject_vars: !use_compatibility_mode && yaml_var.inject_vars.unwrap_or(true),
|
||||
depends_on: yaml_var.depends_on,
|
||||
},
|
||||
Vec::new(),
|
||||
))
|
||||
|
@ -295,9 +331,12 @@ mod tests {
|
|||
};
|
||||
use std::fs::create_dir_all;
|
||||
|
||||
fn create_match_with_warnings(yaml: &str) -> Result<(Match, Vec<Warning>)> {
|
||||
fn create_match_with_warnings(
|
||||
yaml: &str,
|
||||
use_compatibility_mode: bool,
|
||||
) -> Result<(Match, Vec<Warning>)> {
|
||||
let yaml_match: YAMLMatch = serde_yaml::from_str(yaml)?;
|
||||
let (mut m, warnings) = try_convert_into_match(yaml_match)?;
|
||||
let (mut m, warnings) = try_convert_into_match(yaml_match, use_compatibility_mode)?;
|
||||
|
||||
// Reset the IDs to correctly compare them
|
||||
m.id = 0;
|
||||
|
@ -309,7 +348,7 @@ mod tests {
|
|||
}
|
||||
|
||||
fn create_match(yaml: &str) -> Result<Match> {
|
||||
let (m, warnings) = create_match_with_warnings(yaml)?;
|
||||
let (m, warnings) = create_match_with_warnings(yaml, false)?;
|
||||
if !warnings.is_empty() {
|
||||
panic!("warnings were detected but not handled: {:?}", warnings);
|
||||
}
|
||||
|
@ -529,6 +568,7 @@ mod tests {
|
|||
replace: "world"
|
||||
uppercase_style: "capitalize"
|
||||
"#,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -545,6 +585,7 @@ mod tests {
|
|||
uppercase_style: "invalid"
|
||||
propagate_case: true
|
||||
"#,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -554,6 +595,119 @@ mod tests {
|
|||
assert_eq!(warnings.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_maps_correctly() {
|
||||
let mut params = Params::new();
|
||||
params.insert(
|
||||
"layout".to_string(),
|
||||
Value::String("Hi [[name]]!".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
create_match(
|
||||
r#"
|
||||
trigger: "Hello"
|
||||
form: "Hi [[name]]!"
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
Match {
|
||||
cause: MatchCause::Trigger(TriggerCause {
|
||||
triggers: vec!["Hello".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
effect: MatchEffect::Text(TextEffect {
|
||||
replace: "Hi {{form1.name}}!".to_string(),
|
||||
vars: vec![Variable {
|
||||
id: 0,
|
||||
name: "form1".to_string(),
|
||||
var_type: "form".to_string(),
|
||||
params,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_maps_correctly_with_variable_injection() {
|
||||
let mut params = Params::new();
|
||||
params.insert(
|
||||
"layout".to_string(),
|
||||
Value::String("Hi [[name]]! {{signature}}".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
create_match(
|
||||
r#"
|
||||
trigger: "Hello"
|
||||
form: "Hi [[name]]! {{signature}}"
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
Match {
|
||||
cause: MatchCause::Trigger(TriggerCause {
|
||||
triggers: vec!["Hello".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
effect: MatchEffect::Text(TextEffect {
|
||||
replace: "Hi {{form1.name}}! {{signature}}".to_string(),
|
||||
vars: vec![Variable {
|
||||
id: 0,
|
||||
name: "form1".to_string(),
|
||||
var_type: "form".to_string(),
|
||||
params,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn form_maps_correctly_legacy_format() {
|
||||
let mut params = Params::new();
|
||||
params.insert(
|
||||
"layout".to_string(),
|
||||
Value::String("Hi [[name]]!".to_string()),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
create_match_with_warnings(
|
||||
r#"
|
||||
trigger: "Hello"
|
||||
form: "Hi {{name}}!"
|
||||
"#,
|
||||
true
|
||||
)
|
||||
.unwrap()
|
||||
.0,
|
||||
Match {
|
||||
cause: MatchCause::Trigger(TriggerCause {
|
||||
triggers: vec!["Hello".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
effect: MatchEffect::Text(TextEffect {
|
||||
replace: "Hi {{form1.name}}!".to_string(),
|
||||
vars: vec![Variable {
|
||||
id: 0,
|
||||
name: "form1".to_string(),
|
||||
var_type: "form".to_string(),
|
||||
params,
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vars_maps_correctly() {
|
||||
let mut params = Params::new();
|
||||
|
@ -592,6 +746,52 @@ mod tests {
|
|||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vars_inject_vars_and_depends_on() {
|
||||
let vars = vec![
|
||||
Variable {
|
||||
name: "var1".to_string(),
|
||||
var_type: "test".to_string(),
|
||||
depends_on: vec!["test".to_owned()],
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "var2".to_string(),
|
||||
var_type: "test".to_string(),
|
||||
inject_vars: false,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
assert_eq!(
|
||||
create_match(
|
||||
r#"
|
||||
trigger: "Hello"
|
||||
replace: "world"
|
||||
vars:
|
||||
- name: var1
|
||||
type: test
|
||||
depends_on: ["test"]
|
||||
- name: var2
|
||||
type: "test"
|
||||
inject_vars: false
|
||||
"#
|
||||
)
|
||||
.unwrap(),
|
||||
Match {
|
||||
cause: MatchCause::Trigger(TriggerCause {
|
||||
triggers: vec!["Hello".to_string()],
|
||||
..Default::default()
|
||||
}),
|
||||
effect: MatchEffect::Text(TextEffect {
|
||||
replace: "world".to_string(),
|
||||
vars,
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vars_no_params_maps_correctly() {
|
||||
let vars = vec![Variable {
|
||||
|
|
|
@ -125,6 +125,12 @@ pub struct YAMLVariable {
|
|||
|
||||
#[serde(default = "default_params")]
|
||||
pub params: Mapping,
|
||||
|
||||
#[serde(default)]
|
||||
pub inject_vars: Option<bool>,
|
||||
|
||||
#[serde(default)]
|
||||
pub depends_on: Vec<String>,
|
||||
}
|
||||
|
||||
fn default_params() -> Mapping {
|
||||
|
|
|
@ -205,6 +205,8 @@ pub struct Variable {
|
|||
pub name: String,
|
||||
pub var_type: String,
|
||||
pub params: Params,
|
||||
pub inject_vars: bool,
|
||||
pub depends_on: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Variable {
|
||||
|
@ -214,6 +216,8 @@ impl Default for Variable {
|
|||
name: String::new(),
|
||||
var_type: String::new(),
|
||||
params: Params::new(),
|
||||
inject_vars: true,
|
||||
depends_on: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ use log::{error, trace, warn};
|
|||
use anyhow::Result;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Variant};
|
||||
use crate::event::{HotKeyEvent, InputEvent, Key, KeyboardEvent, Status, Variant};
|
||||
use crate::event::{Key::*, MouseButton, MouseEvent};
|
||||
use crate::{event::Status::*, Source, SourceCallback};
|
||||
use crate::{event::Variant::*, hotkey::HotKey};
|
||||
|
@ -50,6 +50,7 @@ const INPUT_MOUSE_MIDDLE_BUTTON: i32 = 3;
|
|||
|
||||
// Take a look at the native.h header file for an explanation of the fields
|
||||
#[repr(C)]
|
||||
#[derive(Debug)]
|
||||
pub struct RawInputEvent {
|
||||
pub event_type: i32,
|
||||
|
||||
|
@ -58,6 +59,12 @@ pub struct RawInputEvent {
|
|||
|
||||
pub key_code: i32,
|
||||
pub status: i32,
|
||||
|
||||
pub is_caps_lock_pressed: i32,
|
||||
pub is_shift_pressed: i32,
|
||||
pub is_control_pressed: i32,
|
||||
pub is_option_pressed: i32,
|
||||
pub is_command_pressed: i32,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
@ -82,15 +89,97 @@ extern "C" {
|
|||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ModifierState {
|
||||
is_ctrl_down: bool,
|
||||
is_shift_down: bool,
|
||||
is_command_down: bool,
|
||||
is_option_down: bool,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CURRENT_SENDER: Arc<Mutex<Option<Sender<InputEvent>>>> = Arc::new(Mutex::new(None));
|
||||
static ref MODIFIER_STATE: Arc<Mutex<ModifierState>> =
|
||||
Arc::new(Mutex::new(ModifierState::default()));
|
||||
}
|
||||
|
||||
extern "C" fn native_callback(raw_event: RawInputEvent) {
|
||||
let lock = CURRENT_SENDER
|
||||
.lock()
|
||||
.expect("unable to acquire CocoaSource sender lock");
|
||||
|
||||
// Most of the times, when pressing a modifier key (such as Alt, Ctrl, Shift, Cmd),
|
||||
// we get both a Pressed and Released event. This is important to keep Espanso's
|
||||
// internal representation of modifiers in sync.
|
||||
// Unfortunately, there are times when the corresponding "release" event is not sent,
|
||||
// and this causes Espanso to mistakenly think that the modifier is still pressed.
|
||||
// This can happen for various reasons, such as when using external bluetooth keyboards
|
||||
// or certain keyboard shortcuts.
|
||||
// Luckily, most key events include the "modifiers flag" information, that tells us which
|
||||
// modifier keys were currently pressed at that time.
|
||||
// We use this modifier flag information to detect "inconsistent" states to send the corresponding
|
||||
// modifier release events, keeping espanso's state in sync.
|
||||
// For more info, see:
|
||||
// https://github.com/federico-terzi/espanso/issues/825
|
||||
// https://github.com/federico-terzi/espanso/issues/858
|
||||
let mut compensating_events = Vec::new();
|
||||
if raw_event.event_type == INPUT_EVENT_TYPE_KEYBOARD {
|
||||
let (key_code, _) = key_code_to_key(raw_event.key_code);
|
||||
let mut current_mod_state = MODIFIER_STATE
|
||||
.lock()
|
||||
.expect("unable to acquire modifier state in cocoa detector");
|
||||
|
||||
if let Key::Alt = &key_code {
|
||||
current_mod_state.is_option_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||
} else if let Key::Meta = &key_code {
|
||||
current_mod_state.is_command_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||
} else if let Key::Shift = &key_code {
|
||||
current_mod_state.is_shift_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||
} else if let Key::Control = &key_code {
|
||||
current_mod_state.is_ctrl_down = raw_event.status == INPUT_STATUS_PRESSED;
|
||||
} else {
|
||||
if current_mod_state.is_command_down && raw_event.is_command_pressed == 0 {
|
||||
compensating_events.push((Key::Meta, 0x37));
|
||||
current_mod_state.is_command_down = false;
|
||||
}
|
||||
if current_mod_state.is_ctrl_down && raw_event.is_control_pressed == 0 {
|
||||
compensating_events.push((Key::Control, 0x3B));
|
||||
current_mod_state.is_ctrl_down = false;
|
||||
}
|
||||
if current_mod_state.is_shift_down && raw_event.is_shift_pressed == 0 {
|
||||
compensating_events.push((Key::Shift, 0x38));
|
||||
current_mod_state.is_shift_down = false;
|
||||
}
|
||||
if current_mod_state.is_option_down && raw_event.is_option_pressed == 0 {
|
||||
compensating_events.push((Key::Alt, 0x3A));
|
||||
current_mod_state.is_option_down = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !compensating_events.is_empty() {
|
||||
warn!(
|
||||
"detected inconsistent modifier state for keys {:?}, sending compensating events...",
|
||||
compensating_events
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(sender) = lock.as_ref() {
|
||||
for (key, code) in compensating_events {
|
||||
if let Err(error) = sender.send(InputEvent::Keyboard(KeyboardEvent {
|
||||
key,
|
||||
value: None,
|
||||
status: Status::Released,
|
||||
variant: None,
|
||||
code,
|
||||
})) {
|
||||
error!(
|
||||
"Unable to send compensating event to Cocoa Sender: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let event: Option<InputEvent> = raw_event.into();
|
||||
if let Some(event) = event {
|
||||
if let Err(error) = sender.send(event) {
|
||||
|
@ -386,6 +475,11 @@ mod tests {
|
|||
buffer_len: 0,
|
||||
key_code: 0,
|
||||
status: INPUT_STATUS_PRESSED,
|
||||
is_caps_lock_pressed: 0,
|
||||
is_shift_pressed: 0,
|
||||
is_control_pressed: 0,
|
||||
is_option_pressed: 0,
|
||||
is_command_pressed: 0,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -52,6 +52,16 @@ typedef struct {
|
|||
|
||||
// Pressed or Released status
|
||||
int32_t status;
|
||||
|
||||
// Modifier keys status, this is needed to "correct" missing modifier release events.
|
||||
// For more info, see the following issues:
|
||||
// https://github.com/federico-terzi/espanso/issues/825
|
||||
// https://github.com/federico-terzi/espanso/issues/858
|
||||
int32_t is_caps_lock_pressed;
|
||||
int32_t is_shift_pressed;
|
||||
int32_t is_control_pressed;
|
||||
int32_t is_option_pressed;
|
||||
int32_t is_command_pressed;
|
||||
} InputEvent;
|
||||
|
||||
typedef void (*EventCallback)(InputEvent data);
|
||||
|
|
|
@ -76,6 +76,19 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) {
|
|||
strncpy(inputEvent.buffer, chars, 23);
|
||||
inputEvent.buffer_len = event.characters.length;
|
||||
|
||||
// We also send the modifier key status to "correct" missing modifier release events
|
||||
if (([event modifierFlags] & NSEventModifierFlagShift) != 0) {
|
||||
inputEvent.is_shift_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) {
|
||||
inputEvent.is_control_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) {
|
||||
inputEvent.is_caps_lock_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) {
|
||||
inputEvent.is_option_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) {
|
||||
inputEvent.is_command_pressed = 1;
|
||||
}
|
||||
|
||||
callback(inputEvent);
|
||||
}else if (event.type == NSEventTypeLeftMouseDown || event.type == NSEventTypeRightMouseDown || event.type == NSEventTypeOtherMouseDown ||
|
||||
event.type == NSEventTypeLeftMouseUp || event.type == NSEventTypeRightMouseUp || event.type == NSEventTypeOtherMouseUp) {
|
||||
|
@ -106,6 +119,20 @@ void * detect_initialize(EventCallback callback, InitializeOptions options) {
|
|||
} else if (event.keyCode == kVK_Option || event.keyCode == kVK_RightOption) {
|
||||
inputEvent.status = (([event modifierFlags] & NSEventModifierFlagOption) == 0) ? INPUT_STATUS_RELEASED : INPUT_STATUS_PRESSED;
|
||||
}
|
||||
|
||||
// We also send the modifier key status to "correct" missing modifier release events
|
||||
if (([event modifierFlags] & NSEventModifierFlagShift) != 0) {
|
||||
inputEvent.is_shift_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagControl) != 0) {
|
||||
inputEvent.is_control_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagCapsLock) != 0) {
|
||||
inputEvent.is_caps_lock_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagOption) != 0) {
|
||||
inputEvent.is_option_pressed = 1;
|
||||
} else if (([event modifierFlags] & NSEventModifierFlagCommand) != 0) {
|
||||
inputEvent.is_command_pressed = 1;
|
||||
}
|
||||
|
||||
callback(inputEvent);
|
||||
}
|
||||
}];
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use regex::{Captures, Regex};
|
||||
use std::{cmp::Ordering, collections::HashMap, path::PathBuf};
|
||||
use yaml_rust::{yaml::Hash, Yaml};
|
||||
|
||||
|
@ -69,24 +70,32 @@ pub fn convert(input_files: HashMap<String, Hash>) -> HashMap<String, ConvertedF
|
|||
});
|
||||
|
||||
if let Some(global_vars) = yaml_global_vars {
|
||||
let mut patched_global_vars: Vec<Yaml> = global_vars.clone();
|
||||
patched_global_vars
|
||||
.iter_mut()
|
||||
.for_each(apply_form_syntax_patch_to_variable);
|
||||
|
||||
let output_global_vars = output_yaml
|
||||
.content
|
||||
.entry(Yaml::String("global_vars".to_string()))
|
||||
.or_insert(Yaml::Array(Vec::new()));
|
||||
if let Yaml::Array(out_global_vars) = output_global_vars {
|
||||
out_global_vars.extend(global_vars.clone());
|
||||
out_global_vars.extend(patched_global_vars);
|
||||
} else {
|
||||
eprintln!("unable to transform global_vars for file: {}", input_path);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(matches) = yaml_matches {
|
||||
let mut patched_matches = matches.clone();
|
||||
apply_form_syntax_patch(&mut patched_matches);
|
||||
|
||||
let output_matches = output_yaml
|
||||
.content
|
||||
.entry(Yaml::String("matches".to_string()))
|
||||
.or_insert(Yaml::Array(Vec::new()));
|
||||
if let Yaml::Array(out_matches) = output_matches {
|
||||
out_matches.extend(matches.clone());
|
||||
out_matches.extend(patched_matches);
|
||||
} else {
|
||||
eprintln!("unable to transform matches for file: {}", input_path);
|
||||
}
|
||||
|
@ -324,3 +333,57 @@ fn map_field_if_present(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is needed to convert the old form's {{control}} syntax to the new [[control]] one.
|
||||
fn apply_form_syntax_patch(matches: &mut Vec<Yaml>) {
|
||||
matches.iter_mut().for_each(|m| {
|
||||
if let Yaml::Hash(fields) = m {
|
||||
if let Some(Yaml::String(form_option)) = fields.get_mut(&Yaml::String("form".to_string())) {
|
||||
let converted = replace_legacy_form_syntax_with_new_one(form_option);
|
||||
if &converted != form_option {
|
||||
form_option.clear();
|
||||
form_option.push_str(&converted);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Yaml::Array(vars)) = fields.get_mut(&Yaml::String("vars".to_string())) {
|
||||
vars
|
||||
.iter_mut()
|
||||
.for_each(apply_form_syntax_patch_to_variable)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn apply_form_syntax_patch_to_variable(variable: &mut Yaml) {
|
||||
if let Yaml::Hash(fields) = variable {
|
||||
if let Some(Yaml::String(var_type)) = fields.get(&Yaml::String("type".to_string())) {
|
||||
if var_type != "form" {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Yaml::Hash(params)) = fields.get_mut(&Yaml::String("params".to_string())) {
|
||||
if let Some(Yaml::String(layout)) = params.get_mut(&Yaml::String("layout".to_string())) {
|
||||
let converted = replace_legacy_form_syntax_with_new_one(layout);
|
||||
if &converted != layout {
|
||||
layout.clear();
|
||||
layout.push_str(&converted);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref LEGACY_FIELD_REGEX: Regex = Regex::new(r"\{\{(?P<name>.*?)\}\}").unwrap();
|
||||
}
|
||||
|
||||
fn replace_legacy_form_syntax_with_new_one(layout: &str) -> String {
|
||||
LEGACY_FIELD_REGEX
|
||||
.replace_all(layout, |caps: &Captures| {
|
||||
let field_name = caps.name("name").unwrap().as_str();
|
||||
format!("[[{}]]", field_name)
|
||||
})
|
||||
.to_string()
|
||||
}
|
||||
|
|
|
@ -200,12 +200,14 @@ mod tests {
|
|||
static BASE_CASE: Dir = include_dir!("test/base");
|
||||
static ALL_PARAMS_CASE: Dir = include_dir!("test/all_params");
|
||||
static OTHER_DIRS_CASE: Dir = include_dir!("test/other_dirs");
|
||||
static FORM_SYNTAX: Dir = include_dir!("test/form_syntax");
|
||||
|
||||
#[allow(clippy::unused_unit)]
|
||||
#[test_case(&SIMPLE_CASE; "simple case")]
|
||||
#[test_case(&BASE_CASE; "base case")]
|
||||
#[test_case(&ALL_PARAMS_CASE; "all config parameters case")]
|
||||
#[test_case(&OTHER_DIRS_CASE; "other directories case")]
|
||||
#[test_case(&FORM_SYNTAX; "form syntax")]
|
||||
fn test_migration(test_data: &Dir) {
|
||||
run_with_temp_dir(test_data, |legacy, expected| {
|
||||
let tmp_out_dir = TempDir::new("espanso-migrate-out").unwrap();
|
||||
|
|
25
espanso-migrate/test/form_syntax/expected/match/base.yml
Normal file
25
espanso-migrate/test/form_syntax/expected/match/base.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
global_vars:
|
||||
- name: global_form
|
||||
type: form
|
||||
params:
|
||||
layout: |
|
||||
Reverse [[name]]
|
||||
|
||||
matches:
|
||||
- trigger: ":greet"
|
||||
form: |
|
||||
Hey [[name]],
|
||||
Happy Birthday!
|
||||
|
||||
- trigger: ":rev"
|
||||
replace: "{{reversed}}"
|
||||
vars:
|
||||
- name: form1
|
||||
type: form
|
||||
params:
|
||||
layout: |
|
||||
Reverse [[name]]
|
||||
- name: reversed
|
||||
type: shell
|
||||
params:
|
||||
cmd: "echo $ESPANSO_FORM1_NAME | rev"
|
25
espanso-migrate/test/form_syntax/legacy/default.yml
Normal file
25
espanso-migrate/test/form_syntax/legacy/default.yml
Normal file
|
@ -0,0 +1,25 @@
|
|||
global_vars:
|
||||
- name: global_form
|
||||
type: form
|
||||
params:
|
||||
layout: |
|
||||
Reverse {{name}}
|
||||
|
||||
matches:
|
||||
- trigger: ":greet"
|
||||
form: |
|
||||
Hey {{name}},
|
||||
Happy Birthday!
|
||||
|
||||
- trigger: ":rev"
|
||||
replace: "{{reversed}}"
|
||||
vars:
|
||||
- name: form1
|
||||
type: form
|
||||
params:
|
||||
layout: |
|
||||
Reverse {{name}}
|
||||
- name: reversed
|
||||
type: shell
|
||||
params:
|
||||
cmd: "echo $ESPANSO_FORM1_NAME | rev"
|
|
@ -21,7 +21,8 @@ use super::split::*;
|
|||
use regex::Regex;
|
||||
|
||||
lazy_static! {
|
||||
static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}").unwrap();
|
||||
// We need to match for both the new [[name]] syntax and the legacy {{name}} one
|
||||
static ref FIELD_REGEX: Regex = Regex::new(r"\{\{(.*?)\}\}|\[\[(.*?)\]\]").unwrap();
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
|
@ -57,6 +58,9 @@ pub fn parse_layout(layout: &str) -> Vec<Vec<Token>> {
|
|||
if let Some(name) = caps.get(1) {
|
||||
let name = name.as_str().to_owned();
|
||||
row.push(Token::Field(name));
|
||||
} else if let Some(name) = caps.get(2) {
|
||||
let name = name.as_str().to_owned();
|
||||
row.push(Token::Field(name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -74,7 +78,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_layout() {
|
||||
let layout = "Hey {{name}},\nHow are you?\n \nCheers";
|
||||
let layout = "Hey [[name]],\nHow are you?\n \nCheers";
|
||||
let result = parse_layout(layout);
|
||||
assert_eq!(
|
||||
result,
|
||||
|
@ -92,7 +96,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_layout_2() {
|
||||
let layout = "Hey {{name}} {{surname}},";
|
||||
let layout = "Hey [[name]] [[surname]],";
|
||||
let result = parse_layout(layout);
|
||||
assert_eq!(
|
||||
result,
|
||||
|
@ -105,4 +109,22 @@ mod tests {
|
|||
],]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_layout_legacy_syntax() {
|
||||
let layout = "Hey {{name}},\nHow are you?\n \nCheers";
|
||||
let result = parse_layout(layout);
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
vec![
|
||||
Token::Text("Hey ".to_owned()),
|
||||
Token::Field("name".to_owned()),
|
||||
Token::Text(",".to_owned())
|
||||
],
|
||||
vec![Token::Text("How are you?".to_owned())],
|
||||
vec![Token::Text("Cheers".to_owned())],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,14 +17,11 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::renderer::VAR_REGEX;
|
||||
use log::error;
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
renderer::render_variables, Extension, ExtensionOutput, ExtensionResult, Params, Value,
|
||||
};
|
||||
use crate::{Extension, ExtensionOutput, ExtensionResult, Params, Value};
|
||||
|
||||
lazy_static! {
|
||||
static ref EMPTY_PARAMS: Params = Params::new();
|
||||
|
@ -59,7 +56,7 @@ impl<'a> Extension for FormExtension<'a> {
|
|||
fn calculate(
|
||||
&self,
|
||||
_: &crate::Context,
|
||||
scope: &crate::Scope,
|
||||
_: &crate::Scope,
|
||||
params: &Params,
|
||||
) -> crate::ExtensionResult {
|
||||
let layout = if let Some(Value::String(layout)) = params.get("layout") {
|
||||
|
@ -68,15 +65,12 @@ impl<'a> Extension for FormExtension<'a> {
|
|||
return crate::ExtensionResult::Error(FormExtensionError::MissingLayout.into());
|
||||
};
|
||||
|
||||
let mut fields = if let Some(Value::Object(fields)) = params.get("fields") {
|
||||
let fields = if let Some(Value::Object(fields)) = params.get("fields") {
|
||||
fields.clone()
|
||||
} else {
|
||||
Params::new()
|
||||
};
|
||||
|
||||
// Inject scope variables into fields (if needed)
|
||||
inject_scope(&mut fields, scope);
|
||||
|
||||
match self.provider.show(layout, &fields, &EMPTY_PARAMS) {
|
||||
FormProviderResult::Success(values) => {
|
||||
ExtensionResult::Success(ExtensionOutput::Multiple(values))
|
||||
|
@ -87,25 +81,6 @@ impl<'a> Extension for FormExtension<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
fn inject_scope(fields: &mut HashMap<String, Value>, scope: &HashMap<&str, ExtensionOutput>) {
|
||||
for value in fields.values_mut() {
|
||||
if let Value::Object(field_options) = value {
|
||||
if let Some(Value::String(default_value)) = field_options.get_mut("default") {
|
||||
if VAR_REGEX.is_match(default_value) {
|
||||
match render_variables(default_value, scope) {
|
||||
Ok(rendered) => *default_value = rendered,
|
||||
Err(err) => error!(
|
||||
"error while injecting variable in form default value: {}",
|
||||
err
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum FormExtensionError {
|
||||
#[error("missing layout parameter")]
|
||||
|
|
|
@ -98,7 +98,10 @@ impl Default for Template {
|
|||
pub struct Variable {
|
||||
pub name: String,
|
||||
pub var_type: String,
|
||||
pub inject_vars: bool,
|
||||
pub params: Params,
|
||||
// Name of the variables this variable depends on
|
||||
pub depends_on: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for Variable {
|
||||
|
@ -106,7 +109,9 @@ impl Default for Variable {
|
|||
Self {
|
||||
name: "".to_string(),
|
||||
var_type: "".to_string(),
|
||||
inject_vars: true,
|
||||
params: Params::new(),
|
||||
depends_on: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* This file is part of esp name: (), var_type: (), params: ()anso.
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 Federico Terzi
|
||||
*
|
||||
|
@ -17,18 +17,19 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::{borrow::Cow, collections::HashMap};
|
||||
|
||||
use crate::{
|
||||
CasingStyle, Context, Extension, ExtensionOutput, ExtensionResult, RenderOptions, RenderResult,
|
||||
Renderer, Scope, Template, Value, Variable,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use log::{error, warn};
|
||||
use regex::{Captures, Regex};
|
||||
use thiserror::Error;
|
||||
use util::get_body_variable_names;
|
||||
|
||||
use self::util::{inject_variables_into_params, render_variables};
|
||||
|
||||
mod resolve;
|
||||
mod util;
|
||||
|
||||
lazy_static! {
|
||||
|
@ -59,23 +60,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
|||
options: &RenderOptions,
|
||||
) -> RenderResult {
|
||||
let body = if VAR_REGEX.is_match(&template.body) {
|
||||
// In order to define a variable evaluation order, we first need to find
|
||||
// the global variables that are being used but for which an explicit order
|
||||
// is not defined.
|
||||
let body_variable_names = get_body_variable_names(&template.body);
|
||||
let local_variable_names: HashSet<&str> =
|
||||
template.vars.iter().map(|var| var.name.as_str()).collect();
|
||||
let missing_global_variable_names: HashSet<&str> = body_variable_names
|
||||
.difference(&local_variable_names)
|
||||
.copied()
|
||||
.collect();
|
||||
let missing_global_variables: Vec<&Variable> = context
|
||||
.global_vars
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|global_var| missing_global_variable_names.contains(&*global_var.name))
|
||||
.collect();
|
||||
|
||||
// Convert "global" variable type aliases when needed
|
||||
let local_variables: Vec<&Variable> =
|
||||
if template.vars.iter().any(|var| var.var_type == "global") {
|
||||
|
@ -99,10 +83,16 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
|||
template.vars.iter().collect()
|
||||
};
|
||||
|
||||
// The implicit global variables will be evaluated first, followed by the local vars
|
||||
let mut variables: Vec<&Variable> = Vec::new();
|
||||
variables.extend(missing_global_variables);
|
||||
variables.extend(local_variables.iter());
|
||||
// Here we execute a graph dependency resolution algorithm to determine a valid
|
||||
// evaluation order for variables.
|
||||
let variables = match resolve::resolve_evaluation_order(
|
||||
&template.body,
|
||||
&local_variables,
|
||||
&context.global_vars,
|
||||
) {
|
||||
Ok(variables) => variables,
|
||||
Err(err) => return RenderResult::Error(err),
|
||||
};
|
||||
|
||||
// Compute the variable outputs
|
||||
let mut scope = Scope::new();
|
||||
|
@ -123,7 +113,31 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
|||
return RenderResult::Error(RendererError::MissingSubMatch.into());
|
||||
}
|
||||
} else if let Some(extension) = self.extensions.get(&variable.var_type) {
|
||||
match extension.calculate(context, &scope, &variable.params) {
|
||||
let variable_params = if variable.inject_vars {
|
||||
match inject_variables_into_params(&variable.params, &scope) {
|
||||
Ok(augmented_params) => Cow::Owned(augmented_params),
|
||||
Err(err) => {
|
||||
error!(
|
||||
"unable to inject variables into params of variable '{}': {}",
|
||||
variable.name, err
|
||||
);
|
||||
|
||||
if variable.var_type == "form" {
|
||||
if let Some(RendererError::MissingVariable(_)) =
|
||||
err.downcast_ref::<RendererError>()
|
||||
{
|
||||
log_new_form_syntax_tip();
|
||||
}
|
||||
}
|
||||
|
||||
return RenderResult::Error(err);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Cow::Borrowed(&variable.params)
|
||||
};
|
||||
|
||||
match extension.calculate(context, &scope, &variable_params) {
|
||||
ExtensionResult::Success(output) => {
|
||||
scope.insert(&variable.name, output);
|
||||
}
|
||||
|
@ -161,6 +175,8 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
|||
template.body.clone()
|
||||
};
|
||||
|
||||
let body = util::unescape_variable_inections(&body);
|
||||
|
||||
// Process the casing style
|
||||
let body_with_casing = match options.casing_style {
|
||||
CasingStyle::None => body,
|
||||
|
@ -192,52 +208,6 @@ impl<'a> Renderer for DefaultRenderer<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
|
||||
let mut replacing_error = None;
|
||||
let output = VAR_REGEX
|
||||
.replace_all(body, |caps: &Captures| {
|
||||
let var_name = caps.name("name").unwrap().as_str();
|
||||
let var_subname = caps.name("subname");
|
||||
match scope.get(var_name) {
|
||||
Some(output) => match output {
|
||||
ExtensionOutput::Single(output) => output,
|
||||
ExtensionOutput::Multiple(results) => match var_subname {
|
||||
Some(var_subname) => {
|
||||
let var_subname = var_subname.as_str();
|
||||
results.get(var_subname).map_or("", |value| &*value)
|
||||
}
|
||||
None => {
|
||||
error!(
|
||||
"nested name missing from multi-value variable: {}",
|
||||
var_name
|
||||
);
|
||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||
"nested name missing from multi-value variable: {}",
|
||||
var_name
|
||||
)));
|
||||
""
|
||||
}
|
||||
},
|
||||
},
|
||||
None => {
|
||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||
"variable '{}' is missing",
|
||||
var_name
|
||||
)));
|
||||
""
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
|
||||
if let Some(error) = replacing_error {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn get_matching_template<'a>(
|
||||
variable: &Variable,
|
||||
templates: &'a [&Template],
|
||||
|
@ -254,6 +224,24 @@ fn get_matching_template<'a>(
|
|||
}
|
||||
}
|
||||
|
||||
fn log_new_form_syntax_tip() {
|
||||
error!("");
|
||||
error!("TIP: This error might be happening because since version 2.1.0-alpha, Espanso changed");
|
||||
error!("the syntax to define form controls. Instead of `{{{{control}}}}` you need to use");
|
||||
error!("[[control]] (using square brackets instead of curly brackets).");
|
||||
error!("");
|
||||
error!("For example, if you have a form defined like the following:");
|
||||
error!(" - trigger: test");
|
||||
error!(" form: |");
|
||||
error!(" Hi {{{{name}}}}!");
|
||||
error!("");
|
||||
error!("You'll need to replace it with:");
|
||||
error!(" - trigger: test");
|
||||
error!(" form: |");
|
||||
error!(" Hi [[name]]!");
|
||||
error!("");
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RendererError {
|
||||
#[error("missing variable: `{0}`")]
|
||||
|
@ -261,6 +249,9 @@ pub enum RendererError {
|
|||
|
||||
#[error("missing sub match")]
|
||||
MissingSubMatch,
|
||||
|
||||
#[error("circular dependency: `{0}` -> `{1}`")]
|
||||
CircularDependency(String, String),
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -285,6 +276,13 @@ mod tests {
|
|||
if let Some(Value::String(string)) = params.get("echo") {
|
||||
return ExtensionResult::Success(ExtensionOutput::Single(string.clone()));
|
||||
}
|
||||
if let (Some(Value::String(name)), Some(Value::String(value))) =
|
||||
(params.get("name"), params.get("value"))
|
||||
{
|
||||
let mut map = HashMap::new();
|
||||
map.insert(name.to_string(), value.to_string());
|
||||
return ExtensionResult::Success(ExtensionOutput::Multiple(map));
|
||||
}
|
||||
// If the "read" param is present, echo the value of the corresponding result in the scope
|
||||
if let Some(Value::String(string)) = params.get("read") {
|
||||
if let Some(ExtensionOutput::Single(value)) = scope.get(string.as_str()) {
|
||||
|
@ -324,6 +322,7 @@ mod tests {
|
|||
params: vec![("echo".to_string(), Value::String((*value).to_string()))]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
})
|
||||
.collect();
|
||||
Template {
|
||||
|
@ -393,6 +392,28 @@ mod tests {
|
|||
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dict_variable_variable() {
|
||||
let renderer = get_renderer();
|
||||
let template = Template {
|
||||
body: "hello {{var.nested}}".to_string(),
|
||||
vars: vec![Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: vec![
|
||||
("name".to_string(), Value::String("nested".to_string())),
|
||||
("value".to_string(), Value::String("dict".to_string())),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello dict"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_variable() {
|
||||
let renderer = get_renderer();
|
||||
|
@ -415,6 +436,7 @@ mod tests {
|
|||
"echo".to_string(),
|
||||
Value::String("world".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -423,6 +445,31 @@ mod tests {
|
|||
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_dict_variable() {
|
||||
let renderer = get_renderer();
|
||||
let template = template("hello {{var.nested}}", &[]);
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: vec![
|
||||
("name".to_string(), Value::String("nested".to_string())),
|
||||
("value".to_string(), Value::String("dict".to_string())),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello dict"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_variable_explicit_ordering() {
|
||||
let renderer = get_renderer();
|
||||
|
@ -435,6 +482,7 @@ mod tests {
|
|||
params: vec![("echo".to_string(), Value::String("Bob".to_string()))]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "var".to_string(),
|
||||
|
@ -454,6 +502,7 @@ mod tests {
|
|||
"read".to_string(),
|
||||
Value::String("local".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
|
@ -462,6 +511,147 @@ mod tests {
|
|||
assert!(matches!(res, RenderResult::Success(str) if str == "hello Bob Bob"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_global_variable() {
|
||||
let renderer = get_renderer();
|
||||
let template = template("hello {{var2}}", &[]);
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![
|
||||
&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("world".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
&Variable {
|
||||
name: "var2".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello world"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_global_variable_circular_dependency_should_fail() {
|
||||
let renderer = get_renderer();
|
||||
let template = template("hello {{var}}", &[]);
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![
|
||||
&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var2}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
&Variable {
|
||||
name: "var2".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var3}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
&Variable {
|
||||
name: "var3".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Error(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn global_variable_depends_on() {
|
||||
let renderer = get_renderer();
|
||||
let template = template("hello {{var}}", &[]);
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![
|
||||
&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("world".to_string()),
|
||||
)]),
|
||||
depends_on: vec!["var2".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
&Variable {
|
||||
name: "var2".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Aborted));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_variable_explicit_ordering() {
|
||||
let renderer = get_renderer();
|
||||
let template = Template {
|
||||
body: "hello {{var}}".to_string(),
|
||||
vars: vec![Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: vec![("echo".to_string(), Value::String("something".to_string()))]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
depends_on: vec!["global".to_string()],
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![&Variable {
|
||||
name: "global".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![("abort".to_string(), Value::Null)]),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Aborted));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_match() {
|
||||
let renderer = get_renderer();
|
||||
|
@ -473,6 +663,7 @@ mod tests {
|
|||
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -503,6 +694,7 @@ mod tests {
|
|||
params: vec![("trigger".to_string(), Value::String("nested".to_string()))]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -527,6 +719,7 @@ mod tests {
|
|||
params: vec![("abort".to_string(), Value::Null)]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -545,10 +738,213 @@ mod tests {
|
|||
params: vec![("error".to_string(), Value::Null)]
|
||||
.into_iter()
|
||||
.collect::<Params>(),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
};
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Error(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_injection() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{fullname}}");
|
||||
template.vars = vec![
|
||||
Variable {
|
||||
name: "firstname".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("John".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "lastname".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("Snow".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "fullname".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{firstname}} {{lastname}}".to_string()),
|
||||
)]),
|
||||
inject_vars: true,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello John Snow"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn disable_variable_injection() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{second}}");
|
||||
template.vars = vec![
|
||||
Variable {
|
||||
name: "first".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![("echo".to_string(), Value::String("one".to_string()))]),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "second".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{first}} two".to_string()),
|
||||
)]),
|
||||
inject_vars: false,
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{first}} two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn escaped_variable_injection() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{second}}");
|
||||
template.vars = vec![
|
||||
Variable {
|
||||
name: "first".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![("echo".to_string(), Value::String("one".to_string()))]),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "second".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("\\{\\{first\\}\\} two".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{first}} two"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_injection_missing_var() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{second}}");
|
||||
template.vars = vec![Variable {
|
||||
name: "second".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("the next is {{missing}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
}];
|
||||
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Error(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_injection_with_global_variable() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{output}}");
|
||||
template.vars = vec![
|
||||
Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "global".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "output".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("global".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello global"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_injection_local_var_takes_precedence_over_global() {
|
||||
let renderer = get_renderer();
|
||||
let mut template = template_for_str("hello {{output}}");
|
||||
template.vars = vec![
|
||||
Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("local".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
Variable {
|
||||
name: "output".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("{{var}}".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
|
||||
let res = renderer.render(
|
||||
&template,
|
||||
&Context {
|
||||
global_vars: vec![&Variable {
|
||||
name: "var".to_string(),
|
||||
var_type: "mock".to_string(),
|
||||
params: Params::from_iter(vec![(
|
||||
"echo".to_string(),
|
||||
Value::String("global".to_string()),
|
||||
)]),
|
||||
..Default::default()
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
&Default::default(),
|
||||
);
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello local"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn variable_escape() {
|
||||
let renderer = get_renderer();
|
||||
let template = template("hello \\{\\{var\\}\\}", &[("var", "world")]);
|
||||
let res = renderer.render(&template, &Default::default(), &Default::default());
|
||||
assert!(matches!(res, RenderResult::Success(str) if str == "hello {{var}}"));
|
||||
}
|
||||
}
|
||||
|
|
204
espanso-render/src/renderer/resolve.rs
Normal file
204
espanso-render/src/renderer/resolve.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
* This file is part of espanso.
|
||||
*
|
||||
* Copyright (C) 2019-2021 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 std::{
|
||||
cell::RefCell,
|
||||
collections::{HashMap, HashSet},
|
||||
};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use log::error;
|
||||
|
||||
use crate::Variable;
|
||||
|
||||
use super::RendererError;
|
||||
|
||||
struct Node<'a> {
|
||||
name: &'a str,
|
||||
variable: Option<&'a Variable>,
|
||||
dependencies: Option<HashSet<&'a str>>,
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_evaluation_order<'a>(
|
||||
body: &'a str,
|
||||
local_vars: &'a [&'a Variable],
|
||||
global_vars: &'a [&'a Variable],
|
||||
) -> Result<Vec<&'a Variable>> {
|
||||
let node_map = generate_nodes(body, local_vars, global_vars);
|
||||
|
||||
let body_node = node_map
|
||||
.get("__match_body")
|
||||
.ok_or_else(|| anyhow!("missing body node"))?;
|
||||
|
||||
let eval_order = RefCell::new(Vec::new());
|
||||
let resolved = RefCell::new(HashSet::new());
|
||||
let seen = RefCell::new(HashSet::new());
|
||||
{
|
||||
resolve_dependencies(body_node, &node_map, &eval_order, &resolved, &seen)?;
|
||||
}
|
||||
|
||||
let eval_order_ref = eval_order.borrow();
|
||||
|
||||
let mut ordered_variables = Vec::new();
|
||||
for var_name in (*eval_order_ref).iter() {
|
||||
let node = node_map
|
||||
.get(var_name)
|
||||
.ok_or_else(|| anyhow!("could not find dependency node for variable: {}", var_name))?;
|
||||
if let Some(var) = node.variable {
|
||||
ordered_variables.push(var);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ordered_variables)
|
||||
}
|
||||
|
||||
fn generate_nodes<'a>(
|
||||
body: &'a str,
|
||||
local_vars: &'a [&'a Variable],
|
||||
global_vars: &'a [&'a Variable],
|
||||
) -> HashMap<&'a str, Node<'a>> {
|
||||
let mut local_vars_nodes = Vec::new();
|
||||
for (index, var) in local_vars.iter().enumerate() {
|
||||
let mut dependencies = HashSet::new();
|
||||
if var.inject_vars {
|
||||
dependencies.extend(super::util::get_params_variable_names(&var.params));
|
||||
}
|
||||
dependencies.extend(var.depends_on.iter().map(|v| v.as_str()));
|
||||
|
||||
// Every local variable depends on the one before it.
|
||||
// Needed to guarantee execution order within local vars.
|
||||
if index > 0 {
|
||||
let previous_var = local_vars.get(index - 1);
|
||||
if let Some(previous_var) = previous_var {
|
||||
dependencies.insert(&previous_var.name);
|
||||
}
|
||||
}
|
||||
|
||||
local_vars_nodes.push(Node {
|
||||
name: &var.name,
|
||||
variable: Some(var),
|
||||
dependencies: Some(dependencies),
|
||||
});
|
||||
}
|
||||
|
||||
let global_vars_nodes = global_vars.iter().map(|var| create_node_from_var(*var));
|
||||
|
||||
// The body depends on all local variables + the variables read inside it (which might be global)
|
||||
let mut body_dependencies: HashSet<&str> =
|
||||
local_vars_nodes.iter().map(|node| node.name).collect();
|
||||
body_dependencies.extend(super::util::get_body_variable_names(body));
|
||||
|
||||
let body_node = Node {
|
||||
name: "__match_body",
|
||||
variable: None,
|
||||
dependencies: Some(body_dependencies),
|
||||
};
|
||||
|
||||
let mut node_map = HashMap::new();
|
||||
|
||||
node_map.insert(body_node.name, body_node);
|
||||
global_vars_nodes.into_iter().for_each(|node| {
|
||||
node_map.insert(node.name, node);
|
||||
});
|
||||
local_vars_nodes.into_iter().for_each(|node| {
|
||||
node_map.insert(node.name, node);
|
||||
});
|
||||
|
||||
node_map
|
||||
}
|
||||
|
||||
fn create_node_from_var(var: &Variable) -> Node {
|
||||
let dependencies = if var.inject_vars || !var.depends_on.is_empty() {
|
||||
let mut vars = HashSet::new();
|
||||
|
||||
if var.inject_vars {
|
||||
vars.extend(super::util::get_params_variable_names(&var.params))
|
||||
}
|
||||
|
||||
vars.extend(var.depends_on.iter().map(|s| s.as_str()));
|
||||
|
||||
Some(vars)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Node {
|
||||
name: &var.name,
|
||||
variable: Some(var),
|
||||
dependencies,
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_dependencies<'a>(
|
||||
node: &'a Node,
|
||||
node_map: &'a HashMap<&'a str, Node<'a>>,
|
||||
eval_order: &'a RefCell<Vec<&'a str>>,
|
||||
resolved: &'a RefCell<HashSet<&'a str>>,
|
||||
seen: &'a RefCell<HashSet<&'a str>>,
|
||||
) -> Result<()> {
|
||||
{
|
||||
let mut seen_ref = seen.borrow_mut();
|
||||
seen_ref.insert(node.name);
|
||||
}
|
||||
|
||||
if let Some(dependencies) = &node.dependencies {
|
||||
for dependency in dependencies.iter() {
|
||||
let has_been_resolved = {
|
||||
let resolved_ref = resolved.borrow();
|
||||
resolved_ref.contains(dependency)
|
||||
};
|
||||
let has_been_seen = {
|
||||
let seen_ref = seen.borrow();
|
||||
seen_ref.contains(dependency)
|
||||
};
|
||||
|
||||
if !has_been_resolved {
|
||||
if has_been_seen {
|
||||
return Err(
|
||||
RendererError::CircularDependency(node.name.to_string(), dependency.to_string()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
match node_map.get(dependency) {
|
||||
Some(dependency_node) => {
|
||||
resolve_dependencies(dependency_node, node_map, eval_order, resolved, seen)?
|
||||
}
|
||||
None => {
|
||||
error!("could not resolve variable {:?}", dependency);
|
||||
if let Some(variable) = &node.variable {
|
||||
if variable.var_type == "form" {
|
||||
super::log_new_form_syntax_tip();
|
||||
}
|
||||
}
|
||||
return Err(RendererError::MissingVariable(dependency.to_string()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut eval_order_ref = eval_order.borrow_mut();
|
||||
eval_order_ref.push(node.name);
|
||||
let mut resolved_ref = resolved.borrow_mut();
|
||||
resolved_ref.insert(node.name);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -17,6 +17,11 @@
|
|||
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use crate::{renderer::RendererError, ExtensionOutput, Params, Scope, Value};
|
||||
use anyhow::Result;
|
||||
use log::error;
|
||||
use regex::Captures;
|
||||
|
||||
use super::VAR_REGEX;
|
||||
use std::collections::HashSet;
|
||||
|
||||
|
@ -29,10 +34,128 @@ pub(crate) fn get_body_variable_names(body: &str) -> HashSet<&str> {
|
|||
variables
|
||||
}
|
||||
|
||||
pub(crate) fn get_params_variable_names(params: &Params) -> HashSet<&str> {
|
||||
let mut names = HashSet::new();
|
||||
|
||||
for (_, value) in params.iter() {
|
||||
let local_names = get_value_variable_names_recursively(value);
|
||||
names.extend(local_names);
|
||||
}
|
||||
|
||||
names
|
||||
}
|
||||
|
||||
fn get_value_variable_names_recursively(value: &Value) -> HashSet<&str> {
|
||||
match value {
|
||||
Value::String(s_value) => get_body_variable_names(s_value),
|
||||
Value::Array(values) => {
|
||||
let mut local_names: HashSet<&str> = HashSet::new();
|
||||
for value in values {
|
||||
local_names.extend(get_value_variable_names_recursively(value));
|
||||
}
|
||||
local_names
|
||||
}
|
||||
Value::Object(fields) => {
|
||||
let mut local_names: HashSet<&str> = HashSet::new();
|
||||
for value in fields.values() {
|
||||
local_names.extend(get_value_variable_names_recursively(value));
|
||||
}
|
||||
local_names
|
||||
}
|
||||
_ => HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn render_variables(body: &str, scope: &Scope) -> Result<String> {
|
||||
let mut replacing_error = None;
|
||||
let output = VAR_REGEX
|
||||
.replace_all(body, |caps: &Captures| {
|
||||
let var_name = caps.name("name").unwrap().as_str();
|
||||
let var_subname = caps.name("subname");
|
||||
match scope.get(var_name) {
|
||||
Some(output) => match output {
|
||||
ExtensionOutput::Single(output) => output,
|
||||
ExtensionOutput::Multiple(results) => match var_subname {
|
||||
Some(var_subname) => {
|
||||
let var_subname = var_subname.as_str();
|
||||
results.get(var_subname).map_or("", |value| &*value)
|
||||
}
|
||||
None => {
|
||||
error!(
|
||||
"nested name missing from multi-value variable: {}",
|
||||
var_name
|
||||
);
|
||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||
"nested name missing from multi-value variable: {}",
|
||||
var_name
|
||||
)));
|
||||
""
|
||||
}
|
||||
},
|
||||
},
|
||||
None => {
|
||||
replacing_error = Some(RendererError::MissingVariable(format!(
|
||||
"variable '{}' is missing",
|
||||
var_name
|
||||
)));
|
||||
""
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string();
|
||||
|
||||
if let Some(error) = replacing_error {
|
||||
return Err(error.into());
|
||||
}
|
||||
|
||||
let unescaped_output = unescape_variable_inections(&output);
|
||||
Ok(unescaped_output)
|
||||
}
|
||||
|
||||
pub(crate) fn unescape_variable_inections(body: &str) -> String {
|
||||
body.replace("\\{\\{", "{{").replace("\\}\\}", "}}")
|
||||
}
|
||||
|
||||
pub(crate) fn inject_variables_into_params(params: &Params, scope: &Scope) -> Result<Params> {
|
||||
let mut params = params.clone();
|
||||
|
||||
for (_, value) in params.iter_mut() {
|
||||
inject_variables_into_value(value, scope)?;
|
||||
}
|
||||
|
||||
Ok(params)
|
||||
}
|
||||
|
||||
fn inject_variables_into_value(value: &mut Value, scope: &Scope) -> Result<()> {
|
||||
match value {
|
||||
Value::String(s_value) => {
|
||||
let new_value = render_variables(s_value, scope)?;
|
||||
|
||||
if &new_value != s_value {
|
||||
s_value.clear();
|
||||
s_value.push_str(&new_value);
|
||||
}
|
||||
}
|
||||
Value::Array(values) => {
|
||||
for value in values {
|
||||
inject_variables_into_value(value, scope)?;
|
||||
}
|
||||
}
|
||||
Value::Object(fields) => {
|
||||
for value in fields.values_mut() {
|
||||
inject_variables_into_value(value, scope)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::iter::FromIterator;
|
||||
use std::{collections::HashMap, iter::FromIterator};
|
||||
|
||||
#[test]
|
||||
fn get_body_variable_names_no_vars() {
|
||||
|
@ -49,4 +172,44 @@ mod tests {
|
|||
HashSet::from_iter(vec!["world", "greet"]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inject_variables_into_params() {
|
||||
let mut params = Params::new();
|
||||
params.insert(
|
||||
"field1".to_string(),
|
||||
Value::String("this contains {{first}}".to_string()),
|
||||
);
|
||||
params.insert("field2".to_string(), Value::Bool(true));
|
||||
params.insert(
|
||||
"field3".to_string(),
|
||||
Value::Array(vec![Value::String("this contains {{first}}".to_string())]),
|
||||
);
|
||||
|
||||
let mut nested = HashMap::new();
|
||||
nested.insert(
|
||||
"subfield1".to_string(),
|
||||
Value::String("also contains {{first}}".to_string()),
|
||||
);
|
||||
params.insert("field4".to_string(), Value::Object(nested));
|
||||
|
||||
let mut scope = Scope::new();
|
||||
scope.insert("first", ExtensionOutput::Single("one".to_string()));
|
||||
|
||||
let result = inject_variables_into_params(¶ms, &scope).unwrap();
|
||||
|
||||
assert_eq!(result.len(), 4);
|
||||
assert_eq!(
|
||||
result.get("field1").unwrap(),
|
||||
&Value::String("this contains one".to_string())
|
||||
);
|
||||
assert_eq!(result.get("field2").unwrap(), &Value::Bool(true));
|
||||
assert_eq!(
|
||||
result.get("field3").unwrap(),
|
||||
&Value::Array(vec![Value::String("this contains one".to_string())])
|
||||
);
|
||||
assert!(
|
||||
matches!(result.get("field4").unwrap(), Value::Object(fields) if fields.get("subfield1").unwrap() == &Value::String("also contains one".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "espanso"
|
||||
version = "2.0.5-alpha"
|
||||
version = "2.1.0-alpha"
|
||||
authors = ["Federico Terzi <federicoterzi96@gmail.com>"]
|
||||
license = "GPL-3.0"
|
||||
description = "Cross-platform Text Expander written in Rust"
|
||||
|
|
|
@ -63,14 +63,7 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
|||
.cloned(),
|
||||
values: params
|
||||
.get("values")
|
||||
.and_then(|val| val.as_array())
|
||||
.map(|arr| {
|
||||
arr
|
||||
.iter()
|
||||
.flat_map(|choice| choice.as_string())
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.and_then(|v| extract_values(v, params.get("trim_string_values")))
|
||||
.unwrap_or_default(),
|
||||
}),
|
||||
Some(Value::String(field_type)) if field_type == "list" => Some(FormField::List {
|
||||
|
@ -80,14 +73,7 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
|||
.cloned(),
|
||||
values: params
|
||||
.get("values")
|
||||
.and_then(|val| val.as_array())
|
||||
.map(|arr| {
|
||||
arr
|
||||
.iter()
|
||||
.flat_map(|choice| choice.as_string())
|
||||
.cloned()
|
||||
.collect()
|
||||
})
|
||||
.and_then(|v| extract_values(v, params.get("trim_string_values")))
|
||||
.unwrap_or_default(),
|
||||
}),
|
||||
// By default, it's considered type 'text'
|
||||
|
@ -113,3 +99,38 @@ fn convert_fields(fields: &Params) -> HashMap<String, FormField> {
|
|||
}
|
||||
out
|
||||
}
|
||||
|
||||
fn extract_values(value: &Value, trim_string_values: Option<&Value>) -> Option<Vec<String>> {
|
||||
let trim_string_values = *trim_string_values
|
||||
.and_then(|v| v.as_bool())
|
||||
.unwrap_or(&true);
|
||||
|
||||
match value {
|
||||
Value::Array(values) => Some(
|
||||
values
|
||||
.iter()
|
||||
.flat_map(|choice| choice.as_string())
|
||||
.cloned()
|
||||
.collect(),
|
||||
),
|
||||
Value::String(values) => Some(
|
||||
values
|
||||
.lines()
|
||||
.filter_map(|line| {
|
||||
if trim_string_values {
|
||||
let trimmed_line = line.trim();
|
||||
if !trimmed_line.is_empty() {
|
||||
Some(trimmed_line)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
Some(line)
|
||||
}
|
||||
})
|
||||
.map(String::from)
|
||||
.collect(),
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,6 +151,8 @@ fn convert_var(var: espanso_config::matches::Variable) -> espanso_render::Variab
|
|||
name: var.name,
|
||||
var_type: var.var_type,
|
||||
params: convert_params(var.params),
|
||||
inject_vars: var.inject_vars,
|
||||
depends_on: var.depends_on,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -225,6 +227,8 @@ impl<'a> Renderer<'a> for RendererAdapter<'a> {
|
|||
name,
|
||||
var_type: "echo".to_string(),
|
||||
params,
|
||||
inject_vars: false,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: espanso
|
||||
version: 2.0.5-alpha
|
||||
version: 2.1.0-alpha
|
||||
summary: A Cross-platform Text Expander written in Rust
|
||||
description: |
|
||||
espanso is a Cross-platform, Text Expander written in Rust.
|
||||
|
|
Loading…
Reference in New Issue
Block a user