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}