diff --git a/espanso-engine/src/event/internal.rs b/espanso-engine/src/event/internal.rs index 872a7ce..0ffb83d 100644 --- a/espanso-engine/src/event/internal.rs +++ b/espanso-engine/src/event/internal.rs @@ -89,3 +89,10 @@ pub struct SecureInputEnabledEvent { pub app_name: String, pub app_path: String, } + +#[derive(Debug, Clone, PartialEq)] +pub struct UndoEvent { + pub match_id: i32, + pub trigger: String, + pub replace: String, +} \ No newline at end of file diff --git a/espanso-engine/src/event/mod.rs b/espanso-engine/src/event/mod.rs index 5d9c53f..f43eb96 100644 --- a/espanso-engine/src/event/mod.rs +++ b/espanso-engine/src/event/mod.rs @@ -71,6 +71,7 @@ pub enum EventType { ImageResolved(internal::ImageResolvedEvent), MatchInjected, DiscardPrevious(internal::DiscardPreviousEvent), + Undo(internal::UndoEvent), Disabled, Enabled, diff --git a/espanso-engine/src/process/default.rs b/espanso-engine/src/process/default.rs index 95e42f5..36670b8 100644 --- a/espanso-engine/src/process/default.rs +++ b/espanso-engine/src/process/default.rs @@ -19,8 +19,7 @@ use log::trace; -use super::{ - middleware::{ +use super::{DisableOptions, MatchFilter, MatchInfoProvider, MatchProvider, MatchSelector, Matcher, MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider, Processor, Renderer, UndoEnabledProvider, middleware::{ action::{ActionMiddleware, EventSequenceProvider}, cause::CauseCompensateMiddleware, cursor_hint::CursorHintMiddleware, @@ -31,18 +30,8 @@ use super::{ multiplex::MultiplexMiddleware, past_discard::PastEventsDiscardMiddleware, render::RenderMiddleware, - }, - DisableOptions, MatchFilter, MatchInfoProvider, MatchProvider, MatchSelector, Matcher, - MatcherMiddlewareConfigProvider, Middleware, Multiplexer, PathProvider, Processor, Renderer, -}; -use crate::{ - event::{Event, EventType}, - process::middleware::{ - context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware, - hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware, - image_resolve::ImageResolverMiddleware, search::SearchMiddleware, - }, -}; + }}; +use crate::{event::{Event, EventType}, process::middleware::{context_menu::ContextMenuMiddleware, disable::DisableMiddleware, exit::ExitMiddleware, hotkey::HotKeyMiddleware, icon_status::IconStatusMiddleware, image_resolve::ImageResolverMiddleware, search::SearchMiddleware, undo::UndoMiddleware}}; use std::collections::VecDeque; pub struct DefaultProcessor<'a> { @@ -65,6 +54,7 @@ impl<'a> DefaultProcessor<'a> { disable_options: DisableOptions, matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider, match_provider: &'a dyn MatchProvider, + undo_enabled_provider: &'a dyn UndoEnabledProvider, ) -> DefaultProcessor<'a> { Self { event_queue: VecDeque::new(), @@ -82,6 +72,7 @@ impl<'a> DefaultProcessor<'a> { Box::new(ImageResolverMiddleware::new(path_provider)), Box::new(CursorHintMiddleware::new()), Box::new(ExitMiddleware::new()), + Box::new(UndoMiddleware::new(undo_enabled_provider)), Box::new(ActionMiddleware::new( match_info_provider, event_sequence_provider, diff --git a/espanso-engine/src/process/middleware/action.rs b/espanso-engine/src/process/middleware/action.rs index 5d65651..41d97ed 100644 --- a/espanso-engine/src/process/middleware/action.rs +++ b/espanso-engine/src/process/middleware/action.rs @@ -126,10 +126,28 @@ impl<'a> Middleware for ActionMiddleware<'a> { }), ) } + EventType::Undo(m_event) => { + // We subtract one, because the backspace that triggered the undo feature + // already removed the last char + let backspace_count = m_event.replace.chars().count() - 1; + + dispatch(Event::caused_by( + event.source_id, + EventType::TextInject(TextInjectRequest { + text: m_event.trigger.clone(), + force_mode: self.match_info_provider.get_force_mode(m_event.match_id), + }), + )); + + Event::caused_by( + event.source_id, + EventType::KeySequenceInject(KeySequenceInjectRequest { + keys: (0..backspace_count).map(|_| Key::Backspace).collect(), + }), + ) + } _ => event, } - - // TODO: handle images } } diff --git a/espanso-engine/src/process/middleware/mod.rs b/espanso-engine/src/process/middleware/mod.rs index 40c72b6..3a7e3eb 100644 --- a/espanso-engine/src/process/middleware/mod.rs +++ b/espanso-engine/src/process/middleware/mod.rs @@ -34,3 +34,4 @@ pub mod multiplex; pub mod past_discard; pub mod render; pub mod search; +pub mod undo; diff --git a/espanso-engine/src/process/middleware/undo.rs b/espanso-engine/src/process/middleware/undo.rs new file mode 100644 index 0000000..29421e2 --- /dev/null +++ b/espanso-engine/src/process/middleware/undo.rs @@ -0,0 +1,115 @@ +/* + * This file is part of espanso. + * + * Copyright (C) 2019-2021 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 + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * espanso is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with espanso. If not, see . + */ + +use std::cell::RefCell; + +use super::super::Middleware; +use crate::event::{ + input::{Key, Status}, + internal::{TextFormat, UndoEvent}, + Event, EventType, +}; + +pub trait UndoEnabledProvider { + fn is_undo_enabled(&self) -> bool; +} + +pub struct UndoMiddleware<'a> { + undo_enabled_provider: &'a dyn UndoEnabledProvider, + record: RefCell>, +} + +impl<'a> UndoMiddleware<'a> { + pub fn new(undo_enabled_provider: &'a dyn UndoEnabledProvider) -> Self { + Self { + undo_enabled_provider, + record: RefCell::new(None), + } + } +} + +impl<'a> Middleware for UndoMiddleware<'a> { + fn name(&self) -> &'static str { + "undo" + } + + fn next(&self, event: Event, _: &mut dyn FnMut(Event)) -> Event { + let mut record = self.record.borrow_mut(); + + if let EventType::TriggerCompensation(m_event) = &event.etype { + *record = Some(InjectionRecord { + id: Some(event.source_id), + trigger: Some(m_event.trigger.clone()), + ..Default::default() + }); + } else if let EventType::Rendered(m_event) = &event.etype { + if let TextFormat::Plain = m_event.format { + if let Some(record) = &mut *record { + if record.id == Some(event.source_id) { + record.injected_text = Some(m_event.body.clone()); + record.match_id = Some(m_event.match_id); + } + } + } + } else if let EventType::Keyboard(m_event) = &event.etype { + if m_event.status == Status::Pressed { + if m_event.key == Key::Backspace { + if let Some(record) = (*record).take() { + if let (Some(trigger), Some(injected_text), Some(match_id)) = + (record.trigger, record.injected_text, record.match_id) + { + if self.undo_enabled_provider.is_undo_enabled() { + return Event::caused_by( + event.source_id, + EventType::Undo(UndoEvent { + match_id, + trigger, + replace: injected_text, + }), + ); + } + } + } + } + *record = None; + } + } else if let EventType::Mouse(_) = &event.etype { + // Any mouse event invalidates the undo feature, as it could + // represent a change in application + *record = None; + } else if let EventType::CursorHintCompensation(_) = &event.etype { + // Cursor hints invalidate the undo feature, as it would be pretty + // complex to determine which delete operations should be performed. + // This might change in the future. + *record = None; + } + + event + } +} + +#[derive(Default)] +struct InjectionRecord { + id: Option, + match_id: Option, + trigger: Option, + injected_text: Option, +} + +// TODO: test diff --git a/espanso-engine/src/process/mod.rs b/espanso-engine/src/process/mod.rs index 8102ad0..2d30cc7 100644 --- a/espanso-engine/src/process/mod.rs +++ b/espanso-engine/src/process/mod.rs @@ -44,6 +44,7 @@ pub use middleware::matcher::{ pub use middleware::multiplex::Multiplexer; pub use middleware::render::{Renderer, RendererError}; pub use middleware::search::MatchProvider; +pub use middleware::undo::UndoEnabledProvider; #[allow(clippy::too_many_arguments)] pub fn default<'a, MatcherState>( @@ -59,6 +60,7 @@ pub fn default<'a, MatcherState>( disable_options: DisableOptions, matcher_options_provider: &'a dyn MatcherMiddlewareConfigProvider, match_provider: &'a dyn MatchProvider, + undo_enabled_provider: &'a dyn UndoEnabledProvider, ) -> impl Processor + 'a { default::DefaultProcessor::new( matchers, @@ -73,5 +75,6 @@ pub fn default<'a, MatcherState>( disable_options, matcher_options_provider, match_provider, + undo_enabled_provider, ) }