keystroke_input.rs

   1use gpui::{
   2    Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
   3    KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
   4};
   5use ui::{
   6    ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
   7    ParentElement as _, Render, Styled as _, Tooltip, Window, prelude::*,
   8};
   9
  10actions!(
  11    keystroke_input,
  12    [
  13        /// Starts recording keystrokes
  14        StartRecording,
  15        /// Stops recording keystrokes
  16        StopRecording,
  17        /// Clears the recorded keystrokes
  18        ClearKeystrokes,
  19    ]
  20);
  21
  22const KEY_CONTEXT_VALUE: &str = "KeystrokeInput";
  23
  24const CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT: std::time::Duration =
  25    std::time::Duration::from_millis(300);
  26
  27enum CloseKeystrokeResult {
  28    Partial,
  29    Close,
  30    None,
  31}
  32
  33impl PartialEq for CloseKeystrokeResult {
  34    fn eq(&self, other: &Self) -> bool {
  35        matches!(
  36            (self, other),
  37            (CloseKeystrokeResult::Partial, CloseKeystrokeResult::Partial)
  38                | (CloseKeystrokeResult::Close, CloseKeystrokeResult::Close)
  39                | (CloseKeystrokeResult::None, CloseKeystrokeResult::None)
  40        )
  41    }
  42}
  43
  44pub struct KeystrokeInput {
  45    keystrokes: Vec<KeybindingKeystroke>,
  46    placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
  47    outer_focus_handle: FocusHandle,
  48    inner_focus_handle: FocusHandle,
  49    intercept_subscription: Option<Subscription>,
  50    _focus_subscriptions: [Subscription; 2],
  51    search: bool,
  52    /// The sequence of close keystrokes being typed
  53    close_keystrokes: Option<Vec<Keystroke>>,
  54    close_keystrokes_start: Option<usize>,
  55    previous_modifiers: Modifiers,
  56    /// In order to support inputting keystrokes that end with a prefix of the
  57    /// close keybind keystrokes, we clear the close keystroke capture info
  58    /// on a timeout after a close keystroke is pressed
  59    ///
  60    /// e.g. if close binding is `esc esc esc` and user wants to search for
  61    /// `ctrl-g esc`, after entering the `ctrl-g esc`, hitting `esc` twice would
  62    /// stop recording because of the sequence of three escapes making it
  63    /// impossible to search for anything ending in `esc`
  64    clear_close_keystrokes_timer: Option<Task<()>>,
  65    #[cfg(test)]
  66    recording: bool,
  67}
  68
  69impl KeystrokeInput {
  70    const KEYSTROKE_COUNT_MAX: usize = 3;
  71
  72    pub fn new(
  73        placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
  74        window: &mut Window,
  75        cx: &mut Context<Self>,
  76    ) -> Self {
  77        let outer_focus_handle = cx.focus_handle();
  78        let inner_focus_handle = cx.focus_handle();
  79        let _focus_subscriptions = [
  80            cx.on_focus_in(&inner_focus_handle, window, Self::on_inner_focus_in),
  81            cx.on_focus_out(&inner_focus_handle, window, Self::on_inner_focus_out),
  82        ];
  83        Self {
  84            keystrokes: Vec::new(),
  85            placeholder_keystrokes,
  86            inner_focus_handle,
  87            outer_focus_handle,
  88            intercept_subscription: None,
  89            _focus_subscriptions,
  90            search: false,
  91            close_keystrokes: None,
  92            close_keystrokes_start: None,
  93            previous_modifiers: Modifiers::default(),
  94            clear_close_keystrokes_timer: None,
  95            #[cfg(test)]
  96            recording: false,
  97        }
  98    }
  99
 100    pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
 101        self.keystrokes = keystrokes;
 102        self.keystrokes_changed(cx);
 103    }
 104
 105    pub fn set_search(&mut self, search: bool) {
 106        self.search = search;
 107    }
 108
 109    pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
 110        if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
 111            && self.keystrokes.is_empty()
 112        {
 113            return placeholders;
 114        }
 115        if !self.search
 116            && self
 117                .keystrokes
 118                .last()
 119                .is_some_and(|last| last.display_key.is_empty())
 120        {
 121            return &self.keystrokes[..self.keystrokes.len() - 1];
 122        }
 123        &self.keystrokes
 124    }
 125
 126    fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
 127        KeybindingKeystroke {
 128            inner: Keystroke {
 129                modifiers,
 130                key: "".to_string(),
 131                key_char: None,
 132            },
 133            display_modifiers: modifiers,
 134            display_key: "".to_string(),
 135        }
 136    }
 137
 138    fn keystrokes_changed(&self, cx: &mut Context<Self>) {
 139        cx.emit(());
 140        cx.notify();
 141    }
 142
 143    fn key_context() -> KeyContext {
 144        let mut key_context = KeyContext::default();
 145        key_context.add(KEY_CONTEXT_VALUE);
 146        key_context
 147    }
 148
 149    fn determine_stop_recording_binding(window: &mut Window) -> Option<gpui::KeyBinding> {
 150        if cfg!(test) {
 151            Some(gpui::KeyBinding::new(
 152                "escape escape escape",
 153                StopRecording,
 154                Some(KEY_CONTEXT_VALUE),
 155            ))
 156        } else {
 157            window.highest_precedence_binding_for_action_in_context(
 158                &StopRecording,
 159                Self::key_context(),
 160            )
 161        }
 162    }
 163
 164    fn upsert_close_keystrokes_start(&mut self, start: usize, cx: &mut Context<Self>) {
 165        if self.close_keystrokes_start.is_some() {
 166            return;
 167        }
 168        self.close_keystrokes_start = Some(start);
 169        self.update_clear_close_keystrokes_timer(cx);
 170    }
 171
 172    fn update_clear_close_keystrokes_timer(&mut self, cx: &mut Context<Self>) {
 173        self.clear_close_keystrokes_timer = Some(cx.spawn(async |this, cx| {
 174            cx.background_executor()
 175                .timer(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT)
 176                .await;
 177            this.update(cx, |this, _cx| {
 178                this.end_close_keystrokes_capture();
 179            })
 180            .ok();
 181        }));
 182    }
 183
 184    /// Interrupt the capture of close keystrokes, but do not clear the close keystrokes
 185    /// from the input
 186    fn end_close_keystrokes_capture(&mut self) -> Option<usize> {
 187        self.close_keystrokes.take();
 188        self.clear_close_keystrokes_timer.take();
 189        self.close_keystrokes_start.take()
 190    }
 191
 192    fn handle_possible_close_keystroke(
 193        &mut self,
 194        keystroke: &Keystroke,
 195        window: &mut Window,
 196        cx: &mut Context<Self>,
 197    ) -> CloseKeystrokeResult {
 198        let Some(keybind_for_close_action) = Self::determine_stop_recording_binding(window) else {
 199            log::trace!("No keybinding to stop recording keystrokes in keystroke input");
 200            self.end_close_keystrokes_capture();
 201            return CloseKeystrokeResult::None;
 202        };
 203        let action_keystrokes = keybind_for_close_action.keystrokes();
 204
 205        if let Some(mut close_keystrokes) = self.close_keystrokes.take() {
 206            let mut index = 0;
 207
 208            while index < action_keystrokes.len() && index < close_keystrokes.len() {
 209                if !close_keystrokes[index].should_match(&action_keystrokes[index]) {
 210                    break;
 211                }
 212                index += 1;
 213            }
 214            if index == close_keystrokes.len() {
 215                if index >= action_keystrokes.len() {
 216                    self.end_close_keystrokes_capture();
 217                    return CloseKeystrokeResult::None;
 218                }
 219                if keystroke.should_match(&action_keystrokes[index]) {
 220                    close_keystrokes.push(keystroke.clone());
 221                    if close_keystrokes.len() == action_keystrokes.len() {
 222                        return CloseKeystrokeResult::Close;
 223                    } else {
 224                        self.close_keystrokes = Some(close_keystrokes);
 225                        self.update_clear_close_keystrokes_timer(cx);
 226                        return CloseKeystrokeResult::Partial;
 227                    }
 228                } else {
 229                    self.end_close_keystrokes_capture();
 230                    return CloseKeystrokeResult::None;
 231                }
 232            }
 233        } else if let Some(first_action_keystroke) = action_keystrokes.first()
 234            && keystroke.should_match(first_action_keystroke)
 235        {
 236            self.close_keystrokes = Some(vec![keystroke.clone()]);
 237            return CloseKeystrokeResult::Partial;
 238        }
 239        self.end_close_keystrokes_capture();
 240        CloseKeystrokeResult::None
 241    }
 242
 243    fn on_modifiers_changed(
 244        &mut self,
 245        event: &ModifiersChangedEvent,
 246        window: &mut Window,
 247        cx: &mut Context<Self>,
 248    ) {
 249        cx.stop_propagation();
 250        let keystrokes_len = self.keystrokes.len();
 251
 252        if self.previous_modifiers.modified()
 253            && event.modifiers.is_subset_of(&self.previous_modifiers)
 254        {
 255            self.previous_modifiers &= event.modifiers;
 256            return;
 257        }
 258        self.keystrokes_changed(cx);
 259
 260        if let Some(last) = self.keystrokes.last_mut()
 261            && last.display_key.is_empty()
 262            && keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
 263        {
 264            if !self.search && !event.modifiers.modified() {
 265                self.keystrokes.pop();
 266                return;
 267            }
 268            if self.search {
 269                if self.previous_modifiers.modified() {
 270                    last.display_modifiers |= event.modifiers;
 271                    last.inner.modifiers |= event.modifiers;
 272                } else {
 273                    self.keystrokes.push(Self::dummy(event.modifiers));
 274                }
 275                self.previous_modifiers |= event.modifiers;
 276            } else {
 277                last.display_modifiers = event.modifiers;
 278                last.inner.modifiers = event.modifiers;
 279                return;
 280            }
 281        } else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
 282            self.keystrokes.push(Self::dummy(event.modifiers));
 283            if self.search {
 284                self.previous_modifiers |= event.modifiers;
 285            }
 286        }
 287        if keystrokes_len >= Self::KEYSTROKE_COUNT_MAX {
 288            self.clear_keystrokes(&ClearKeystrokes, window, cx);
 289        }
 290    }
 291
 292    fn handle_keystroke(
 293        &mut self,
 294        keystroke: &Keystroke,
 295        window: &mut Window,
 296        cx: &mut Context<Self>,
 297    ) {
 298        cx.stop_propagation();
 299
 300        let close_keystroke_result = self.handle_possible_close_keystroke(keystroke, window, cx);
 301        if close_keystroke_result == CloseKeystrokeResult::Close {
 302            self.stop_recording(&StopRecording, window, cx);
 303            return;
 304        }
 305
 306        let mut keystroke =
 307            KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
 308        if let Some(last) = self.keystrokes.last()
 309            && last.display_key.is_empty()
 310            && (!self.search || self.previous_modifiers.modified())
 311        {
 312            let display_key = keystroke.display_key.clone();
 313            let inner_key = keystroke.inner.key.clone();
 314            keystroke = last.clone();
 315            keystroke.display_key = display_key;
 316            keystroke.inner.key = inner_key;
 317            self.keystrokes.pop();
 318        }
 319
 320        if close_keystroke_result == CloseKeystrokeResult::Partial {
 321            self.upsert_close_keystrokes_start(self.keystrokes.len(), cx);
 322            if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX {
 323                return;
 324            }
 325        }
 326
 327        if self.keystrokes.len() >= Self::KEYSTROKE_COUNT_MAX {
 328            self.clear_keystrokes(&ClearKeystrokes, window, cx);
 329            return;
 330        }
 331
 332        self.keystrokes.push(keystroke.clone());
 333        self.keystrokes_changed(cx);
 334
 335        if self.search {
 336            self.previous_modifiers = keystroke.display_modifiers;
 337            return;
 338        }
 339        if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
 340            && keystroke.display_modifiers.modified()
 341        {
 342            self.keystrokes
 343                .push(Self::dummy(keystroke.display_modifiers));
 344        }
 345    }
 346
 347    fn on_inner_focus_in(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
 348        if self.intercept_subscription.is_none() {
 349            let listener = cx.listener(|this, event: &gpui::KeystrokeEvent, window, cx| {
 350                this.handle_keystroke(&event.keystroke, window, cx);
 351            });
 352            self.intercept_subscription = Some(cx.intercept_keystrokes(listener))
 353        }
 354    }
 355
 356    fn on_inner_focus_out(
 357        &mut self,
 358        _event: gpui::FocusOutEvent,
 359        _window: &mut Window,
 360        cx: &mut Context<Self>,
 361    ) {
 362        self.intercept_subscription.take();
 363        cx.notify();
 364    }
 365
 366    fn render_keystrokes(&self, is_recording: bool) -> impl Iterator<Item = Div> {
 367        let keystrokes = if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
 368            && self.keystrokes.is_empty()
 369        {
 370            if is_recording {
 371                &[]
 372            } else {
 373                placeholders.as_slice()
 374            }
 375        } else {
 376            &self.keystrokes
 377        };
 378        keystrokes.iter().map(move |keystroke| {
 379            h_flex().children(ui::render_keybinding_keystroke(
 380                keystroke,
 381                Some(Color::Default),
 382                Some(rems(0.875).into()),
 383                ui::PlatformStyle::platform(),
 384                false,
 385            ))
 386        })
 387    }
 388
 389    pub fn start_recording(
 390        &mut self,
 391        _: &StartRecording,
 392        window: &mut Window,
 393        cx: &mut Context<Self>,
 394    ) {
 395        window.focus(&self.inner_focus_handle);
 396        self.clear_keystrokes(&ClearKeystrokes, window, cx);
 397        self.previous_modifiers = window.modifiers();
 398        #[cfg(test)]
 399        {
 400            self.recording = true;
 401        }
 402        cx.stop_propagation();
 403    }
 404
 405    pub fn stop_recording(
 406        &mut self,
 407        _: &StopRecording,
 408        window: &mut Window,
 409        cx: &mut Context<Self>,
 410    ) {
 411        if !self.is_recording(window) {
 412            return;
 413        }
 414        window.focus(&self.outer_focus_handle);
 415        if let Some(close_keystrokes_start) = self.close_keystrokes_start.take()
 416            && close_keystrokes_start < self.keystrokes.len()
 417        {
 418            self.keystrokes.drain(close_keystrokes_start..);
 419            self.keystrokes_changed(cx);
 420        }
 421        self.end_close_keystrokes_capture();
 422        #[cfg(test)]
 423        {
 424            self.recording = false;
 425        }
 426        cx.notify();
 427    }
 428
 429    pub fn clear_keystrokes(
 430        &mut self,
 431        _: &ClearKeystrokes,
 432        _window: &mut Window,
 433        cx: &mut Context<Self>,
 434    ) {
 435        self.keystrokes.clear();
 436        self.keystrokes_changed(cx);
 437        self.end_close_keystrokes_capture();
 438    }
 439
 440    fn is_recording(&self, window: &Window) -> bool {
 441        #[cfg(test)]
 442        {
 443            if true {
 444                // in tests, we just need a simple bool that is toggled on start and stop recording
 445                return self.recording;
 446            }
 447        }
 448        // however, in the real world, checking if the inner focus handle is focused
 449        // is a much more reliable check, as the intercept keystroke handlers are installed
 450        // on focus of the inner focus handle, thereby ensuring our recording state does
 451        // not get de-synced
 452        self.inner_focus_handle.is_focused(window)
 453    }
 454}
 455
 456impl EventEmitter<()> for KeystrokeInput {}
 457
 458impl Focusable for KeystrokeInput {
 459    fn focus_handle(&self, _cx: &gpui::App) -> FocusHandle {
 460        self.outer_focus_handle.clone()
 461    }
 462}
 463
 464impl Render for KeystrokeInput {
 465    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
 466        let colors = cx.theme().colors();
 467        let is_focused = self.outer_focus_handle.contains_focused(window, cx);
 468        let is_recording = self.is_recording(window);
 469
 470        let horizontal_padding = rems_from_px(64.);
 471
 472        let recording_bg_color = colors
 473            .editor_background
 474            .blend(colors.text_accent.opacity(0.1));
 475
 476        let recording_pulse = |color: Color| {
 477            Icon::new(IconName::Circle)
 478                .size(IconSize::Small)
 479                .color(Color::Error)
 480                .with_animation(
 481                    "recording-pulse",
 482                    Animation::new(std::time::Duration::from_secs(2))
 483                        .repeat()
 484                        .with_easing(gpui::pulsating_between(0.4, 0.8)),
 485                    {
 486                        let color = color.color(cx);
 487                        move |this, delta| this.color(Color::Custom(color.opacity(delta)))
 488                    },
 489                )
 490        };
 491
 492        let recording_indicator = h_flex()
 493            .h_4()
 494            .pr_1()
 495            .gap_0p5()
 496            .border_1()
 497            .border_color(colors.border)
 498            .bg(colors
 499                .editor_background
 500                .blend(colors.text_accent.opacity(0.1)))
 501            .rounded_sm()
 502            .child(recording_pulse(Color::Error))
 503            .child(
 504                Label::new("REC")
 505                    .size(LabelSize::XSmall)
 506                    .weight(FontWeight::SEMIBOLD)
 507                    .color(Color::Error),
 508            );
 509
 510        let search_indicator = h_flex()
 511            .h_4()
 512            .pr_1()
 513            .gap_0p5()
 514            .border_1()
 515            .border_color(colors.border)
 516            .bg(colors
 517                .editor_background
 518                .blend(colors.text_accent.opacity(0.1)))
 519            .rounded_sm()
 520            .child(recording_pulse(Color::Accent))
 521            .child(
 522                Label::new("SEARCH")
 523                    .size(LabelSize::XSmall)
 524                    .weight(FontWeight::SEMIBOLD)
 525                    .color(Color::Accent),
 526            );
 527
 528        let record_icon = if self.search {
 529            IconName::MagnifyingGlass
 530        } else {
 531            IconName::PlayFilled
 532        };
 533
 534        h_flex()
 535            .id("keystroke-input")
 536            .track_focus(&self.outer_focus_handle)
 537            .py_2()
 538            .px_3()
 539            .gap_2()
 540            .min_h_10()
 541            .w_full()
 542            .flex_1()
 543            .justify_between()
 544            .rounded_sm()
 545            .overflow_hidden()
 546            .map(|this| {
 547                if is_recording {
 548                    this.bg(recording_bg_color)
 549                } else {
 550                    this.bg(colors.editor_background)
 551                }
 552            })
 553            .border_1()
 554            .border_color(colors.border_variant)
 555            .when(is_focused, |parent| {
 556                parent.border_color(colors.border_focused)
 557            })
 558            .key_context(Self::key_context())
 559            .on_action(cx.listener(Self::start_recording))
 560            .on_action(cx.listener(Self::clear_keystrokes))
 561            .child(
 562                h_flex()
 563                    .w(horizontal_padding)
 564                    .gap_0p5()
 565                    .justify_start()
 566                    .flex_none()
 567                    .when(is_recording, |this| {
 568                        this.map(|this| {
 569                            if self.search {
 570                                this.child(search_indicator)
 571                            } else {
 572                                this.child(recording_indicator)
 573                            }
 574                        })
 575                    }),
 576            )
 577            .child(
 578                h_flex()
 579                    .id("keystroke-input-inner")
 580                    .track_focus(&self.inner_focus_handle)
 581                    .on_modifiers_changed(cx.listener(Self::on_modifiers_changed))
 582                    .size_full()
 583                    .when(!self.search, |this| {
 584                        this.focus(|mut style| {
 585                            style.border_color = Some(colors.border_focused);
 586                            style
 587                        })
 588                    })
 589                    .w_full()
 590                    .min_w_0()
 591                    .justify_center()
 592                    .flex_wrap()
 593                    .gap(ui::DynamicSpacing::Base04.rems(cx))
 594                    .children(self.render_keystrokes(is_recording)),
 595            )
 596            .child(
 597                h_flex()
 598                    .w(horizontal_padding)
 599                    .gap_0p5()
 600                    .justify_end()
 601                    .flex_none()
 602                    .map(|this| {
 603                        if is_recording {
 604                            this.child(
 605                                IconButton::new("stop-record-btn", IconName::Stop)
 606                                    .shape(IconButtonShape::Square)
 607                                    .map(|this| {
 608                                        this.tooltip(Tooltip::for_action_title(
 609                                            if self.search {
 610                                                "Stop Searching"
 611                                            } else {
 612                                                "Stop Recording"
 613                                            },
 614                                            &StopRecording,
 615                                        ))
 616                                    })
 617                                    .icon_color(Color::Error)
 618                                    .on_click(cx.listener(|this, _event, window, cx| {
 619                                        this.stop_recording(&StopRecording, window, cx);
 620                                    })),
 621                            )
 622                        } else {
 623                            this.child(
 624                                IconButton::new("record-btn", record_icon)
 625                                    .shape(IconButtonShape::Square)
 626                                    .map(|this| {
 627                                        this.tooltip(Tooltip::for_action_title(
 628                                            if self.search {
 629                                                "Start Searching"
 630                                            } else {
 631                                                "Start Recording"
 632                                            },
 633                                            &StartRecording,
 634                                        ))
 635                                    })
 636                                    .when(!is_focused, |this| this.icon_color(Color::Muted))
 637                                    .on_click(cx.listener(|this, _event, window, cx| {
 638                                        this.start_recording(&StartRecording, window, cx);
 639                                    })),
 640                            )
 641                        }
 642                    })
 643                    .child(
 644                        IconButton::new("clear-btn", IconName::Backspace)
 645                            .shape(IconButtonShape::Square)
 646                            .tooltip(Tooltip::for_action_title(
 647                                "Clear Keystrokes",
 648                                &ClearKeystrokes,
 649                            ))
 650                            .when(!is_recording || !is_focused, |this| {
 651                                this.icon_color(Color::Muted)
 652                            })
 653                            .on_click(cx.listener(|this, _event, window, cx| {
 654                                this.clear_keystrokes(&ClearKeystrokes, window, cx);
 655                            })),
 656                    ),
 657            )
 658    }
 659}
 660
 661#[cfg(test)]
 662mod tests {
 663    use super::*;
 664    use fs::FakeFs;
 665    use gpui::{Entity, TestAppContext, VisualTestContext};
 666    use itertools::Itertools as _;
 667    use project::Project;
 668    use settings::SettingsStore;
 669    use workspace::Workspace;
 670
 671    pub struct KeystrokeInputTestHelper {
 672        input: Entity<KeystrokeInput>,
 673        current_modifiers: Modifiers,
 674        cx: VisualTestContext,
 675    }
 676
 677    impl KeystrokeInputTestHelper {
 678        /// Creates a new test helper with default settings
 679        pub fn new(mut cx: VisualTestContext) -> Self {
 680            let input = cx.new_window_entity(|window, cx| KeystrokeInput::new(None, window, cx));
 681
 682            let mut helper = Self {
 683                input,
 684                current_modifiers: Modifiers::default(),
 685                cx,
 686            };
 687
 688            helper.start_recording();
 689            helper
 690        }
 691
 692        /// Sets search mode on the input
 693        pub fn with_search_mode(&mut self, search: bool) -> &mut Self {
 694            self.input.update(&mut self.cx, |input, _| {
 695                input.set_search(search);
 696            });
 697            self
 698        }
 699
 700        /// Sends a keystroke event based on string description
 701        /// Examples: "a", "ctrl-a", "cmd-shift-z", "escape"
 702        #[track_caller]
 703        pub fn send_keystroke(&mut self, keystroke_input: &str) -> &mut Self {
 704            self.expect_is_recording(true);
 705            let keystroke_str = if keystroke_input.ends_with('-') {
 706                format!("{}_", keystroke_input)
 707            } else {
 708                keystroke_input.to_string()
 709            };
 710
 711            let mut keystroke = Keystroke::parse(&keystroke_str)
 712                .unwrap_or_else(|_| panic!("Invalid keystroke: {}", keystroke_input));
 713
 714            // Remove the dummy key if we added it for modifier-only keystrokes
 715            if keystroke_input.ends_with('-') && keystroke_str.ends_with("_") {
 716                keystroke.key = "".to_string();
 717            }
 718
 719            // Combine current modifiers with keystroke modifiers
 720            keystroke.modifiers |= self.current_modifiers;
 721
 722            self.update_input(|input, window, cx| {
 723                input.handle_keystroke(&keystroke, window, cx);
 724            });
 725
 726            // Don't update current_modifiers for keystrokes with actual keys
 727            if keystroke.key.is_empty() {
 728                self.current_modifiers = keystroke.modifiers;
 729            }
 730            self
 731        }
 732
 733        /// Sends a modifier change event based on string description
 734        /// Examples: "+ctrl", "-ctrl", "+cmd+shift", "-all"
 735        #[track_caller]
 736        pub fn send_modifiers(&mut self, modifiers: &str) -> &mut Self {
 737            self.expect_is_recording(true);
 738            let new_modifiers = if modifiers == "-all" {
 739                Modifiers::default()
 740            } else {
 741                self.parse_modifier_change(modifiers)
 742            };
 743
 744            let event = ModifiersChangedEvent {
 745                modifiers: new_modifiers,
 746                capslock: gpui::Capslock::default(),
 747            };
 748
 749            self.update_input(|input, window, cx| {
 750                input.on_modifiers_changed(&event, window, cx);
 751            });
 752
 753            self.current_modifiers = new_modifiers;
 754            self
 755        }
 756
 757        /// Sends multiple events in sequence
 758        /// Each event string is either a keystroke or modifier change
 759        #[track_caller]
 760        pub fn send_events(&mut self, events: &[&str]) -> &mut Self {
 761            self.expect_is_recording(true);
 762            for event in events {
 763                if event.starts_with('+') || event.starts_with('-') {
 764                    self.send_modifiers(event);
 765                } else {
 766                    self.send_keystroke(event);
 767                }
 768            }
 769            self
 770        }
 771
 772        #[track_caller]
 773        fn expect_keystrokes_equal(actual: &[Keystroke], expected: &[&str]) {
 774            let expected_keystrokes: Result<Vec<Keystroke>, _> = expected
 775                .iter()
 776                .map(|s| {
 777                    let keystroke_str = if s.ends_with('-') {
 778                        format!("{}_", s)
 779                    } else {
 780                        s.to_string()
 781                    };
 782
 783                    let mut keystroke = Keystroke::parse(&keystroke_str)?;
 784
 785                    // Remove the dummy key if we added it for modifier-only keystrokes
 786                    if s.ends_with('-') && keystroke_str.ends_with("_") {
 787                        keystroke.key = "".to_string();
 788                    }
 789
 790                    Ok(keystroke)
 791                })
 792                .collect();
 793
 794            let expected_keystrokes = expected_keystrokes
 795                .unwrap_or_else(|e: anyhow::Error| panic!("Invalid expected keystroke: {}", e));
 796
 797            assert_eq!(
 798                actual.len(),
 799                expected_keystrokes.len(),
 800                "Keystroke count mismatch. Expected: {:?}, Actual: {:?}",
 801                expected_keystrokes
 802                    .iter()
 803                    .map(|k| k.unparse())
 804                    .collect::<Vec<_>>(),
 805                actual.iter().map(|k| k.unparse()).collect::<Vec<_>>()
 806            );
 807
 808            for (i, (actual, expected)) in actual.iter().zip(expected_keystrokes.iter()).enumerate()
 809            {
 810                assert_eq!(
 811                    actual.unparse(),
 812                    expected.unparse(),
 813                    "Keystroke {} mismatch. Expected: '{}', Actual: '{}'",
 814                    i,
 815                    expected.unparse(),
 816                    actual.unparse()
 817                );
 818            }
 819        }
 820
 821        /// Verifies that the keystrokes match the expected strings
 822        #[track_caller]
 823        pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
 824            let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
 825                input
 826                    .keystrokes
 827                    .iter()
 828                    .map(|keystroke| keystroke.inner.clone())
 829                    .collect()
 830            });
 831            Self::expect_keystrokes_equal(&actual, expected);
 832            self
 833        }
 834
 835        #[track_caller]
 836        pub fn expect_close_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
 837            let actual = self
 838                .input
 839                .read_with(&self.cx, |input, _| input.close_keystrokes.clone())
 840                .unwrap_or_default();
 841            Self::expect_keystrokes_equal(&actual, expected);
 842            self
 843        }
 844
 845        /// Verifies that there are no keystrokes
 846        #[track_caller]
 847        pub fn expect_empty(&mut self) -> &mut Self {
 848            self.expect_keystrokes(&[])
 849        }
 850
 851        /// Starts recording keystrokes
 852        #[track_caller]
 853        pub fn start_recording(&mut self) -> &mut Self {
 854            self.expect_is_recording(false);
 855            self.input.update_in(&mut self.cx, |input, window, cx| {
 856                input.start_recording(&StartRecording, window, cx);
 857            });
 858            self
 859        }
 860
 861        /// Stops recording keystrokes
 862        pub fn stop_recording(&mut self) -> &mut Self {
 863            self.expect_is_recording(true);
 864            self.input.update_in(&mut self.cx, |input, window, cx| {
 865                input.stop_recording(&StopRecording, window, cx);
 866            });
 867            self
 868        }
 869
 870        /// Clears all keystrokes
 871        #[track_caller]
 872        pub fn clear_keystrokes(&mut self) -> &mut Self {
 873            let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx);
 874            self.input.update_in(&mut self.cx, |input, window, cx| {
 875                input.clear_keystrokes(&ClearKeystrokes, window, cx);
 876            });
 877            KeystrokeUpdateTracker::finish(change_tracker, &self.cx);
 878            self.current_modifiers = Default::default();
 879            self
 880        }
 881
 882        /// Verifies the recording state
 883        #[track_caller]
 884        pub fn expect_is_recording(&mut self, expected: bool) -> &mut Self {
 885            let actual = self
 886                .input
 887                .update_in(&mut self.cx, |input, window, _| input.is_recording(window));
 888            assert_eq!(
 889                actual, expected,
 890                "Recording state mismatch. Expected: {}, Actual: {}",
 891                expected, actual
 892            );
 893            self
 894        }
 895
 896        pub async fn wait_for_close_keystroke_capture_end(&mut self) -> &mut Self {
 897            let task = self.input.update_in(&mut self.cx, |input, _, _| {
 898                input.clear_close_keystrokes_timer.take()
 899            });
 900            let task = task.expect("No close keystroke capture end timer task");
 901            self.cx
 902                .executor()
 903                .advance_clock(CLOSE_KEYSTROKE_CAPTURE_END_TIMEOUT);
 904            task.await;
 905            self
 906        }
 907
 908        /// Parses modifier change strings like "+ctrl", "-shift", "+cmd+alt"
 909        #[track_caller]
 910        fn parse_modifier_change(&self, modifiers_str: &str) -> Modifiers {
 911            let mut modifiers = self.current_modifiers;
 912
 913            assert!(!modifiers_str.is_empty(), "Empty modifier string");
 914
 915            let value;
 916            let split_char;
 917            let remaining;
 918            if let Some(to_add) = modifiers_str.strip_prefix('+') {
 919                value = true;
 920                split_char = '+';
 921                remaining = to_add;
 922            } else {
 923                let to_remove = modifiers_str
 924                    .strip_prefix('-')
 925                    .expect("Modifier string must start with '+' or '-'");
 926                value = false;
 927                split_char = '-';
 928                remaining = to_remove;
 929            }
 930
 931            for modifier in remaining.split(split_char) {
 932                match modifier {
 933                    "ctrl" | "control" => modifiers.control = value,
 934                    "alt" | "option" => modifiers.alt = value,
 935                    "shift" => modifiers.shift = value,
 936                    "cmd" | "command" | "platform" => modifiers.platform = value,
 937                    "fn" | "function" => modifiers.function = value,
 938                    _ => panic!("Unknown modifier: {}", modifier),
 939                }
 940            }
 941
 942            modifiers
 943        }
 944
 945        #[track_caller]
 946        fn update_input<R>(
 947            &mut self,
 948            cb: impl FnOnce(&mut KeystrokeInput, &mut Window, &mut Context<KeystrokeInput>) -> R,
 949        ) -> R {
 950            let change_tracker = KeystrokeUpdateTracker::new(self.input.clone(), &mut self.cx);
 951            let result = self.input.update_in(&mut self.cx, cb);
 952            KeystrokeUpdateTracker::finish(change_tracker, &self.cx);
 953            result
 954        }
 955    }
 956
 957    struct KeystrokeUpdateTracker {
 958        initial_keystrokes: Vec<KeybindingKeystroke>,
 959        _subscription: Subscription,
 960        input: Entity<KeystrokeInput>,
 961        received_keystrokes_updated: bool,
 962    }
 963
 964    impl KeystrokeUpdateTracker {
 965        fn new(input: Entity<KeystrokeInput>, cx: &mut VisualTestContext) -> Entity<Self> {
 966            cx.new(|cx| Self {
 967                initial_keystrokes: input.read_with(cx, |input, _| input.keystrokes.clone()),
 968                _subscription: cx.subscribe(&input, |this: &mut Self, _, _, _| {
 969                    this.received_keystrokes_updated = true;
 970                }),
 971                input,
 972                received_keystrokes_updated: false,
 973            })
 974        }
 975        #[track_caller]
 976        fn finish(this: Entity<Self>, cx: &VisualTestContext) {
 977            let (received_keystrokes_updated, initial_keystrokes_str, updated_keystrokes_str) =
 978                this.read_with(cx, |this, cx| {
 979                    let updated_keystrokes = this
 980                        .input
 981                        .read_with(cx, |input, _| input.keystrokes.clone());
 982                    let initial_keystrokes_str = keystrokes_str(&this.initial_keystrokes);
 983                    let updated_keystrokes_str = keystrokes_str(&updated_keystrokes);
 984                    (
 985                        this.received_keystrokes_updated,
 986                        initial_keystrokes_str,
 987                        updated_keystrokes_str,
 988                    )
 989                });
 990            if received_keystrokes_updated {
 991                assert_ne!(
 992                    initial_keystrokes_str, updated_keystrokes_str,
 993                    "Received keystrokes_updated event, expected different keystrokes"
 994                );
 995            } else {
 996                assert_eq!(
 997                    initial_keystrokes_str, updated_keystrokes_str,
 998                    "Received no keystrokes_updated event, expected same keystrokes"
 999                );
1000            }
1001
1002            fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
1003                ks.iter().map(|ks| ks.inner.unparse()).join(" ")
1004            }
1005        }
1006    }
1007
1008    async fn init_test(cx: &mut TestAppContext) -> KeystrokeInputTestHelper {
1009        cx.update(|cx| {
1010            let settings_store = SettingsStore::test(cx);
1011            cx.set_global(settings_store);
1012            theme::init(theme::LoadThemes::JustBase, cx);
1013            language::init(cx);
1014            project::Project::init_settings(cx);
1015            workspace::init_settings(cx);
1016        });
1017
1018        let fs = FakeFs::new(cx.executor());
1019        let project = Project::test(fs, [], cx).await;
1020        let workspace =
1021            cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
1022        let cx = VisualTestContext::from_window(*workspace, cx);
1023        KeystrokeInputTestHelper::new(cx)
1024    }
1025
1026    #[gpui::test]
1027    async fn test_basic_keystroke_input(cx: &mut TestAppContext) {
1028        init_test(cx)
1029            .await
1030            .send_keystroke("a")
1031            .clear_keystrokes()
1032            .expect_empty();
1033    }
1034
1035    #[gpui::test]
1036    async fn test_modifier_handling(cx: &mut TestAppContext) {
1037        init_test(cx)
1038            .await
1039            .with_search_mode(true)
1040            .send_events(&["+ctrl", "a", "-ctrl"])
1041            .expect_keystrokes(&["ctrl-a"]);
1042    }
1043
1044    #[gpui::test]
1045    async fn test_multiple_modifiers(cx: &mut TestAppContext) {
1046        init_test(cx)
1047            .await
1048            .send_keystroke("cmd-shift-z")
1049            .expect_keystrokes(&["cmd-shift-z", "cmd-shift-"]);
1050    }
1051
1052    #[gpui::test]
1053    async fn test_search_mode_behavior(cx: &mut TestAppContext) {
1054        init_test(cx)
1055            .await
1056            .with_search_mode(true)
1057            .send_events(&["+cmd", "shift-f", "-cmd"])
1058            // In search mode, when completing a modifier-only keystroke with a key,
1059            // only the original modifiers are preserved, not the keystroke's modifiers
1060            .expect_keystrokes(&["cmd-f"]);
1061    }
1062
1063    #[gpui::test]
1064    async fn test_keystroke_limit(cx: &mut TestAppContext) {
1065        init_test(cx)
1066            .await
1067            .send_keystroke("a")
1068            .send_keystroke("b")
1069            .send_keystroke("c")
1070            .expect_keystrokes(&["a", "b", "c"]) // At max limit
1071            .send_keystroke("d")
1072            .expect_empty(); // Should clear when exceeding limit
1073    }
1074
1075    #[gpui::test]
1076    async fn test_modifier_release_all(cx: &mut TestAppContext) {
1077        init_test(cx)
1078            .await
1079            .with_search_mode(true)
1080            .send_events(&["+ctrl+shift", "a", "-all"])
1081            .expect_keystrokes(&["ctrl-shift-a"]);
1082    }
1083
1084    #[gpui::test]
1085    async fn test_search_new_modifiers_not_added_until_all_released(cx: &mut TestAppContext) {
1086        init_test(cx)
1087            .await
1088            .with_search_mode(true)
1089            .send_events(&["+ctrl+shift", "a", "-ctrl"])
1090            .expect_keystrokes(&["ctrl-shift-a"])
1091            .send_events(&["+ctrl"])
1092            .expect_keystrokes(&["ctrl-shift-a", "ctrl-shift-"]);
1093    }
1094
1095    #[gpui::test]
1096    async fn test_previous_modifiers_no_effect_when_not_search(cx: &mut TestAppContext) {
1097        init_test(cx)
1098            .await
1099            .with_search_mode(false)
1100            .send_events(&["+ctrl+shift", "a", "-all"])
1101            .expect_keystrokes(&["ctrl-shift-a"]);
1102    }
1103
1104    #[gpui::test]
1105    async fn test_keystroke_limit_overflow_non_search_mode(cx: &mut TestAppContext) {
1106        init_test(cx)
1107            .await
1108            .with_search_mode(false)
1109            .send_events(&["a", "b", "c", "d"]) // 4 keystrokes, exceeds limit of 3
1110            .expect_empty(); // Should clear when exceeding limit
1111    }
1112
1113    #[gpui::test]
1114    async fn test_complex_modifier_sequences(cx: &mut TestAppContext) {
1115        init_test(cx)
1116            .await
1117            .with_search_mode(true)
1118            .send_events(&["+ctrl", "+shift", "+alt", "a", "-ctrl", "-shift", "-alt"])
1119            .expect_keystrokes(&["ctrl-shift-alt-a"]);
1120    }
1121
1122    #[gpui::test]
1123    async fn test_modifier_only_keystrokes_search_mode(cx: &mut TestAppContext) {
1124        init_test(cx)
1125            .await
1126            .with_search_mode(true)
1127            .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"])
1128            .expect_keystrokes(&["ctrl-shift-"]); // Modifier-only sequences create modifier-only keystrokes
1129    }
1130
1131    #[gpui::test]
1132    async fn test_modifier_only_keystrokes_non_search_mode(cx: &mut TestAppContext) {
1133        init_test(cx)
1134            .await
1135            .with_search_mode(false)
1136            .send_events(&["+ctrl", "+shift", "-ctrl", "-shift"])
1137            .expect_empty(); // Modifier-only sequences get filtered in non-search mode
1138    }
1139
1140    #[gpui::test]
1141    async fn test_rapid_modifier_changes(cx: &mut TestAppContext) {
1142        init_test(cx)
1143            .await
1144            .with_search_mode(true)
1145            .send_events(&["+ctrl", "-ctrl", "+shift", "-shift", "+alt", "a", "-alt"])
1146            .expect_keystrokes(&["ctrl-", "shift-", "alt-a"]);
1147    }
1148
1149    #[gpui::test]
1150    async fn test_clear_keystrokes_search_mode(cx: &mut TestAppContext) {
1151        init_test(cx)
1152            .await
1153            .with_search_mode(true)
1154            .send_events(&["+ctrl", "a", "-ctrl", "b"])
1155            .expect_keystrokes(&["ctrl-a", "b"])
1156            .clear_keystrokes()
1157            .expect_empty();
1158    }
1159
1160    #[gpui::test]
1161    async fn test_non_search_mode_modifier_key_sequence(cx: &mut TestAppContext) {
1162        init_test(cx)
1163            .await
1164            .with_search_mode(false)
1165            .send_events(&["+ctrl", "a"])
1166            .expect_keystrokes(&["ctrl-a", "ctrl-"])
1167            .send_events(&["-ctrl"])
1168            .expect_keystrokes(&["ctrl-a"]); // Non-search mode filters trailing empty keystrokes
1169    }
1170
1171    #[gpui::test]
1172    async fn test_all_modifiers_at_once(cx: &mut TestAppContext) {
1173        init_test(cx)
1174            .await
1175            .with_search_mode(true)
1176            .send_events(&["+ctrl+shift+alt+cmd", "a", "-all"])
1177            .expect_keystrokes(&["ctrl-shift-alt-cmd-a"]);
1178    }
1179
1180    #[gpui::test]
1181    async fn test_keystrokes_at_exact_limit(cx: &mut TestAppContext) {
1182        init_test(cx)
1183            .await
1184            .with_search_mode(true)
1185            .send_events(&["a", "b", "c"]) // exactly 3 keystrokes (at limit)
1186            .expect_keystrokes(&["a", "b", "c"])
1187            .send_events(&["d"]) // should clear when exceeding
1188            .expect_empty();
1189    }
1190
1191    #[gpui::test]
1192    async fn test_function_modifier_key(cx: &mut TestAppContext) {
1193        init_test(cx)
1194            .await
1195            .with_search_mode(true)
1196            .send_events(&["+fn", "f1", "-fn"])
1197            .expect_keystrokes(&["fn-f1"]);
1198    }
1199
1200    #[gpui::test]
1201    async fn test_start_stop_recording(cx: &mut TestAppContext) {
1202        init_test(cx)
1203            .await
1204            .send_events(&["a", "b"])
1205            .expect_keystrokes(&["a", "b"]) // start_recording clears existing keystrokes
1206            .stop_recording()
1207            .expect_is_recording(false)
1208            .start_recording()
1209            .send_events(&["c"])
1210            .expect_keystrokes(&["c"]);
1211    }
1212
1213    #[gpui::test]
1214    async fn test_modifier_sequence_with_interruption(cx: &mut TestAppContext) {
1215        init_test(cx)
1216            .await
1217            .with_search_mode(true)
1218            .send_events(&["+ctrl", "+shift", "a", "-shift", "b", "-ctrl"])
1219            .expect_keystrokes(&["ctrl-shift-a", "ctrl-b"]);
1220    }
1221
1222    #[gpui::test]
1223    async fn test_empty_key_sequence_search_mode(cx: &mut TestAppContext) {
1224        init_test(cx)
1225            .await
1226            .with_search_mode(true)
1227            .send_events(&[]) // No events at all
1228            .expect_empty();
1229    }
1230
1231    #[gpui::test]
1232    async fn test_modifier_sequence_completion_search_mode(cx: &mut TestAppContext) {
1233        init_test(cx)
1234            .await
1235            .with_search_mode(true)
1236            .send_events(&["+ctrl", "+shift", "-shift", "a", "-ctrl"])
1237            .expect_keystrokes(&["ctrl-shift-a"]);
1238    }
1239
1240    #[gpui::test]
1241    async fn test_triple_escape_stops_recording_search_mode(cx: &mut TestAppContext) {
1242        init_test(cx)
1243            .await
1244            .with_search_mode(true)
1245            .send_events(&["a", "escape", "escape", "escape"])
1246            .expect_keystrokes(&["a"]) // Triple escape removes final escape, stops recording
1247            .expect_is_recording(false);
1248    }
1249
1250    #[gpui::test]
1251    async fn test_triple_escape_stops_recording_non_search_mode(cx: &mut TestAppContext) {
1252        init_test(cx)
1253            .await
1254            .with_search_mode(false)
1255            .send_events(&["a", "escape", "escape", "escape"])
1256            .expect_keystrokes(&["a"]); // Triple escape stops recording but only removes final escape
1257    }
1258
1259    #[gpui::test]
1260    async fn test_triple_escape_at_keystroke_limit(cx: &mut TestAppContext) {
1261        init_test(cx)
1262            .await
1263            .with_search_mode(true)
1264            .send_events(&["a", "b", "c", "escape", "escape", "escape"]) // 6 keystrokes total, exceeds limit
1265            .expect_keystrokes(&["a", "b", "c"]); // Triple escape stops recording and removes escapes, leaves original keystrokes
1266    }
1267
1268    #[gpui::test]
1269    async fn test_interrupted_escape_sequence(cx: &mut TestAppContext) {
1270        init_test(cx)
1271            .await
1272            .with_search_mode(true)
1273            .send_events(&["escape", "escape", "a", "escape"]) // Partial escape sequence interrupted by 'a'
1274            .expect_keystrokes(&["escape", "escape", "a"]); // Escape sequence interrupted by 'a', no close triggered
1275    }
1276
1277    #[gpui::test]
1278    async fn test_interrupted_escape_sequence_within_limit(cx: &mut TestAppContext) {
1279        init_test(cx)
1280            .await
1281            .with_search_mode(true)
1282            .send_events(&["escape", "escape", "a"]) // Partial escape sequence interrupted by 'a' (3 keystrokes, at limit)
1283            .expect_keystrokes(&["escape", "escape", "a"]); // Should not trigger close, interruption resets escape detection
1284    }
1285
1286    #[gpui::test]
1287    async fn test_partial_escape_sequence_no_close(cx: &mut TestAppContext) {
1288        init_test(cx)
1289            .await
1290            .with_search_mode(true)
1291            .send_events(&["escape", "escape"]) // Only 2 escapes, not enough to close
1292            .expect_keystrokes(&["escape", "escape"])
1293            .expect_is_recording(true); // Should remain in keystrokes, no close triggered
1294    }
1295
1296    #[gpui::test]
1297    async fn test_recording_state_after_triple_escape(cx: &mut TestAppContext) {
1298        init_test(cx)
1299            .await
1300            .with_search_mode(true)
1301            .send_events(&["a", "escape", "escape", "escape"])
1302            .expect_keystrokes(&["a"]) // Triple escape stops recording, removes final escape
1303            .expect_is_recording(false);
1304    }
1305
1306    #[gpui::test]
1307    async fn test_triple_escape_mixed_with_other_keystrokes(cx: &mut TestAppContext) {
1308        init_test(cx)
1309            .await
1310            .with_search_mode(true)
1311            .send_events(&["a", "escape", "b", "escape", "escape"]) // Mixed sequence, should not trigger close
1312            .expect_keystrokes(&["a", "escape", "b"]); // No complete triple escape sequence, stays at limit
1313    }
1314
1315    #[gpui::test]
1316    async fn test_triple_escape_only(cx: &mut TestAppContext) {
1317        init_test(cx)
1318            .await
1319            .with_search_mode(true)
1320            .send_events(&["escape", "escape", "escape"]) // Pure triple escape sequence
1321            .expect_empty();
1322    }
1323
1324    #[gpui::test]
1325    async fn test_end_close_keystroke_capture(cx: &mut TestAppContext) {
1326        init_test(cx)
1327            .await
1328            .send_events(&["+ctrl", "g", "-ctrl", "escape"])
1329            .expect_keystrokes(&["ctrl-g", "escape"])
1330            .wait_for_close_keystroke_capture_end()
1331            .await
1332            .send_events(&["escape", "escape"])
1333            .expect_keystrokes(&["ctrl-g", "escape", "escape"])
1334            .expect_close_keystrokes(&["escape", "escape"])
1335            .send_keystroke("escape")
1336            .expect_keystrokes(&["ctrl-g", "escape"]);
1337    }
1338
1339    #[gpui::test]
1340    async fn test_search_previous_modifiers_are_sticky(cx: &mut TestAppContext) {
1341        init_test(cx)
1342            .await
1343            .with_search_mode(true)
1344            .send_events(&["+ctrl+alt", "-ctrl", "j"])
1345            .expect_keystrokes(&["ctrl-alt-j"]);
1346    }
1347
1348    #[gpui::test]
1349    async fn test_previous_modifiers_can_be_entered_separately(cx: &mut TestAppContext) {
1350        init_test(cx)
1351            .await
1352            .with_search_mode(true)
1353            .send_events(&["+ctrl", "-ctrl"])
1354            .expect_keystrokes(&["ctrl-"])
1355            .send_events(&["+alt", "-alt"])
1356            .expect_keystrokes(&["ctrl-", "alt-"]);
1357    }
1358
1359    #[gpui::test]
1360    async fn test_previous_modifiers_reset_on_key(cx: &mut TestAppContext) {
1361        init_test(cx)
1362            .await
1363            .with_search_mode(true)
1364            .send_events(&["+ctrl+alt", "-ctrl", "+shift"])
1365            .expect_keystrokes(&["ctrl-shift-alt-"])
1366            .send_keystroke("j")
1367            .expect_keystrokes(&["ctrl-shift-alt-j"])
1368            .send_keystroke("i")
1369            .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i"])
1370            .send_events(&["-shift-alt", "+cmd"])
1371            .expect_keystrokes(&["ctrl-shift-alt-j", "shift-alt-i", "cmd-"]);
1372    }
1373
1374    #[gpui::test]
1375    async fn test_previous_modifiers_reset_on_release_all(cx: &mut TestAppContext) {
1376        init_test(cx)
1377            .await
1378            .with_search_mode(true)
1379            .send_events(&["+ctrl+alt", "-ctrl", "+shift"])
1380            .expect_keystrokes(&["ctrl-shift-alt-"])
1381            .send_events(&["-all", "j"])
1382            .expect_keystrokes(&["ctrl-shift-alt-", "j"]);
1383    }
1384
1385    #[gpui::test]
1386    async fn test_search_repeat_modifiers(cx: &mut TestAppContext) {
1387        init_test(cx)
1388            .await
1389            .with_search_mode(true)
1390            .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"])
1391            .expect_keystrokes(&["ctrl-", "alt-", "shift-"])
1392            .send_events(&["+cmd"])
1393            .expect_empty();
1394    }
1395
1396    #[gpui::test]
1397    async fn test_not_search_repeat_modifiers(cx: &mut TestAppContext) {
1398        init_test(cx)
1399            .await
1400            .with_search_mode(false)
1401            .send_events(&["+ctrl", "-ctrl", "+alt", "-alt", "+shift", "-shift"])
1402            .expect_empty();
1403    }
1404}