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