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