From bd024c62902d2e7a67040e6effe2183cbe626e6f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 10 Oct 2025 00:04:41 +0200 Subject: [PATCH] Start --- crates/project/src/project.rs | 1 + crates/project/src/project_search.rs | 137 +++++++++++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 crates/project/src/project_search.rs diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f301c7800a5b098ddc93a7badc1617f7842e62d1..6fbca802bd3b6527173ec79e5d847b4ccf2b3e7c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,6 +11,7 @@ pub mod lsp_command; pub mod lsp_store; mod manifest_tree; pub mod prettier_store; +mod project_search; pub mod project_settings; pub mod search; mod task_inventory; diff --git a/crates/project/src/project_search.rs b/crates/project/src/project_search.rs new file mode 100644 index 0000000000000000000000000000000000000000..5dedcbd94a3cb3ace971f84be14b0df01f318dc0 --- /dev/null +++ b/crates/project/src/project_search.rs @@ -0,0 +1,137 @@ +use std::{ + ops::Range, + pin::{Pin, pin}, +}; + +use futures::{FutureExt, StreamExt, future::BoxFuture, select_biased}; +use gpui::{App, AsyncApp, Entity, WeakEntity}; +use language::{Buffer, BufferSnapshot}; +use smol::channel::{Receiver, Sender, bounded, unbounded}; +use text::Anchor; +use util::{ResultExt, maybe}; +use worktree::{Entry, Snapshot, WorktreeSettings}; + +use crate::{ + ProjectPath, + buffer_store::BufferStore, + search::{SearchQuery, SearchResult}, +}; + +pub(crate) struct ProjectSearcher { + buffer_store: WeakEntity, + pub(crate) snapshots: Vec<(Snapshot, WorktreeSettings)>, +} + +impl ProjectSearcher { + pub(crate) fn search(self, query: SearchQuery, cx: &mut App) -> Receiver { + let executor = cx.background_executor().clone(); + let (tx, rx) = unbounded(); + cx.spawn(async move |cx| { + const MAX_CONCURRENT_BUFFER_OPENS: usize = 64; + let (find_all_matches_tx, find_all_matches_rx) = bounded(MAX_CONCURRENT_BUFFER_OPENS); + let (get_buffer_for_full_scan_tx, get_buffer_for_full_scan_rx) = + bounded(MAX_CONCURRENT_BUFFER_OPENS); + let worker_pool = executor.scoped(|scope| { + let (input_paths_tx, input_paths_rx) = bounded(64); + let (find_first_match_tx, find_first_match_rx) = bounded(64); + + for _ in 0..executor.num_cpus() { + let worker = Worker { + query: &query, + input_paths_rx: input_paths_rx.clone(), + find_first_match_rx: find_first_match_rx.clone(), + find_first_match_tx: find_first_match_tx.clone(), + get_buffer_for_full_scan_tx: get_buffer_for_full_scan_tx.clone(), + find_all_matches_rx: find_all_matches_rx.clone(), + publish_matches: todo!(), + }; + scope.spawn(worker.run()); + } + scope.spawn(self.provide_search_paths(&query, input_paths_tx)) + }); + self.open_buffers(get_buffer_for_full_scan_rx, find_all_matches_tx, cx) + .await; + worker_pool.await; + }) + .detach(); + rx + } + + async fn provide_search_paths<'a>(&'a self, query: &SearchQuery, tx: Sender<&'a Entry>) { + for (snapshot, _) in &self.snapshots { + for entry in snapshot.entries(query.include_ignored(), 0) { + let Ok(_) = tx.send(entry).await else { + return; + }; + } + } + } + + /// Background workers cannot open buffers by themselves, hence main thread will do it on their behalf. + async fn open_buffers<'a>( + &'a self, + rx: Receiver, + find_all_matches_tx: Sender<(Entity, BufferSnapshot)>, + cx: &mut AsyncApp, + ) { + _ = maybe!(async move { + while let Ok(requested_path) = rx.recv().await { + let Some(buffer) = self + .buffer_store + .update(cx, |this, cx| this.open_buffer(requested_path, cx))? + .await + .log_err() + else { + continue; + }; + let snapshot = buffer.read_with(cx, |this, _| this.snapshot())?; + find_all_matches_tx.send((buffer, snapshot)).await?; + } + Result::<_, anyhow::Error>::Ok(()) + }) + .await; + } +} + +struct Worker<'search> { + query: &'search SearchQuery, + /// Start off with all paths in project and filter them based on: + /// - Include filters + /// - Exclude filters + /// - Only open buffers + /// - Scan ignored files + /// Put another way: filter out files that can't match (without looking at file contents) + input_paths_rx: Receiver<&'search Entry>, + /// After that, figure out which paths contain at least one match (look at file contents). That's called "partial scan". + find_first_match_tx: Sender<()>, + find_first_match_rx: Receiver<()>, + /// Of those that contain at least one match, look for rest of matches (and figure out their ranges). + /// But wait - first, we need to go back to the main thread to open a buffer (& create an entity for it). + get_buffer_for_full_scan_tx: Sender, + /// Ok, we're back in background: run full scan & find all matches in a given buffer snapshot. + find_all_matches_rx: Receiver<(Entity, BufferSnapshot)>, + /// Cool, we have results; let's share them with the world. + publish_matches: Sender<(Entity, Vec>)>, +} + +impl Worker<'_> { + async fn run(self) { + let mut find_all_matches = pin!(self.find_all_matches_rx.fuse()); + let mut find_first_match = pin!(self.find_first_match_rx.fuse()); + let mut scan_path = pin!(self.input_paths_rx.fuse()); + loop { + select_biased! { + find_all_matches = find_all_matches.next() => { + + }, + find_first_match = find_first_match.next() => { + + }, + scan_path = scan_path.next() => { + + }, + complete => break, + } + } + } +}