Improve the RollingMatcher implementation

This commit is contained in:
Federico Terzi 2021-03-10 21:12:23 +01:00
parent 1103278ccd
commit 784e074795
5 changed files with 289 additions and 49 deletions

View File

@ -21,7 +21,7 @@
pub enum Event {
Key {
key: Key,
char: Option<String>
chars: Option<String>
},
VirtualSeparator,
}

View File

@ -18,6 +18,7 @@
*/
use std::any::Any;
use event::Event;
#[macro_use]
extern crate lazy_static;
@ -25,7 +26,6 @@ extern crate lazy_static;
pub mod event;
pub mod rolling;
pub trait Matcher {
// TODO: create suitable event type
fn process(&self, prev_state: Option<&dyn Any>, event: Option<bool>) -> (Box<dyn Any>, Vec<i32>);
pub trait Matcher<'a, State, Id> where Id: Clone {
fn process(&'a self, prev_state: Option<&'a State>, event: Event) -> (State, Vec<Id>);
}

View File

@ -18,44 +18,90 @@
*/
use unicase::UniCase;
use crate::Matcher;
use crate::event::{Event, Key};
use super::{RollingItem, RollingMatch, tree::{MatcherTreeNode, MatcherTreeRef}};
use super::tree::{MatcherTreeNode, MatcherTreeRef};
pub struct RollingMatcherState<'a, Id> {
nodes: Vec<&'a MatcherTreeNode<Id>>,
}
pub struct RollingMatcher {
impl <'a, Id> Default for RollingMatcherState<'a, Id> {
fn default() -> Self {
Self {
nodes: Vec::new(),
}
}
}
pub struct RollingMatcher<Id> {
char_word_separators: Vec<String>,
key_word_separators: Vec<Key>,
root: MatcherTreeNode,
root: MatcherTreeNode<Id>,
}
// impl Matcher for RollingMatcher {
// fn process(
// &self,
// prev_state: &dyn std::any::Any,
// event: Option<bool>,
// ) -> (Box<dyn std::any::Any>, Vec<i32>) {
// todo!()
// }
// }
impl RollingMatcher {
// TODO: to find the matches, we first call the `find_refs` to get the list of matching nodes
// then we scan them and if any of those references is of variant `Matches`, then we return those
// match ids, otherwise None
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>>,
event: Event,
) -> (RollingMatcherState<'a, Id>, Vec<Id>) {
let mut next_refs = Vec::new();
// First compute the old refs
if let Some(prev_state) = prev_state {
for node_ref in prev_state.nodes.iter() {
next_refs.extend(self.find_refs(node_ref, &event));
}
}
// Calculate new ones
let root_refs = self.find_refs(&self.root, &event);
next_refs.extend(root_refs);
let mut next_nodes = Vec::new();
for node_ref in next_refs {
match node_ref {
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,
};
(current_state, Vec::new())
}
}
impl <Id> RollingMatcher<Id> {
pub fn new(matches: &[RollingMatch<Id>]) -> Self {
todo!()
// Self {
// }
}
// TODO: test
fn find_refs<'a>(&'a self, node: &'a MatcherTreeNode, event: &Event) -> Vec<&'a MatcherTreeRef> {
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, char } = event {
if let Event::Key { key, chars } = event {
// Key matching
if let Some((_, node_ref)) = node.keys.iter().find(|(_key, _)| _key == key) {
refs.push(node_ref);
}
if let Some(char) = char {
if let Some(char) = chars {
// Char matching
if let Some((_, node_ref)) = node.chars.iter().find(|(_char, _)| _char == char) {
refs.push(node_ref);
@ -84,10 +130,10 @@ impl RollingMatcher {
fn is_word_separator(&self, event: &Event) -> bool {
match event {
Event::Key { key, char } => {
Event::Key { key, chars } => {
if self.key_word_separators.contains(&key) {
true
} else if let Some(char) = char {
} else if let Some(char) = chars {
self.char_word_separators.contains(&char)
} else {
false
@ -104,7 +150,36 @@ mod tests {
use super::*;
#[test]
fn 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()
}))
)
],
..Default::default()
};
let matcher = RollingMatcher {
char_word_separators: vec![".".to_owned()],
key_word_separators: vec![Key::ArrowUp],
root,
};
let (state, matches) = matcher.process(None, Event::Key {
key: Key::Other,
chars: Some("h".to_string()),
});
assert_eq!(matches.len(), 0);
let (state, matches) = matcher.process(Some(&state), Event::Key {
key: Key::Other,
chars: Some("i".to_string()),
});
assert_eq!(matches, vec![1]);
}
}

View File

@ -1,7 +1,7 @@
/*
* This file is part of espanso.
*
* Copyright (C) 2019-2021 Federico Terzi
* Copyright (C) 2019-202 case_insensitive: (), preserve_case_markers: (), left_word: (), right_word: ()1 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
@ -17,19 +17,173 @@
* along with espanso. If not, see <https://www.gnu.org/licenses/>.
*/
use self::item::RollingItem;
use crate::event::Key;
mod item;
mod tree;
mod matcher;
mod tree;
pub struct RollingMatch {
id: i32,
#[derive(Debug, PartialEq)]
pub enum RollingItem {
WordSeparator,
Key(Key),
Char(String),
CharInsensitive(String),
}
#[derive(Debug, PartialEq)]
pub struct RollingMatch<Id> {
id: Id,
items: Vec<RollingItem>,
}
impl RollingMatch {
pub fn new(id: i32, items: Vec<RollingItem>) -> Self {
impl<Id> RollingMatch<Id> {
pub fn new(id: Id, items: Vec<RollingItem>) -> Self {
Self { id, items }
}
pub fn from_string(id: Id, string: &str, opt: &StringMatchOptions) -> Self {
let mut items = Vec::new();
if opt.left_word {
items.push(RollingItem::WordSeparator);
}
for (index, c) in string.chars().enumerate() {
if opt.case_insensitive {
// If preserve case markers is true, we want to keep the first two chars
// as case-sensitive. This is needed to implement the "propagate_case" option.
if opt.preserve_case_markers && index < 2 {
items.push(RollingItem::Char(c.to_string()))
} else {
items.push(RollingItem::CharInsensitive(c.to_string()))
}
} else {
items.push(RollingItem::Char(c.to_string()))
}
}
if opt.right_word {
items.push(RollingItem::WordSeparator);
}
Self { id, items }
}
}
pub struct StringMatchOptions {
case_insensitive: bool,
preserve_case_markers: bool,
left_word: bool,
right_word: bool,
}
impl Default for StringMatchOptions {
fn default() -> Self {
Self {
case_insensitive: false,
preserve_case_markers: false,
left_word: false,
right_word: false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_from_string_base_case() {
assert_eq!(
RollingMatch::from_string(1, "test", &StringMatchOptions::default()),
RollingMatch {
id: 1,
items: vec![
RollingItem::Char("t".to_string()),
RollingItem::Char("e".to_string()),
RollingItem::Char("s".to_string()),
RollingItem::Char("t".to_string()),
]
}
)
}
#[test]
fn test_match_from_string_left_word() {
assert_eq!(
RollingMatch::from_string(1, "test", &StringMatchOptions {
left_word: true,
..Default::default()
}),
RollingMatch {
id: 1,
items: vec![
RollingItem::WordSeparator,
RollingItem::Char("t".to_string()),
RollingItem::Char("e".to_string()),
RollingItem::Char("s".to_string()),
RollingItem::Char("t".to_string()),
]
}
)
}
#[test]
fn test_match_from_string_right_word() {
assert_eq!(
RollingMatch::from_string(1, "test", &StringMatchOptions {
right_word: true,
..Default::default()
}),
RollingMatch {
id: 1,
items: vec![
RollingItem::Char("t".to_string()),
RollingItem::Char("e".to_string()),
RollingItem::Char("s".to_string()),
RollingItem::Char("t".to_string()),
RollingItem::WordSeparator,
]
}
)
}
#[test]
fn test_match_from_string_case_insensitive() {
assert_eq!(
RollingMatch::from_string(1, "test", &StringMatchOptions {
case_insensitive: true,
..Default::default()
}),
RollingMatch {
id: 1,
items: vec![
RollingItem::CharInsensitive("t".to_string()),
RollingItem::CharInsensitive("e".to_string()),
RollingItem::CharInsensitive("s".to_string()),
RollingItem::CharInsensitive("t".to_string()),
]
}
)
}
#[test]
fn test_match_from_string_preserve_case_markers() {
assert_eq!(
RollingMatch::from_string(1, "test", &StringMatchOptions {
case_insensitive: true,
preserve_case_markers: true,
..Default::default()
}),
RollingMatch {
id: 1,
items: vec![
RollingItem::Char("t".to_string()),
RollingItem::Char("e".to_string()),
RollingItem::CharInsensitive("s".to_string()),
RollingItem::CharInsensitive("t".to_string()),
]
}
)
}
}

View File

@ -21,26 +21,37 @@ use unicase::UniCase;
use crate::event::Key;
use super::item::RollingItem;
use super::RollingItem;
#[derive(Debug)]
pub(crate) enum MatcherTreeRef {
Matches(Vec<i32>),
Node(Box<MatcherTreeNode>),
pub(crate) enum MatcherTreeRef<Id> {
Matches(Vec<Id>),
Node(Box<MatcherTreeNode<Id>>),
}
#[derive(Debug)]
pub(crate) struct MatcherTreeNode {
pub word_separators: Option<MatcherTreeRef>,
pub keys: Vec<(Key, MatcherTreeRef)>,
pub chars: Vec<(String, MatcherTreeRef)>,
pub chars_insensitive: Vec<(UniCase<String>, MatcherTreeRef)>,
pub(crate) struct MatcherTreeNode<Id> {
pub word_separators: Option<MatcherTreeRef<Id>>,
pub keys: Vec<(Key, MatcherTreeRef<Id>)>,
pub chars: Vec<(String, MatcherTreeRef<Id>)>,
pub chars_insensitive: Vec<(UniCase<String>, MatcherTreeRef<Id>)>,
}
impl MatcherTreeNode {
// TODO: test
pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode {
todo!()
impl <Id> Default for MatcherTreeNode<Id> {
fn default() -> Self {
Self {
word_separators: None,
keys: Vec::new(),
chars: Vec::new(),
chars_insensitive: Vec::new(),
}
}
}
impl <Id> MatcherTreeNode<Id> {
// TODO: test
pub fn from_items(items: &[RollingItem]) -> MatcherTreeNode<Id> {
// TODO: implement the tree building algorithm
todo!()
}
}