Cargo.lock 🔗
@@ -6206,6 +6206,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
+ "project_panel",
"serde",
"serde_json",
"settings",
Giorgi Merebashvili and Kirill Bulatov created
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 <kirill@zed.dev>
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(-)
@@ -6206,6 +6206,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
+ "project_panel",
"serde",
"serde_json",
"settings",
@@ -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,
@@ -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
@@ -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();
@@ -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);
@@ -1,4 +1,4 @@
-mod project_panel_settings;
+pub mod project_panel_settings;
mod utils;
use anyhow::{Context as _, Result};
@@ -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