tab_switcher: Keep tab switcher open when closing last tab in active pane (#53279)

## Context

Closes #53252

When using `ToggleAll` to open the tab switcher and then closing the
last tab in the currently active pane via `CloseSelectedItem`, the tab
switcher would unexpectedly dismiss. Closing tabs from an inactive pane
worked correctly.

**Root cause:** `force_remove_pane` in `Workspace` unconditionally calls
`window.focus(fallback_pane)` when the active pane is removed. This
focus change causes the tab switcher picker's editor to receive a
`Blurred` event, which
triggers `Picker::cancel` → `delegate.dismissed` → `DismissEvent`,
dismissing the modal.

**Fix:** When a modal is active, skip the `window.focus` call and
instead call `set_active_pane` directly. This keeps the active pane
pointer up to date without stealing focus from the modal.

Video of manual test after fix :

[Screencast from 2026-04-07
00-24-56.webm](https://github.com/user-attachments/assets/eeb74313-1713-48db-8421-db740ef7a7b2)

## How to Review

- `crates/workspace/src/workspace.rs` : In `force_remove_pane`, when the
removed pane was the active pane, the fallback pane now only receives
focus if no modal is currently open. Otherwise, `set_active_pane` is
called directly, which updates `active_pane`, `last_active_center_pane`,
and the status bar without touching window focus.

- `crates/tab_switcher/src/tab_switcher_tests.rs` : New test
`test_toggle_all_stays_open_after_closing_last_tab_in_active_pane`
reproduces the issue: two panes each with one file, the active pane's
tab is closed via `CloseSelectedItem`, and the test asserts the tab
switcher remains open with the other file still listed.

## Self-Review Checklist

- [x] I've reviewed my own diff for quality, security, and reliability
- [ ] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the UI/UX checklist
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes

- Fixed tab switcher dismissing when closing the last tab in the active
pane
This commit is contained in:
saberoueslati
2026-04-15 07:26:23 +01:00
committed by GitHub
parent d3d8f1500d
commit e613b2c9d3
2 changed files with 62 additions and 4 deletions
@@ -549,3 +549,59 @@ async fn test_open_in_active_pane_closes_file_in_all_panes(cx: &mut gpui::TestAp
);
}
}
#[gpui::test]
async fn test_toggle_all_stays_open_after_closing_last_tab_in_active_pane(
cx: &mut gpui::TestAppContext,
) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
path!("/root"),
json!({
"a.txt": "",
"b.txt": "",
}),
)
.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());
let tab_a = open_buffer("a.txt", &workspace, cx).await;
workspace.update_in(cx, |workspace, window, cx| {
workspace.split_pane(
workspace.active_pane().clone(),
workspace::SplitDirection::Right,
window,
cx,
);
});
open_buffer("b.txt", &workspace, cx).await;
// Right pane (with b.txt) is now the active pane.
cx.dispatch_action(ToggleAll);
let tab_switcher = get_active_tab_switcher(&workspace, cx);
tab_switcher.update(cx, |picker, _| {
assert_eq!(picker.delegate.matches.len(), 2);
// Explicitly select b.txt (index 0, the most recently activated item)
// to close the last tab in the active (right) pane.
picker.delegate.selected_index = 0;
});
cx.dispatch_action(CloseSelectedItem);
cx.run_until_parked();
// Tab switcher must remain open with a.txt as the only match
let tab_switcher = get_active_tab_switcher(&workspace, cx);
tab_switcher.update(cx, |picker, cx| {
assert_eq!(picker.delegate.matches.len(), 1);
assert_match_at_position(picker, 0, tab_a.boxed_clone());
let _ = cx;
});
}
+6 -4
View File
@@ -6497,10 +6497,12 @@ impl Workspace {
if let Some(focus_on) = focus_on {
focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
} else if self.active_pane() == pane {
self.panes
.last()
.unwrap()
.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
let fallback_pane = self.panes.last().unwrap().clone();
if self.has_active_modal(window, cx) {
self.set_active_pane(&fallback_pane, window, cx);
} else {
fallback_pane.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx), cx));
}
}
if self.last_active_center_pane == Some(pane.downgrade()) {
self.last_active_center_pane = None;