From 61c17b26a4570e19355a68422587ec2dea04b9e6 Mon Sep 17 00:00:00 2001 From: Federico Terzi Date: Sat, 8 Aug 2020 22:25:13 +0200 Subject: [PATCH] Add experimental backspace undo feature --- src/config/mod.rs | 6 ++ src/engine.rs | 132 ++++++++++++++++++++++++--------------- src/matcher/mod.rs | 1 + src/matcher/scrolling.rs | 26 +++++++- 4 files changed, 112 insertions(+), 53 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index d335f7e..a11e212 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -131,6 +131,9 @@ fn default_show_notifications() -> bool { fn default_auto_restart() -> bool { true } +fn default_undo_backspace() -> bool { + true +} fn default_show_icon() -> bool { true } @@ -215,6 +218,9 @@ pub struct Configs { #[serde(default = "default_enable_active")] pub enable_active: bool, + #[serde(default = "default_undo_backspace")] + pub undo_backspace: bool, + #[serde(default)] pub paste_shortcut: PasteShortcut, diff --git a/src/engine.rs b/src/engine.rs index f48c810..b8c3b6a 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -19,7 +19,7 @@ use crate::clipboard::ClipboardManager; use crate::config::BackendType; -use crate::config::ConfigManager; +use crate::config::{Configs, ConfigManager}; use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver}; use crate::keyboard::KeyboardManager; use crate::matcher::{Match, MatchReceiver}; @@ -50,6 +50,8 @@ pub struct Engine< is_injecting: Arc, enabled: RefCell, + // Trigger string and injected text len pair + last_expansion_data: RefCell>, } impl< @@ -70,6 +72,7 @@ impl< is_injecting: Arc, ) -> Engine<'a, S, C, M, U, R> { let enabled = RefCell::new(true); + let last_expansion_data = RefCell::new(None); Engine { keyboard_manager, @@ -79,6 +82,7 @@ impl< renderer, is_injecting, enabled, + last_expansion_data, } } @@ -147,17 +151,63 @@ impl< } } + fn inject_text(&self, config: &Configs, target_string: &str, force_clipboard: bool) { + let backend = if force_clipboard { + &BackendType::Clipboard + } else if config.backend == BackendType::Auto { + if cfg!(target_os = "linux") { + let all_ascii = target_string.chars().all(|c| c.is_ascii()); + if all_ascii { + debug!( + "All elements of the replacement are ascii, using Inject backend" + ); + &BackendType::Inject + } else { + debug!("There are non-ascii characters, using Clipboard backend"); + &BackendType::Clipboard + } + } else { + &BackendType::Inject + } + } else { + &config.backend + }; + + match backend { + BackendType::Inject => { + // To handle newlines, substitute each "\n" char with an Enter key press. + let splits = target_string.split('\n'); + + for (i, split) in splits.enumerate() { + if i > 0 { + self.keyboard_manager.send_enter(&config); + } + + self.keyboard_manager.send_string(&config, split); + } + } + BackendType::Clipboard => { + self.clipboard_manager.set_clipboard(&target_string); + self.keyboard_manager.trigger_paste(&config); + } + _ => { + error!("Unsupported backend type evaluation."); + return; + } + } + } + fn inject_match( &self, m: &Match, trailing_separator: Option, trigger_offset: usize, skip_delete: bool, - ) { + ) -> Option<(String, i32)> { let config = self.config_manager.active_config(); if !config.enable_active { - return; + return None; } // Block espanso from reinterpreting its own actions @@ -179,6 +229,8 @@ impl< .renderer .render_match(m, trigger_offset, config, vec![]); + let mut expansion_data: Option<(String, i32)> = None; + match rendered { RenderResult::Text(mut target_string) => { // If a trailing separator was counted in the match, add it back to the target string @@ -213,54 +265,13 @@ impl< None }; - let backend = if m.force_clipboard { - &BackendType::Clipboard - } else if config.backend == BackendType::Auto { - if cfg!(target_os = "linux") { - let all_ascii = target_string.chars().all(|c| c.is_ascii()); - if all_ascii { - debug!( - "All elements of the replacement are ascii, using Inject backend" - ); - &BackendType::Inject - } else { - debug!("There are non-ascii characters, using Clipboard backend"); - &BackendType::Clipboard - } - } else { - &BackendType::Inject - } - } else { - &config.backend - }; + // If the preserve_clipboard option is enabled, save the current + // clipboard content to restore it later. + previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled(); - match backend { - BackendType::Inject => { - // To handle newlines, substitute each "\n" char with an Enter key press. - let splits = target_string.split('\n'); + self.inject_text(&config, &target_string, m.force_clipboard); - for (i, split) in splits.enumerate() { - if i > 0 { - self.keyboard_manager.send_enter(&config); - } - - self.keyboard_manager.send_string(&config, split); - } - } - BackendType::Clipboard => { - // If the preserve_clipboard option is enabled, save the current - // clipboard content to restore it later. - previous_clipboard_content = - self.return_content_if_preserve_clipboard_is_enabled(); - - self.clipboard_manager.set_clipboard(&target_string); - self.keyboard_manager.trigger_paste(&config); - } - _ => { - error!("Unsupported backend type evaluation."); - return; - } - } + expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32)); if let Some(moves) = cursor_rewind { // Simulate left arrow key presses to bring the cursor into the desired position @@ -294,6 +305,8 @@ impl< // Re-allow espanso to interpret actions self.is_injecting.store(false, Release); + + expansion_data } } @@ -311,7 +324,26 @@ impl< > MatchReceiver for Engine<'a, S, C, M, U, R> { fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize) { - self.inject_match(m, trailing_separator, trigger_offset, false); + let expansion_data = self.inject_match(m, trailing_separator, trigger_offset, false); + let mut last_expansion_data = self.last_expansion_data.borrow_mut(); + (*last_expansion_data) = expansion_data; + } + + fn on_undo(&self) { + let config = self.config_manager.active_config(); + + if !config.undo_backspace { + return; + } + + let last_expansion_data = self.last_expansion_data.borrow(); + if let Some(ref last_expansion_data) = *last_expansion_data { + let (trigger_string, injected_text_len) = last_expansion_data; + // Delete the previously injected text, minus one character as it has been consumed by the backspace + self.keyboard_manager.delete_string(&config, *injected_text_len - 1); + // Restore previous text + self.inject_text(&config, trigger_string, false); + } } fn on_enable_update(&self, status: bool) { diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs index 27e2336..00b602c 100644 --- a/src/matcher/mod.rs +++ b/src/matcher/mod.rs @@ -273,6 +273,7 @@ pub trait MatchReceiver { fn on_match(&self, m: &Match, trailing_separator: Option, trigger_offset: usize); fn on_enable_update(&self, status: bool); fn on_passive(&self); + fn on_undo(&self); } pub trait Matcher: KeyEventReceiver { diff --git a/src/matcher/scrolling.rs b/src/matcher/scrolling.rs index 0dd38a4..c070c4a 100644 --- a/src/matcher/scrolling.rs +++ b/src/matcher/scrolling.rs @@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> { passive_press_time: RefCell, is_enabled: RefCell, was_previous_char_word_separator: RefCell, + was_previous_char_a_match: RefCell, } #[derive(Clone)] @@ -57,6 +58,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> { passive_press_time, is_enabled: RefCell::new(true), was_previous_char_word_separator: RefCell::new(true), + was_previous_char_a_match: RefCell::new(true), } } @@ -111,6 +113,9 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat } } + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + (*was_previous_char_a_match) = false; + 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(); @@ -192,9 +197,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat if let Some(entry) = found_entry { let mtc = entry._match; - if let Some(last) = current_set_queue.back_mut() { - last.clear(); - } + current_set_queue.clear(); let trailing_separator = if !mtc.word { // If it's not a word match, it cannot have a trailing separator @@ -216,12 +219,17 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat self.receiver .on_match(mtc, trailing_separator, entry.trigger_offset); + + + (*was_previous_char_a_match) = true; } } fn handle_modifier(&self, m: KeyModifier) { let config = self.config_manager.default_config(); + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + // TODO: at the moment, activating the passive key triggers the toggle key // study a mechanism to avoid this problem @@ -253,8 +261,16 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat if m == BACKSPACE { let mut current_set_queue = self.current_set_queue.borrow_mut(); current_set_queue.pop_back(); + + if (*was_previous_char_a_match) { + current_set_queue.clear(); + self.receiver.on_undo(); + } } + // Disable the "backspace undo" feature + (*was_previous_char_a_match) = false; + // Consider modifiers as separators to improve word matches reliability if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK { let mut was_previous_char_word_separator = @@ -269,6 +285,10 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat let mut was_previous_char_word_separator = self.was_previous_char_word_separator.borrow_mut(); *was_previous_char_word_separator = true; + + // Disable the "backspace undo" feature + let mut was_previous_char_a_match = self.was_previous_char_a_match.borrow_mut(); + (*was_previous_char_a_match) = false; } }