diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index eb57afece1..dcade2781e 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -365,14 +365,7 @@ impl FileFinderDelegate { history_items: Vec, cx: &mut ViewContext, ) -> Self { - cx.observe(&project, |file_finder, _, cx| { - //todo We should probably not re-render on every project anything - file_finder - .picker - .update(cx, |picker, cx| picker.refresh(cx)) - }) - .detach(); - + Self::subscribe_to_updates(&project, cx); Self { file_finder, workspace, @@ -389,6 +382,20 @@ impl FileFinderDelegate { } } + fn subscribe_to_updates(project: &Model, cx: &mut ViewContext) { + cx.subscribe(project, |file_finder, _, event, cx| { + match event { + project::Event::WorktreeUpdatedEntries(_, _) + | project::Event::WorktreeAdded + | project::Event::WorktreeRemoved(_) => file_finder + .picker + .update(cx, |picker, cx| picker.refresh(cx)), + _ => {} + }; + }) + .detach(); + } + fn spawn_search( &mut self, query: PathLikeWithPosition, diff --git a/crates/file_finder/src/file_finder_tests.rs b/crates/file_finder/src/file_finder_tests.rs index f0e1626bd4..8342677d38 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, path::Path, time::Duration}; +use std::{assert_eq, future::IntoFuture, path::Path, time::Duration}; use super::*; use editor::Editor; use gpui::{Entity, TestAppContext, VisualTestContext}; use menu::{Confirm, SelectNext}; +use project::worktree::FS_WATCH_LATENCY; use serde_json::json; use workspace::{AppState, Workspace}; @@ -1337,6 +1338,137 @@ async fn test_nonexistent_history_items_not_shown(cx: &mut gpui::TestAppContext) }); } +#[gpui::test] +async fn test_search_results_refreshed_on_worktree_updates(cx: &mut gpui::TestAppContext) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/src", + json!({ + "lib.rs": "// Lib file", + "main.rs": "// Bar file", + "read.me": "// Readme file", + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + + // Initial state + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "lib.rs"); + assert_match_at_position(finder, 1, "main.rs"); + }); + + // Delete main.rs + app_state + .fs + .remove_file("/src/main.rs".as_ref(), Default::default()) + .await + .expect("unable to remove file"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // main.rs is in not among search results anymore + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 1); + assert_match_at_position(finder, 0, "lib.rs"); + }); + + // Create util.rs + app_state + .fs + .create_file("/src/util.rs".as_ref(), Default::default()) + .await + .expect("unable to create file"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // util.rs is among search results + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "lib.rs"); + assert_match_at_position(finder, 1, "util.rs"); + }); +} + +#[gpui::test] +async fn test_search_results_refreshed_on_adding_and_removing_worktrees( + cx: &mut gpui::TestAppContext, +) { + let app_state = init_test(cx); + + app_state + .fs + .as_fake() + .insert_tree( + "/test", + json!({ + "project_1": { + "bar.rs": "// Bar file", + "lib.rs": "// Lib file", + }, + "project_2": { + "Cargo.toml": "// Cargo file", + "main.rs": "// Main file", + } + }), + ) + .await; + + let project = Project::test(app_state.fs.clone(), ["/test/project_1".as_ref()], cx).await; + let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_1_id = project.update(cx, |project, cx| { + let worktree = project.worktrees().last().expect("worktree not found"); + worktree.read(cx).id() + }); + + // Initial state + let picker = open_file_picker(&workspace, cx); + cx.simulate_input("rs"); + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 2); + assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + }); + + // Add new worktree + project + .update(cx, |project, cx| { + project + .find_or_create_local_worktree("/test/project_2", true, cx) + .into_future() + }) + .await + .expect("unable to create workdir"); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // main.rs is among search results + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 3); + assert_match_at_position(finder, 0, "bar.rs"); + assert_match_at_position(finder, 1, "lib.rs"); + assert_match_at_position(finder, 2, "main.rs"); + }); + + // Remove the first worktree + project.update(cx, |project, cx| { + project.remove_worktree(worktree_1_id, cx); + }); + cx.executor().advance_clock(FS_WATCH_LATENCY); + + // Files from the first worktree are not in the search results anymore + picker.update(cx, |finder, _| { + assert_eq!(finder.delegate.matches.len(), 1); + assert_match_at_position(finder, 0, "main.rs"); + }); +} + async fn open_close_queried_buffer( input: &str, expected_matches: usize, diff --git a/crates/project_core/src/worktree.rs b/crates/project_core/src/worktree.rs index 3a662fe3d0..2bd3d10cc9 100644 --- a/crates/project_core/src/worktree.rs +++ b/crates/project_core/src/worktree.rs @@ -68,6 +68,11 @@ use util::{ ResultExt, }; +#[cfg(feature = "test-support")] +pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); +#[cfg(not(feature = "test-support"))] +const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); + #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); @@ -652,7 +657,7 @@ fn start_background_scan_tasks( let abs_path = abs_path.to_path_buf(); let background = cx.background_executor().clone(); async move { - let events = fs.watch(&abs_path, Duration::from_millis(100)).await; + let events = fs.watch(&abs_path, FS_WATCH_LATENCY).await; let case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" @@ -4340,7 +4345,7 @@ impl BackgroundScanner { return self.executor.simulate_random_delay().await; } - smol::Timer::after(Duration::from_millis(100)).await; + smol::Timer::after(FS_WATCH_LATENCY).await; } }