739 lines
17 KiB
Rust
739 lines
17 KiB
Rust
/*
|
|
* 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 super::{MatchSet, MatchStore};
|
|
use crate::{
|
|
counter::StructId,
|
|
error::NonFatalErrorSet,
|
|
matches::{group::MatchGroup, Match, Variable},
|
|
};
|
|
use anyhow::Context;
|
|
use std::{
|
|
collections::{HashMap, HashSet},
|
|
path::PathBuf,
|
|
};
|
|
|
|
pub(crate) struct DefaultMatchStore {
|
|
pub groups: HashMap<String, MatchGroup>,
|
|
}
|
|
|
|
impl DefaultMatchStore {
|
|
pub fn load(paths: &[String]) -> (Self, Vec<NonFatalErrorSet>) {
|
|
let mut groups = HashMap::new();
|
|
let mut non_fatal_error_sets = Vec::new();
|
|
|
|
// Because match groups can imports other match groups,
|
|
// we have to load them recursively starting from the
|
|
// top-level ones.
|
|
load_match_groups_recursively(&mut groups, paths, &mut non_fatal_error_sets);
|
|
|
|
(Self { groups }, non_fatal_error_sets)
|
|
}
|
|
}
|
|
|
|
impl MatchStore for DefaultMatchStore {
|
|
fn query(&self, paths: &[String]) -> MatchSet {
|
|
let mut matches: Vec<&Match> = Vec::new();
|
|
let mut global_vars: Vec<&Variable> = Vec::new();
|
|
let mut visited_paths = HashSet::new();
|
|
let mut visited_matches = HashSet::new();
|
|
let mut visited_global_vars = HashSet::new();
|
|
|
|
query_matches_for_paths(
|
|
&self.groups,
|
|
&mut visited_paths,
|
|
&mut visited_matches,
|
|
&mut visited_global_vars,
|
|
&mut matches,
|
|
&mut global_vars,
|
|
paths,
|
|
);
|
|
|
|
MatchSet {
|
|
matches,
|
|
global_vars,
|
|
}
|
|
}
|
|
|
|
fn loaded_paths(&self) -> Vec<String> {
|
|
self.groups.keys().cloned().collect()
|
|
}
|
|
}
|
|
|
|
fn load_match_groups_recursively(
|
|
groups: &mut HashMap<String, MatchGroup>,
|
|
paths: &[String],
|
|
non_fatal_error_sets: &mut Vec<NonFatalErrorSet>,
|
|
) {
|
|
for path in paths.iter() {
|
|
if !groups.contains_key(path) {
|
|
let group_path = PathBuf::from(path);
|
|
match MatchGroup::load(&group_path)
|
|
.with_context(|| format!("unable to load match group {:?}", group_path))
|
|
{
|
|
Ok((group, non_fatal_error_set)) => {
|
|
let imports = group.imports.clone();
|
|
groups.insert(path.clone(), group);
|
|
|
|
if let Some(non_fatal_error_set) = non_fatal_error_set {
|
|
non_fatal_error_sets.push(non_fatal_error_set);
|
|
}
|
|
|
|
load_match_groups_recursively(groups, &imports, non_fatal_error_sets);
|
|
}
|
|
Err(err) => {
|
|
non_fatal_error_sets.push(NonFatalErrorSet::single_error(&group_path, err));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn query_matches_for_paths<'a>(
|
|
groups: &'a HashMap<String, MatchGroup>,
|
|
visited_paths: &mut HashSet<String>,
|
|
visited_matches: &mut HashSet<StructId>,
|
|
visited_global_vars: &mut HashSet<StructId>,
|
|
matches: &mut Vec<&'a Match>,
|
|
global_vars: &mut Vec<&'a Variable>,
|
|
paths: &[String],
|
|
) {
|
|
for path in paths.iter() {
|
|
if !visited_paths.contains(path) {
|
|
visited_paths.insert(path.clone());
|
|
|
|
if let Some(group) = groups.get(path) {
|
|
query_matches_for_paths(
|
|
groups,
|
|
visited_paths,
|
|
visited_matches,
|
|
visited_global_vars,
|
|
matches,
|
|
global_vars,
|
|
&group.imports,
|
|
);
|
|
|
|
for m in group.matches.iter() {
|
|
if !visited_matches.contains(&m.id) {
|
|
matches.push(m);
|
|
visited_matches.insert(m.id);
|
|
}
|
|
}
|
|
|
|
for var in group.global_vars.iter() {
|
|
if !visited_global_vars.contains(&var.id) {
|
|
global_vars.push(var);
|
|
visited_global_vars.insert(var.id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{
|
|
matches::{MatchCause, MatchEffect, TextEffect, TriggerCause},
|
|
util::tests::use_test_directory,
|
|
};
|
|
use std::fs::create_dir_all;
|
|
|
|
fn create_match(trigger: &str, replace: &str) -> Match {
|
|
Match {
|
|
cause: MatchCause::Trigger(TriggerCause {
|
|
triggers: vec![trigger.to_string()],
|
|
..Default::default()
|
|
}),
|
|
effect: MatchEffect::Text(TextEffect {
|
|
replace: replace.to_string(),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_matches(matches: &[(&str, &str)]) -> Vec<Match> {
|
|
matches
|
|
.iter()
|
|
.map(|(trigger, replace)| create_match(trigger, replace))
|
|
.collect()
|
|
}
|
|
|
|
fn create_test_var(name: &str) -> Variable {
|
|
Variable {
|
|
name: name.to_string(),
|
|
var_type: "test".to_string(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn create_vars(vars: &[&str]) -> Vec<Variable> {
|
|
vars.iter().map(|var| create_test_var(var)).collect()
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_loads_correctly() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
imports:
|
|
- "sub/sub.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) =
|
|
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
assert_eq!(match_store.groups.len(), 3);
|
|
|
|
let base_group = &match_store
|
|
.groups
|
|
.get(&base_file.to_string_lossy().to_string())
|
|
.unwrap()
|
|
.matches;
|
|
let base_group: Vec<Match> = base_group
|
|
.iter()
|
|
.map(|m| {
|
|
let mut copy = m.clone();
|
|
copy.id = 0;
|
|
copy
|
|
})
|
|
.collect();
|
|
|
|
assert_eq!(base_group, create_matches(&[("hello", "world")]));
|
|
|
|
let another_group = &match_store
|
|
.groups
|
|
.get(&another_file.to_string_lossy().to_string())
|
|
.unwrap()
|
|
.matches;
|
|
let another_group: Vec<Match> = another_group
|
|
.iter()
|
|
.map(|m| {
|
|
let mut copy = m.clone();
|
|
copy.id = 0;
|
|
copy
|
|
})
|
|
.collect();
|
|
assert_eq!(
|
|
another_group,
|
|
create_matches(&[("hello", "world2"), ("foo", "bar")])
|
|
);
|
|
|
|
let sub_group = &match_store
|
|
.groups
|
|
.get(&sub_file.to_string_lossy().to_string())
|
|
.unwrap()
|
|
.matches;
|
|
let sub_group: Vec<Match> = sub_group
|
|
.iter()
|
|
.map(|m| {
|
|
let mut copy = m.clone();
|
|
copy.id = 0;
|
|
copy
|
|
})
|
|
.collect();
|
|
assert_eq!(sub_group, create_matches(&[("hello", "world3")]));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_handles_circular_dependency() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
imports:
|
|
- "sub/sub.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
imports:
|
|
- "../_another.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) =
|
|
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
|
|
|
|
assert_eq!(match_store.groups.len(), 3);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_query_single_path_with_imports() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
global_vars:
|
|
- name: var1
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
imports:
|
|
- "sub/sub.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
global_vars:
|
|
- name: var2
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) =
|
|
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
|
|
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.matches
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut m| {
|
|
m.id = 0;
|
|
m
|
|
})
|
|
.collect::<Vec<Match>>(),
|
|
create_matches(&[
|
|
("hello", "world3"),
|
|
("hello", "world2"),
|
|
("foo", "bar"),
|
|
("hello", "world"),
|
|
])
|
|
);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.global_vars
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut v| {
|
|
v.id = 0;
|
|
v
|
|
})
|
|
.collect::<Vec<Variable>>(),
|
|
create_vars(&["var2", "var1"])
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_query_handles_circular_depencencies() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
global_vars:
|
|
- name: var1
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
imports:
|
|
- "sub/sub.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
imports:
|
|
- "../_another.yml" # Circular import
|
|
|
|
global_vars:
|
|
- name: var2
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) =
|
|
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
|
|
let match_set = match_store.query(&[base_file.to_string_lossy().to_string()]);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.matches
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut m| {
|
|
m.id = 0;
|
|
m
|
|
})
|
|
.collect::<Vec<Match>>(),
|
|
create_matches(&[
|
|
("hello", "world3"),
|
|
("hello", "world2"),
|
|
("foo", "bar"),
|
|
("hello", "world"),
|
|
])
|
|
);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.global_vars
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut v| {
|
|
v.id = 0;
|
|
v
|
|
})
|
|
.collect::<Vec<Variable>>(),
|
|
create_vars(&["var2", "var1"])
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_query_multiple_paths() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
global_vars:
|
|
- name: var1
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
global_vars:
|
|
- name: var2
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) = DefaultMatchStore::load(&[
|
|
base_file.to_string_lossy().to_string(),
|
|
sub_file.to_string_lossy().to_string(),
|
|
]);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
|
|
let match_set = match_store.query(&[
|
|
base_file.to_string_lossy().to_string(),
|
|
sub_file.to_string_lossy().to_string(),
|
|
]);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.matches
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut m| {
|
|
m.id = 0;
|
|
m
|
|
})
|
|
.collect::<Vec<Match>>(),
|
|
create_matches(&[
|
|
("hello", "world2"),
|
|
("foo", "bar"),
|
|
("hello", "world"),
|
|
("hello", "world3"),
|
|
])
|
|
);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.global_vars
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut v| {
|
|
v.id = 0;
|
|
v
|
|
})
|
|
.collect::<Vec<Variable>>(),
|
|
create_vars(&["var1", "var2"])
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn match_store_query_handle_duplicates_when_imports_and_paths_overlap() {
|
|
use_test_directory(|_, match_dir, _| {
|
|
let sub_dir = match_dir.join("sub");
|
|
create_dir_all(&sub_dir).unwrap();
|
|
|
|
let base_file = match_dir.join("base.yml");
|
|
std::fs::write(
|
|
&base_file,
|
|
r#"
|
|
imports:
|
|
- "_another.yml"
|
|
|
|
global_vars:
|
|
- name: var1
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let another_file = match_dir.join("_another.yml");
|
|
std::fs::write(
|
|
&another_file,
|
|
r#"
|
|
imports:
|
|
- "sub/sub.yml"
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world2"
|
|
- trigger: "foo"
|
|
replace: "bar"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let sub_file = sub_dir.join("sub.yml");
|
|
std::fs::write(
|
|
&sub_file,
|
|
r#"
|
|
global_vars:
|
|
- name: var2
|
|
type: test
|
|
|
|
matches:
|
|
- trigger: "hello"
|
|
replace: "world3"
|
|
"#,
|
|
)
|
|
.unwrap();
|
|
|
|
let (match_store, non_fatal_error_sets) =
|
|
DefaultMatchStore::load(&[base_file.to_string_lossy().to_string()]);
|
|
assert_eq!(non_fatal_error_sets.len(), 0);
|
|
|
|
let match_set = match_store.query(&[
|
|
base_file.to_string_lossy().to_string(),
|
|
sub_file.to_string_lossy().to_string(),
|
|
]);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.matches
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut m| {
|
|
m.id = 0;
|
|
m
|
|
})
|
|
.collect::<Vec<Match>>(),
|
|
create_matches(&[
|
|
("hello", "world3"), // This appears only once, though it appears 2 times
|
|
("hello", "world2"),
|
|
("foo", "bar"),
|
|
("hello", "world"),
|
|
])
|
|
);
|
|
|
|
assert_eq!(
|
|
match_set
|
|
.global_vars
|
|
.into_iter()
|
|
.cloned()
|
|
.map(|mut v| {
|
|
v.id = 0;
|
|
v
|
|
})
|
|
.collect::<Vec<Variable>>(),
|
|
create_vars(&["var2", "var1"])
|
|
);
|
|
});
|
|
}
|
|
|
|
// TODO: add fatal and non-fatal error cases
|
|
}
|