First draft of the word separation feature, proposed in issue #82
This commit is contained in:
parent
bddf5bcb5c
commit
6c1977f48a
|
@ -50,6 +50,7 @@ fn default_log_level() -> i32 { 0 }
|
||||||
fn default_ipc_server_port() -> i32 { 34982 }
|
fn default_ipc_server_port() -> i32 { 34982 }
|
||||||
fn default_use_system_agent() -> bool { true }
|
fn default_use_system_agent() -> bool { true }
|
||||||
fn default_config_caching_interval() -> i32 { 800 }
|
fn default_config_caching_interval() -> i32 { 800 }
|
||||||
|
fn default_word_separators() -> Vec<char> { vec![' ', ',', '.', '\r', '\n'] }
|
||||||
fn default_toggle_interval() -> u32 { 230 }
|
fn default_toggle_interval() -> u32 { 230 }
|
||||||
fn default_backspace_limit() -> i32 { 3 }
|
fn default_backspace_limit() -> i32 { 3 }
|
||||||
fn default_exclude_default_matches() -> bool {false}
|
fn default_exclude_default_matches() -> bool {false}
|
||||||
|
@ -87,6 +88,9 @@ pub struct Configs {
|
||||||
#[serde(default = "default_config_caching_interval")]
|
#[serde(default = "default_config_caching_interval")]
|
||||||
pub config_caching_interval: i32,
|
pub config_caching_interval: i32,
|
||||||
|
|
||||||
|
#[serde(default = "default_word_separators")]
|
||||||
|
pub word_separators: Vec<char>, // TODO: add parsing test
|
||||||
|
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub toggle_key: KeyModifier,
|
pub toggle_key: KeyModifier,
|
||||||
|
|
||||||
|
|
|
@ -103,16 +103,22 @@ lazy_static! {
|
||||||
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIManager>
|
||||||
MatchReceiver for Engine<'a, S, C, M, U>{
|
MatchReceiver for Engine<'a, S, C, M, U>{
|
||||||
|
|
||||||
fn on_match(&self, m: &Match) {
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>) {
|
||||||
let config = self.config_manager.active_config();
|
let config = self.config_manager.active_config();
|
||||||
|
|
||||||
if config.disabled {
|
if config.disabled {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.keyboard_manager.delete_string(m.trigger.chars().count() as i32);
|
let char_count = if trailing_separator.is_none() {
|
||||||
|
m.trigger.chars().count() as i32
|
||||||
|
}else{
|
||||||
|
m.trigger.chars().count() as i32 + 1 // Count also the separator
|
||||||
|
};
|
||||||
|
|
||||||
let target_string = if m._has_vars {
|
self.keyboard_manager.delete_string(char_count);
|
||||||
|
|
||||||
|
let mut target_string = if m._has_vars {
|
||||||
let mut output_map = HashMap::new();
|
let mut output_map = HashMap::new();
|
||||||
|
|
||||||
for variable in m.vars.iter() {
|
for variable in m.vars.iter() {
|
||||||
|
@ -142,6 +148,11 @@ impl <'a, S: KeyboardManager, C: ClipboardManager, M: ConfigManager<'a>, U: UIMa
|
||||||
m.replace.clone()
|
m.replace.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If a trailing separator was counted in the match, add it back to the target string
|
||||||
|
if let Some(trailing_separator) = trailing_separator {
|
||||||
|
target_string.push(trailing_separator);
|
||||||
|
}
|
||||||
|
|
||||||
match config.backend {
|
match config.backend {
|
||||||
BackendType::Inject => {
|
BackendType::Inject => {
|
||||||
// Send the expected string. On linux, newlines are managed automatically
|
// Send the expected string. On linux, newlines are managed automatically
|
||||||
|
|
|
@ -30,6 +30,7 @@ pub struct Match {
|
||||||
pub trigger: String,
|
pub trigger: String,
|
||||||
pub replace: String,
|
pub replace: String,
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
pub word: bool,
|
||||||
|
|
||||||
#[serde(skip_serializing)]
|
#[serde(skip_serializing)]
|
||||||
pub _has_vars: bool,
|
pub _has_vars: bool,
|
||||||
|
@ -57,6 +58,7 @@ impl<'a> From<&'a AutoMatch> for Match{
|
||||||
trigger: other.trigger.clone(),
|
trigger: other.trigger.clone(),
|
||||||
replace: other.replace.clone(),
|
replace: other.replace.clone(),
|
||||||
vars: other.vars.clone(),
|
vars: other.vars.clone(),
|
||||||
|
word: other.word.clone(),
|
||||||
_has_vars: has_vars,
|
_has_vars: has_vars,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,9 +72,13 @@ struct AutoMatch {
|
||||||
|
|
||||||
#[serde(default = "default_vars")]
|
#[serde(default = "default_vars")]
|
||||||
pub vars: Vec<MatchVariable>,
|
pub vars: Vec<MatchVariable>,
|
||||||
|
|
||||||
|
#[serde(default = "default_word")]
|
||||||
|
pub word: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
fn default_vars() -> Vec<MatchVariable> {Vec::new()}
|
||||||
|
fn default_word() -> bool {false}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct MatchVariable {
|
pub struct MatchVariable {
|
||||||
|
@ -85,7 +91,7 @@ pub struct MatchVariable {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait MatchReceiver {
|
pub trait MatchReceiver {
|
||||||
fn on_match(&self, m: &Match);
|
fn on_match(&self, m: &Match, trailing_separator: Option<char>);
|
||||||
fn on_enable_update(&self, status: bool);
|
fn on_enable_update(&self, status: bool);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,12 +31,19 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
|
||||||
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
current_set_queue: RefCell<VecDeque<Vec<MatchEntry<'a>>>>,
|
||||||
toggle_press_time: RefCell<SystemTime>,
|
toggle_press_time: RefCell<SystemTime>,
|
||||||
is_enabled: RefCell<bool>,
|
is_enabled: RefCell<bool>,
|
||||||
|
was_previous_char_word_separator: RefCell<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
struct MatchEntry<'a> {
|
struct MatchEntry<'a> {
|
||||||
start: usize,
|
start: usize,
|
||||||
count: usize,
|
count: usize,
|
||||||
_match: &'a Match
|
_match: &'a Match,
|
||||||
|
|
||||||
|
// Usually false, becomes true if the match was detected and has the "word" option.
|
||||||
|
// This is needed to trigger the replacement only if the next char is a
|
||||||
|
// word separator ( such as space ).
|
||||||
|
waiting_for_separator: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
|
@ -49,7 +56,8 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
|
||||||
receiver,
|
receiver,
|
||||||
current_set_queue,
|
current_set_queue,
|
||||||
toggle_press_time,
|
toggle_press_time,
|
||||||
is_enabled: RefCell::new(true)
|
is_enabled: RefCell::new(true),
|
||||||
|
was_previous_char_word_separator: RefCell::new(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,33 +83,72 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtain the configuration for the active application if present,
|
||||||
|
// otherwise get the default one
|
||||||
|
let active_config = self.config_manager.active_config();
|
||||||
|
|
||||||
|
// Check if the current char is a word separator
|
||||||
|
let is_current_word_separator = active_config.word_separators.contains(
|
||||||
|
&c.chars().nth(0).unwrap_or_default()
|
||||||
|
);
|
||||||
|
if is_current_word_separator {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut was_previous_word_separator = self.was_previous_char_word_separator.borrow_mut();
|
||||||
|
|
||||||
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
let mut current_set_queue = self.current_set_queue.borrow_mut();
|
||||||
|
|
||||||
let new_matches: Vec<MatchEntry> = self.config_manager.matches().iter()
|
let new_matches: Vec<MatchEntry> = active_config.matches.iter()
|
||||||
.filter(|&x| x.trigger.starts_with(c))
|
.filter(|&x| {
|
||||||
|
if !x.trigger.starts_with(c) {
|
||||||
|
false
|
||||||
|
}else{
|
||||||
|
if x.word {
|
||||||
|
*was_previous_word_separator
|
||||||
|
}else{
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.map(|x | MatchEntry{
|
.map(|x | MatchEntry{
|
||||||
start: 1,
|
start: 1,
|
||||||
count: x.trigger.chars().count(),
|
count: x.trigger.chars().count(),
|
||||||
_match: &x
|
_match: &x,
|
||||||
|
waiting_for_separator: false
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
|
// TODO: use an associative structure to improve the efficiency of this first "new_matches" lookup.
|
||||||
|
|
||||||
let combined_matches: Vec<MatchEntry> = match current_set_queue.back() {
|
let mut combined_matches: Vec<MatchEntry> = match current_set_queue.back_mut() {
|
||||||
Some(last_matches) => {
|
Some(last_matches) => {
|
||||||
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
let mut updated: Vec<MatchEntry> = last_matches.iter()
|
||||||
.filter(|&x| {
|
.filter(|&x| {
|
||||||
let nchar = x._match.trigger.chars().nth(x.start);
|
if x.waiting_for_separator {
|
||||||
if let Some(nchar) = nchar {
|
// The match is only waiting for a separator to call the replacement
|
||||||
c.starts_with(nchar)
|
is_current_word_separator
|
||||||
}else{
|
}else{
|
||||||
false
|
let nchar = x._match.trigger.chars().nth(x.start);
|
||||||
|
if let Some(nchar) = nchar {
|
||||||
|
c.starts_with(nchar)
|
||||||
|
}else{
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.map(|x | MatchEntry{
|
.map(|x | {
|
||||||
start: x.start+1,
|
let new_start = if x.waiting_for_separator {
|
||||||
count: x.count,
|
x.start // Avoid incrementing, we are only waiting for a separator
|
||||||
_match: &x._match
|
}else{
|
||||||
|
x.start+1 // Increment, we want to check if the next char matches
|
||||||
|
};
|
||||||
|
|
||||||
|
MatchEntry{
|
||||||
|
start: new_start,
|
||||||
|
count: x.count,
|
||||||
|
_match: &x._match,
|
||||||
|
waiting_for_separator: x.waiting_for_separator
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
@ -113,9 +160,24 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
|
|
||||||
let mut found_match = None;
|
let mut found_match = None;
|
||||||
|
|
||||||
for entry in combined_matches.iter() {
|
for entry in combined_matches.iter_mut() {
|
||||||
if entry.start == entry.count {
|
let is_found_match = if entry.start == entry.count {
|
||||||
found_match = Some(entry._match);
|
if !entry._match.word {
|
||||||
|
true
|
||||||
|
}else{
|
||||||
|
if entry.waiting_for_separator {
|
||||||
|
true
|
||||||
|
}else{
|
||||||
|
entry.waiting_for_separator = true;
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
if is_found_match {
|
||||||
|
found_match = Some(entry.clone());
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -126,12 +188,27 @@ impl <'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMa
|
||||||
current_set_queue.pop_front();
|
current_set_queue.pop_front();
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(_match) = found_match {
|
if let Some(match_entry) = found_match {
|
||||||
if let Some(last) = current_set_queue.back_mut() {
|
if let Some(last) = current_set_queue.back_mut() {
|
||||||
last.clear();
|
last.clear();
|
||||||
}
|
}
|
||||||
self.receiver.on_match(_match);
|
|
||||||
|
let trailing_separator = if !match_entry.waiting_for_separator {
|
||||||
|
None
|
||||||
|
}else{
|
||||||
|
let as_char = c.chars().nth(0);
|
||||||
|
match as_char {
|
||||||
|
Some(c) => {
|
||||||
|
Some(c) // Current char is the trailing separator
|
||||||
|
},
|
||||||
|
None => {None},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
self.receiver.on_match(match_entry._match, trailing_separator);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
*was_previous_word_separator = is_current_word_separator;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_modifier(&self, m: KeyModifier) {
|
fn handle_modifier(&self, m: KeyModifier) {
|
||||||
|
|
Loading…
Reference in New Issue
Block a user