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