Add experimental backspace undo feature

This commit is contained in:
Federico Terzi 2020-08-08 22:25:13 +02:00
parent a6b78e7142
commit 61c17b26a4
4 changed files with 112 additions and 53 deletions

View File

@ -131,6 +131,9 @@ fn default_show_notifications() -> bool {
fn default_auto_restart() -> bool { fn default_auto_restart() -> bool {
true true
} }
fn default_undo_backspace() -> bool {
true
}
fn default_show_icon() -> bool { fn default_show_icon() -> bool {
true true
} }
@ -215,6 +218,9 @@ pub struct Configs {
#[serde(default = "default_enable_active")] #[serde(default = "default_enable_active")]
pub enable_active: bool, pub enable_active: bool,
#[serde(default = "default_undo_backspace")]
pub undo_backspace: bool,
#[serde(default)] #[serde(default)]
pub paste_shortcut: PasteShortcut, pub paste_shortcut: PasteShortcut,

View File

@ -19,7 +19,7 @@
use crate::clipboard::ClipboardManager; use crate::clipboard::ClipboardManager;
use crate::config::BackendType; use crate::config::BackendType;
use crate::config::ConfigManager; use crate::config::{Configs, ConfigManager};
use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver}; use crate::event::{ActionEventReceiver, ActionType, SystemEvent, SystemEventReceiver};
use crate::keyboard::KeyboardManager; use crate::keyboard::KeyboardManager;
use crate::matcher::{Match, MatchReceiver}; use crate::matcher::{Match, MatchReceiver};
@ -50,6 +50,8 @@ pub struct Engine<
is_injecting: Arc<AtomicBool>, is_injecting: Arc<AtomicBool>,
enabled: RefCell<bool>, enabled: RefCell<bool>,
// Trigger string and injected text len pair
last_expansion_data: RefCell<Option<(String, i32)>>,
} }
impl< impl<
@ -70,6 +72,7 @@ impl<
is_injecting: Arc<AtomicBool>, is_injecting: Arc<AtomicBool>,
) -> Engine<'a, S, C, M, U, R> { ) -> Engine<'a, S, C, M, U, R> {
let enabled = RefCell::new(true); let enabled = RefCell::new(true);
let last_expansion_data = RefCell::new(None);
Engine { Engine {
keyboard_manager, keyboard_manager,
@ -79,6 +82,7 @@ impl<
renderer, renderer,
is_injecting, is_injecting,
enabled, 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( fn inject_match(
&self, &self,
m: &Match, m: &Match,
trailing_separator: Option<char>, trailing_separator: Option<char>,
trigger_offset: usize, trigger_offset: usize,
skip_delete: bool, skip_delete: bool,
) { ) -> Option<(String, i32)> {
let config = self.config_manager.active_config(); let config = self.config_manager.active_config();
if !config.enable_active { if !config.enable_active {
return; return None;
} }
// Block espanso from reinterpreting its own actions // Block espanso from reinterpreting its own actions
@ -179,6 +229,8 @@ impl<
.renderer .renderer
.render_match(m, trigger_offset, config, vec![]); .render_match(m, trigger_offset, config, vec![]);
let mut expansion_data: Option<(String, i32)> = None;
match rendered { match rendered {
RenderResult::Text(mut target_string) => { RenderResult::Text(mut target_string) => {
// If a trailing separator was counted in the match, add it back to the target string // If a trailing separator was counted in the match, add it back to the target string
@ -213,54 +265,13 @@ impl<
None 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
};
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 => {
// If the preserve_clipboard option is enabled, save the current // If the preserve_clipboard option is enabled, save the current
// clipboard content to restore it later. // clipboard content to restore it later.
previous_clipboard_content = previous_clipboard_content = self.return_content_if_preserve_clipboard_is_enabled();
self.return_content_if_preserve_clipboard_is_enabled();
self.clipboard_manager.set_clipboard(&target_string); self.inject_text(&config, &target_string, m.force_clipboard);
self.keyboard_manager.trigger_paste(&config);
} expansion_data = Some((m.triggers[trigger_offset].clone(), target_string.chars().count() as i32));
_ => {
error!("Unsupported backend type evaluation.");
return;
}
}
if let Some(moves) = cursor_rewind { if let Some(moves) = cursor_rewind {
// Simulate left arrow key presses to bring the cursor into the desired position // Simulate left arrow key presses to bring the cursor into the desired position
@ -294,6 +305,8 @@ impl<
// Re-allow espanso to interpret actions // Re-allow espanso to interpret actions
self.is_injecting.store(false, Release); self.is_injecting.store(false, Release);
expansion_data
} }
} }
@ -311,7 +324,26 @@ impl<
> MatchReceiver for Engine<'a, S, C, M, U, R> > MatchReceiver for Engine<'a, S, C, M, U, R>
{ {
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize) { fn on_match(&self, m: &Match, trailing_separator: Option<char>, 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) { fn on_enable_update(&self, status: bool) {

View File

@ -273,6 +273,7 @@ pub trait MatchReceiver {
fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize); fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
fn on_enable_update(&self, status: bool); fn on_enable_update(&self, status: bool);
fn on_passive(&self); fn on_passive(&self);
fn on_undo(&self);
} }
pub trait Matcher: KeyEventReceiver { pub trait Matcher: KeyEventReceiver {

View File

@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
passive_press_time: RefCell<SystemTime>, passive_press_time: RefCell<SystemTime>,
is_enabled: RefCell<bool>, is_enabled: RefCell<bool>,
was_previous_char_word_separator: RefCell<bool>, was_previous_char_word_separator: RefCell<bool>,
was_previous_char_a_match: RefCell<bool>,
} }
#[derive(Clone)] #[derive(Clone)]
@ -57,6 +58,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> ScrollingMatcher<'a, R, M> {
passive_press_time, passive_press_time,
is_enabled: RefCell::new(true), is_enabled: RefCell::new(true),
was_previous_char_word_separator: 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 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();
@ -192,9 +197,7 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
if let Some(entry) = found_entry { if let Some(entry) = found_entry {
let mtc = entry._match; let mtc = entry._match;
if let Some(last) = current_set_queue.back_mut() { current_set_queue.clear();
last.clear();
}
let trailing_separator = if !mtc.word { let trailing_separator = if !mtc.word {
// If it's not a word match, it cannot have a trailing separator // 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 self.receiver
.on_match(mtc, trailing_separator, entry.trigger_offset); .on_match(mtc, trailing_separator, entry.trigger_offset);
(*was_previous_char_a_match) = true;
} }
} }
fn handle_modifier(&self, m: KeyModifier) { fn handle_modifier(&self, m: KeyModifier) {
let config = self.config_manager.default_config(); 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 // TODO: at the moment, activating the passive key triggers the toggle key
// study a mechanism to avoid this problem // study a mechanism to avoid this problem
@ -253,7 +261,15 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
if m == BACKSPACE { if m == BACKSPACE {
let mut current_set_queue = self.current_set_queue.borrow_mut(); let mut current_set_queue = self.current_set_queue.borrow_mut();
current_set_queue.pop_back(); 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 // Consider modifiers as separators to improve word matches reliability
if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK { if m != LEFT_SHIFT && m != RIGHT_SHIFT && m != CAPS_LOCK {
@ -269,6 +285,10 @@ impl<'a, R: MatchReceiver, M: ConfigManager<'a>> super::Matcher for ScrollingMat
let mut was_previous_char_word_separator = let mut was_previous_char_word_separator =
self.was_previous_char_word_separator.borrow_mut(); self.was_previous_char_word_separator.borrow_mut();
*was_previous_char_word_separator = true; *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;
} }
} }