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

saberoueslati created

## 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

Change summary

crates/tab_switcher/src/tab_switcher_tests.rs | 56 +++++++++++++++++++++
crates/workspace/src/workspace.rs             | 10 ++-
2 files changed, 62 insertions(+), 4 deletions(-)

Detailed changes

crates/tab_switcher/src/tab_switcher_tests.rs 🔗

@@ -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;
+    });
+}

crates/workspace/src/workspace.rs 🔗

@@ -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;