diff --git a/crates/tab_switcher/src/tab_switcher.rs b/crates/tab_switcher/src/tab_switcher.rs index 62be1daf1b..68ab07bb63 100644 --- a/crates/tab_switcher/src/tab_switcher.rs +++ b/crates/tab_switcher/src/tab_switcher.rs @@ -16,6 +16,7 @@ use project::Project; use schemars::JsonSchema; use serde::Deserialize; use settings::Settings; +use std::time::Duration; use std::{cmp::Reverse, sync::Arc}; use ui::{ DecoratedIcon, IconDecoration, IconDecorationKind, ListItem, ListItemSpacing, Tooltip, @@ -29,6 +30,7 @@ use workspace::{ }; const PANEL_WIDTH_REMS: f32 = 28.; +const POPOVER_DELAY: Duration = Duration::from_millis(300); /// Toggles the tab switcher interface. #[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)] @@ -54,6 +56,8 @@ actions!( pub struct TabSwitcher { picker: Entity>, init_modifiers: Option, + visible: bool, + _show_task: Option>, } impl ModalView for TabSwitcher {} @@ -169,6 +173,17 @@ impl TabSwitcher { } else { window.modifiers().modified().then_some(window.modifiers()) }; + let has_modifiers = init_modifiers.is_some(); + let _show_task = has_modifiers.then(|| { + cx.spawn_in(window, async move |this, cx| { + cx.background_executor().timer(POPOVER_DELAY).await; + this.update_in(cx, |this, _window, cx| { + this.visible = true; + cx.notify(); + }) + .ok(); + }) + }); Self { picker: cx.new(|cx| { if is_global { @@ -179,6 +194,8 @@ impl TabSwitcher { .initial_width(rems(PANEL_WIDTH_REMS)) }), init_modifiers, + visible: !has_modifiers, + _show_task, } } @@ -195,8 +212,12 @@ impl TabSwitcher { self.init_modifiers = None; if self.picker.read(cx).delegate.matches.is_empty() { cx.emit(DismissEvent) - } else { + } else if self.visible { window.dispatch_action(menu::Confirm.boxed_clone(), cx); + } else { + self.picker.update(cx, |picker, cx| { + picker.delegate.confirm(false, window, cx); + }); } } } @@ -225,12 +246,16 @@ impl Focusable for TabSwitcher { impl Render for TabSwitcher { fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + let picker = self.picker.clone(); v_flex() .key_context("TabSwitcher") .w(rems(PANEL_WIDTH_REMS)) .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_close_selected_item)) - .child(self.picker.clone()) + .when(self.visible, |el| el.child(picker.clone())) + .when(!self.visible, |el| { + el.child(div().size_0().overflow_hidden().child(picker.clone())) + }) } } diff --git a/crates/tab_switcher/src/tab_switcher_tests.rs b/crates/tab_switcher/src/tab_switcher_tests.rs index 0700781ebb..dd1c183152 100644 --- a/crates/tab_switcher/src/tab_switcher_tests.rs +++ b/crates/tab_switcher/src/tab_switcher_tests.rs @@ -255,6 +255,61 @@ async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { assert_tab_switcher_is_closed(workspace, cx); } +#[gpui::test] +async fn test_quick_switch_before_popover_visible(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/root"), + json!({ + "1.txt": "First file", + "2.txt": "Second file", + "3.txt": "Third file", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + open_buffer("1.txt", &workspace, cx).await; + open_buffer("2.txt", &workspace, cx).await; + let _tab_3 = open_buffer("3.txt", &workspace, cx).await; + + // Simulate quick Ctrl+Tab: press modifier, open switcher, release modifier + // all before the POPOVER_DELAY (300ms) elapses. + cx.simulate_modifiers_change(Modifiers::control()); + let tab_switcher = open_tab_switcher(false, &workspace, cx); + + // Verify the switcher is not visible yet (before delay) + tab_switcher.read_with(cx, |picker, cx| { + let tab_switcher = picker + .delegate + .tab_switcher + .upgrade() + .expect("tab switcher should exist"); + assert!(!tab_switcher.read(cx).visible); + }); + + // Release modifiers before delay — should confirm the pre-selected item (2.txt) + cx.simulate_modifiers_change(Modifiers::none()); + + cx.read(|cx| { + let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); + assert_eq!( + active_editor.read(cx).title(cx), + "2.txt", + "quick switch should select previous tab, not a random one" + ); + }); + assert_tab_switcher_is_closed(workspace, cx); +} + fn init_test(cx: &mut TestAppContext) -> Arc { cx.update(|cx| { let state = AppState::test(cx);