Add clear recent files history command (#42176)

Libon created

![2025-11-07
181619](https://github.com/user-attachments/assets/a9bef7a6-dc0b-4db2-85e5-2e1df7b21cfa)


Release Notes:

- Added "workspace: clear navigation history" command

Change summary

crates/file_finder/src/file_finder_tests.rs | 96 +++++++++++++++++++++++
crates/workspace/src/pane.rs                | 19 ++++
crates/workspace/src/workspace.rs           | 13 +++
3 files changed, 128 insertions(+)

Detailed changes

crates/file_finder/src/file_finder_tests.rs 🔗

@@ -3452,3 +3452,99 @@ async fn test_paths_with_starting_slash(cx: &mut TestAppContext) {
         assert_eq!(active_editor.read(cx).title(cx), "file1.txt");
     });
 }
+
+#[gpui::test]
+async fn test_clear_navigation_history(cx: &mut TestAppContext) {
+    let app_state = init_test(cx);
+    app_state
+        .fs
+        .as_fake()
+        .insert_tree(
+            path!("/src"),
+            json!({
+                "test": {
+                    "first.rs": "// First file",
+                    "second.rs": "// Second file",
+                    "third.rs": "// Third file",
+                }
+            }),
+        )
+        .await;
+
+    let project = Project::test(app_state.fs.clone(), [path!("/src").as_ref()], cx).await;
+    let (workspace, cx) = cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+
+    workspace.update_in(cx, |_workspace, window, cx| window.focused(cx));
+
+    // Open some files to generate navigation history
+    open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await;
+    open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+    let history_before_clear =
+        open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await;
+
+    assert_eq!(
+        history_before_clear.len(),
+        2,
+        "Should have history items before clearing"
+    );
+
+    // Verify that file finder shows history items
+    let picker = open_file_picker(&workspace, cx);
+    cx.simulate_input("fir");
+    picker.update(cx, |finder, _| {
+        let matches = collect_search_matches(finder);
+        assert!(
+            !matches.history.is_empty(),
+            "File finder should show history items before clearing"
+        );
+    });
+    workspace.update_in(cx, |_, window, cx| {
+        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
+    });
+
+    // Verify navigation state before clear
+    workspace.update(cx, |workspace, cx| {
+        let pane = workspace.active_pane();
+        pane.read(cx).can_navigate_backward()
+    });
+
+    // Clear navigation history
+    cx.dispatch_action(workspace::ClearNavigationHistory);
+
+    // Verify that navigation is disabled immediately after clear
+    workspace.update(cx, |workspace, cx| {
+        let pane = workspace.active_pane();
+        assert!(
+            !pane.read(cx).can_navigate_backward(),
+            "Should not be able to navigate backward after clearing history"
+        );
+        assert!(
+            !pane.read(cx).can_navigate_forward(),
+            "Should not be able to navigate forward after clearing history"
+        );
+    });
+
+    // Verify that file finder no longer shows history items
+    let picker = open_file_picker(&workspace, cx);
+    cx.simulate_input("fir");
+    picker.update(cx, |finder, _| {
+        let matches = collect_search_matches(finder);
+        assert!(
+            matches.history.is_empty(),
+            "File finder should not show history items after clearing"
+        );
+    });
+    workspace.update_in(cx, |_, window, cx| {
+        window.dispatch_action(menu::Cancel.boxed_clone(), cx);
+    });
+
+    // Verify history is empty by opening a new file
+    // (this should not show any previous history)
+    let history_after_clear =
+        open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await;
+    assert_eq!(
+        history_after_clear.len(),
+        0,
+        "Should have no history items after clearing"
+    );
+}

crates/workspace/src/pane.rs 🔗

@@ -4041,6 +4041,25 @@ impl NavHistory {
         self.0.lock().mode = NavigationMode::Normal;
     }
 
+    pub fn clear(&mut self, cx: &mut App) {
+        let mut state = self.0.lock();
+
+        if state.backward_stack.is_empty()
+            && state.forward_stack.is_empty()
+            && state.closed_stack.is_empty()
+            && state.paths_by_item.is_empty()
+        {
+            return;
+        }
+
+        state.mode = NavigationMode::Normal;
+        state.backward_stack.clear();
+        state.forward_stack.clear();
+        state.closed_stack.clear();
+        state.paths_by_item.clear();
+        state.did_update(cx);
+    }
+
     pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option<NavigationEntry> {
         let mut state = self.0.lock();
         let entry = match mode {

crates/workspace/src/workspace.rs 🔗

@@ -199,6 +199,8 @@ actions!(
         AddFolderToProject,
         /// Clears all notifications.
         ClearAllNotifications,
+        /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**.
+        ClearNavigationHistory,
         /// Closes the active dock.
         CloseActiveDock,
         /// Closes all docks.
@@ -1917,6 +1919,12 @@ impl Workspace {
             .collect()
     }
 
+    pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context<Workspace>) {
+        for pane in &self.panes {
+            pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx));
+        }
+    }
+
     fn navigate_history(
         &mut self,
         pane: WeakEntity<Pane>,
@@ -5858,6 +5866,11 @@ impl Workspace {
                     workspace.clear_all_notifications(cx);
                 },
             ))
+            .on_action(cx.listener(
+                |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| {
+                    workspace.clear_navigation_history(window, cx);
+                },
+            ))
             .on_action(cx.listener(
                 |workspace: &mut Workspace, _: &SuppressNotification, _, cx| {
                     if let Some((notification_id, _)) = workspace.notifications.pop() {