Add experimental backspace undo feature
This commit is contained in:
		
							parent
							
								
									a6b78e7142
								
							
						
					
					
						commit
						61c17b26a4
					
				| 
						 | 
				
			
			@ -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,
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										130
									
								
								src/engine.rs
									
									
									
									
									
								
							
							
						
						
									
										130
									
								
								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<AtomicBool>,
 | 
			
		||||
 | 
			
		||||
    enabled: RefCell<bool>,
 | 
			
		||||
    // Trigger string and injected text len pair
 | 
			
		||||
    last_expansion_data: RefCell<Option<(String, i32)>>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl<
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +72,7 @@ impl<
 | 
			
		|||
        is_injecting: Arc<AtomicBool>,
 | 
			
		||||
    ) -> 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<char>,
 | 
			
		||||
        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
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                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
 | 
			
		||||
                // clipboard content to restore it later.
 | 
			
		||||
                        previous_clipboard_content =
 | 
			
		||||
                            self.return_content_if_preserve_clipboard_is_enabled();
 | 
			
		||||
                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;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                self.inject_text(&config, &target_string, m.force_clipboard);
 | 
			
		||||
 | 
			
		||||
                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<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) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -273,6 +273,7 @@ pub trait MatchReceiver {
 | 
			
		|||
    fn on_match(&self, m: &Match, trailing_separator: Option<char>, trigger_offset: usize);
 | 
			
		||||
    fn on_enable_update(&self, status: bool);
 | 
			
		||||
    fn on_passive(&self);
 | 
			
		||||
    fn on_undo(&self);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub trait Matcher: KeyEventReceiver {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -33,6 +33,7 @@ pub struct ScrollingMatcher<'a, R: MatchReceiver, M: ConfigManager<'a>> {
 | 
			
		|||
    passive_press_time: RefCell<SystemTime>,
 | 
			
		||||
    is_enabled: RefCell<bool>,
 | 
			
		||||
    was_previous_char_word_separator: RefCell<bool>,
 | 
			
		||||
    was_previous_char_a_match: RefCell<bool>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[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,7 +261,15 @@ 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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -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;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue
	
	Block a user