From abaabce3b701406643210e173f19dcfdab0a7af6 Mon Sep 17 00:00:00 2001 From: Giorgi Merebashvili Date: Fri, 13 Feb 2026 20:05:40 +0400 Subject: [PATCH] file_finder: Remove project's root name from the file finder history (#46957) Closes #45135 Since neither the project search nor file finder search was showing project's root name, including it in the history was unnecessary, especially when the user had `project_panel.hide_root` set to `true`. Release Notes: - Made file finder to respect `project_panel.hide_root` settings --------- Co-authored-by: Kirill Bulatov --- Cargo.lock | 1 + assets/settings/default.json | 3 +- crates/file_finder/Cargo.toml | 1 + crates/file_finder/src/file_finder.rs | 18 +- crates/file_finder/src/file_finder_tests.rs | 263 ++++++++++++++++++++ crates/project_panel/src/project_panel.rs | 2 +- docs/src/visual-customization.md | 3 +- 7 files changed, 286 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99e53af895f3f080a953ecc8c0d23023fe997ae4..d2c546a597b96bdd80e1ae41aacda1a70a27ee8b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6206,6 +6206,7 @@ dependencies = [ "picker", "pretty_assertions", "project", + "project_panel", "serde", "serde_json", "settings", diff --git a/assets/settings/default.json b/assets/settings/default.json index 8e76ada7e9a193f666451b63743e0ad89b29e5c0..0d1762ea2adffa245f8eca5e2e8de233e6ed328d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -799,7 +799,8 @@ "sort_mode": "directories_first", // Whether to enable drag-and-drop operations in the project panel. "drag_and_drop": true, - // Whether to hide the root entry when only one folder is open in the window. + // Whether to hide the root entry when only one folder is open in the window; + // this also affects how file paths appear in the file finder history. "hide_root": false, // Whether to hide the hidden entries in the project panel. "hide_hidden": false, diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 6a4135187816326e6f174d339a382365544889cd..8800c7cdcb86735e3b884bd7bd1fbbf5a0522174 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -32,6 +32,7 @@ ui.workspace = true util.workspace = true workspace.workspace = true zed_actions.workspace = true +project_panel.workspace = true [dev-dependencies] ctor.workspace = true diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 4e11960b6b1a49aa6a774125f05cd0413da0038c..a1e64964ff578ed263e9e89a610997423f33f7c0 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -21,6 +21,7 @@ use picker::{Picker, PickerDelegate}; use project::{ PathMatchCandidateSet, Project, ProjectPath, WorktreeId, worktree_store::WorktreeStore, }; +use project_panel::project_panel_settings::ProjectPanelSettings; use settings::Settings; use std::{ borrow::Cow, @@ -1055,8 +1056,21 @@ impl FileFinderDelegate { if let Some(panel_match) = panel_match { self.labels_for_path_match(&panel_match.0, path_style) } else if let Some(worktree) = worktree { - let full_path = - worktree.read(cx).root_name().join(&entry_path.project.path); + let multiple_folders_open = self + .project + .read(cx) + .visible_worktrees(cx) + .filter(|worktree| !worktree.read(cx).is_single_file()) + .nth(1) + .is_some(); + + let full_path = if ProjectPanelSettings::get_global(cx).hide_root + && !multiple_folders_open + { + entry_path.project.path.clone() + } else { + worktree.read(cx).root_name().join(&entry_path.project.path) + }; let mut components = full_path.components(); let filename = components.next_back().unwrap_or(""); let prefix = components.rest(); diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 59ade17eb797bfc9549e6642f0a0b7375a0df831..c81d13420b179cc7ce0d8afd2aee26673673f09e 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1702,6 +1702,269 @@ async fn test_history_match_positions(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_history_labels_do_not_include_worktree_root_name(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + cx.update(|cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_root: true, + ..settings + }, + cx, + ); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/my_project"), + json!({ + "src": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + let picker = open_file_picker(&workspace, cx); + picker.update_in(cx, |finder, window, cx| { + let matches = &finder.delegate.matches.matches; + assert!(matches.len() >= 2); + + for m in matches.iter() { + if let Match::History { panel_match, .. } = m { + assert!( + panel_match.is_none(), + "History items with no query should not have a panel match" + ); + } + } + + let separator = PathStyle::local().primary_separator(); + + let (file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx); + assert_eq!(file_label.text(), "second.rs"); + assert_eq!( + path_label.text(), + format!("src{separator}"), + "History path label must not contain root name 'my_project'" + ); + + let (file_label, path_label) = finder.delegate.labels_for_match(&matches[1], window, cx); + assert_eq!(file_label.text(), "first.rs"); + assert_eq!( + path_label.text(), + format!("src{separator}"), + "History path label must not contain root name 'my_project'" + ); + }); + + // Now type a query so history items get panel_match populated, + // and verify labels stay consistent with the no-query case. + let picker = active_file_picker(&workspace, cx); + picker + .update_in(cx, |finder, window, cx| { + finder + .delegate + .update_matches("first".to_string(), window, cx) + }) + .await; + picker.update_in(cx, |finder, window, cx| { + let matches = &finder.delegate.matches.matches; + let history_match = matches + .iter() + .find(|m| matches!(m, Match::History { .. })) + .expect("Should have a history match for 'first'"); + + let (file_label, path_label) = finder.delegate.labels_for_match(history_match, window, cx); + assert_eq!(file_label.text(), "first.rs"); + let separator = PathStyle::local().primary_separator(); + assert_eq!( + path_label.text(), + format!("src{separator}"), + "Queried history path label must not contain root name 'my_project'" + ); + }); +} + +#[gpui::test] +async fn test_history_labels_include_worktree_root_name_when_hide_root_false( + cx: &mut gpui::TestAppContext, +) { + let app_state = init_test(cx); + + cx.update(|cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_root: false, + ..settings + }, + cx, + ); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/my_project"), + json!({ + "src": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), [path!("/my_project").as_ref()], cx).await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("sec", 1, "second.rs", &workspace, cx).await; + + let picker = open_file_picker(&workspace, cx); + picker.update_in(cx, |finder, window, cx| { + let matches = &finder.delegate.matches.matches; + let separator = PathStyle::local().primary_separator(); + + let (_file_label, path_label) = finder.delegate.labels_for_match(&matches[0], window, cx); + assert_eq!( + path_label.text(), + format!("my_project{separator}src{separator}"), + "With hide_root=false, history path label should include root name 'my_project'" + ); + }); +} + +#[gpui::test] +async fn test_history_labels_include_worktree_root_name_when_hide_root_true_and_multiple_folders( + cx: &mut gpui::TestAppContext, +) { + let app_state = init_test(cx); + + cx.update(|cx| { + let settings = *ProjectPanelSettings::get_global(cx); + ProjectPanelSettings::override_global( + ProjectPanelSettings { + hide_root: true, + ..settings + }, + cx, + ); + }); + + app_state + .fs + .as_fake() + .insert_tree( + path!("/my_project"), + json!({ + "src": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + } + }), + ) + .await; + + app_state + .fs + .as_fake() + .insert_tree( + path!("/my_second_project"), + json!({ + "src": { + "third.rs": "// Third Rust file", + "fourth.rs": "// Fourth Rust file", + } + }), + ) + .await; + + let project = Project::test( + app_state.fs.clone(), + [ + path!("/my_project").as_ref(), + path!("/my_second_project").as_ref(), + ], + cx, + ) + .await; + let (multi_workspace, cx) = + cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx)); + let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone()); + + open_close_queried_buffer("fir", 1, "first.rs", &workspace, cx).await; + open_close_queried_buffer("thi", 1, "third.rs", &workspace, cx).await; + + let picker = open_file_picker(&workspace, cx); + picker.update_in(cx, |finder, window, cx| { + let matches = &finder.delegate.matches.matches; + assert!(matches.len() >= 2, "Should have at least 2 history matches"); + + let separator = PathStyle::local().primary_separator(); + + let first_match = matches + .iter() + .find(|m| { + if let Match::History { path, .. } = m { + path.project.path.file_name() + .map(|n| n.to_string()) + .map_or(false, |name| name == "first.rs") + } else { + false + } + }) + .expect("Should have history match for first.rs"); + + let third_match = matches + .iter() + .find(|m| { + if let Match::History { path, .. } = m { + path.project.path.file_name() + .map(|n| n.to_string()) + .map_or(false, |name| name == "third.rs") + } else { + false + } + }) + .expect("Should have history match for third.rs"); + + let (_file_label, path_label) = + finder.delegate.labels_for_match(first_match, window, cx); + assert_eq!( + path_label.text(), + format!("my_project{separator}src{separator}"), + "With hide_root=true and multiple folders, history path label should include root name 'my_project'" + ); + + let (_file_label, path_label) = + finder.delegate.labels_for_match(third_match, window, cx); + assert_eq!( + path_label.text(), + format!("my_second_project{separator}src{separator}"), + "With hide_root=true and multiple folders, history path label should include root name 'my_second_project'" + ); + }); +} + #[gpui::test] async fn test_external_files_history(cx: &mut gpui::TestAppContext) { let app_state = init_test(cx); diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 255b0b0e6abcb31755d9e30bb00549329596f0e2..f92edad80c014aadca0f028285149992b9173a06 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,4 +1,4 @@ -mod project_panel_settings; +pub mod project_panel_settings; mod utils; use anyhow::{Context as _, Result}; diff --git a/docs/src/visual-customization.md b/docs/src/visual-customization.md index 1a8e608678ff2d6e526721ea122b8e30b69a2264..647872bf0586eb4a21879d7103a2329d750c2154 100644 --- a/docs/src/visual-customization.md +++ b/docs/src/visual-customization.md @@ -467,7 +467,8 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k }, // Sort order for entries (directories_first, mixed, files_first) "sort_mode": "directories_first", - // Whether to hide the root entry when only one folder is open in the window. + // Whether to hide the root entry when only one folder is open in the window; + // this also affects how file paths appear in the file finder history. "hide_root": false, // Whether to hide the hidden entries in the project panel. "hide_hidden": false