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}