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