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::{hash_map, HashMap, HashSet};
9use futures::Future;
10use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
11use gpui::{
12 AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task,
13};
14use language::{Buffer, DiagnosticEntry, Language, LanguageRegistry};
15use lsp::{DiagnosticSeverity, LanguageServer};
16use postage::{prelude::Stream, watch};
17use smol::block_on;
18use std::{
19 path::{Path, PathBuf},
20 sync::{atomic::AtomicBool, Arc},
21};
22use util::{ResultExt, TryFutureExt as _};
23
24pub use fs::*;
25pub use worktree::*;
26
27pub struct Project {
28 worktrees: Vec<ModelHandle<Worktree>>,
29 active_entry: Option<ProjectEntry>,
30 languages: Arc<LanguageRegistry>,
31 language_servers: HashMap<(WorktreeId, String), Arc<LanguageServer>>,
32 client: Arc<client::Client>,
33 user_store: ModelHandle<UserStore>,
34 fs: Arc<dyn Fs>,
35 client_state: ProjectClientState,
36 collaborators: HashMap<PeerId, Collaborator>,
37 subscriptions: Vec<client::Subscription>,
38 language_servers_with_diagnostics_running: isize,
39}
40
41enum ProjectClientState {
42 Local {
43 is_shared: bool,
44 remote_id_tx: watch::Sender<Option<u64>>,
45 remote_id_rx: watch::Receiver<Option<u64>>,
46 _maintain_remote_id_task: Task<Option<()>>,
47 },
48 Remote {
49 sharing_has_stopped: bool,
50 remote_id: u64,
51 replica_id: ReplicaId,
52 },
53}
54
55#[derive(Clone, Debug)]
56pub struct Collaborator {
57 pub user: Arc<User>,
58 pub peer_id: PeerId,
59 pub replica_id: ReplicaId,
60}
61
62#[derive(Clone, Debug, PartialEq)]
63pub enum Event {
64 ActiveEntryChanged(Option<ProjectEntry>),
65 WorktreeRemoved(WorktreeId),
66 DiskBasedDiagnosticsStarted,
67 DiskBasedDiagnosticsUpdated,
68 DiskBasedDiagnosticsFinished,
69 DiagnosticsUpdated(ProjectPath),
70}
71
72#[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
73pub struct ProjectPath {
74 pub worktree_id: WorktreeId,
75 pub path: Arc<Path>,
76}
77
78#[derive(Clone, Debug, Default, PartialEq)]
79pub struct DiagnosticSummary {
80 pub error_count: usize,
81 pub warning_count: usize,
82 pub info_count: usize,
83 pub hint_count: usize,
84}
85
86impl DiagnosticSummary {
87 fn new<'a, T: 'a>(diagnostics: impl IntoIterator<Item = &'a DiagnosticEntry<T>>) -> Self {
88 let mut this = Self {
89 error_count: 0,
90 warning_count: 0,
91 info_count: 0,
92 hint_count: 0,
93 };
94
95 for entry in diagnostics {
96 if entry.diagnostic.is_primary {
97 match entry.diagnostic.severity {
98 DiagnosticSeverity::ERROR => this.error_count += 1,
99 DiagnosticSeverity::WARNING => this.warning_count += 1,
100 DiagnosticSeverity::INFORMATION => this.info_count += 1,
101 DiagnosticSeverity::HINT => this.hint_count += 1,
102 _ => {}
103 }
104 }
105 }
106
107 this
108 }
109
110 pub fn to_proto(&self, path: Arc<Path>) -> proto::DiagnosticSummary {
111 proto::DiagnosticSummary {
112 path: path.to_string_lossy().to_string(),
113 error_count: self.error_count as u32,
114 warning_count: self.warning_count as u32,
115 info_count: self.info_count as u32,
116 hint_count: self.hint_count as u32,
117 }
118 }
119}
120
121#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
122pub struct ProjectEntry {
123 pub worktree_id: WorktreeId,
124 pub entry_id: usize,
125}
126
127impl Project {
128 pub fn local(
129 client: Arc<Client>,
130 user_store: ModelHandle<UserStore>,
131 languages: Arc<LanguageRegistry>,
132 fs: Arc<dyn Fs>,
133 cx: &mut MutableAppContext,
134 ) -> ModelHandle<Self> {
135 cx.add_model(|cx: &mut ModelContext<Self>| {
136 let (remote_id_tx, remote_id_rx) = watch::channel();
137 let _maintain_remote_id_task = cx.spawn_weak({
138 let rpc = client.clone();
139 move |this, mut cx| {
140 async move {
141 let mut status = rpc.status();
142 while let Some(status) = status.recv().await {
143 if let Some(this) = this.upgrade(&cx) {
144 let remote_id = if let client::Status::Connected { .. } = status {
145 let response = rpc.request(proto::RegisterProject {}).await?;
146 Some(response.project_id)
147 } else {
148 None
149 };
150
151 if let Some(project_id) = remote_id {
152 let mut registrations = Vec::new();
153 this.read_with(&cx, |this, cx| {
154 for worktree in &this.worktrees {
155 let worktree_id = worktree.id() as u64;
156 let worktree = worktree.read(cx).as_local().unwrap();
157 registrations.push(rpc.request(
158 proto::RegisterWorktree {
159 project_id,
160 worktree_id,
161 root_name: worktree.root_name().to_string(),
162 authorized_logins: worktree.authorized_logins(),
163 },
164 ));
165 }
166 });
167 for registration in registrations {
168 registration.await?;
169 }
170 }
171 this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx));
172 }
173 }
174 Ok(())
175 }
176 .log_err()
177 }
178 });
179
180 Self {
181 worktrees: Default::default(),
182 collaborators: Default::default(),
183 client_state: ProjectClientState::Local {
184 is_shared: false,
185 remote_id_tx,
186 remote_id_rx,
187 _maintain_remote_id_task,
188 },
189 subscriptions: Vec::new(),
190 active_entry: None,
191 languages,
192 client,
193 user_store,
194 fs,
195 language_servers_with_diagnostics_running: 0,
196 language_servers: Default::default(),
197 }
198 })
199 }
200
201 pub async fn remote(
202 remote_id: u64,
203 client: Arc<Client>,
204 user_store: ModelHandle<UserStore>,
205 languages: Arc<LanguageRegistry>,
206 fs: Arc<dyn Fs>,
207 cx: &mut AsyncAppContext,
208 ) -> Result<ModelHandle<Self>> {
209 client.authenticate_and_connect(&cx).await?;
210
211 let response = client
212 .request(proto::JoinProject {
213 project_id: remote_id,
214 })
215 .await?;
216
217 let replica_id = response.replica_id as ReplicaId;
218
219 let mut worktrees = Vec::new();
220 for worktree in response.worktrees {
221 worktrees.push(
222 Worktree::remote(
223 remote_id,
224 replica_id,
225 worktree,
226 client.clone(),
227 user_store.clone(),
228 cx,
229 )
230 .await?,
231 );
232 }
233
234 let user_ids = response
235 .collaborators
236 .iter()
237 .map(|peer| peer.user_id)
238 .collect();
239 user_store
240 .update(cx, |user_store, cx| user_store.load_users(user_ids, cx))
241 .await?;
242 let mut collaborators = HashMap::default();
243 for message in response.collaborators {
244 let collaborator = Collaborator::from_proto(message, &user_store, cx).await?;
245 collaborators.insert(collaborator.peer_id, collaborator);
246 }
247
248 Ok(cx.add_model(|cx| {
249 let mut this = Self {
250 worktrees: Vec::new(),
251 active_entry: None,
252 collaborators,
253 languages,
254 user_store,
255 fs,
256 subscriptions: vec![
257 client.subscribe_to_entity(remote_id, cx, Self::handle_unshare_project),
258 client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
259 client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
260 client.subscribe_to_entity(remote_id, cx, Self::handle_share_worktree),
261 client.subscribe_to_entity(remote_id, cx, Self::handle_unregister_worktree),
262 client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
263 client.subscribe_to_entity(
264 remote_id,
265 cx,
266 Self::handle_update_diagnostic_summary,
267 ),
268 client.subscribe_to_entity(
269 remote_id,
270 cx,
271 Self::handle_disk_based_diagnostics_updating,
272 ),
273 client.subscribe_to_entity(
274 remote_id,
275 cx,
276 Self::handle_disk_based_diagnostics_updated,
277 ),
278 client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
279 client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
280 ],
281 client,
282 client_state: ProjectClientState::Remote {
283 sharing_has_stopped: false,
284 remote_id,
285 replica_id,
286 },
287 language_servers_with_diagnostics_running: 0,
288 language_servers: Default::default(),
289 };
290 for worktree in worktrees {
291 this.add_worktree(worktree, cx);
292 }
293 this
294 }))
295 }
296
297 fn set_remote_id(&mut self, remote_id: Option<u64>, cx: &mut ModelContext<Self>) {
298 if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state {
299 *remote_id_tx.borrow_mut() = remote_id;
300 }
301
302 self.subscriptions.clear();
303 if let Some(remote_id) = remote_id {
304 let client = &self.client;
305 self.subscriptions.extend([
306 client.subscribe_to_entity(remote_id, cx, Self::handle_open_buffer),
307 client.subscribe_to_entity(remote_id, cx, Self::handle_close_buffer),
308 client.subscribe_to_entity(remote_id, cx, Self::handle_add_collaborator),
309 client.subscribe_to_entity(remote_id, cx, Self::handle_remove_collaborator),
310 client.subscribe_to_entity(remote_id, cx, Self::handle_update_worktree),
311 client.subscribe_to_entity(remote_id, cx, Self::handle_update_buffer),
312 client.subscribe_to_entity(remote_id, cx, Self::handle_save_buffer),
313 client.subscribe_to_entity(remote_id, cx, Self::handle_buffer_saved),
314 client.subscribe_to_entity(remote_id, cx, Self::handle_format_buffer),
315 ]);
316 }
317 }
318
319 pub fn remote_id(&self) -> Option<u64> {
320 match &self.client_state {
321 ProjectClientState::Local { remote_id_rx, .. } => *remote_id_rx.borrow(),
322 ProjectClientState::Remote { remote_id, .. } => Some(*remote_id),
323 }
324 }
325
326 pub fn next_remote_id(&self) -> impl Future<Output = u64> {
327 let mut id = None;
328 let mut watch = None;
329 match &self.client_state {
330 ProjectClientState::Local { remote_id_rx, .. } => watch = Some(remote_id_rx.clone()),
331 ProjectClientState::Remote { remote_id, .. } => id = Some(*remote_id),
332 }
333
334 async move {
335 if let Some(id) = id {
336 return id;
337 }
338 let mut watch = watch.unwrap();
339 loop {
340 let id = *watch.borrow();
341 if let Some(id) = id {
342 return id;
343 }
344 watch.recv().await;
345 }
346 }
347 }
348
349 pub fn replica_id(&self) -> ReplicaId {
350 match &self.client_state {
351 ProjectClientState::Local { .. } => 0,
352 ProjectClientState::Remote { replica_id, .. } => *replica_id,
353 }
354 }
355
356 pub fn collaborators(&self) -> &HashMap<PeerId, Collaborator> {
357 &self.collaborators
358 }
359
360 pub fn worktrees(&self) -> &[ModelHandle<Worktree>] {
361 &self.worktrees
362 }
363
364 pub fn worktree_for_id(
365 &self,
366 id: WorktreeId,
367 cx: &AppContext,
368 ) -> Option<ModelHandle<Worktree>> {
369 self.worktrees
370 .iter()
371 .find(|worktree| worktree.read(cx).id() == id)
372 .cloned()
373 }
374
375 pub fn share(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
376 let rpc = self.client.clone();
377 cx.spawn(|this, mut cx| async move {
378 let project_id = this.update(&mut cx, |this, _| {
379 if let ProjectClientState::Local {
380 is_shared,
381 remote_id_rx,
382 ..
383 } = &mut this.client_state
384 {
385 *is_shared = true;
386 remote_id_rx
387 .borrow()
388 .ok_or_else(|| anyhow!("no project id"))
389 } else {
390 Err(anyhow!("can't share a remote project"))
391 }
392 })?;
393
394 rpc.request(proto::ShareProject { project_id }).await?;
395 let mut tasks = Vec::new();
396 this.update(&mut cx, |this, cx| {
397 for worktree in &this.worktrees {
398 worktree.update(cx, |worktree, cx| {
399 let worktree = worktree.as_local_mut().unwrap();
400 tasks.push(worktree.share(project_id, cx));
401 });
402 }
403 });
404 for task in tasks {
405 task.await?;
406 }
407 this.update(&mut cx, |_, cx| cx.notify());
408 Ok(())
409 })
410 }
411
412 pub fn unshare(&self, cx: &mut ModelContext<Self>) -> Task<anyhow::Result<()>> {
413 let rpc = self.client.clone();
414 cx.spawn(|this, mut cx| async move {
415 let project_id = this.update(&mut cx, |this, _| {
416 if let ProjectClientState::Local {
417 is_shared,
418 remote_id_rx,
419 ..
420 } = &mut this.client_state
421 {
422 *is_shared = false;
423 remote_id_rx
424 .borrow()
425 .ok_or_else(|| anyhow!("no project id"))
426 } else {
427 Err(anyhow!("can't share a remote project"))
428 }
429 })?;
430
431 rpc.send(proto::UnshareProject { project_id }).await?;
432 this.update(&mut cx, |this, cx| {
433 this.collaborators.clear();
434 for worktree in &this.worktrees {
435 worktree.update(cx, |worktree, _| {
436 worktree.as_local_mut().unwrap().unshare();
437 });
438 }
439 cx.notify()
440 });
441 Ok(())
442 })
443 }
444
445 pub fn is_read_only(&self) -> bool {
446 match &self.client_state {
447 ProjectClientState::Local { .. } => false,
448 ProjectClientState::Remote {
449 sharing_has_stopped,
450 ..
451 } => *sharing_has_stopped,
452 }
453 }
454
455 pub fn is_local(&self) -> bool {
456 match &self.client_state {
457 ProjectClientState::Local { .. } => true,
458 ProjectClientState::Remote { .. } => false,
459 }
460 }
461
462 pub fn open_buffer(
463 &mut self,
464 path: ProjectPath,
465 cx: &mut ModelContext<Self>,
466 ) -> Task<Result<ModelHandle<Buffer>>> {
467 let worktree = if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) {
468 worktree
469 } else {
470 return cx.spawn(|_, _| async move { Err(anyhow!("no such worktree")) });
471 };
472 let buffer_task = worktree.update(cx, |worktree, cx| worktree.open_buffer(path.path, cx));
473 cx.spawn(|this, mut cx| async move {
474 let (buffer, buffer_is_new) = buffer_task.await?;
475 if buffer_is_new {
476 this.update(&mut cx, |this, cx| {
477 this.assign_language_to_buffer(worktree, buffer.clone(), cx)
478 });
479 }
480 Ok(buffer)
481 })
482 }
483
484 pub fn save_buffer_as(
485 &self,
486 buffer: ModelHandle<Buffer>,
487 abs_path: PathBuf,
488 cx: &mut ModelContext<Project>,
489 ) -> Task<Result<()>> {
490 let worktree_task = self.worktree_for_abs_path(&abs_path, cx);
491 cx.spawn(|this, mut cx| async move {
492 let (worktree, path) = worktree_task.await?;
493 worktree
494 .update(&mut cx, |worktree, cx| {
495 worktree
496 .as_local_mut()
497 .unwrap()
498 .save_buffer_as(buffer.clone(), path, cx)
499 })
500 .await?;
501 this.update(&mut cx, |this, cx| {
502 this.assign_language_to_buffer(worktree, buffer, cx)
503 });
504 Ok(())
505 })
506 }
507
508 fn assign_language_to_buffer(
509 &mut self,
510 worktree: ModelHandle<Worktree>,
511 buffer: ModelHandle<Buffer>,
512 cx: &mut ModelContext<Self>,
513 ) -> Option<()> {
514 // Set the buffer's language
515 let full_path = buffer.read(cx).file()?.full_path();
516 let language = self.languages.select_language(&full_path)?.clone();
517 buffer.update(cx, |buffer, cx| {
518 buffer.set_language(Some(language.clone()), cx);
519 });
520
521 // For local worktrees, start a language server if needed.
522 let worktree = worktree.read(cx);
523 let worktree_id = worktree.id();
524 let worktree_abs_path = worktree.as_local()?.abs_path().clone();
525 let language_server = match self
526 .language_servers
527 .entry((worktree_id, language.name().to_string()))
528 {
529 hash_map::Entry::Occupied(e) => Some(e.get().clone()),
530 hash_map::Entry::Vacant(e) => {
531 Self::start_language_server(self.client.clone(), language, &worktree_abs_path, cx)
532 .map(|server| e.insert(server).clone())
533 }
534 };
535
536 buffer.update(cx, |buffer, cx| {
537 buffer.set_language_server(language_server, cx)
538 });
539
540 None
541 }
542
543 fn start_language_server(
544 rpc: Arc<Client>,
545 language: Arc<Language>,
546 worktree_path: &Path,
547 cx: &mut ModelContext<Self>,
548 ) -> Option<Arc<LanguageServer>> {
549 enum LspEvent {
550 DiagnosticsStart,
551 DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
552 DiagnosticsFinish,
553 }
554
555 let language_server = language
556 .start_server(worktree_path, cx)
557 .log_err()
558 .flatten()?;
559 let disk_based_sources = language
560 .disk_based_diagnostic_sources()
561 .cloned()
562 .unwrap_or_default();
563 let disk_based_diagnostics_progress_token =
564 language.disk_based_diagnostics_progress_token().cloned();
565 let has_disk_based_diagnostic_progress_token =
566 disk_based_diagnostics_progress_token.is_some();
567 let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded();
568
569 // Listen for `PublishDiagnostics` notifications.
570 language_server
571 .on_notification::<lsp::notification::PublishDiagnostics, _>({
572 let diagnostics_tx = diagnostics_tx.clone();
573 move |params| {
574 if !has_disk_based_diagnostic_progress_token {
575 block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
576 }
577 block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))).ok();
578 if !has_disk_based_diagnostic_progress_token {
579 block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
580 }
581 }
582 })
583 .detach();
584
585 // Listen for `Progress` notifications. Send an event when the language server
586 // transitions between running jobs and not running any jobs.
587 let mut running_jobs_for_this_server: i32 = 0;
588 language_server
589 .on_notification::<lsp::notification::Progress, _>(move |params| {
590 let token = match params.token {
591 lsp::NumberOrString::Number(_) => None,
592 lsp::NumberOrString::String(token) => Some(token),
593 };
594
595 if token == disk_based_diagnostics_progress_token {
596 match params.value {
597 lsp::ProgressParamsValue::WorkDone(progress) => match progress {
598 lsp::WorkDoneProgress::Begin(_) => {
599 running_jobs_for_this_server += 1;
600 if running_jobs_for_this_server == 1 {
601 block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok();
602 }
603 }
604 lsp::WorkDoneProgress::End(_) => {
605 running_jobs_for_this_server -= 1;
606 if running_jobs_for_this_server == 0 {
607 block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok();
608 }
609 }
610 _ => {}
611 },
612 }
613 }
614 })
615 .detach();
616
617 // Process all the LSP events.
618 cx.spawn_weak(|this, mut cx| async move {
619 while let Ok(message) = diagnostics_rx.recv().await {
620 let this = cx.read(|cx| this.upgrade(cx))?;
621 match message {
622 LspEvent::DiagnosticsStart => {
623 let send = this.update(&mut cx, |this, cx| {
624 this.disk_based_diagnostics_started(cx);
625 this.remote_id().map(|project_id| {
626 rpc.send(proto::DiskBasedDiagnosticsUpdating { project_id })
627 })
628 });
629 if let Some(send) = send {
630 send.await.log_err();
631 }
632 }
633 LspEvent::DiagnosticsUpdate(params) => {
634 this.update(&mut cx, |this, cx| {
635 this.update_diagnostics(params, &disk_based_sources, cx)
636 .log_err();
637 });
638 }
639 LspEvent::DiagnosticsFinish => {
640 let send = this.update(&mut cx, |this, cx| {
641 this.disk_based_diagnostics_finished(cx);
642 this.remote_id().map(|project_id| {
643 rpc.send(proto::DiskBasedDiagnosticsUpdated { project_id })
644 })
645 });
646 if let Some(send) = send {
647 send.await.log_err();
648 }
649 }
650 }
651 }
652 Some(())
653 })
654 .detach();
655
656 Some(language_server)
657 }
658
659 fn update_diagnostics(
660 &mut self,
661 diagnostics: lsp::PublishDiagnosticsParams,
662 disk_based_sources: &HashSet<String>,
663 cx: &mut ModelContext<Self>,
664 ) -> Result<()> {
665 let path = diagnostics
666 .uri
667 .to_file_path()
668 .map_err(|_| anyhow!("URI is not a file"))?;
669 for tree in &self.worktrees {
670 let relative_path = tree.update(cx, |tree, _| {
671 path.strip_prefix(tree.as_local()?.abs_path()).ok()
672 });
673 if let Some(relative_path) = relative_path {
674 let worktree_id = tree.read(cx).id();
675 let project_path = ProjectPath {
676 worktree_id,
677 path: relative_path.into(),
678 };
679 tree.update(cx, |tree, cx| {
680 tree.as_local_mut().unwrap().update_diagnostics(
681 project_path.path.clone(),
682 diagnostics,
683 disk_based_sources,
684 cx,
685 )
686 })?;
687 cx.emit(Event::DiagnosticsUpdated(project_path));
688 break;
689 }
690 }
691 Ok(())
692 }
693
694 pub fn worktree_for_abs_path(
695 &self,
696 abs_path: &Path,
697 cx: &mut ModelContext<Self>,
698 ) -> Task<Result<(ModelHandle<Worktree>, PathBuf)>> {
699 for tree in &self.worktrees {
700 if let Some(relative_path) = tree
701 .read(cx)
702 .as_local()
703 .and_then(|t| abs_path.strip_prefix(t.abs_path()).ok())
704 {
705 return Task::ready(Ok((tree.clone(), relative_path.into())));
706 }
707 }
708
709 let worktree = self.add_local_worktree(abs_path, cx);
710 cx.background().spawn(async move {
711 let worktree = worktree.await?;
712 Ok((worktree, PathBuf::new()))
713 })
714 }
715
716 pub fn is_shared(&self) -> bool {
717 match &self.client_state {
718 ProjectClientState::Local { is_shared, .. } => *is_shared,
719 ProjectClientState::Remote { .. } => false,
720 }
721 }
722
723 pub fn add_local_worktree(
724 &self,
725 abs_path: impl AsRef<Path>,
726 cx: &mut ModelContext<Self>,
727 ) -> Task<Result<ModelHandle<Worktree>>> {
728 let fs = self.fs.clone();
729 let client = self.client.clone();
730 let user_store = self.user_store.clone();
731 let path = Arc::from(abs_path.as_ref());
732 cx.spawn(|project, mut cx| async move {
733 let worktree =
734 Worktree::open_local(client.clone(), user_store, path, fs, &mut cx).await?;
735
736 let (remote_project_id, is_shared) = project.update(&mut cx, |project, cx| {
737 project.add_worktree(worktree.clone(), cx);
738 (project.remote_id(), project.is_shared())
739 });
740
741 if let Some(project_id) = remote_project_id {
742 let worktree_id = worktree.id() as u64;
743 let register_message = worktree.update(&mut cx, |worktree, _| {
744 let worktree = worktree.as_local_mut().unwrap();
745 proto::RegisterWorktree {
746 project_id,
747 worktree_id,
748 root_name: worktree.root_name().to_string(),
749 authorized_logins: worktree.authorized_logins(),
750 }
751 });
752 client.request(register_message).await?;
753 if is_shared {
754 worktree
755 .update(&mut cx, |worktree, cx| {
756 worktree.as_local_mut().unwrap().share(project_id, cx)
757 })
758 .await?;
759 }
760 }
761
762 Ok(worktree)
763 })
764 }
765
766 fn add_worktree(&mut self, worktree: ModelHandle<Worktree>, cx: &mut ModelContext<Self>) {
767 cx.observe(&worktree, |_, _, cx| cx.notify()).detach();
768 self.worktrees.push(worktree);
769 cx.notify();
770 }
771
772 pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
773 let new_active_entry = entry.and_then(|project_path| {
774 let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
775 let entry = worktree.read(cx).entry_for_path(project_path.path)?;
776 Some(ProjectEntry {
777 worktree_id: project_path.worktree_id,
778 entry_id: entry.id,
779 })
780 });
781 if new_active_entry != self.active_entry {
782 self.active_entry = new_active_entry;
783 cx.emit(Event::ActiveEntryChanged(new_active_entry));
784 }
785 }
786
787 pub fn path_for_entry(&self, entry: ProjectEntry, cx: &AppContext) -> Option<ProjectPath> {
788 let worktree = self.worktree_for_id(entry.worktree_id, cx)?.read(cx);
789 Some(ProjectPath {
790 worktree_id: entry.worktree_id,
791 path: worktree.entry_for_id(entry.entry_id)?.path.clone(),
792 })
793 }
794
795 pub fn is_running_disk_based_diagnostics(&self) -> bool {
796 self.language_servers_with_diagnostics_running > 0
797 }
798
799 pub fn diagnostic_summary(&self, cx: &AppContext) -> DiagnosticSummary {
800 let mut summary = DiagnosticSummary::default();
801 for (_, path_summary) in self.diagnostic_summaries(cx) {
802 summary.error_count += path_summary.error_count;
803 summary.warning_count += path_summary.warning_count;
804 summary.info_count += path_summary.info_count;
805 summary.hint_count += path_summary.hint_count;
806 }
807 summary
808 }
809
810 pub fn diagnostic_summaries<'a>(
811 &'a self,
812 cx: &'a AppContext,
813 ) -> impl Iterator<Item = (ProjectPath, DiagnosticSummary)> + 'a {
814 self.worktrees.iter().flat_map(move |worktree| {
815 let worktree = worktree.read(cx);
816 let worktree_id = worktree.id();
817 worktree
818 .diagnostic_summaries()
819 .map(move |(path, summary)| (ProjectPath { worktree_id, path }, summary))
820 })
821 }
822
823 fn disk_based_diagnostics_started(&mut self, cx: &mut ModelContext<Self>) {
824 self.language_servers_with_diagnostics_running += 1;
825 if self.language_servers_with_diagnostics_running == 1 {
826 cx.emit(Event::DiskBasedDiagnosticsStarted);
827 }
828 }
829
830 fn disk_based_diagnostics_finished(&mut self, cx: &mut ModelContext<Self>) {
831 cx.emit(Event::DiskBasedDiagnosticsUpdated);
832 self.language_servers_with_diagnostics_running -= 1;
833 if self.language_servers_with_diagnostics_running == 0 {
834 cx.emit(Event::DiskBasedDiagnosticsFinished);
835 }
836 }
837
838 pub fn active_entry(&self) -> Option<ProjectEntry> {
839 self.active_entry
840 }
841
842 // RPC message handlers
843
844 fn handle_unshare_project(
845 &mut self,
846 _: TypedEnvelope<proto::UnshareProject>,
847 _: Arc<Client>,
848 cx: &mut ModelContext<Self>,
849 ) -> Result<()> {
850 if let ProjectClientState::Remote {
851 sharing_has_stopped,
852 ..
853 } = &mut self.client_state
854 {
855 *sharing_has_stopped = true;
856 self.collaborators.clear();
857 cx.notify();
858 Ok(())
859 } else {
860 unreachable!()
861 }
862 }
863
864 fn handle_add_collaborator(
865 &mut self,
866 mut envelope: TypedEnvelope<proto::AddProjectCollaborator>,
867 _: Arc<Client>,
868 cx: &mut ModelContext<Self>,
869 ) -> Result<()> {
870 let user_store = self.user_store.clone();
871 let collaborator = envelope
872 .payload
873 .collaborator
874 .take()
875 .ok_or_else(|| anyhow!("empty collaborator"))?;
876
877 cx.spawn(|this, mut cx| {
878 async move {
879 let collaborator =
880 Collaborator::from_proto(collaborator, &user_store, &mut cx).await?;
881 this.update(&mut cx, |this, cx| {
882 this.collaborators
883 .insert(collaborator.peer_id, collaborator);
884 cx.notify();
885 });
886 Ok(())
887 }
888 .log_err()
889 })
890 .detach();
891
892 Ok(())
893 }
894
895 fn handle_remove_collaborator(
896 &mut self,
897 envelope: TypedEnvelope<proto::RemoveProjectCollaborator>,
898 _: Arc<Client>,
899 cx: &mut ModelContext<Self>,
900 ) -> Result<()> {
901 let peer_id = PeerId(envelope.payload.peer_id);
902 let replica_id = self
903 .collaborators
904 .remove(&peer_id)
905 .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))?
906 .replica_id;
907 for worktree in &self.worktrees {
908 worktree.update(cx, |worktree, cx| {
909 worktree.remove_collaborator(peer_id, replica_id, cx);
910 })
911 }
912 Ok(())
913 }
914
915 fn handle_share_worktree(
916 &mut self,
917 envelope: TypedEnvelope<proto::ShareWorktree>,
918 client: Arc<Client>,
919 cx: &mut ModelContext<Self>,
920 ) -> Result<()> {
921 let remote_id = self.remote_id().ok_or_else(|| anyhow!("invalid project"))?;
922 let replica_id = self.replica_id();
923 let worktree = envelope
924 .payload
925 .worktree
926 .ok_or_else(|| anyhow!("invalid worktree"))?;
927 let user_store = self.user_store.clone();
928 cx.spawn(|this, mut cx| {
929 async move {
930 let worktree =
931 Worktree::remote(remote_id, replica_id, worktree, client, user_store, &mut cx)
932 .await?;
933 this.update(&mut cx, |this, cx| this.add_worktree(worktree, cx));
934 Ok(())
935 }
936 .log_err()
937 })
938 .detach();
939 Ok(())
940 }
941
942 fn handle_unregister_worktree(
943 &mut self,
944 envelope: TypedEnvelope<proto::UnregisterWorktree>,
945 _: Arc<Client>,
946 cx: &mut ModelContext<Self>,
947 ) -> Result<()> {
948 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
949 self.worktrees
950 .retain(|worktree| worktree.read(cx).as_remote().unwrap().id() != worktree_id);
951 cx.notify();
952 Ok(())
953 }
954
955 fn handle_update_worktree(
956 &mut self,
957 envelope: TypedEnvelope<proto::UpdateWorktree>,
958 _: Arc<Client>,
959 cx: &mut ModelContext<Self>,
960 ) -> Result<()> {
961 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
962 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
963 worktree.update(cx, |worktree, cx| {
964 let worktree = worktree.as_remote_mut().unwrap();
965 worktree.update_from_remote(envelope, cx)
966 })?;
967 }
968 Ok(())
969 }
970
971 fn handle_update_diagnostic_summary(
972 &mut self,
973 envelope: TypedEnvelope<proto::UpdateDiagnosticSummary>,
974 _: Arc<Client>,
975 cx: &mut ModelContext<Self>,
976 ) -> Result<()> {
977 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
978 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
979 if let Some(summary) = envelope.payload.summary {
980 let project_path = ProjectPath {
981 worktree_id,
982 path: Path::new(&summary.path).into(),
983 };
984 worktree.update(cx, |worktree, _| {
985 worktree
986 .as_remote_mut()
987 .unwrap()
988 .update_diagnostic_summary(project_path.path.clone(), &summary);
989 });
990 cx.emit(Event::DiagnosticsUpdated(project_path));
991 }
992 }
993 Ok(())
994 }
995
996 fn handle_disk_based_diagnostics_updating(
997 &mut self,
998 _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
999 _: Arc<Client>,
1000 cx: &mut ModelContext<Self>,
1001 ) -> Result<()> {
1002 self.disk_based_diagnostics_started(cx);
1003 Ok(())
1004 }
1005
1006 fn handle_disk_based_diagnostics_updated(
1007 &mut self,
1008 _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
1009 _: Arc<Client>,
1010 cx: &mut ModelContext<Self>,
1011 ) -> Result<()> {
1012 self.disk_based_diagnostics_finished(cx);
1013 Ok(())
1014 }
1015
1016 pub fn handle_update_buffer(
1017 &mut self,
1018 envelope: TypedEnvelope<proto::UpdateBuffer>,
1019 _: Arc<Client>,
1020 cx: &mut ModelContext<Self>,
1021 ) -> Result<()> {
1022 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1023 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
1024 worktree.update(cx, |worktree, cx| {
1025 worktree.handle_update_buffer(envelope, cx)
1026 })?;
1027 }
1028 Ok(())
1029 }
1030
1031 pub fn handle_save_buffer(
1032 &mut self,
1033 envelope: TypedEnvelope<proto::SaveBuffer>,
1034 rpc: Arc<Client>,
1035 cx: &mut ModelContext<Self>,
1036 ) -> Result<()> {
1037 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1038 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
1039 worktree.update(cx, |worktree, cx| {
1040 worktree.handle_save_buffer(envelope, rpc, cx)
1041 })?;
1042 }
1043 Ok(())
1044 }
1045
1046 pub fn handle_format_buffer(
1047 &mut self,
1048 envelope: TypedEnvelope<proto::FormatBuffer>,
1049 rpc: Arc<Client>,
1050 cx: &mut ModelContext<Self>,
1051 ) -> Result<()> {
1052 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1053 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
1054 worktree.update(cx, |worktree, cx| {
1055 worktree.handle_format_buffer(envelope, rpc, cx)
1056 })?;
1057 }
1058 Ok(())
1059 }
1060
1061 pub fn handle_open_buffer(
1062 &mut self,
1063 envelope: TypedEnvelope<proto::OpenBuffer>,
1064 rpc: Arc<Client>,
1065 cx: &mut ModelContext<Self>,
1066 ) -> anyhow::Result<()> {
1067 let receipt = envelope.receipt();
1068 let peer_id = envelope.original_sender_id()?;
1069 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1070 let worktree = self
1071 .worktree_for_id(worktree_id, cx)
1072 .ok_or_else(|| anyhow!("no such worktree"))?;
1073
1074 let task = self.open_buffer(
1075 ProjectPath {
1076 worktree_id,
1077 path: PathBuf::from(envelope.payload.path).into(),
1078 },
1079 cx,
1080 );
1081 cx.spawn(|_, mut cx| {
1082 async move {
1083 let buffer = task.await?;
1084 let response = worktree.update(&mut cx, |worktree, cx| {
1085 worktree
1086 .as_local_mut()
1087 .unwrap()
1088 .open_remote_buffer(peer_id, buffer, cx)
1089 });
1090 rpc.respond(receipt, response).await?;
1091 Ok(())
1092 }
1093 .log_err()
1094 })
1095 .detach();
1096 Ok(())
1097 }
1098
1099 pub fn handle_close_buffer(
1100 &mut self,
1101 envelope: TypedEnvelope<proto::CloseBuffer>,
1102 _: Arc<Client>,
1103 cx: &mut ModelContext<Self>,
1104 ) -> anyhow::Result<()> {
1105 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1106 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
1107 worktree.update(cx, |worktree, cx| {
1108 worktree
1109 .as_local_mut()
1110 .unwrap()
1111 .close_remote_buffer(envelope, cx)
1112 })?;
1113 }
1114 Ok(())
1115 }
1116
1117 pub fn handle_buffer_saved(
1118 &mut self,
1119 envelope: TypedEnvelope<proto::BufferSaved>,
1120 _: Arc<Client>,
1121 cx: &mut ModelContext<Self>,
1122 ) -> Result<()> {
1123 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
1124 if let Some(worktree) = self.worktree_for_id(worktree_id, cx) {
1125 worktree.update(cx, |worktree, cx| {
1126 worktree.handle_buffer_saved(envelope, cx)
1127 })?;
1128 }
1129 Ok(())
1130 }
1131
1132 pub fn match_paths<'a>(
1133 &self,
1134 query: &'a str,
1135 include_ignored: bool,
1136 smart_case: bool,
1137 max_results: usize,
1138 cancel_flag: &'a AtomicBool,
1139 cx: &AppContext,
1140 ) -> impl 'a + Future<Output = Vec<PathMatch>> {
1141 let include_root_name = self.worktrees.len() > 1;
1142 let candidate_sets = self
1143 .worktrees
1144 .iter()
1145 .map(|worktree| CandidateSet {
1146 snapshot: worktree.read(cx).snapshot(),
1147 include_ignored,
1148 include_root_name,
1149 })
1150 .collect::<Vec<_>>();
1151
1152 let background = cx.background().clone();
1153 async move {
1154 fuzzy::match_paths(
1155 candidate_sets.as_slice(),
1156 query,
1157 smart_case,
1158 max_results,
1159 cancel_flag,
1160 background,
1161 )
1162 .await
1163 }
1164 }
1165}
1166
1167struct CandidateSet {
1168 snapshot: Snapshot,
1169 include_ignored: bool,
1170 include_root_name: bool,
1171}
1172
1173impl<'a> PathMatchCandidateSet<'a> for CandidateSet {
1174 type Candidates = CandidateSetIter<'a>;
1175
1176 fn id(&self) -> usize {
1177 self.snapshot.id().to_usize()
1178 }
1179
1180 fn len(&self) -> usize {
1181 if self.include_ignored {
1182 self.snapshot.file_count()
1183 } else {
1184 self.snapshot.visible_file_count()
1185 }
1186 }
1187
1188 fn prefix(&self) -> Arc<str> {
1189 if self.snapshot.root_entry().map_or(false, |e| e.is_file()) {
1190 self.snapshot.root_name().into()
1191 } else if self.include_root_name {
1192 format!("{}/", self.snapshot.root_name()).into()
1193 } else {
1194 "".into()
1195 }
1196 }
1197
1198 fn candidates(&'a self, start: usize) -> Self::Candidates {
1199 CandidateSetIter {
1200 traversal: self.snapshot.files(self.include_ignored, start),
1201 }
1202 }
1203}
1204
1205struct CandidateSetIter<'a> {
1206 traversal: Traversal<'a>,
1207}
1208
1209impl<'a> Iterator for CandidateSetIter<'a> {
1210 type Item = PathMatchCandidate<'a>;
1211
1212 fn next(&mut self) -> Option<Self::Item> {
1213 self.traversal.next().map(|entry| {
1214 if let EntryKind::File(char_bag) = entry.kind {
1215 PathMatchCandidate {
1216 path: &entry.path,
1217 char_bag,
1218 }
1219 } else {
1220 unreachable!()
1221 }
1222 })
1223 }
1224}
1225
1226impl Entity for Project {
1227 type Event = Event;
1228
1229 fn release(&mut self, cx: &mut gpui::MutableAppContext) {
1230 match &self.client_state {
1231 ProjectClientState::Local { remote_id_rx, .. } => {
1232 if let Some(project_id) = *remote_id_rx.borrow() {
1233 let rpc = self.client.clone();
1234 cx.spawn(|_| async move {
1235 if let Err(err) = rpc.send(proto::UnregisterProject { project_id }).await {
1236 log::error!("error unregistering project: {}", err);
1237 }
1238 })
1239 .detach();
1240 }
1241 }
1242 ProjectClientState::Remote { remote_id, .. } => {
1243 let rpc = self.client.clone();
1244 let project_id = *remote_id;
1245 cx.spawn(|_| async move {
1246 if let Err(err) = rpc.send(proto::LeaveProject { project_id }).await {
1247 log::error!("error leaving project: {}", err);
1248 }
1249 })
1250 .detach();
1251 }
1252 }
1253 }
1254
1255 fn app_will_quit(
1256 &mut self,
1257 _: &mut MutableAppContext,
1258 ) -> Option<std::pin::Pin<Box<dyn 'static + Future<Output = ()>>>> {
1259 use futures::FutureExt;
1260
1261 let shutdown_futures = self
1262 .language_servers
1263 .drain()
1264 .filter_map(|(_, server)| server.shutdown())
1265 .collect::<Vec<_>>();
1266 Some(
1267 async move {
1268 futures::future::join_all(shutdown_futures).await;
1269 }
1270 .boxed(),
1271 )
1272 }
1273}
1274
1275impl Collaborator {
1276 fn from_proto(
1277 message: proto::Collaborator,
1278 user_store: &ModelHandle<UserStore>,
1279 cx: &mut AsyncAppContext,
1280 ) -> impl Future<Output = Result<Self>> {
1281 let user = user_store.update(cx, |user_store, cx| {
1282 user_store.fetch_user(message.user_id, cx)
1283 });
1284
1285 async move {
1286 Ok(Self {
1287 peer_id: PeerId(message.peer_id),
1288 user: user.await?,
1289 replica_id: message.replica_id as ReplicaId,
1290 })
1291 }
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::{Event, *};
1298 use client::test::FakeHttpClient;
1299 use fs::RealFs;
1300 use futures::StreamExt;
1301 use gpui::{test::subscribe, TestAppContext};
1302 use language::{
1303 tree_sitter_rust, Diagnostic, LanguageConfig, LanguageRegistry, LanguageServerConfig, Point,
1304 };
1305 use lsp::Url;
1306 use serde_json::json;
1307 use std::{os::unix, path::PathBuf};
1308 use util::test::temp_tree;
1309
1310 #[gpui::test]
1311 async fn test_populate_and_search(mut cx: gpui::TestAppContext) {
1312 let dir = temp_tree(json!({
1313 "root": {
1314 "apple": "",
1315 "banana": {
1316 "carrot": {
1317 "date": "",
1318 "endive": "",
1319 }
1320 },
1321 "fennel": {
1322 "grape": "",
1323 }
1324 }
1325 }));
1326
1327 let root_link_path = dir.path().join("root_link");
1328 unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
1329 unix::fs::symlink(
1330 &dir.path().join("root/fennel"),
1331 &dir.path().join("root/finnochio"),
1332 )
1333 .unwrap();
1334
1335 let project = build_project(&mut cx);
1336
1337 let tree = project
1338 .update(&mut cx, |project, cx| {
1339 project.add_local_worktree(&root_link_path, cx)
1340 })
1341 .await
1342 .unwrap();
1343
1344 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1345 .await;
1346 cx.read(|cx| {
1347 let tree = tree.read(cx);
1348 assert_eq!(tree.file_count(), 5);
1349 assert_eq!(
1350 tree.inode_for_path("fennel/grape"),
1351 tree.inode_for_path("finnochio/grape")
1352 );
1353 });
1354
1355 let cancel_flag = Default::default();
1356 let results = project
1357 .read_with(&cx, |project, cx| {
1358 project.match_paths("bna", false, false, 10, &cancel_flag, cx)
1359 })
1360 .await;
1361 assert_eq!(
1362 results
1363 .into_iter()
1364 .map(|result| result.path)
1365 .collect::<Vec<Arc<Path>>>(),
1366 vec![
1367 PathBuf::from("banana/carrot/date").into(),
1368 PathBuf::from("banana/carrot/endive").into(),
1369 ]
1370 );
1371 }
1372
1373 #[gpui::test]
1374 async fn test_language_server_diagnostics(mut cx: gpui::TestAppContext) {
1375 let (language_server_config, mut fake_server) =
1376 LanguageServerConfig::fake(cx.background()).await;
1377 let progress_token = language_server_config
1378 .disk_based_diagnostics_progress_token
1379 .clone()
1380 .unwrap();
1381
1382 let mut languages = LanguageRegistry::new();
1383 languages.add(Arc::new(Language::new(
1384 LanguageConfig {
1385 name: "Rust".to_string(),
1386 path_suffixes: vec!["rs".to_string()],
1387 language_server: Some(language_server_config),
1388 ..Default::default()
1389 },
1390 Some(tree_sitter_rust::language()),
1391 )));
1392
1393 let dir = temp_tree(json!({
1394 "a.rs": "fn a() { A }",
1395 "b.rs": "const y: i32 = 1",
1396 }));
1397
1398 let http_client = FakeHttpClient::with_404_response();
1399 let client = Client::new(http_client.clone());
1400 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
1401
1402 let project = cx.update(|cx| {
1403 Project::local(
1404 client,
1405 user_store,
1406 Arc::new(languages),
1407 Arc::new(RealFs),
1408 cx,
1409 )
1410 });
1411
1412 let tree = project
1413 .update(&mut cx, |project, cx| {
1414 project.add_local_worktree(dir.path(), cx)
1415 })
1416 .await
1417 .unwrap();
1418 let worktree_id = tree.read_with(&cx, |tree, _| tree.id());
1419
1420 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1421 .await;
1422
1423 // Cause worktree to start the fake language server
1424 let _buffer = project
1425 .update(&mut cx, |project, cx| {
1426 project.open_buffer(
1427 ProjectPath {
1428 worktree_id,
1429 path: Path::new("b.rs").into(),
1430 },
1431 cx,
1432 )
1433 })
1434 .await
1435 .unwrap();
1436
1437 let mut events = subscribe(&project, &mut cx);
1438
1439 fake_server.start_progress(&progress_token).await;
1440 assert_eq!(
1441 events.next().await.unwrap(),
1442 Event::DiskBasedDiagnosticsStarted
1443 );
1444
1445 fake_server.start_progress(&progress_token).await;
1446 fake_server.end_progress(&progress_token).await;
1447 fake_server.start_progress(&progress_token).await;
1448
1449 fake_server
1450 .notify::<lsp::notification::PublishDiagnostics>(lsp::PublishDiagnosticsParams {
1451 uri: Url::from_file_path(dir.path().join("a.rs")).unwrap(),
1452 version: None,
1453 diagnostics: vec![lsp::Diagnostic {
1454 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1455 severity: Some(lsp::DiagnosticSeverity::ERROR),
1456 message: "undefined variable 'A'".to_string(),
1457 ..Default::default()
1458 }],
1459 })
1460 .await;
1461 assert_eq!(
1462 events.next().await.unwrap(),
1463 Event::DiagnosticsUpdated(ProjectPath {
1464 worktree_id,
1465 path: Arc::from(Path::new("a.rs"))
1466 })
1467 );
1468
1469 fake_server.end_progress(&progress_token).await;
1470 fake_server.end_progress(&progress_token).await;
1471 assert_eq!(
1472 events.next().await.unwrap(),
1473 Event::DiskBasedDiagnosticsUpdated
1474 );
1475 assert_eq!(
1476 events.next().await.unwrap(),
1477 Event::DiskBasedDiagnosticsFinished
1478 );
1479
1480 let (buffer, _) = tree
1481 .update(&mut cx, |tree, cx| tree.open_buffer("a.rs", cx))
1482 .await
1483 .unwrap();
1484
1485 buffer.read_with(&cx, |buffer, _| {
1486 let snapshot = buffer.snapshot();
1487 let diagnostics = snapshot
1488 .diagnostics_in_range::<_, Point>(0..buffer.len())
1489 .collect::<Vec<_>>();
1490 assert_eq!(
1491 diagnostics,
1492 &[DiagnosticEntry {
1493 range: Point::new(0, 9)..Point::new(0, 10),
1494 diagnostic: Diagnostic {
1495 severity: lsp::DiagnosticSeverity::ERROR,
1496 message: "undefined variable 'A'".to_string(),
1497 group_id: 0,
1498 is_primary: true,
1499 ..Default::default()
1500 }
1501 }]
1502 )
1503 });
1504 }
1505
1506 #[gpui::test]
1507 async fn test_search_worktree_without_files(mut cx: gpui::TestAppContext) {
1508 let dir = temp_tree(json!({
1509 "root": {
1510 "dir1": {},
1511 "dir2": {
1512 "dir3": {}
1513 }
1514 }
1515 }));
1516
1517 let project = build_project(&mut cx);
1518 let tree = project
1519 .update(&mut cx, |project, cx| {
1520 project.add_local_worktree(&dir.path(), cx)
1521 })
1522 .await
1523 .unwrap();
1524
1525 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
1526 .await;
1527
1528 let cancel_flag = Default::default();
1529 let results = project
1530 .read_with(&cx, |project, cx| {
1531 project.match_paths("dir", false, false, 10, &cancel_flag, cx)
1532 })
1533 .await;
1534
1535 assert!(results.is_empty());
1536 }
1537
1538 fn build_project(cx: &mut TestAppContext) -> ModelHandle<Project> {
1539 let languages = Arc::new(LanguageRegistry::new());
1540 let fs = Arc::new(RealFs);
1541 let http_client = FakeHttpClient::with_404_response();
1542 let client = client::Client::new(http_client.clone());
1543 let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http_client, cx));
1544 cx.update(|cx| Project::local(client, user_store, languages, fs, cx))
1545 }
1546}