Finalize first draft of RollingMatcher

This commit is contained in:
Federico Terzi 2021-03-11 21:16:18 +01:00
parent caf72c9aef
commit b2f28bb739
6 changed files with 458 additions and 89 deletions

View File

@ -26,7 +26,7 @@ pub enum Event {
VirtualSeparator,
}
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum Key {
// Modifiers
Alt,

View File

@ -17,7 +17,6 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use std::any::Any;
use event::Event;
#[macro_use]
@ -27,5 +26,5 @@ pub mod event;
pub mod rolling;
pub trait Matcher<'a, State, Id> where Id: Clone {
fn process(&'a self, prev_state: Option<&'a State>, event: Event) -> (State, Vec<Id>);
fn process(&'a self, prev_state: Option<&State>, event: Event) -> (State, Vec<Id>);
}

View File

@ -1,28 +0,0 @@
use crate::event::Key;
/*
* 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/>.
*/
pub enum RollingItem {
WordSeparator,
Key(Key),
Char(String),
CharInsensitive(String),
}

View File

@ -17,19 +17,35 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use unicase::UniCase;
use crate::Matcher;
use super::{
tree::{MatcherTreeNode, MatcherTreeRef},
RollingMatch,
};
use crate::event::{Event, Key};
use super::{RollingItem, RollingMatch, tree::{MatcherTreeNode, MatcherTreeRef}};
use crate::Matcher;
use unicase::UniCase;
#[derive(Clone)]
pub struct RollingMatcherState<'a, Id> {
nodes: Vec<&'a MatcherTreeNode<Id>>,
}
impl <'a, Id> Default for RollingMatcherState<'a, Id> {
impl<'a, Id> Default for RollingMatcherState<'a, Id> {
fn default() -> Self {
Self { nodes: Vec::new() }
}
}
pub struct RollingMatcherOptions {
char_word_separators: Vec<String>,
key_word_separators: Vec<Key>,
}
impl Default for RollingMatcherOptions {
fn default() -> Self {
Self {
nodes: Vec::new(),
char_word_separators: Vec::new(),
key_word_separators: Vec::new(),
}
}
}
@ -41,11 +57,13 @@ pub struct RollingMatcher<Id> {
root: MatcherTreeNode<Id>,
}
impl <'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher<Id> where Id: Clone {
impl<'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher<Id>
where
Id: Clone,
{
fn process(
&'a self,
prev_state: Option<&'a RollingMatcherState<'a, Id>>,
prev_state: Option<&RollingMatcherState<'a, Id>>,
event: Event,
) -> (RollingMatcherState<'a, Id>, Vec<Id>) {
let mut next_refs = Vec::new();
@ -68,31 +86,34 @@ impl <'a, Id> Matcher<'a, RollingMatcherState<'a, Id>, Id> for RollingMatcher<Id
MatcherTreeRef::Matches(matches) => {
// Reset the state and return the matches
return (RollingMatcherState::default(), matches.clone());
},
}
MatcherTreeRef::Node(node) => {
next_nodes.push(node.as_ref());
}
}
}
let current_state = RollingMatcherState {
nodes: next_nodes,
};
let current_state = RollingMatcherState { nodes: next_nodes };
(current_state, Vec::new())
}
}
impl <Id> RollingMatcher<Id> {
pub fn new(matches: &[RollingMatch<Id>]) -> Self {
todo!()
// Self {
// }
impl<Id: Clone> RollingMatcher<Id> {
pub fn new(matches: &[RollingMatch<Id>], opt: RollingMatcherOptions) -> Self {
let root = MatcherTreeNode::from_matches(matches);
Self {
root,
char_word_separators: opt.char_word_separators,
key_word_separators: opt.key_word_separators,
}
}
// TODO: test
fn find_refs<'a>(&'a self, node: &'a MatcherTreeNode<Id>, event: &Event) -> Vec<&'a MatcherTreeRef<Id>> {
fn find_refs<'a>(
&'a self,
node: &'a MatcherTreeNode<Id>,
event: &Event,
) -> Vec<&'a MatcherTreeRef<Id>> {
let mut refs = Vec::new();
if let Event::Key { key, chars } = event {
@ -140,7 +161,6 @@ impl <Id> RollingMatcher<Id> {
}
}
Event::VirtualSeparator => true,
_ => false,
}
}
}
@ -148,38 +168,107 @@ impl <Id> RollingMatcher<Id> {
#[cfg(test)]
mod tests {
use super::*;
use crate::rolling::{StringMatchOptions};
fn get_matches_after_str<Id: Clone>(string: &str, matcher: &RollingMatcher<Id>) -> Vec<Id> {
let mut prev_state = None;
let mut matches = Vec::new();
for c in string.chars() {
let (state, _matches) = matcher.process(
prev_state.as_ref(),
Event::Key {
key: Key::Other,
chars: Some(c.to_string()),
},
);
prev_state = Some(state);
matches = _matches;
}
matches
}
#[test]
fn test() { // TODO: convert to actual test
let root = MatcherTreeNode {
chars: vec![
("h".to_string(), MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![
("i".to_string(), MatcherTreeRef::Matches(vec![1])),
],
..Default::default()
}))
)
fn matcher_process_simple_strings() {
let matcher = RollingMatcher::new(
&[
RollingMatch::from_string(1, "hi", &StringMatchOptions::default()),
RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
RollingMatch::from_string(3, "my", &StringMatchOptions::default()),
RollingMatch::from_string(4, "myself", &StringMatchOptions::default()),
RollingMatch::from_string(5, "hi", &StringMatchOptions::default()),
],
..Default::default()
};
RollingMatcherOptions {
..Default::default()
},
);
let matcher = RollingMatcher {
char_word_separators: vec![".".to_owned()],
key_word_separators: vec![Key::ArrowUp],
root,
};
assert_eq!(get_matches_after_str("hi", &matcher), vec![1, 5]);
assert_eq!(get_matches_after_str("my", &matcher), vec![3]);
assert_eq!(get_matches_after_str("invalid", &matcher), vec![]);
}
let (state, matches) = matcher.process(None, Event::Key {
key: Key::Other,
chars: Some("h".to_string()),
});
assert_eq!(matches.len(), 0);
#[test]
fn matcher_process_word_matches() {
let matcher = RollingMatcher::new(
&[
RollingMatch::from_string(
1,
"hi",
&StringMatchOptions {
left_word: true,
right_word: true,
..Default::default()
},
),
RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
],
RollingMatcherOptions {
char_word_separators: vec![".".to_string(), ",".to_string()],
..Default::default()
},
);
let (state, matches) = matcher.process(Some(&state), Event::Key {
key: Key::Other,
chars: Some("i".to_string()),
});
assert_eq!(matches, vec![1]);
assert_eq!(get_matches_after_str("hi", &matcher), vec![]);
assert_eq!(get_matches_after_str(".hi,", &matcher), vec![1]);
}
#[test]
fn matcher_process_case_insensitive() {
let matcher = RollingMatcher::new(
&[
RollingMatch::from_string(
1,
"hi",
&StringMatchOptions {
case_insensitive: true,
..Default::default()
},
),
RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
RollingMatch::from_string(
3,
"arty",
&StringMatchOptions {
case_insensitive: true,
preserve_case_markers: true,
..Default::default()
},
),
],
RollingMatcherOptions {
char_word_separators: vec![".".to_string(), ",".to_string()],
..Default::default()
},
);
assert_eq!(get_matches_after_str("hi", &matcher), vec![1]);
assert_eq!(get_matches_after_str("Hi", &matcher), vec![1]);
assert_eq!(get_matches_after_str("HI", &matcher), vec![1]);
assert_eq!(get_matches_after_str("arty", &matcher), vec![3]);
assert_eq!(get_matches_after_str("arTY", &matcher), vec![3]);
assert_eq!(get_matches_after_str("ARTY", &matcher), vec![]);
}
}

View File

@ -22,7 +22,7 @@ use crate::event::Key;
mod matcher;
mod tree;
#[derive(Debug, PartialEq)]
#[derive(Debug, Clone, PartialEq)]
pub enum RollingItem {
WordSeparator,
Key(Key),
@ -68,6 +68,13 @@ impl<Id> RollingMatch<Id> {
Self { id, items }
}
pub fn from_items(id: Id, items: &[RollingItem]) -> Self {
Self {
id,
items: items.to_vec(),
}
}
}
pub struct StringMatchOptions {

View File

@ -21,15 +21,15 @@ use unicase::UniCase;
use crate::event::Key;
use super::RollingItem;
use super::{RollingItem, RollingMatch};
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub(crate) enum MatcherTreeRef<Id> {
Matches(Vec<Id>),
Node(Box<MatcherTreeNode<Id>>),
}
#[derive(Debug)]
#[derive(Debug, PartialEq)]
pub(crate) struct MatcherTreeNode<Id> {
pub word_separators: Option<MatcherTreeRef<Id>>,
pub keys: Vec<(Key, MatcherTreeRef<Id>)>,
@ -37,7 +37,7 @@ pub(crate) struct MatcherTreeNode<Id> {
pub chars_insensitive: Vec<(UniCase<String>, MatcherTreeRef<Id>)>,
}
impl <Id> Default for MatcherTreeNode<Id> {
impl<Id> Default for MatcherTreeNode<Id> {
fn default() -> Self {
Self {
word_separators: None,
@ -48,10 +48,312 @@ impl <Id> Default for MatcherTreeNode<Id> {
}
}
impl <Id> MatcherTreeNode<Id> {
// TODO: test
pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode<Id> {
// TODO: implement the tree building algorithm
todo!()
impl<Id> MatcherTreeNode<Id>
where
Id: Clone,
{
pub fn from_matches(matches: &[RollingMatch<Id>]) -> MatcherTreeNode<Id> {
let mut root = MatcherTreeNode::default();
for m in matches {
insert_items_recursively(m.id.clone(), &mut root, &m.items);
}
root
}
}
fn insert_items_recursively<Id>(id: Id, node: &mut MatcherTreeNode<Id>, items: &[RollingItem]) {
if items.is_empty() {
return;
}
if items.len() == 1 {
let item = items.get(0).unwrap();
match item {
RollingItem::WordSeparator => {
let mut new_matches = Vec::new();
if let Some(node_ref) = node.word_separators.take() {
if let MatcherTreeRef::Matches(matches) = node_ref {
new_matches.extend(matches);
}
}
new_matches.push(id);
node.word_separators = Some(MatcherTreeRef::Matches(new_matches))
}
RollingItem::Key(key) => {
if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) {
if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
matches.push(id)
} else {
entry.1 = MatcherTreeRef::Matches(vec![id])
};
} else {
node
.keys
.push((key.clone(), MatcherTreeRef::Matches(vec![id])));
}
}
RollingItem::Char(c) => {
if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) {
if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
matches.push(id)
} else {
entry.1 = MatcherTreeRef::Matches(vec![id])
};
} else {
node
.chars
.push((c.clone(), MatcherTreeRef::Matches(vec![id])));
}
}
RollingItem::CharInsensitive(c) => {
let uni_char = UniCase::new(c.clone());
if let Some(entry) = node
.chars_insensitive
.iter_mut()
.find(|(_c, _)| _c == &uni_char)
{
if let MatcherTreeRef::Matches(matches) = &mut entry.1 {
matches.push(id)
} else {
entry.1 = MatcherTreeRef::Matches(vec![id])
};
} else {
node
.chars_insensitive
.push((uni_char, MatcherTreeRef::Matches(vec![id])));
}
}
}
} else {
let item = items.get(0).unwrap();
match item {
RollingItem::WordSeparator => match node.word_separators.as_mut() {
Some(MatcherTreeRef::Node(next_node)) => {
insert_items_recursively(id, next_node.as_mut(), &items[1..])
}
None => {
let mut next_node = Box::new(MatcherTreeNode::default());
insert_items_recursively(id, next_node.as_mut(), &items[1..]);
node.word_separators = Some(MatcherTreeRef::Node(next_node));
}
_ => {}
},
RollingItem::Key(key) => {
if let Some(entry) = node.keys.iter_mut().find(|(_key, _)| _key == key) {
if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
insert_items_recursively(id, next_node, &items[1..])
}
} else {
let mut next_node = Box::new(MatcherTreeNode::default());
insert_items_recursively(id, next_node.as_mut(), &items[1..]);
node
.keys
.push((key.clone(), MatcherTreeRef::Node(next_node)));
}
}
RollingItem::Char(c) => {
if let Some(entry) = node.chars.iter_mut().find(|(_c, _)| _c == c) {
if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
insert_items_recursively(id, next_node, &items[1..])
}
} else {
let mut next_node = Box::new(MatcherTreeNode::default());
insert_items_recursively(id, next_node.as_mut(), &items[1..]);
node
.chars
.push((c.clone(), MatcherTreeRef::Node(next_node)));
}
}
RollingItem::CharInsensitive(c) => {
let uni_char = UniCase::new(c.clone());
if let Some(entry) = node
.chars_insensitive
.iter_mut()
.find(|(_c, _)| _c == &uni_char)
{
if let MatcherTreeRef::Node(next_node) = &mut entry.1 {
insert_items_recursively(id, next_node, &items[1..])
}
} else {
let mut next_node = Box::new(MatcherTreeNode::default());
insert_items_recursively(id, next_node.as_mut(), &items[1..]);
node
.chars_insensitive
.push((uni_char, MatcherTreeRef::Node(next_node)));
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rolling::StringMatchOptions;
#[test]
fn generate_tree_from_items_simple_strings() {
let root = MatcherTreeNode::from_matches(&[
RollingMatch::from_string(1, "hi", &StringMatchOptions::default()),
RollingMatch::from_string(2, "hey", &StringMatchOptions::default()),
RollingMatch::from_string(3, "my", &StringMatchOptions::default()),
RollingMatch::from_string(4, "myself", &StringMatchOptions::default()),
]);
assert_eq!(
root,
MatcherTreeNode {
chars: vec![
(
"h".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![
("i".to_string(), MatcherTreeRef::Matches(vec![1])),
(
"e".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2]))],
..Default::default()
}))
),
],
..Default::default()
}))
),
(
"m".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![3])),],
..Default::default()
}))
)
],
..Default::default()
}
)
}
#[test]
fn generate_tree_from_items_keys() {
let root = MatcherTreeNode::from_matches(&[
RollingMatch::from_items(
1,
&[
RollingItem::Key(Key::ArrowUp),
RollingItem::Key(Key::ArrowLeft),
],
),
RollingMatch::from_items(
2,
&[
RollingItem::Key(Key::ArrowUp),
RollingItem::Key(Key::ArrowRight),
],
),
]);
assert_eq!(
root,
MatcherTreeNode {
keys: vec![(
Key::ArrowUp,
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
keys: vec![
(Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])),
(Key::ArrowRight, MatcherTreeRef::Matches(vec![2])),
],
..Default::default()
}))
),],
..Default::default()
}
)
}
#[test]
fn generate_tree_from_items_mixed() {
let root = MatcherTreeNode::from_matches(&[
RollingMatch::from_items(
1,
&[
RollingItem::Key(Key::ArrowUp),
RollingItem::Key(Key::ArrowLeft),
],
),
RollingMatch::from_string(
2,
"my",
&StringMatchOptions {
left_word: true,
..Default::default()
},
),
RollingMatch::from_string(
3,
"hi",
&StringMatchOptions {
left_word: true,
right_word: true,
..Default::default()
},
),
RollingMatch::from_string(
4,
"no",
&StringMatchOptions {
case_insensitive: true,
..Default::default()
},
),
]);
assert_eq!(
root,
MatcherTreeNode {
keys: vec![(
Key::ArrowUp,
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
keys: vec![(Key::ArrowLeft, MatcherTreeRef::Matches(vec![1])),],
..Default::default()
}))
),],
word_separators: Some(MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![
(
"m".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![("y".to_string(), MatcherTreeRef::Matches(vec![2])),],
..Default::default()
}))
),
(
"h".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars: vec![(
"i".to_string(),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
word_separators: Some(MatcherTreeRef::Matches(vec![3])),
..Default::default()
}))
),],
..Default::default()
}))
),
],
..Default::default()
}))),
chars_insensitive: vec![(
UniCase::new("n".to_string()),
MatcherTreeRef::Node(Box::new(MatcherTreeNode {
chars_insensitive: vec![(
UniCase::new("o".to_string()),
MatcherTreeRef::Matches(vec![4])
),],
..Default::default()
}))
),],
..Default::default()
}
)
}
}