diff --git a/Cargo.lock b/Cargo.lock index 265516111e..cc4c4bda01 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5381,8 +5381,10 @@ dependencies = [ "language", "menu", "picker", + "pretty_assertions", "project", "schemars", + "search", "serde", "serde_derive", "serde_json", diff --git a/assets/settings/default.json b/assets/settings/default.json index c99fed8b5e..3f65db94b2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -959,7 +959,17 @@ // "skip_focus_for_active_in_search": false // // Default: true - "skip_focus_for_active_in_search": true + "skip_focus_for_active_in_search": true, + // Whether to show the git status in the file finder. + "git_status": true, + // Whether to use gitignored files when searching. + // Only the file Zed had indexed will be used, not necessary all the gitignored files. + // + // Can accept 3 values: + // * `true`: Use all gitignored files + // * `false`: Use only the files Zed had indexed + // * `null`: Be smart and search for ignored when called from a gitignored worktree + "include_ignored": null }, // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index 3298a8c6bf..aabfa4362a 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -24,6 +24,7 @@ menu.workspace = true picker.workspace = true project.workspace = true schemars.workspace = true +search.workspace = true settings.workspace = true serde.workspace = true serde_derive.workspace = true @@ -40,6 +41,7 @@ editor = { workspace = true, features = ["test-support"] } gpui = { workspace = true, features = ["test-support"] } language = { workspace = true, features = ["test-support"] } picker = { workspace = true, features = ["test-support"] } +pretty_assertions.workspace = true serde_json.workspace = true theme = { workspace = true, features = ["test-support"] } workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 86fdaa9d08..77ec7fac9b 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -24,6 +24,7 @@ use new_path_prompt::NewPathPrompt; use open_path_prompt::OpenPathPrompt; use picker::{Picker, PickerDelegate}; use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId}; +use search::ToggleIncludeIgnored; use settings::Settings; use std::{ borrow::Cow, @@ -37,8 +38,8 @@ use std::{ }; use text::Point; use ui::{ - ContextMenu, HighlightedLabel, ListItem, ListItemSpacing, PopoverMenu, PopoverMenuHandle, - prelude::*, + ContextMenu, HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, PopoverMenu, + PopoverMenuHandle, Tooltip, prelude::*, }; use util::{ResultExt, maybe, paths::PathWithPosition, post_inc}; use workspace::{ @@ -222,6 +223,26 @@ impl FileFinder { }); } + fn handle_toggle_ignored( + &mut self, + _: &ToggleIncludeIgnored, + window: &mut Window, + cx: &mut Context, + ) { + self.picker.update(cx, |picker, cx| { + picker.delegate.include_ignored = match picker.delegate.include_ignored { + Some(true) => match FileFinderSettings::get_global(cx).include_ignored { + Some(_) => Some(false), + None => None, + }, + Some(false) => Some(true), + None => Some(true), + }; + picker.delegate.include_ignored_refresh = + picker.delegate.update_matches(picker.query(cx), window, cx); + }); + } + fn go_to_file_split_left( &mut self, _: &pane::SplitLeft, @@ -325,6 +346,7 @@ impl Render for FileFinder { .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) .on_action(cx.listener(Self::handle_select_prev)) .on_action(cx.listener(Self::handle_toggle_menu)) + .on_action(cx.listener(Self::handle_toggle_ignored)) .on_action(cx.listener(Self::go_to_file_split_left)) .on_action(cx.listener(Self::go_to_file_split_right)) .on_action(cx.listener(Self::go_to_file_split_up)) @@ -351,6 +373,8 @@ pub struct FileFinderDelegate { first_update: bool, popover_menu_handle: PopoverMenuHandle, focus_handle: FocusHandle, + include_ignored: Option, + include_ignored_refresh: Task<()>, } /// Use a custom ordering for file finder: the regular one @@ -736,6 +760,8 @@ impl FileFinderDelegate { first_update: true, popover_menu_handle: PopoverMenuHandle::default(), focus_handle: cx.focus_handle(), + include_ignored: FileFinderSettings::get_global(cx).include_ignored, + include_ignored_refresh: Task::ready(()), } } @@ -779,7 +805,11 @@ impl FileFinderDelegate { let worktree = worktree.read(cx); PathMatchCandidateSet { snapshot: worktree.snapshot(), - include_ignored: true, + include_ignored: self.include_ignored.unwrap_or_else(|| { + worktree + .root_entry() + .map_or(false, |entry| entry.is_ignored) + }), include_root_name, candidates: project::Candidates::Files, } @@ -1468,39 +1498,76 @@ impl PickerDelegate for FileFinderDelegate { h_flex() .w_full() .p_2() - .gap_2() - .justify_end() + .justify_between() .border_t_1() .border_color(cx.theme().colors().border_variant) .child( - Button::new("open-selection", "Open").on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx) - }), - ) - .child( - PopoverMenu::new("menu-popover") - .with_handle(self.popover_menu_handle.clone()) - .attach(gpui::Corner::TopRight) - .anchor(gpui::Corner::BottomRight) - .trigger( - Button::new("actions-trigger", "Split…") - .selected_label_color(Color::Accent), - ) - .menu({ + IconButton::new("toggle-ignored", IconName::Sliders) + .on_click({ + let focus_handle = self.focus_handle.clone(); + move |_, window, cx| { + focus_handle.dispatch_action(&ToggleIncludeIgnored, window, cx); + } + }) + .style(ButtonStyle::Subtle) + .shape(IconButtonShape::Square) + .toggle_state(self.include_ignored.unwrap_or(false)) + .tooltip({ + let focus_handle = self.focus_handle.clone(); move |window, cx| { - Some(ContextMenu::build(window, cx, { - let context = context.clone(); - move |menu, _, _| { - menu.context(context) - .action("Split Left", pane::SplitLeft.boxed_clone()) - .action("Split Right", pane::SplitRight.boxed_clone()) - .action("Split Up", pane::SplitUp.boxed_clone()) - .action("Split Down", pane::SplitDown.boxed_clone()) - } - })) + Tooltip::for_action_in( + "Use ignored files", + &ToggleIncludeIgnored, + &focus_handle, + window, + cx, + ) } }), ) + .child( + h_flex() + .p_2() + .gap_2() + .child( + Button::new("open-selection", "Open").on_click(|_, window, cx| { + window.dispatch_action(menu::Confirm.boxed_clone(), cx) + }), + ) + .child( + PopoverMenu::new("menu-popover") + .with_handle(self.popover_menu_handle.clone()) + .attach(gpui::Corner::TopRight) + .anchor(gpui::Corner::BottomRight) + .trigger( + Button::new("actions-trigger", "Split…") + .selected_label_color(Color::Accent), + ) + .menu({ + move |window, cx| { + Some(ContextMenu::build(window, cx, { + let context = context.clone(); + move |menu, _, _| { + menu.context(context) + .action( + "Split Left", + pane::SplitLeft.boxed_clone(), + ) + .action( + "Split Right", + pane::SplitRight.boxed_clone(), + ) + .action("Split Up", pane::SplitUp.boxed_clone()) + .action( + "Split Down", + pane::SplitDown.boxed_clone(), + ) + } + })) + } + }), + ), + ) .into_any(), ) } diff --git a/crates/file_finder/src/file_finder_settings.rs b/crates/file_finder/src/file_finder_settings.rs index 4a2f2bd2a3..350e1de3b3 100644 --- a/crates/file_finder/src/file_finder_settings.rs +++ b/crates/file_finder/src/file_finder_settings.rs @@ -8,6 +8,7 @@ pub struct FileFinderSettings { pub file_icons: bool, pub modal_max_width: Option, pub skip_focus_for_active_in_search: bool, + pub include_ignored: Option, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] @@ -24,6 +25,20 @@ pub struct FileFinderSettingsContent { /// /// Default: true pub skip_focus_for_active_in_search: Option, + /// Determines whether to show the git status in the file finder + /// + /// Default: true + pub git_status: Option, + /// Whether to use gitignored files when searching. + /// Only the file Zed had indexed will be used, not necessary all the gitignored files. + /// + /// Can accept 3 values: + /// * `Some(true)`: Use all gitignored files + /// * `Some(false)`: Use only the files Zed had indexed + /// * `None`: Be smart and search for ignored when called from a gitignored worktree + /// + /// Default: None + pub include_ignored: Option>, } impl Settings for FileFinderSettings { diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index 0b80c21264..371675fbae 100644 --- a/crates/file_finder/src/file_finder_tests.rs +++ b/crates/file_finder/src/file_finder_tests.rs @@ -1,9 +1,10 @@ -use std::{assert_eq, future::IntoFuture, path::Path, time::Duration}; +use std::{future::IntoFuture, path::Path, time::Duration}; use super::*; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use menu::{Confirm, SelectNext, SelectPrevious}; +use pretty_assertions::assert_eq; use project::{FS_WATCH_LATENCY, RemoveOptions}; use serde_json::json; use util::path; @@ -646,6 +647,31 @@ async fn test_ignored_root(cx: &mut TestAppContext) { .await; let (picker, workspace, cx) = build_find_picker(project, cx); + picker + .update_in(cx, |picker, window, cx| { + picker + .delegate + .spawn_search(test_path_position("hi"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![ + PathBuf::from("ignored-root/hi"), + PathBuf::from("tracked-root/hi"), + PathBuf::from("ignored-root/hiccup"), + PathBuf::from("tracked-root/hiccup"), + PathBuf::from("ignored-root/height"), + PathBuf::from("ignored-root/happiness"), + PathBuf::from("tracked-root/happiness"), + ], + "All ignored files that were indexed are found for default ignored mode" + ); + }); + cx.dispatch_action(ToggleIncludeIgnored); picker .update_in(cx, |picker, window, cx| { picker @@ -668,7 +694,29 @@ async fn test_ignored_root(cx: &mut TestAppContext) { PathBuf::from("ignored-root/happiness"), PathBuf::from("tracked-root/happiness"), ], - "All ignored files that were indexed are found" + "All ignored files should be found, for the toggled on ignored mode" + ); + }); + + picker + .update_in(cx, |picker, window, cx| { + picker.delegate.include_ignored = Some(false); + picker + .delegate + .spawn_search(test_path_position("hi"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![ + PathBuf::from("tracked-root/hi"), + PathBuf::from("tracked-root/hiccup"), + PathBuf::from("tracked-root/happiness"), + ], + "Only non-ignored files should be found for the turned off ignored mode" ); }); @@ -686,6 +734,7 @@ async fn test_ignored_root(cx: &mut TestAppContext) { }) .await .unwrap(); + cx.run_until_parked(); workspace .update_in(cx, |workspace, window, cx| { workspace.active_pane().update(cx, |pane, cx| { @@ -695,8 +744,37 @@ async fn test_ignored_root(cx: &mut TestAppContext) { }) .await .unwrap(); + cx.run_until_parked(); + picker .update_in(cx, |picker, window, cx| { + picker.delegate.include_ignored = None; + picker + .delegate + .spawn_search(test_path_position("hi"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![ + PathBuf::from("ignored-root/hi"), + PathBuf::from("tracked-root/hi"), + PathBuf::from("ignored-root/hiccup"), + PathBuf::from("tracked-root/hiccup"), + PathBuf::from("ignored-root/height"), + PathBuf::from("ignored-root/happiness"), + PathBuf::from("tracked-root/happiness"), + ], + "Only for the worktree with the ignored root, all indexed ignored files are found in the auto ignored mode" + ); + }); + + picker + .update_in(cx, |picker, window, cx| { + picker.delegate.include_ignored = Some(true); picker .delegate .spawn_search(test_path_position("hi"), window, cx) @@ -719,7 +797,29 @@ async fn test_ignored_root(cx: &mut TestAppContext) { PathBuf::from("ignored-root/happiness"), PathBuf::from("tracked-root/happiness"), ], - "All ignored files that were indexed are found" + "All ignored files that were indexed are found in the turned on ignored mode" + ); + }); + + picker + .update_in(cx, |picker, window, cx| { + picker.delegate.include_ignored = Some(false); + picker + .delegate + .spawn_search(test_path_position("hi"), window, cx) + }) + .await; + picker.update(cx, |picker, _| { + let matches = collect_search_matches(picker); + assert_eq!(matches.history.len(), 0); + assert_eq!( + matches.search, + vec![ + PathBuf::from("tracked-root/hi"), + PathBuf::from("tracked-root/hiccup"), + PathBuf::from("tracked-root/happiness"), + ], + "Only non-ignored files should be found for the turned off ignored mode" ); }); }