diff --git a/crates/settings_ui/src/ui_components/keystroke_input.rs b/crates/settings_ui/src/ui_components/keystroke_input.rs index 08ffe3575bcf1365add16f8afbcce370baaf48f2..a34d0a2bbd113e1434f9f4d1d924fd76897e2cf9 100644 --- a/crates/settings_ui/src/ui_components/keystroke_input.rs +++ b/crates/settings_ui/src/ui_components/keystroke_input.rs @@ -1,6 +1,6 @@ use gpui::{ Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, - Keystroke, Modifiers, ModifiersChangedEvent, Subscription, actions, + Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions, }; use ui::{ ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize, @@ -21,6 +21,9 @@ actions!( const KEY_CONTEXT_VALUE: &'static str = "KeystrokeInput"; +const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration = + std::time::Duration::from_millis(300); + enum CloseKeystrokeResult { Partial, Close, @@ -46,10 +49,19 @@ pub struct KeystrokeInput { intercept_subscription: Option, _focus_subscriptions: [Subscription; 2], search: bool, - /// Handles triple escape to stop recording + /// The sequence of close keystrokes being typed close_keystrokes: Option>, close_keystrokes_start: Option, previous_modifiers: Modifiers, + /// In order to support inputting keystrokes that end with a prefix of the + /// close keybind keystrokes, we clear the close keystroke capture info + /// on a timeout after a close keystroke is pressed + /// + /// e.g. if close binding is `esc esc esc` and user wants to search for + /// `ctrl-g esc`, after entering the `ctrl-g esc`, hitting `esc` twice would + /// stop recording because of the sequence of three escapes making it + /// impossible to search for anything ending in `esc` + clear_close_keystrokes_timer: Option>, #[cfg(test)] recording: bool, } @@ -79,6 +91,7 @@ impl KeystrokeInput { close_keystrokes: None, close_keystrokes_start: None, previous_modifiers: Modifiers::default(), + clear_close_keystrokes_timer: None, #[cfg(test)] recording: false, } @@ -144,6 +157,34 @@ impl KeystrokeInput { } } + fn upsert_close_keystrokes_start(&mut self, start: usize, cx: &mut Context) { + if self.close_keystrokes_start.is_some() { + return; + } + self.close_keystrokes_start = Some(start); + self.update_clear_close_keystrokes_timer(cx); + } + + fn update_clear_close_keystrokes_timer(&mut self, cx: &mut Context) { + self.clear_close_keystrokes_timer = Some(cx.spawn(async |this, cx| { + cx.background_executor() + .timer(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT) + .await; + this.update(cx, |this, _cx| { + this.end_close_keystrokes_capture(); + }) + .ok(); + })); + } + + /// Interrupt the capture of close keystrokes, but do not clear the close keystrokes + /// from the input + fn end_close_keystrokes_capture(&mut self) -> Option { + self.close_keystrokes.take(); + self.clear_close_keystrokes_timer.take(); + return self.close_keystrokes_start.take(); + } + fn handle_possible_close_keystroke( &mut self, keystroke: &Keystroke, @@ -152,8 +193,7 @@ impl KeystrokeInput { ) -> CloseKeystrokeResult { let Some(keybind_for_close_action) = Self::determine_stop_recording_binding(window) else { log::trace!("No keybinding to stop recording keystrokes in keystroke input"); - self.close_keystrokes.take(); - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; }; let action_keystrokes = keybind_for_close_action.keystrokes(); @@ -169,20 +209,20 @@ impl KeystrokeInput { } if index == close_keystrokes.len() { if index >= action_keystrokes.len() { - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } if keystroke.should_match(&action_keystrokes[index]) { - if action_keystrokes.len() >= 1 && index == action_keystrokes.len() - 1 { - self.stop_recording(&StopRecording, window, cx); + close_keystrokes.push(keystroke.clone()); + if close_keystrokes.len() == action_keystrokes.len() { return CloseKeystrokeResult::Close; } else { - close_keystrokes.push(keystroke.clone()); self.close_keystrokes = Some(close_keystrokes); + self.update_clear_close_keystrokes_timer(cx); return CloseKeystrokeResult::Partial; } } else { - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } } @@ -192,7 +232,7 @@ impl KeystrokeInput { self.close_keystrokes = Some(vec![keystroke.clone()]); return CloseKeystrokeResult::Partial; } - self.close_keystrokes_start.take(); + self.end_close_keystrokes_capture(); return CloseKeystrokeResult::None; } @@ -248,36 +288,22 @@ impl KeystrokeInput { cx: &mut Context, ) { let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx); - if close_keystroke_result != CloseKeystrokeResult::Close { - let key_len = self.keystrokes.len(); - if let Some(last) = self.keystrokes.last_mut() - && last.key.is_empty() - && key_len <= Self::KEYSTROKE_COUNT_MAX - { - if self.search { - last.key = keystroke.key.clone(); - if close_keystroke_result == CloseKeystrokeResult::Partial - && self.close_keystrokes_start.is_none() - { - self.close_keystrokes_start = Some(self.keystrokes.len() - 1); - } - if self.search { - self.previous_modifiers = keystroke.modifiers; - } - self.keystrokes_changed(cx); - cx.stop_propagation(); - return; - } else { - self.keystrokes.pop(); - } - } - if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Close { + self.stop_recording(&StopRecording, window, cx); + return; + } + let key_len = self.keystrokes.len(); + if let Some(last) = self.keystrokes.last_mut() + && last.key.is_empty() + && key_len <= Self::KEYSTROKE_COUNT_MAX + { + if self.search { + last.key = keystroke.key.clone(); if close_keystroke_result == CloseKeystrokeResult::Partial && self.close_keystrokes_start.is_none() { - self.close_keystrokes_start = Some(self.keystrokes.len()); + self.upsert_close_keystrokes_start(self.keystrokes.len() - 1, cx); } - self.keystrokes.push(keystroke.clone()); if self.search { self.previous_modifiers = keystroke.modifiers; } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX @@ -285,9 +311,29 @@ impl KeystrokeInput { { self.keystrokes.push(Self::dummy(keystroke.modifiers)); } - } else if close_keystroke_result != CloseKeystrokeResult::Partial { - self.clear_keystrokes(&ClearKeystrokes, window, cx); + self.keystrokes_changed(cx); + cx.stop_propagation(); + return; + } else { + self.keystrokes.pop(); + } + } + if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX { + if close_keystroke_result == CloseKeystrokeResult::Partial + && self.close_keystrokes_start.is_none() + { + self.upsert_close_keystrokes_start(self.keystrokes.len(), cx); } + self.keystrokes.push(keystroke.clone()); + if self.search { + self.previous_modifiers = keystroke.modifiers; + } else if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX + && keystroke.modifiers.modified() + { + self.keystrokes.push(Self::dummy(keystroke.modifiers)); + } + } else if close_keystroke_result != CloseKeystrokeResult::Partial { + self.clear_keystrokes(&ClearKeystrokes, window, cx); } self.keystrokes_changed(cx); cx.stop_propagation(); @@ -365,8 +411,9 @@ impl KeystrokeInput { && close_keystrokes_start < self.keystrokes.len() { self.keystrokes.drain(close_keystrokes_start..); + self.keystrokes_changed(cx); } - self.close_keystrokes.take(); + self.end_close_keystrokes_capture(); #[cfg(test)] { self.recording = false; @@ -645,6 +692,7 @@ mod tests { /// Sends a keystroke event based on string description /// Examples: "a", "ctrl-a", "cmd-shift-z", "escape" + #[track_caller] pub fn send_keystroke(&mut self, keystroke_input: &str) -> &mut Self { self.expect_is_recording(true); let keystroke_str = if keystroke_input.ends_with('-') { @@ -677,6 +725,7 @@ mod tests { /// Sends a modifier change event based on string description /// Examples: "+ctrl", "-ctrl", "+cmd+shift", "-all" + #[track_caller] pub fn send_modifiers(&mut self, modifiers: &str) -> &mut Self { self.expect_is_recording(true); let new_modifiers = if modifiers == "-all" { @@ -700,6 +749,7 @@ mod tests { /// Sends multiple events in sequence /// Each event string is either a keystroke or modifier change + #[track_caller] pub fn send_events(&mut self, events: &[&str]) -> &mut Self { self.expect_is_recording(true); for event in events { @@ -712,9 +762,8 @@ mod tests { self } - /// Verifies that the keystrokes match the expected strings #[track_caller] - pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + fn expect_keystrokes_equal(actual: &[Keystroke], expected: &[&str]) { let expected_keystrokes: Result, _> = expected .iter() .map(|s| { @@ -738,9 +787,6 @@ mod tests { let expected_keystrokes = expected_keystrokes .unwrap_or_else(|e: anyhow::Error| panic!("Invalid expected keystroke: {}", e)); - let actual = self - .input - .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); assert_eq!( actual.len(), expected_keystrokes.len(), @@ -763,6 +809,25 @@ mod tests { actual.unparse() ); } + } + + /// Verifies that the keystrokes match the expected strings + #[track_caller] + pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + let actual = self + .input + .read_with(&mut self.cx, |input, _| input.keystrokes.clone()); + Self::expect_keystrokes_equal(&actual, expected); + self + } + + #[track_caller] + pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self { + let actual = self + .input + .read_with(&mut self.cx, |input, _| input.close_keystrokes.clone()) + .unwrap_or_default(); + Self::expect_keystrokes_equal(&actual, expected); self } @@ -813,6 +878,18 @@ mod tests { self } + pub async fn wait_for_close_keystroke_capture_end(&mut self) -> &mut Self { + let task = self.input.update_in(&mut self.cx, |input, _, _| { + input.clear_close_keystrokes_timer.take() + }); + let task = task.expect("No close keystroke capture end timer task"); + self.cx + .executor() + .advance_clock(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT); + task.await; + self + } + /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt" fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers { let mut modifiers = self.current_modifiers; @@ -1162,4 +1239,19 @@ mod tests { .send_events(&["escape", "escape", "escape"]) // Pure triple escape sequence .expect_empty(); } + + #[gpui::test] + async fn test_end_close_keystroke_capture(cx: &mut TestAppContext) { + init_test(cx) + .await + .send_events(&["+ctrl", "g", "-ctrl", "escape"]) + .expect_keystrokes(&["ctrl-g", "escape"]) + .wait_for_close_keystroke_capture_end() + .await + .send_events(&["escape", "escape"]) + .expect_keystrokes(&["ctrl-g", "escape", "escape"]) + .expect_close_keystrokes(&["escape", "escape"]) + .send_keystroke("escape") + .expect_keystrokes(&["ctrl-g", "escape"]); + } }