worktree_store.rs

  1use std::{
  2    path::{Path, PathBuf},
  3    sync::{atomic::AtomicUsize, Arc},
  4};
  5
  6use anyhow::{anyhow, Context as _, Result};
  7use collections::{HashMap, HashSet};
  8use fs::Fs;
  9use futures::{
 10    future::{BoxFuture, Shared},
 11    FutureExt, SinkExt,
 12};
 13use gpui::{
 14    AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, Task, WeakModel,
 15};
 16use postage::oneshot;
 17use rpc::{
 18    proto::{self, SSH_PROJECT_ID},
 19    AnyProtoClient, ErrorExt, TypedEnvelope,
 20};
 21use smol::{
 22    channel::{Receiver, Sender},
 23    stream::StreamExt,
 24};
 25use text::ReplicaId;
 26use util::{paths::compare_paths, ResultExt};
 27use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
 28
 29use crate::{search::SearchQuery, ProjectPath};
 30
 31struct MatchingEntry {
 32    worktree_path: Arc<Path>,
 33    path: ProjectPath,
 34    respond: oneshot::Sender<ProjectPath>,
 35}
 36
 37enum WorktreeStoreState {
 38    Local {
 39        fs: Arc<dyn Fs>,
 40    },
 41    Remote {
 42        upstream_client: AnyProtoClient,
 43        upstream_project_id: u64,
 44    },
 45}
 46
 47pub struct WorktreeStore {
 48    next_entry_id: Arc<AtomicUsize>,
 49    downstream_client: Option<(AnyProtoClient, u64)>,
 50    retain_worktrees: bool,
 51    worktrees: Vec<WorktreeHandle>,
 52    worktrees_reordered: bool,
 53    #[allow(clippy::type_complexity)]
 54    loading_worktrees:
 55        HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
 56    state: WorktreeStoreState,
 57}
 58
 59pub enum WorktreeStoreEvent {
 60    WorktreeAdded(Model<Worktree>),
 61    WorktreeRemoved(EntityId, WorktreeId),
 62    WorktreeReleased(EntityId, WorktreeId),
 63    WorktreeOrderChanged,
 64    WorktreeUpdateSent(Model<Worktree>),
 65}
 66
 67impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
 68
 69impl WorktreeStore {
 70    pub fn init(client: &AnyProtoClient) {
 71        client.add_model_request_handler(Self::handle_create_project_entry);
 72        client.add_model_request_handler(Self::handle_rename_project_entry);
 73        client.add_model_request_handler(Self::handle_copy_project_entry);
 74        client.add_model_request_handler(Self::handle_delete_project_entry);
 75        client.add_model_request_handler(Self::handle_expand_project_entry);
 76    }
 77
 78    pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
 79        Self {
 80            next_entry_id: Default::default(),
 81            loading_worktrees: Default::default(),
 82            downstream_client: None,
 83            worktrees: Vec::new(),
 84            worktrees_reordered: false,
 85            retain_worktrees,
 86            state: WorktreeStoreState::Local { fs },
 87        }
 88    }
 89
 90    pub fn remote(
 91        retain_worktrees: bool,
 92        upstream_client: AnyProtoClient,
 93        upstream_project_id: u64,
 94    ) -> Self {
 95        Self {
 96            next_entry_id: Default::default(),
 97            loading_worktrees: Default::default(),
 98            downstream_client: None,
 99            worktrees: Vec::new(),
100            worktrees_reordered: false,
101            retain_worktrees,
102            state: WorktreeStoreState::Remote {
103                upstream_client,
104                upstream_project_id,
105            },
106        }
107    }
108
109    /// Iterates through all worktrees, including ones that don't appear in the project panel
110    pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Model<Worktree>> {
111        self.worktrees
112            .iter()
113            .filter_map(move |worktree| worktree.upgrade())
114    }
115
116    /// Iterates through all user-visible worktrees, the ones that appear in the project panel.
117    pub fn visible_worktrees<'a>(
118        &'a self,
119        cx: &'a AppContext,
120    ) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
121        self.worktrees()
122            .filter(|worktree| worktree.read(cx).is_visible())
123    }
124
125    pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
126        self.worktrees()
127            .find(|worktree| worktree.read(cx).id() == id)
128    }
129
130    pub fn worktree_for_entry(
131        &self,
132        entry_id: ProjectEntryId,
133        cx: &AppContext,
134    ) -> Option<Model<Worktree>> {
135        self.worktrees()
136            .find(|worktree| worktree.read(cx).contains_entry(entry_id))
137    }
138
139    pub fn find_worktree(
140        &self,
141        abs_path: &Path,
142        cx: &AppContext,
143    ) -> Option<(Model<Worktree>, PathBuf)> {
144        for tree in self.worktrees() {
145            if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
146                return Some((tree.clone(), relative_path.into()));
147            }
148        }
149        None
150    }
151
152    pub fn find_or_create_worktree(
153        &mut self,
154        abs_path: impl AsRef<Path>,
155        visible: bool,
156        cx: &mut ModelContext<Self>,
157    ) -> Task<Result<(Model<Worktree>, PathBuf)>> {
158        let abs_path = abs_path.as_ref();
159        if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
160            Task::ready(Ok((tree, relative_path)))
161        } else {
162            let worktree = self.create_worktree(abs_path, visible, cx);
163            cx.background_executor()
164                .spawn(async move { Ok((worktree.await?, PathBuf::new())) })
165        }
166    }
167
168    pub fn entry_for_id<'a>(
169        &'a self,
170        entry_id: ProjectEntryId,
171        cx: &'a AppContext,
172    ) -> Option<&'a Entry> {
173        self.worktrees()
174            .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))
175    }
176
177    pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
178        self.worktree_for_id(path.worktree_id, cx)?
179            .read(cx)
180            .entry_for_path(&path.path)
181            .cloned()
182    }
183
184    pub fn create_worktree(
185        &mut self,
186        abs_path: impl AsRef<Path>,
187        visible: bool,
188        cx: &mut ModelContext<Self>,
189    ) -> Task<Result<Model<Worktree>>> {
190        let path: Arc<Path> = abs_path.as_ref().into();
191        if !self.loading_worktrees.contains_key(&path) {
192            let task = match &self.state {
193                WorktreeStoreState::Remote {
194                    upstream_client, ..
195                } => {
196                    if upstream_client.is_via_collab() {
197                        Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
198                    } else {
199                        self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
200                    }
201                }
202                WorktreeStoreState::Local { fs } => {
203                    self.create_local_worktree(fs.clone(), abs_path, visible, cx)
204                }
205            };
206
207            self.loading_worktrees.insert(path.clone(), task.shared());
208        }
209        let task = self.loading_worktrees.get(&path).unwrap().clone();
210        cx.spawn(|this, mut cx| async move {
211            let result = task.await;
212            this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path))
213                .ok();
214            match result {
215                Ok(worktree) => Ok(worktree),
216                Err(err) => Err((*err).cloned()),
217            }
218        })
219    }
220
221    fn create_ssh_worktree(
222        &mut self,
223        client: AnyProtoClient,
224        abs_path: impl AsRef<Path>,
225        visible: bool,
226        cx: &mut ModelContext<Self>,
227    ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
228        let path_key: Arc<Path> = abs_path.as_ref().into();
229        let mut abs_path = path_key.clone().to_string_lossy().to_string();
230        // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
231        // in which case want to strip the leading the `/`.
232        // On the host-side, the `~` will get expanded.
233        // That's what git does too: https://github.com/libgit2/libgit2/issues/3345#issuecomment-127050850
234        if abs_path.starts_with("/~") {
235            abs_path = abs_path[1..].to_string();
236        }
237        if abs_path.is_empty() || abs_path == "/" {
238            abs_path = "~/".to_string();
239        }
240        cx.spawn(|this, mut cx| async move {
241            let this = this.upgrade().context("Dropped worktree store")?;
242
243            let response = client
244                .request(proto::AddWorktree {
245                    project_id: SSH_PROJECT_ID,
246                    path: abs_path.clone(),
247                    visible,
248                })
249                .await?;
250
251            if let Some(existing_worktree) = this.read_with(&cx, |this, cx| {
252                this.worktree_for_id(WorktreeId::from_proto(response.worktree_id), cx)
253            })? {
254                return Ok(existing_worktree);
255            }
256
257            let root_name = PathBuf::from(&response.canonicalized_path)
258                .file_name()
259                .map(|n| n.to_string_lossy().to_string())
260                .unwrap_or(response.canonicalized_path.to_string());
261
262            let worktree = cx.update(|cx| {
263                Worktree::remote(
264                    SSH_PROJECT_ID,
265                    0,
266                    proto::WorktreeMetadata {
267                        id: response.worktree_id,
268                        root_name,
269                        visible,
270                        abs_path: response.canonicalized_path,
271                    },
272                    client,
273                    cx,
274                )
275            })?;
276
277            this.update(&mut cx, |this, cx| {
278                this.add(&worktree, cx);
279            })?;
280            Ok(worktree)
281        })
282    }
283
284    fn create_local_worktree(
285        &mut self,
286        fs: Arc<dyn Fs>,
287        abs_path: impl AsRef<Path>,
288        visible: bool,
289        cx: &mut ModelContext<Self>,
290    ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
291        let next_entry_id = self.next_entry_id.clone();
292        let path: Arc<Path> = abs_path.as_ref().into();
293
294        cx.spawn(move |this, mut cx| async move {
295            let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
296
297            let worktree = worktree?;
298            this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
299
300            if visible {
301                cx.update(|cx| {
302                    cx.add_recent_document(&path);
303                })
304                .log_err();
305            }
306
307            Ok(worktree)
308        })
309    }
310
311    pub fn add(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
312        let worktree_id = worktree.read(cx).id();
313        debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id));
314
315        let push_strong_handle = self.retain_worktrees || worktree.read(cx).is_visible();
316        let handle = if push_strong_handle {
317            WorktreeHandle::Strong(worktree.clone())
318        } else {
319            WorktreeHandle::Weak(worktree.downgrade())
320        };
321        if self.worktrees_reordered {
322            self.worktrees.push(handle);
323        } else {
324            let i = match self
325                .worktrees
326                .binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {
327                    other.upgrade().map(|worktree| worktree.read(cx).abs_path())
328                }) {
329                Ok(i) | Err(i) => i,
330            };
331            self.worktrees.insert(i, handle);
332        }
333
334        cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));
335        self.send_project_updates(cx);
336
337        let handle_id = worktree.entity_id();
338        cx.observe_release(worktree, move |this, worktree, cx| {
339            cx.emit(WorktreeStoreEvent::WorktreeReleased(
340                handle_id,
341                worktree.id(),
342            ));
343            cx.emit(WorktreeStoreEvent::WorktreeRemoved(
344                handle_id,
345                worktree.id(),
346            ));
347            this.send_project_updates(cx);
348        })
349        .detach();
350    }
351
352    pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
353        self.worktrees.retain(|worktree| {
354            if let Some(worktree) = worktree.upgrade() {
355                if worktree.read(cx).id() == id_to_remove {
356                    cx.emit(WorktreeStoreEvent::WorktreeRemoved(
357                        worktree.entity_id(),
358                        id_to_remove,
359                    ));
360                    false
361                } else {
362                    true
363                }
364            } else {
365                false
366            }
367        });
368        self.send_project_updates(cx);
369    }
370
371    pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
372        self.worktrees_reordered = worktrees_reordered;
373    }
374
375    fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
376        match &self.state {
377            WorktreeStoreState::Remote {
378                upstream_client,
379                upstream_project_id,
380                ..
381            } => Some((upstream_client.clone(), *upstream_project_id)),
382            WorktreeStoreState::Local { .. } => None,
383        }
384    }
385
386    pub fn set_worktrees_from_proto(
387        &mut self,
388        worktrees: Vec<proto::WorktreeMetadata>,
389        replica_id: ReplicaId,
390        cx: &mut ModelContext<Self>,
391    ) -> Result<()> {
392        let mut old_worktrees_by_id = self
393            .worktrees
394            .drain(..)
395            .filter_map(|worktree| {
396                let worktree = worktree.upgrade()?;
397                Some((worktree.read(cx).id(), worktree))
398            })
399            .collect::<HashMap<_, _>>();
400
401        let (client, project_id) = self
402            .upstream_client()
403            .clone()
404            .ok_or_else(|| anyhow!("invalid project"))?;
405
406        for worktree in worktrees {
407            if let Some(old_worktree) =
408                old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
409            {
410                let push_strong_handle =
411                    self.retain_worktrees || old_worktree.read(cx).is_visible();
412                let handle = if push_strong_handle {
413                    WorktreeHandle::Strong(old_worktree.clone())
414                } else {
415                    WorktreeHandle::Weak(old_worktree.downgrade())
416                };
417                self.worktrees.push(handle);
418            } else {
419                self.add(
420                    &Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),
421                    cx,
422                );
423            }
424        }
425        self.send_project_updates(cx);
426
427        Ok(())
428    }
429
430    pub fn move_worktree(
431        &mut self,
432        source: WorktreeId,
433        destination: WorktreeId,
434        cx: &mut ModelContext<Self>,
435    ) -> Result<()> {
436        if source == destination {
437            return Ok(());
438        }
439
440        let mut source_index = None;
441        let mut destination_index = None;
442        for (i, worktree) in self.worktrees.iter().enumerate() {
443            if let Some(worktree) = worktree.upgrade() {
444                let worktree_id = worktree.read(cx).id();
445                if worktree_id == source {
446                    source_index = Some(i);
447                    if destination_index.is_some() {
448                        break;
449                    }
450                } else if worktree_id == destination {
451                    destination_index = Some(i);
452                    if source_index.is_some() {
453                        break;
454                    }
455                }
456            }
457        }
458
459        let source_index =
460            source_index.with_context(|| format!("Missing worktree for id {source}"))?;
461        let destination_index =
462            destination_index.with_context(|| format!("Missing worktree for id {destination}"))?;
463
464        if source_index == destination_index {
465            return Ok(());
466        }
467
468        let worktree_to_move = self.worktrees.remove(source_index);
469        self.worktrees.insert(destination_index, worktree_to_move);
470        self.worktrees_reordered = true;
471        cx.emit(WorktreeStoreEvent::WorktreeOrderChanged);
472        cx.notify();
473        Ok(())
474    }
475
476    pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
477        for worktree in &self.worktrees {
478            if let Some(worktree) = worktree.upgrade() {
479                worktree.update(cx, |worktree, _| {
480                    if let Some(worktree) = worktree.as_remote_mut() {
481                        worktree.disconnected_from_host();
482                    }
483                });
484            }
485        }
486    }
487
488    pub fn send_project_updates(&mut self, cx: &mut ModelContext<Self>) {
489        let Some((downstream_client, project_id)) = self.downstream_client.clone() else {
490            return;
491        };
492
493        let update = proto::UpdateProject {
494            project_id,
495            worktrees: self.worktree_metadata_protos(cx),
496        };
497
498        // collab has bad concurrency guarantees, so we send requests in serial.
499        let update_project = if downstream_client.is_via_collab() {
500            Some(downstream_client.request(update))
501        } else {
502            downstream_client.send(update).log_err();
503            None
504        };
505        cx.spawn(|this, mut cx| async move {
506            if let Some(update_project) = update_project {
507                update_project.await?;
508            }
509
510            this.update(&mut cx, |this, cx| {
511                let worktrees = this.worktrees().collect::<Vec<_>>();
512
513                for worktree in worktrees {
514                    worktree.update(cx, |worktree, cx| {
515                        let client = downstream_client.clone();
516                        worktree.observe_updates(project_id, cx, {
517                            move |update| {
518                                let client = client.clone();
519                                async move {
520                                    if client.is_via_collab() {
521                                        client
522                                            .request(update)
523                                            .map(|result| result.log_err().is_some())
524                                            .await
525                                    } else {
526                                        client.send(update).log_err().is_some()
527                                    }
528                                }
529                            }
530                        });
531                    });
532
533                    cx.emit(WorktreeStoreEvent::WorktreeUpdateSent(worktree.clone()))
534                }
535
536                anyhow::Ok(())
537            })
538        })
539        .detach_and_log_err(cx);
540    }
541
542    pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec<proto::WorktreeMetadata> {
543        self.worktrees()
544            .map(|worktree| {
545                let worktree = worktree.read(cx);
546                proto::WorktreeMetadata {
547                    id: worktree.id().to_proto(),
548                    root_name: worktree.root_name().into(),
549                    visible: worktree.is_visible(),
550                    abs_path: worktree.abs_path().to_string_lossy().into(),
551                }
552            })
553            .collect()
554    }
555
556    pub fn shared(
557        &mut self,
558        remote_id: u64,
559        downsteam_client: AnyProtoClient,
560        cx: &mut ModelContext<Self>,
561    ) {
562        self.retain_worktrees = true;
563        self.downstream_client = Some((downsteam_client, remote_id));
564
565        // When shared, retain all worktrees
566        for worktree_handle in self.worktrees.iter_mut() {
567            match worktree_handle {
568                WorktreeHandle::Strong(_) => {}
569                WorktreeHandle::Weak(worktree) => {
570                    if let Some(worktree) = worktree.upgrade() {
571                        *worktree_handle = WorktreeHandle::Strong(worktree);
572                    }
573                }
574            }
575        }
576        self.send_project_updates(cx);
577    }
578
579    pub fn unshared(&mut self, cx: &mut ModelContext<Self>) {
580        self.retain_worktrees = false;
581        self.downstream_client.take();
582
583        // When not shared, only retain the visible worktrees
584        for worktree_handle in self.worktrees.iter_mut() {
585            if let WorktreeHandle::Strong(worktree) = worktree_handle {
586                let is_visible = worktree.update(cx, |worktree, _| {
587                    worktree.stop_observing_updates();
588                    worktree.is_visible()
589                });
590                if !is_visible {
591                    *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
592                }
593            }
594        }
595    }
596
597    /// search over all worktrees and return buffers that *might* match the search.
598    pub fn find_search_candidates(
599        &self,
600        query: SearchQuery,
601        limit: usize,
602        open_entries: HashSet<ProjectEntryId>,
603        fs: Arc<dyn Fs>,
604        cx: &ModelContext<Self>,
605    ) -> Receiver<ProjectPath> {
606        let snapshots = self
607            .visible_worktrees(cx)
608            .filter_map(|tree| {
609                let tree = tree.read(cx);
610                Some((tree.snapshot(), tree.as_local()?.settings()))
611            })
612            .collect::<Vec<_>>();
613
614        let executor = cx.background_executor().clone();
615
616        // We want to return entries in the order they are in the worktrees, so we have one
617        // thread that iterates over the worktrees (and ignored directories) as necessary,
618        // and pushes a oneshot::Receiver to the output channel and a oneshot::Sender to the filter
619        // channel.
620        // We spawn a number of workers that take items from the filter channel and check the query
621        // against the version of the file on disk.
622        let (filter_tx, filter_rx) = smol::channel::bounded(64);
623        let (output_tx, mut output_rx) = smol::channel::bounded(64);
624        let (matching_paths_tx, matching_paths_rx) = smol::channel::unbounded();
625
626        let input = cx.background_executor().spawn({
627            let fs = fs.clone();
628            let query = query.clone();
629            async move {
630                Self::find_candidate_paths(
631                    fs,
632                    snapshots,
633                    open_entries,
634                    query,
635                    filter_tx,
636                    output_tx,
637                )
638                .await
639                .log_err();
640            }
641        });
642        const MAX_CONCURRENT_FILE_SCANS: usize = 64;
643        let filters = cx.background_executor().spawn(async move {
644            let fs = &fs;
645            let query = &query;
646            executor
647                .scoped(move |scope| {
648                    for _ in 0..MAX_CONCURRENT_FILE_SCANS {
649                        let filter_rx = filter_rx.clone();
650                        scope.spawn(async move {
651                            Self::filter_paths(fs, filter_rx, query).await.log_err();
652                        })
653                    }
654                })
655                .await;
656        });
657        cx.background_executor()
658            .spawn(async move {
659                let mut matched = 0;
660                while let Some(mut receiver) = output_rx.next().await {
661                    let Some(path) = receiver.next().await else {
662                        continue;
663                    };
664                    let Ok(_) = matching_paths_tx.send(path).await else {
665                        break;
666                    };
667                    matched += 1;
668                    if matched == limit {
669                        break;
670                    }
671                }
672                drop(input);
673                drop(filters);
674            })
675            .detach();
676        matching_paths_rx
677    }
678
679    fn scan_ignored_dir<'a>(
680        fs: &'a Arc<dyn Fs>,
681        snapshot: &'a worktree::Snapshot,
682        path: &'a Path,
683        query: &'a SearchQuery,
684        include_root: bool,
685        filter_tx: &'a Sender<MatchingEntry>,
686        output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
687    ) -> BoxFuture<'a, Result<()>> {
688        async move {
689            let abs_path = snapshot.abs_path().join(path);
690            let Some(mut files) = fs
691                .read_dir(&abs_path)
692                .await
693                .with_context(|| format!("listing ignored path {abs_path:?}"))
694                .log_err()
695            else {
696                return Ok(());
697            };
698
699            let mut results = Vec::new();
700
701            while let Some(Ok(file)) = files.next().await {
702                let Some(metadata) = fs
703                    .metadata(&file)
704                    .await
705                    .with_context(|| format!("fetching fs metadata for {abs_path:?}"))
706                    .log_err()
707                    .flatten()
708                else {
709                    continue;
710                };
711                if metadata.is_symlink || metadata.is_fifo {
712                    continue;
713                }
714                results.push((
715                    file.strip_prefix(snapshot.abs_path())?.to_path_buf(),
716                    !metadata.is_dir,
717                ))
718            }
719            results.sort_by(|(a_path, a_is_file), (b_path, b_is_file)| {
720                compare_paths((a_path, *a_is_file), (b_path, *b_is_file))
721            });
722            for (path, is_file) in results {
723                if is_file {
724                    if query.filters_path() {
725                        let matched_path = if include_root {
726                            let mut full_path = PathBuf::from(snapshot.root_name());
727                            full_path.push(&path);
728                            query.file_matches(&full_path)
729                        } else {
730                            query.file_matches(&path)
731                        };
732                        if !matched_path {
733                            continue;
734                        }
735                    }
736                    let (tx, rx) = oneshot::channel();
737                    output_tx.send(rx).await?;
738                    filter_tx
739                        .send(MatchingEntry {
740                            respond: tx,
741                            worktree_path: snapshot.abs_path().clone(),
742                            path: ProjectPath {
743                                worktree_id: snapshot.id(),
744                                path: Arc::from(path),
745                            },
746                        })
747                        .await?;
748                } else {
749                    Self::scan_ignored_dir(
750                        fs,
751                        snapshot,
752                        &path,
753                        query,
754                        include_root,
755                        filter_tx,
756                        output_tx,
757                    )
758                    .await?;
759                }
760            }
761            Ok(())
762        }
763        .boxed()
764    }
765
766    async fn find_candidate_paths(
767        fs: Arc<dyn Fs>,
768        snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>,
769        open_entries: HashSet<ProjectEntryId>,
770        query: SearchQuery,
771        filter_tx: Sender<MatchingEntry>,
772        output_tx: Sender<oneshot::Receiver<ProjectPath>>,
773    ) -> Result<()> {
774        let include_root = snapshots.len() > 1;
775        for (snapshot, settings) in snapshots {
776            let mut entries: Vec<_> = snapshot.entries(query.include_ignored(), 0).collect();
777            entries.sort_by(|a, b| compare_paths((&a.path, a.is_file()), (&b.path, b.is_file())));
778            for entry in entries {
779                if entry.is_dir() && entry.is_ignored {
780                    if !settings.is_path_excluded(&entry.path) {
781                        Self::scan_ignored_dir(
782                            &fs,
783                            &snapshot,
784                            &entry.path,
785                            &query,
786                            include_root,
787                            &filter_tx,
788                            &output_tx,
789                        )
790                        .await?;
791                    }
792                    continue;
793                }
794
795                if entry.is_fifo || !entry.is_file() {
796                    continue;
797                }
798
799                if query.filters_path() {
800                    let matched_path = if include_root {
801                        let mut full_path = PathBuf::from(snapshot.root_name());
802                        full_path.push(&entry.path);
803                        query.file_matches(&full_path)
804                    } else {
805                        query.file_matches(&entry.path)
806                    };
807                    if !matched_path {
808                        continue;
809                    }
810                }
811
812                let (mut tx, rx) = oneshot::channel();
813
814                if open_entries.contains(&entry.id) {
815                    tx.send(ProjectPath {
816                        worktree_id: snapshot.id(),
817                        path: entry.path.clone(),
818                    })
819                    .await?;
820                } else {
821                    filter_tx
822                        .send(MatchingEntry {
823                            respond: tx,
824                            worktree_path: snapshot.abs_path().clone(),
825                            path: ProjectPath {
826                                worktree_id: snapshot.id(),
827                                path: entry.path.clone(),
828                            },
829                        })
830                        .await?;
831                }
832
833                output_tx.send(rx).await?;
834            }
835        }
836        Ok(())
837    }
838
839    async fn filter_paths(
840        fs: &Arc<dyn Fs>,
841        mut input: Receiver<MatchingEntry>,
842        query: &SearchQuery,
843    ) -> Result<()> {
844        while let Some(mut entry) = input.next().await {
845            let abs_path = entry.worktree_path.join(&entry.path.path);
846            let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
847                continue;
848            };
849            if query.detect(file).unwrap_or(false) {
850                entry.respond.send(entry.path).await?
851            }
852        }
853
854        Ok(())
855    }
856
857    pub async fn handle_create_project_entry(
858        this: Model<Self>,
859        envelope: TypedEnvelope<proto::CreateProjectEntry>,
860        mut cx: AsyncAppContext,
861    ) -> Result<proto::ProjectEntryResponse> {
862        let worktree = this.update(&mut cx, |this, cx| {
863            let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
864            this.worktree_for_id(worktree_id, cx)
865                .ok_or_else(|| anyhow!("worktree not found"))
866        })??;
867        Worktree::handle_create_entry(worktree, envelope.payload, cx).await
868    }
869
870    pub async fn handle_rename_project_entry(
871        this: Model<Self>,
872        envelope: TypedEnvelope<proto::RenameProjectEntry>,
873        mut cx: AsyncAppContext,
874    ) -> Result<proto::ProjectEntryResponse> {
875        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
876        let worktree = this.update(&mut cx, |this, cx| {
877            this.worktree_for_entry(entry_id, cx)
878                .ok_or_else(|| anyhow!("worktree not found"))
879        })??;
880        Worktree::handle_rename_entry(worktree, envelope.payload, cx).await
881    }
882
883    pub async fn handle_copy_project_entry(
884        this: Model<Self>,
885        envelope: TypedEnvelope<proto::CopyProjectEntry>,
886        mut cx: AsyncAppContext,
887    ) -> Result<proto::ProjectEntryResponse> {
888        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
889        let worktree = this.update(&mut cx, |this, cx| {
890            this.worktree_for_entry(entry_id, cx)
891                .ok_or_else(|| anyhow!("worktree not found"))
892        })??;
893        Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
894    }
895
896    pub async fn handle_delete_project_entry(
897        this: Model<Self>,
898        envelope: TypedEnvelope<proto::DeleteProjectEntry>,
899        mut cx: AsyncAppContext,
900    ) -> Result<proto::ProjectEntryResponse> {
901        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
902        let worktree = this.update(&mut cx, |this, cx| {
903            this.worktree_for_entry(entry_id, cx)
904                .ok_or_else(|| anyhow!("worktree not found"))
905        })??;
906        Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
907    }
908
909    pub async fn handle_expand_project_entry(
910        this: Model<Self>,
911        envelope: TypedEnvelope<proto::ExpandProjectEntry>,
912        mut cx: AsyncAppContext,
913    ) -> Result<proto::ExpandProjectEntryResponse> {
914        let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
915        let worktree = this
916            .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
917            .ok_or_else(|| anyhow!("invalid request"))?;
918        Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
919    }
920}
921
922#[derive(Clone, Debug)]
923enum WorktreeHandle {
924    Strong(Model<Worktree>),
925    Weak(WeakModel<Worktree>),
926}
927
928impl WorktreeHandle {
929    fn upgrade(&self) -> Option<Model<Worktree>> {
930        match self {
931            WorktreeHandle::Strong(handle) => Some(handle.clone()),
932            WorktreeHandle::Weak(handle) => handle.upgrade(),
933        }
934    }
935}