keystroke_input.rs

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