project.rs

  1pub mod fs;
  2mod ignore;
  3mod worktree;
  4
  5use anyhow::{anyhow, Result};
  6use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
  7use clock::ReplicaId;
  8use collections::HashMap;
  9use futures::Future;
 10use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
 11use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
 12use language::{Buffer, DiagnosticEntry, LanguageRegistry};
 13use lsp::DiagnosticSeverity;
 14use postage::{prelude::Stream, watch};
 15use std::{
 16    path::Path,
 17    sync::{atomic::AtomicBool, Arc},
 18};
 19use util::TryFutureExt as _;
 20
 21pub use fs::*;
 22pub use worktree::*;
 23
 24pub struct Project {
 25    worktrees: Vec<ModelHandle<Worktree>>,
 26    active_worktree: Option<usize>,
 27    active_entry: Option<ProjectEntry>,
 28    languages: Arc<LanguageRegistry>,
 29    client: Arc<client::Client>,
 30    user_store: ModelHandle<UserStore>,
 31    fs: Arc<dyn Fs>,
 32    client_state: ProjectClientState,
 33    collaborators: HashMap<PeerId, Collaborator>,
 34    subscriptions: Vec<client::Subscription>,
 35}
 36
 37enum ProjectClientState {
 38    Local {
 39        is_shared: bool,
 40        remote_id_tx: watch::Sender<Option<u64>>,
 41        remote_id_rx: watch::Receiver<Option<u64>>,
 42        _maintain_remote_id_task: Task<Option<()>>,
 43    },
 44    Remote {
 45        remote_id: u64,
 46        replica_id: ReplicaId,
 47    },
 48}
 49
 50#[derive(Clone, Debug)]
 51pub struct Collaborator {
 52    pub user: Arc<User>,
 53    pub peer_id: PeerId,
 54    pub replica_id: ReplicaId,
 55}
 56
 57pub enum Event {
 58    ActiveEntryChanged(Option<ProjectEntry>),
 59    WorktreeRemoved(usize),
 60}
 61
 62#[derive(Clone, Debug, Eq, PartialEq, Hash)]
 63pub struct ProjectPath {
 64    pub worktree_id: usize,
 65    pub path: Arc<Path>,
 66}
 67
 68#[derive(Clone)]
 69pub struct DiagnosticSummary {
 70    pub error_count: usize,
 71    pub warning_count: usize,
 72    pub info_count: usize,
 73    pub hint_count: usize,
 74}
 75
 76impl DiagnosticSummary {
 77    fn new<T>(diagnostics: &[DiagnosticEntry<T>]) -> Self {
 78        let mut this = Self {
 79            error_count: 0,
 80            warning_count: 0,
 81            info_count: 0,
 82            hint_count: 0,
 83        };
 84
 85        for entry in diagnostics {
 86            if entry.diagnostic.is_primary {
 87                match entry.diagnostic.severity {
 88                    DiagnosticSeverity::ERROR => this.error_count += 1,
 89                    DiagnosticSeverity::WARNING => this.warning_count += 1,
 90                    DiagnosticSeverity::INFORMATION => this.info_count += 1,
 91                    DiagnosticSeverity::HINT => this.hint_count += 1,
 92                    _ => {}
 93                }
 94            }
 95        }
 96
 97        this
 98    }
 99}
100
101#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
102pub struct ProjectEntry {
103    pub worktree_id: usize,
104    pub entry_id: usize,
105}
106
107impl Project {
108    pub fn local(
109        languages: Arc<LanguageRegistry>,
110        client: Arc<Client>,
111        user_store: ModelHandle<UserStore>,
112        fs: Arc<dyn Fs>,
113        cx: &mut ModelContext<Self>,
114    ) -> Self {
115        let (remote_id_tx, remote_id_rx) = watch::channel();
116        let _maintain_remote_id_task = cx.spawn_weak({
117            let rpc = client.clone();
118            move |this, mut cx| {
119                async move {
120                    let mut status = rpc.status();
121                    while let Some(status) = status.recv().await {
122                        if let Some(this) = this.upgrade(&cx) {
123                            let remote_id = if let client::Status::Connected { .. } = status {
124                                let response = rpc.request(proto::RegisterProject {}).await?;
125                                Some(response.project_id)
126                            } else {
127                                None
128                            };
129                            this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
130                        }
131                    }
132                    Ok(())
133                }
134                .log_err()
135            }
136        });
137
138        Self {
139            worktrees: Default::default(),
140            collaborators: Default::default(),
141            client_state: ProjectClientState::Local {
142                is_shared: false,
143                remote_id_tx,
144                remote_id_rx,
145                _maintain_remote_id_task,
146            },
147            subscriptions: Vec::new(),
148            active_worktree: None,
149            active_entry: None,
150            languages,
151            client,
152            user_store,
153            fs,
154        }
155    }
156
157    pub async fn open_remote(
158        remote_id: u64,
159        languages: Arc<LanguageRegistry>,
160        client: Arc<Client>,
161        user_store: ModelHandle<UserStore>,
162        fs: Arc<dyn Fs>,
163        cx: &mut AsyncAppContext,
164    ) -> Result<ModelHandle<Self>> {
165        client.authenticate_and_connect(&cx).await?;
166
167        let response = client
168            .request(proto::JoinProject {
169                project_id: remote_id,
170            })
171            .await?;
172
173        let replica_id = response.replica_id as ReplicaId;
174
175        let mut worktrees = Vec::new();
176        for worktree in response.worktrees {
177            worktrees.push(
178                Worktree::remote(
179                    remote_id,
180                    replica_id,
181                    worktree,
182                    client.clone(),
183                    user_store.clone(),
184                    languages.clone(),
185                    cx,
186                )
187                .await?,
188            );
189        }
190
191        let user_ids = response
192            .collaborators
193            .iter()
194            .map(|peer| peer.user_id)
195            .collect();
196        user_store
197            .update(cx, |user_store, cx| user_store.load_users(user_ids, cx))
198            .await?;
199        let mut collaborators = HashMap::default();
200        for message in response.collaborators {
201            let collaborator = Collaborator::from_proto(message, &user_store, cx).await?;
202            collaborators.insert(collaborator.peer_id, collaborator);
203        }
204
205        Ok(cx.add_model(|cx| Self {
206            worktrees,
207            active_worktree: None,
208            active_entry: None,
209            collaborators,
210            languages,
211            user_store,
212            fs,
213            subscriptions: vec![
214                client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
215                client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
216                client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
217                client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
218                client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
219                client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
220                client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
221            ],
222            client,
223            client_state: ProjectClientState::Remote {
224                remote_id,
225                replica_id,
226            },
227        }))
228    }
229
230    fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
231        if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
232            *remote_id_tx.borrow_mut() = remote_id;
233        }
234
235        self.subscriptions.clear();
236        if let Some(remote_id) = remote_id {
237            self.subscriptions.extend([
238                self.client
239                    .subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
240                self.client
241                    .subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
242                self.client
243                    .subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
244            ]);
245        }
246    }
247
248    pub fn remote_id(&self) -> Option<u64> {
249        match &self.client_state {
250            ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
251            ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
252        }
253    }
254
255    pub fn replica_id(&self) -> ReplicaId {
256        match &self.client_state {
257            ProjectClientState::Local { .. } => 0,
258            ProjectClientState::Remote { replica_id, .. } => *replica_id,
259        }
260    }
261
262    pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
263        &self.collaborators
264    }
265
266    pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
267        &self.worktrees
268    }
269
270    pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
271        self.worktrees
272            .iter()
273            .find(|worktree| worktree.id() == id)
274            .cloned()
275    }
276
277    pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
278        let rpc = self.client.clone();
279        cx.spawn(|this, mut cx| async move {
280            let remote_id = this.update(&mut cx, |this, _| {
281                if let ProjectClientState::Local {
282                    is_shared,
283                    remote_id_rx,
284                    ..
285                } = &mut this.client_state
286                {
287                    *is_shared = true;
288                    Ok(*remote_id_rx.borrow())
289                } else {
290                    Err(anyhow!("can't share a remote project"))
291                }
292            })?;
293
294            let remote_id = remote_id.ok_or_else(|| anyhow!("no project id"))?;
295            rpc.send(proto::ShareProject {
296                project_id: remote_id,
297            })
298            .await?;
299
300            this.update(&mut cx, |this, cx| {
301                for worktree in &this.worktrees {
302                    worktree.update(cx, |worktree, cx| {
303                        worktree
304                            .as_local_mut()
305                            .unwrap()
306                            .share(remote_id, cx)
307                            .detach();
308                    });
309                }
310            });
311            Ok(())
312        })
313    }
314
315    pub fn open_buffer(
316        &self,
317        path: ProjectPath,
318        cx: &mut ModelContext<Self>,
319    ) -> Task<Result<ModelHandle<Buffer>>> {
320        if let Some(worktree) = self.worktree_for_id(path.worktree_id) {
321            worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx))
322        } else {
323            cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) })
324        }
325    }
326
327    fn is_shared(&self) -> bool {
328        match &self.client_state {
329            ProjectClientState::Local { is_shared, .. } => *is_shared,
330            ProjectClientState::Remote { .. } => false,
331        }
332    }
333
334    pub fn add_local_worktree(
335        &mut self,
336        abs_path: &Path,
337        cx: &mut ModelContext<Self>,
338    ) -> Task<Result<ModelHandle<Worktree>>> {
339        let fs = self.fs.clone();
340        let client = self.client.clone();
341        let user_store = self.user_store.clone();
342        let languages = self.languages.clone();
343        let path = Arc::from(abs_path);
344        cx.spawn(|project, mut cx| async move {
345            let worktree =
346                Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx)
347                    .await?;
348
349            let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| {
350                project.add_worktree(worktree.clone(), cx);
351                (project.remote_id(), project.is_shared())
352            });
353
354            if let Some(project_id) = remote_project_id {
355                let register_message = worktree.update(&mut cx, |worktree, _| {
356                    let worktree = worktree.as_local_mut().unwrap();
357                    proto::RegisterWorktree {
358                        project_id,
359                        root_name: worktree.root_name().to_string(),
360                        authorized_logins: worktree.authorized_logins(),
361                    }
362                });
363                client.request(register_message).await?;
364                if is_shared {
365                    worktree
366                        .update(&mut cx, |worktree, cx| {
367                            worktree.as_local_mut().unwrap().share(project_id, cx)
368                        })
369                        .await?;
370                }
371            }
372
373            Ok(worktree)
374        })
375    }
376
377    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
378        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
379        if self.active_worktree.is_none() {
380            self.set_active_worktree(Some(worktree.id()), cx);
381        }
382        self.worktrees.push(worktree);
383        cx.notify();
384    }
385
386    fn set_active_worktree(&mut self, worktree_id: Option<usize>, cx: &mut ModelContext<Self>) {
387        if self.active_worktree != worktree_id {
388            self.active_worktree = worktree_id;
389            cx.notify();
390        }
391    }
392
393    pub fn active_worktree(&self) -> Option<ModelHandle<Worktree>> {
394        self.active_worktree
395            .and_then(|worktree_id| self.worktree_for_id(worktree_id))
396    }
397
398    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
399        let new_active_entry = entry.and_then(|project_path| {
400            let worktree = self.worktree_for_id(project_path.worktree_id)?;
401            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
402            Some(ProjectEntry {
403                worktree_id: project_path.worktree_id,
404                entry_id: entry.id,
405            })
406        });
407        if new_active_entry != self.active_entry {
408            if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) {
409                self.set_active_worktree(Some(worktree_id), cx);
410            }
411            self.active_entry = new_active_entry;
412            cx.emit(Event::ActiveEntryChanged(new_active_entry));
413        }
414    }
415
416    pub fn diagnostic_summaries<'a>(
417        &'a self,
418        cx: &'a AppContext,
419    ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
420        self.worktrees.iter().flat_map(move |worktree| {
421            let worktree_id = worktree.id();
422            worktree
423                .read(cx)
424                .diagnostic_summaries()
425                .map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
426        })
427    }
428
429    pub fn active_entry(&self) -> Option<ProjectEntry> {
430        self.active_entry
431    }
432
433    // RPC message handlers
434
435    fn handle_add_collaborator(
436        &mut self,
437        mut envelope: TypedEnvelope<proto::AddProjectCollaborator>,
438        _: Arc<Client>,
439        cx: &mut ModelContext<Self>,
440    ) -> Result<()> {
441        let user_store = self.user_store.clone();
442        let collaborator = envelope
443            .payload
444            .collaborator
445            .take()
446            .ok_or_else(|| anyhow!("empty collaborator"))?;
447
448        cx.spawn(|this, mut cx| {
449            async move {
450                let collaborator =
451                    Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
452                this.update(&mut cx, |this, cx| {
453                    this.collaborators
454                        .insert(collaborator.peer_id, collaborator);
455                    cx.notify();
456                });
457                Ok(())
458            }
459            .log_err()
460        })
461        .detach();
462
463        Ok(())
464    }
465
466    fn handle_remove_collaborator(
467        &mut self,
468        envelope: TypedEnvelope<proto::RemoveProjectCollaborator>,
469        _: Arc<Client>,
470        cx: &mut ModelContext<Self>,
471    ) -> Result<()> {
472        let peer_id = PeerId(envelope.payload.peer_id);
473        let replica_id = self
474            .collaborators
475            .remove(&peer_id)
476            .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
477            .replica_id;
478        for worktree in &self.worktrees {
479            worktree.update(cx, |worktree, cx| {
480                worktree.remove_collaborator(peer_id, replica_id, cx);
481            })
482        }
483        Ok(())
484    }
485
486    fn handle_share_worktree(
487        &mut self,
488        envelope: TypedEnvelope<proto::ShareWorktree>,
489        client: Arc<Client>,
490        cx: &mut ModelContext<Self>,
491    ) -> Result<()> {
492        let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
493        let replica_id = self.replica_id();
494        let worktree = envelope
495            .payload
496            .worktree
497            .ok_or_else(|| anyhow!("invalid worktree"))?;
498        let user_store = self.user_store.clone();
499        let languages = self.languages.clone();
500        cx.spawn(|this, mut cx| {
501            async move {
502                let worktree = Worktree::remote(
503                    remote_id, replica_id, worktree, client, user_store, languages, &mut cx,
504                )
505                .await?;
506                this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
507                Ok(())
508            }
509            .log_err()
510        })
511        .detach();
512        Ok(())
513    }
514
515    fn handle_unregister_worktree(
516        &mut self,
517        envelope: TypedEnvelope<proto::UnregisterWorktree>,
518        _: Arc<Client>,
519        cx: &mut ModelContext<Self>,
520    ) -> Result<()> {
521        self.worktrees.retain(|worktree| {
522            worktree.read(cx).as_remote().unwrap().remote_id() != envelope.payload.worktree_id
523        });
524        cx.notify();
525        Ok(())
526    }
527
528    fn handle_update_worktree(
529        &mut self,
530        envelope: TypedEnvelope<proto::UpdateWorktree>,
531        _: Arc<Client>,
532        cx: &mut ModelContext<Self>,
533    ) -> Result<()> {
534        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
535            worktree.update(cx, |worktree, cx| {
536                let worktree = worktree.as_remote_mut().unwrap();
537                worktree.update_from_remote(envelope, cx)
538            })?;
539        }
540        Ok(())
541    }
542
543    pub fn handle_update_buffer(
544        &mut self,
545        envelope: TypedEnvelope<proto::UpdateBuffer>,
546        _: Arc<Client>,
547        cx: &mut ModelContext<Self>,
548    ) -> Result<()> {
549        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
550            worktree.update(cx, |worktree, cx| {
551                worktree.handle_update_buffer(envelope, cx)
552            })?;
553        }
554        Ok(())
555    }
556
557    pub fn handle_buffer_saved(
558        &mut self,
559        envelope: TypedEnvelope<proto::BufferSaved>,
560        _: Arc<Client>,
561        cx: &mut ModelContext<Self>,
562    ) -> Result<()> {
563        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
564            worktree.update(cx, |worktree, cx| {
565                worktree.handle_buffer_saved(envelope, cx)
566            })?;
567        }
568        Ok(())
569    }
570
571    pub fn match_paths<'a>(
572        &self,
573        query: &'a str,
574        include_ignored: bool,
575        smart_case: bool,
576        max_results: usize,
577        cancel_flag: &'a AtomicBool,
578        cx: &AppContext,
579    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
580        let include_root_name = self.worktrees.len() > 1;
581        let candidate_sets = self
582            .worktrees
583            .iter()
584            .map(|worktree| CandidateSet {
585                snapshot: worktree.read(cx).snapshot(),
586                include_ignored,
587                include_root_name,
588            })
589            .collect::<Vec<_>>();
590
591        let background = cx.background().clone();
592        async move {
593            fuzzy::match_paths(
594                candidate_sets.as_slice(),
595                query,
596                smart_case,
597                max_results,
598                cancel_flag,
599                background,
600            )
601            .await
602        }
603    }
604}
605
606struct CandidateSet {
607    snapshot: Snapshot,
608    include_ignored: bool,
609    include_root_name: bool,
610}
611
612impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
613    type Candidates = CandidateSetIter<'a>;
614
615    fn id(&self) -> usize {
616        self.snapshot.id()
617    }
618
619    fn len(&self) -> usize {
620        if self.include_ignored {
621            self.snapshot.file_count()
622        } else {
623            self.snapshot.visible_file_count()
624        }
625    }
626
627    fn prefix(&self) -> Arc<str> {
628        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
629            self.snapshot.root_name().into()
630        } else if self.include_root_name {
631            format!("{}/", self.snapshot.root_name()).into()
632        } else {
633            "".into()
634        }
635    }
636
637    fn candidates(&'a self, start: usize) -> Self::Candidates {
638        CandidateSetIter {
639            traversal: self.snapshot.files(self.include_ignored, start),
640        }
641    }
642}
643
644struct CandidateSetIter<'a> {
645    traversal: Traversal<'a>,
646}
647
648impl<'a> Iterator for CandidateSetIter<'a> {
649    type Item = PathMatchCandidate<'a>;
650
651    fn next(&mut self) -> Option<Self::Item> {
652        self.traversal.next().map(|entry| {
653            if let EntryKind::File(char_bag) = entry.kind {
654                PathMatchCandidate {
655                    path: &entry.path,
656                    char_bag,
657                }
658            } else {
659                unreachable!()
660            }
661        })
662    }
663}
664
665impl Entity for Project {
666    type Event = Event;
667
668    fn release(&mut self, cx: &mut gpui::MutableAppContext) {
669        if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state {
670            if let Some(project_id) = *remote_id_rx.borrow() {
671                let rpc = self.client.clone();
672                cx.spawn(|_| async move {
673                    if let Err(err) = rpc.send(proto::UnregisterProject { project_id }).await {
674                        log::error!("error unregistering project: {}", err);
675                    }
676                })
677                .detach();
678            }
679        }
680    }
681}
682
683impl Collaborator {
684    fn from_proto(
685        message: proto::Collaborator,
686        user_store: &ModelHandle<UserStore>,
687        cx: &mut AsyncAppContext,
688    ) -> impl Future<Output = Result<Self>> {
689        let user = user_store.update(cx, |user_store, cx| {
690            user_store.fetch_user(message.user_id, cx)
691        });
692
693        async move {
694            Ok(Self {
695                peer_id: PeerId(message.peer_id),
696                user: user.await?,
697                replica_id: message.replica_id as ReplicaId,
698            })
699        }
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706    use client::{http::ServerResponse, test::FakeHttpClient};
707    use fs::RealFs;
708    use gpui::TestAppContext;
709    use language::LanguageRegistry;
710    use serde_json::json;
711    use std::{os::unix, path::PathBuf};
712    use util::test::temp_tree;
713
714    #[gpui::test]
715    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
716        let dir = temp_tree(json!({
717            "root": {
718                "apple": "",
719                "banana": {
720                    "carrot": {
721                        "date": "",
722                        "endive": "",
723                    }
724                },
725                "fennel": {
726                    "grape": "",
727                }
728            }
729        }));
730
731        let root_link_path = dir.path().join("root_link");
732        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
733        unix::fs::symlink(
734            &dir.path().join("root/fennel"),
735            &dir.path().join("root/finnochio"),
736        )
737        .unwrap();
738
739        let project = build_project(&mut cx);
740
741        let tree = project
742            .update(&mut cx, |project, cx| {
743                project.add_local_worktree(&root_link_path, cx)
744            })
745            .await
746            .unwrap();
747
748        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
749            .await;
750        cx.read(|cx| {
751            let tree = tree.read(cx);
752            assert_eq!(tree.file_count(), 5);
753            assert_eq!(
754                tree.inode_for_path("fennel/grape"),
755                tree.inode_for_path("finnochio/grape")
756            );
757        });
758
759        let cancel_flag = Default::default();
760        let results = project
761            .read_with(&cx, |project, cx| {
762                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
763            })
764            .await;
765        assert_eq!(
766            results
767                .into_iter()
768                .map(|result| result.path)
769                .collect::<Vec<Arc<Path>>>(),
770            vec![
771                PathBuf::from("banana/carrot/date").into(),
772                PathBuf::from("banana/carrot/endive").into(),
773            ]
774        );
775    }
776
777    #[gpui::test]
778    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
779        let dir = temp_tree(json!({
780            "root": {
781                "dir1": {},
782                "dir2": {
783                    "dir3": {}
784                }
785            }
786        }));
787
788        let project = build_project(&mut cx);
789        let tree = project
790            .update(&mut cx, |project, cx| {
791                project.add_local_worktree(&dir.path(), cx)
792            })
793            .await
794            .unwrap();
795
796        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
797            .await;
798
799        let cancel_flag = Default::default();
800        let results = project
801            .read_with(&cx, |project, cx| {
802                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
803            })
804            .await;
805
806        assert!(results.is_empty());
807    }
808
809    fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
810        let languages = Arc::new(LanguageRegistry::new());
811        let fs = Arc::new(RealFs);
812        let client = client::Client::new();
813        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
814        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
815        cx.add_model(|cx| Project::local(languages, client, user_store, fs, cx))
816    }
817}