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_register_worktree),
217                client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
218                client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
219                client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
220            ],
221            client,
222            client_state: ProjectClientState::Remote {
223                remote_id,
224                replica_id,
225            },
226        }))
227    }
228
229    fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
230        if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
231            *remote_id_tx.borrow_mut() = remote_id;
232        }
233
234        for worktree in &self.worktrees {
235            worktree.update(cx, |worktree, _| {
236                if let Some(worktree) = worktree.as_local_mut() {
237                    worktree.set_project_remote_id(remote_id);
238                }
239            });
240        }
241
242        self.subscriptions.clear();
243        if let Some(remote_id) = remote_id {
244            self.subscriptions.extend([
245                self.client
246                    .subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
247                self.client
248                    .subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
249                self.client
250                    .subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
251            ]);
252        }
253    }
254
255    pub fn remote_id(&self) -> Option<u64> {
256        match &self.client_state {
257            ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
258            ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
259        }
260    }
261
262    pub fn replica_id(&self) -> ReplicaId {
263        match &self.client_state {
264            ProjectClientState::Local { .. } => 0,
265            ProjectClientState::Remote { replica_id, .. } => *replica_id,
266        }
267    }
268
269    pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
270        &self.collaborators
271    }
272
273    pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
274        &self.worktrees
275    }
276
277    pub fn worktree_for_id(&self, id: usize) -> Option<ModelHandle<Worktree>> {
278        self.worktrees
279            .iter()
280            .find(|worktree| worktree.id() == id)
281            .cloned()
282    }
283
284    pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
285        let rpc = self.client.clone();
286        cx.spawn(|this, mut cx| async move {
287            let remote_id = this.update(&mut cx, |this, _| {
288                if let ProjectClientState::Local {
289                    is_shared,
290                    remote_id_rx,
291                    ..
292                } = &mut this.client_state
293                {
294                    *is_shared = true;
295                    Ok(*remote_id_rx.borrow())
296                } else {
297                    Err(anyhow!("can't share a remote project"))
298                }
299            })?;
300
301            let remote_id = remote_id.ok_or_else(|| anyhow!("no project id"))?;
302            rpc.send(proto::ShareProject {
303                project_id: remote_id,
304            })
305            .await?;
306
307            this.update(&mut cx, |this, cx| {
308                for worktree in &this.worktrees {
309                    worktree.update(cx, |worktree, cx| {
310                        worktree.as_local_mut().unwrap().share(cx).detach();
311                    });
312                }
313            });
314            Ok(())
315        })
316    }
317
318    pub fn open_buffer(
319        &self,
320        path: ProjectPath,
321        cx: &mut ModelContext<Self>,
322    ) -> Task<Result<ModelHandle<Buffer>>> {
323        if let Some(worktree) = self.worktree_for_id(path.worktree_id) {
324            worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx))
325        } else {
326            cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) })
327        }
328    }
329
330    pub fn add_local_worktree(
331        &mut self,
332        abs_path: &Path,
333        cx: &mut ModelContext<Self>,
334    ) -> Task<Result<ModelHandle<Worktree>>> {
335        let fs = self.fs.clone();
336        let client = self.client.clone();
337        let user_store = self.user_store.clone();
338        let languages = self.languages.clone();
339        let path = Arc::from(abs_path);
340        cx.spawn(|this, mut cx| async move {
341            let worktree =
342                Worktree::open_local(client.clone(), user_store, path, fs, languages, &mut cx)
343                    .await?;
344            this.update(&mut cx, |this, cx| {
345                if let Some(project_id) = this.remote_id() {
346                    worktree.update(cx, |worktree, cx| {
347                        let worktree = worktree.as_local_mut().unwrap();
348                        worktree.set_project_remote_id(Some(project_id));
349                        let serialized_worktree = worktree.to_proto(cx);
350                        let authorized_logins = worktree.authorized_logins();
351                        cx.foreground()
352                            .spawn(async move {
353                                client
354                                    .request(proto::RegisterWorktree {
355                                        project_id,
356                                        worktree: Some(serialized_worktree),
357                                        authorized_logins,
358                                    })
359                                    .log_err();
360                            })
361                            .detach();
362                    });
363                }
364                this.add_worktree(worktree.clone(), cx);
365            });
366            Ok(worktree)
367        })
368    }
369
370    fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
371        cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
372        if self.active_worktree.is_none() {
373            self.set_active_worktree(Some(worktree.id()), cx);
374        }
375        self.worktrees.push(worktree);
376        cx.notify();
377    }
378
379    fn set_active_worktree(&mut self, worktree_id: Option<usize>, cx: &mut ModelContext<Self>) {
380        if self.active_worktree != worktree_id {
381            self.active_worktree = worktree_id;
382            cx.notify();
383        }
384    }
385
386    pub fn active_worktree(&self) -> Option<ModelHandle<Worktree>> {
387        self.active_worktree
388            .and_then(|worktree_id| self.worktree_for_id(worktree_id))
389    }
390
391    pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
392        let new_active_entry = entry.and_then(|project_path| {
393            let worktree = self.worktree_for_id(project_path.worktree_id)?;
394            let entry = worktree.read(cx).entry_for_path(project_path.path)?;
395            Some(ProjectEntry {
396                worktree_id: project_path.worktree_id,
397                entry_id: entry.id,
398            })
399        });
400        if new_active_entry != self.active_entry {
401            if let Some(worktree_id) = new_active_entry.map(|e| e.worktree_id) {
402                self.set_active_worktree(Some(worktree_id), cx);
403            }
404            self.active_entry = new_active_entry;
405            cx.emit(Event::ActiveEntryChanged(new_active_entry));
406        }
407    }
408
409    pub fn diagnostic_summaries<'a>(
410        &'a self,
411        cx: &'a AppContext,
412    ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
413        self.worktrees.iter().flat_map(move |worktree| {
414            let worktree_id = worktree.id();
415            worktree
416                .read(cx)
417                .diagnostic_summaries()
418                .map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
419        })
420    }
421
422    pub fn active_entry(&self) -> Option<ProjectEntry> {
423        self.active_entry
424    }
425
426    // RPC message handlers
427
428    fn handle_add_collaborator(
429        &mut self,
430        mut envelope: TypedEnvelope<proto::AddProjectCollaborator>,
431        _: Arc<Client>,
432        cx: &mut ModelContext<Self>,
433    ) -> Result<()> {
434        let user_store = self.user_store.clone();
435        let collaborator = envelope
436            .payload
437            .collaborator
438            .take()
439            .ok_or_else(|| anyhow!("empty collaborator"))?;
440
441        cx.spawn(|this, mut cx| {
442            async move {
443                let collaborator =
444                    Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
445                this.update(&mut cx, |this, cx| {
446                    this.collaborators
447                        .insert(collaborator.peer_id, collaborator);
448                    cx.notify();
449                });
450                Ok(())
451            }
452            .log_err()
453        })
454        .detach();
455
456        Ok(())
457    }
458
459    fn handle_remove_collaborator(
460        &mut self,
461        envelope: TypedEnvelope<proto::RemoveProjectCollaborator>,
462        _: Arc<Client>,
463        cx: &mut ModelContext<Self>,
464    ) -> Result<()> {
465        let peer_id = PeerId(envelope.payload.peer_id);
466        let replica_id = self
467            .collaborators
468            .remove(&peer_id)
469            .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
470            .replica_id;
471        for worktree in &self.worktrees {
472            worktree.update(cx, |worktree, cx| {
473                worktree.remove_collaborator(peer_id, replica_id, cx);
474            })
475        }
476        Ok(())
477    }
478
479    fn handle_register_worktree(
480        &mut self,
481        envelope: TypedEnvelope<proto::RegisterWorktree>,
482        client: Arc<Client>,
483        cx: &mut ModelContext<Self>,
484    ) -> Result<()> {
485        let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
486        let replica_id = self.replica_id();
487        let worktree = envelope
488            .payload
489            .worktree
490            .ok_or_else(|| anyhow!("invalid worktree"))?;
491        let user_store = self.user_store.clone();
492        let languages = self.languages.clone();
493        cx.spawn(|this, mut cx| {
494            async move {
495                let worktree = Worktree::remote(
496                    remote_id, replica_id, worktree, client, user_store, languages, &mut cx,
497                )
498                .await?;
499                this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
500                Ok(())
501            }
502            .log_err()
503        })
504        .detach();
505        Ok(())
506    }
507
508    fn handle_update_worktree(
509        &mut self,
510        envelope: TypedEnvelope<proto::UpdateWorktree>,
511        _: Arc<Client>,
512        cx: &mut ModelContext<Self>,
513    ) -> Result<()> {
514        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
515            worktree.update(cx, |worktree, cx| {
516                let worktree = worktree.as_remote_mut().unwrap();
517                worktree.update_from_remote(envelope, cx)
518            })?;
519        }
520        Ok(())
521    }
522
523    pub fn handle_update_buffer(
524        &mut self,
525        envelope: TypedEnvelope<proto::UpdateBuffer>,
526        _: Arc<Client>,
527        cx: &mut ModelContext<Self>,
528    ) -> Result<()> {
529        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
530            worktree.update(cx, |worktree, cx| {
531                worktree.handle_update_buffer(envelope, cx)
532            })?;
533        }
534        Ok(())
535    }
536
537    pub fn handle_buffer_saved(
538        &mut self,
539        envelope: TypedEnvelope<proto::BufferSaved>,
540        _: Arc<Client>,
541        cx: &mut ModelContext<Self>,
542    ) -> Result<()> {
543        if let Some(worktree) = self.worktree_for_id(envelope.payload.worktree_id as usize) {
544            worktree.update(cx, |worktree, cx| {
545                worktree.handle_buffer_saved(envelope, cx)
546            })?;
547        }
548        Ok(())
549    }
550
551    pub fn match_paths<'a>(
552        &self,
553        query: &'a str,
554        include_ignored: bool,
555        smart_case: bool,
556        max_results: usize,
557        cancel_flag: &'a AtomicBool,
558        cx: &AppContext,
559    ) -> impl 'a + Future<Output = Vec<PathMatch>> {
560        let include_root_name = self.worktrees.len() > 1;
561        let candidate_sets = self
562            .worktrees
563            .iter()
564            .map(|worktree| CandidateSet {
565                snapshot: worktree.read(cx).snapshot(),
566                include_ignored,
567                include_root_name,
568            })
569            .collect::<Vec<_>>();
570
571        let background = cx.background().clone();
572        async move {
573            fuzzy::match_paths(
574                candidate_sets.as_slice(),
575                query,
576                smart_case,
577                max_results,
578                cancel_flag,
579                background,
580            )
581            .await
582        }
583    }
584}
585
586struct CandidateSet {
587    snapshot: Snapshot,
588    include_ignored: bool,
589    include_root_name: bool,
590}
591
592impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
593    type Candidates = CandidateSetIter<'a>;
594
595    fn id(&self) -> usize {
596        self.snapshot.id()
597    }
598
599    fn len(&self) -> usize {
600        if self.include_ignored {
601            self.snapshot.file_count()
602        } else {
603            self.snapshot.visible_file_count()
604        }
605    }
606
607    fn prefix(&self) -> Arc<str> {
608        if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
609            self.snapshot.root_name().into()
610        } else if self.include_root_name {
611            format!("{}/", self.snapshot.root_name()).into()
612        } else {
613            "".into()
614        }
615    }
616
617    fn candidates(&'a self, start: usize) -> Self::Candidates {
618        CandidateSetIter {
619            traversal: self.snapshot.files(self.include_ignored, start),
620        }
621    }
622}
623
624struct CandidateSetIter<'a> {
625    traversal: Traversal<'a>,
626}
627
628impl<'a> Iterator for CandidateSetIter<'a> {
629    type Item = PathMatchCandidate<'a>;
630
631    fn next(&mut self) -> Option<Self::Item> {
632        self.traversal.next().map(|entry| {
633            if let EntryKind::File(char_bag) = entry.kind {
634                PathMatchCandidate {
635                    path: &entry.path,
636                    char_bag,
637                }
638            } else {
639                unreachable!()
640            }
641        })
642    }
643}
644
645impl Entity for Project {
646    type Event = Event;
647
648    fn release(&mut self, cx: &mut gpui::MutableAppContext) {
649        if let ProjectClientState::Local { remote_id_rx, .. } = &self.client_state {
650            if let Some(project_id) = *remote_id_rx.borrow() {
651                let rpc = self.client.clone();
652                cx.spawn(|_| async move {
653                    if let Err(err) = rpc.send(proto::UnregisterProject { project_id }).await {
654                        log::error!("error unregistering project: {}", err);
655                    }
656                })
657                .detach();
658            }
659        }
660    }
661}
662
663impl Collaborator {
664    fn from_proto(
665        message: proto::Collaborator,
666        user_store: &ModelHandle<UserStore>,
667        cx: &mut AsyncAppContext,
668    ) -> impl Future<Output = Result<Self>> {
669        let user = user_store.update(cx, |user_store, cx| {
670            user_store.fetch_user(message.user_id, cx)
671        });
672
673        async move {
674            Ok(Self {
675                peer_id: PeerId(message.peer_id),
676                user: user.await?,
677                replica_id: message.replica_id as ReplicaId,
678            })
679        }
680    }
681}
682
683#[cfg(test)]
684mod tests {
685    use super::*;
686    use client::{http::ServerResponse, test::FakeHttpClient};
687    use fs::RealFs;
688    use gpui::TestAppContext;
689    use language::LanguageRegistry;
690    use serde_json::json;
691    use std::{os::unix, path::PathBuf};
692    use util::test::temp_tree;
693
694    #[gpui::test]
695    async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
696        let dir = temp_tree(json!({
697            "root": {
698                "apple": "",
699                "banana": {
700                    "carrot": {
701                        "date": "",
702                        "endive": "",
703                    }
704                },
705                "fennel": {
706                    "grape": "",
707                }
708            }
709        }));
710
711        let root_link_path = dir.path().join("root_link");
712        unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
713        unix::fs::symlink(
714            &dir.path().join("root/fennel"),
715            &dir.path().join("root/finnochio"),
716        )
717        .unwrap();
718
719        let project = build_project(&mut cx);
720
721        let tree = project
722            .update(&mut cx, |project, cx| {
723                project.add_local_worktree(&root_link_path, cx)
724            })
725            .await
726            .unwrap();
727
728        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
729            .await;
730        cx.read(|cx| {
731            let tree = tree.read(cx);
732            assert_eq!(tree.file_count(), 5);
733            assert_eq!(
734                tree.inode_for_path("fennel/grape"),
735                tree.inode_for_path("finnochio/grape")
736            );
737        });
738
739        let cancel_flag = Default::default();
740        let results = project
741            .read_with(&cx, |project, cx| {
742                project.match_paths("bna", false, false, 10, &cancel_flag, cx)
743            })
744            .await;
745        assert_eq!(
746            results
747                .into_iter()
748                .map(|result| result.path)
749                .collect::<Vec<Arc<Path>>>(),
750            vec![
751                PathBuf::from("banana/carrot/date").into(),
752                PathBuf::from("banana/carrot/endive").into(),
753            ]
754        );
755    }
756
757    #[gpui::test]
758    async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
759        let dir = temp_tree(json!({
760            "root": {
761                "dir1": {},
762                "dir2": {
763                    "dir3": {}
764                }
765            }
766        }));
767
768        let project = build_project(&mut cx);
769        let tree = project
770            .update(&mut cx, |project, cx| {
771                project.add_local_worktree(&dir.path(), cx)
772            })
773            .await
774            .unwrap();
775
776        cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
777            .await;
778
779        let cancel_flag = Default::default();
780        let results = project
781            .read_with(&cx, |project, cx| {
782                project.match_paths("dir", false, false, 10, &cancel_flag, cx)
783            })
784            .await;
785
786        assert!(results.is_empty());
787    }
788
789    fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
790        let languages = Arc::new(LanguageRegistry::new());
791        let fs = Arc::new(RealFs);
792        let client = client::Client::new();
793        let http_client = FakeHttpClient::new(|_| async move { Ok(ServerResponse::new(404)) });
794        let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
795        cx.add_model(|cx| Project::local(languages, client, user_store, fs, cx))
796    }
797}