1use std::{
2 cell::RefCell,
3 path::{Path, PathBuf},
4 sync::{atomic::AtomicUsize, Arc},
5};
6
7use anyhow::{anyhow, Context as _, Result};
8use client::DevServerProjectId;
9use collections::{HashMap, HashSet};
10use fs::Fs;
11use futures::{
12 future::{BoxFuture, Shared},
13 FutureExt, SinkExt,
14};
15use gpui::{
16 AppContext, AsyncAppContext, EntityId, EventEmitter, Model, ModelContext, Task, WeakModel,
17};
18use postage::oneshot;
19use rpc::{
20 proto::{self, SSH_PROJECT_ID},
21 AnyProtoClient, ErrorExt, TypedEnvelope,
22};
23use smol::{
24 channel::{Receiver, Sender},
25 stream::StreamExt,
26};
27use text::ReplicaId;
28use util::{paths::compare_paths, ResultExt};
29use worktree::{Entry, ProjectEntryId, Worktree, WorktreeId, WorktreeSettings};
30
31use crate::{search::SearchQuery, ProjectPath};
32
33struct MatchingEntry {
34 worktree_path: Arc<Path>,
35 path: ProjectPath,
36 respond: oneshot::Sender<ProjectPath>,
37}
38
39enum WorktreeStoreState {
40 Local {
41 fs: Arc<dyn Fs>,
42 },
43 Remote {
44 dev_server_project_id: Option<DevServerProjectId>,
45 upstream_client: AnyProtoClient,
46 upstream_project_id: u64,
47 },
48}
49
50pub struct WorktreeStore {
51 next_entry_id: Arc<AtomicUsize>,
52 downstream_client: Option<(AnyProtoClient, u64)>,
53 retain_worktrees: bool,
54 worktrees: Vec<WorktreeHandle>,
55 worktrees_reordered: bool,
56 #[allow(clippy::type_complexity)]
57 loading_worktrees:
58 HashMap<Arc<Path>, Shared<Task<Result<Model<Worktree>, Arc<anyhow::Error>>>>>,
59 state: WorktreeStoreState,
60}
61
62pub enum WorktreeStoreEvent {
63 WorktreeAdded(Model<Worktree>),
64 WorktreeRemoved(EntityId, WorktreeId),
65 WorktreeReleased(EntityId, WorktreeId),
66 WorktreeOrderChanged,
67 WorktreeUpdateSent(Model<Worktree>),
68}
69
70impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {}
71
72impl WorktreeStore {
73 pub fn init(client: &AnyProtoClient) {
74 client.add_model_request_handler(Self::handle_create_project_entry);
75 client.add_model_request_handler(Self::handle_rename_project_entry);
76 client.add_model_request_handler(Self::handle_copy_project_entry);
77 client.add_model_request_handler(Self::handle_delete_project_entry);
78 client.add_model_request_handler(Self::handle_expand_project_entry);
79 }
80
81 pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {
82 Self {
83 next_entry_id: Default::default(),
84 loading_worktrees: Default::default(),
85 downstream_client: None,
86 worktrees: Vec::new(),
87 worktrees_reordered: false,
88 retain_worktrees,
89 state: WorktreeStoreState::Local { fs },
90 }
91 }
92
93 pub fn remote(
94 retain_worktrees: bool,
95 upstream_client: AnyProtoClient,
96 upstream_project_id: u64,
97 dev_server_project_id: Option<DevServerProjectId>,
98 ) -> Self {
99 Self {
100 next_entry_id: Default::default(),
101 loading_worktrees: Default::default(),
102 downstream_client: None,
103 worktrees: Vec::new(),
104 worktrees_reordered: false,
105 retain_worktrees,
106 state: WorktreeStoreState::Remote {
107 upstream_client,
108 upstream_project_id,
109 dev_server_project_id,
110 },
111 }
112 }
113
114 /// Iterates through all worktrees, including ones that don't appear in the project panel
115 pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Model<Worktree>> {
116 self.worktrees
117 .iter()
118 .filter_map(move |worktree| worktree.upgrade())
119 }
120
121 /// Iterates through all user-visible worktrees, the ones that appear in the project panel.
122 pub fn visible_worktrees<'a>(
123 &'a self,
124 cx: &'a AppContext,
125 ) -> impl 'a + DoubleEndedIterator<Item = Model<Worktree>> {
126 self.worktrees()
127 .filter(|worktree| worktree.read(cx).is_visible())
128 }
129
130 pub fn worktree_for_id(&self, id: WorktreeId, cx: &AppContext) -> Option<Model<Worktree>> {
131 self.worktrees()
132 .find(|worktree| worktree.read(cx).id() == id)
133 }
134
135 pub fn worktree_for_entry(
136 &self,
137 entry_id: ProjectEntryId,
138 cx: &AppContext,
139 ) -> Option<Model<Worktree>> {
140 self.worktrees()
141 .find(|worktree| worktree.read(cx).contains_entry(entry_id))
142 }
143
144 pub fn find_worktree(
145 &self,
146 abs_path: &Path,
147 cx: &AppContext,
148 ) -> Option<(Model<Worktree>, PathBuf)> {
149 for tree in self.worktrees() {
150 if let Ok(relative_path) = abs_path.strip_prefix(tree.read(cx).abs_path()) {
151 return Some((tree.clone(), relative_path.into()));
152 }
153 }
154 None
155 }
156
157 pub fn find_or_create_worktree(
158 &mut self,
159 abs_path: impl AsRef<Path>,
160 visible: bool,
161 cx: &mut ModelContext<Self>,
162 ) -> Task<Result<(Model<Worktree>, PathBuf)>> {
163 let abs_path = abs_path.as_ref();
164 if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {
165 Task::ready(Ok((tree, relative_path)))
166 } else {
167 let worktree = self.create_worktree(abs_path, visible, cx);
168 cx.background_executor()
169 .spawn(async move { Ok((worktree.await?, PathBuf::new())) })
170 }
171 }
172
173 pub fn entry_for_id<'a>(
174 &'a self,
175 entry_id: ProjectEntryId,
176 cx: &'a AppContext,
177 ) -> Option<&'a Entry> {
178 self.worktrees()
179 .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))
180 }
181
182 pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Entry> {
183 self.worktree_for_id(path.worktree_id, cx)?
184 .read(cx)
185 .entry_for_path(&path.path)
186 .cloned()
187 }
188
189 pub fn create_worktree(
190 &mut self,
191 abs_path: impl AsRef<Path>,
192 visible: bool,
193 cx: &mut ModelContext<Self>,
194 ) -> Task<Result<Model<Worktree>>> {
195 let path: Arc<Path> = abs_path.as_ref().into();
196 if !self.loading_worktrees.contains_key(&path) {
197 let task = match &self.state {
198 WorktreeStoreState::Remote {
199 upstream_client,
200 dev_server_project_id,
201 ..
202 } => {
203 if let Some(dev_server_project_id) = dev_server_project_id {
204 self.create_dev_server_worktree(
205 upstream_client.clone(),
206 *dev_server_project_id,
207 abs_path,
208 cx,
209 )
210 } else if upstream_client.is_via_collab() {
211 Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))
212 } else {
213 self.create_ssh_worktree(upstream_client.clone(), abs_path, visible, cx)
214 }
215 }
216 WorktreeStoreState::Local { fs } => {
217 self.create_local_worktree(fs.clone(), abs_path, visible, cx)
218 }
219 };
220
221 self.loading_worktrees.insert(path.clone(), task.shared());
222 }
223 let task = self.loading_worktrees.get(&path).unwrap().clone();
224 cx.spawn(|this, mut cx| async move {
225 let result = task.await;
226 this.update(&mut cx, |this, _| this.loading_worktrees.remove(&path))
227 .ok();
228 match result {
229 Ok(worktree) => Ok(worktree),
230 Err(err) => Err((*err).cloned()),
231 }
232 })
233 }
234
235 fn create_ssh_worktree(
236 &mut self,
237 client: AnyProtoClient,
238 abs_path: impl AsRef<Path>,
239 visible: bool,
240 cx: &mut ModelContext<Self>,
241 ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
242 let path_key: Arc<Path> = abs_path.as_ref().into();
243 let mut abs_path = path_key.clone().to_string_lossy().to_string();
244 // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`
245 // in which case want to strip the leading the `/`.
246 // On the host-side, the `~` will get expanded.
247 // That's what git does too: https://github.com/libgit2/libgit2/issues/3345#issuecomment-127050850
248 if abs_path.starts_with("/~") {
249 abs_path = abs_path[1..].to_string();
250 }
251 if abs_path.is_empty() || abs_path == "/" {
252 abs_path = "~/".to_string();
253 }
254 cx.spawn(|this, mut cx| async move {
255 let this = this.upgrade().context("Dropped worktree store")?;
256
257 let response = client
258 .request(proto::AddWorktree {
259 project_id: SSH_PROJECT_ID,
260 path: abs_path.clone(),
261 visible,
262 })
263 .await?;
264
265 if let Some(existing_worktree) = this.read_with(&cx, |this, cx| {
266 this.worktree_for_id(WorktreeId::from_proto(response.worktree_id), cx)
267 })? {
268 return Ok(existing_worktree);
269 }
270
271 let root_name = PathBuf::from(&response.canonicalized_path)
272 .file_name()
273 .map(|n| n.to_string_lossy().to_string())
274 .unwrap_or(response.canonicalized_path.to_string());
275
276 let worktree = cx.update(|cx| {
277 Worktree::remote(
278 SSH_PROJECT_ID,
279 0,
280 proto::WorktreeMetadata {
281 id: response.worktree_id,
282 root_name,
283 visible,
284 abs_path: response.canonicalized_path,
285 },
286 client,
287 cx,
288 )
289 })?;
290
291 this.update(&mut cx, |this, cx| {
292 this.add(&worktree, cx);
293 })?;
294 Ok(worktree)
295 })
296 }
297
298 fn create_local_worktree(
299 &mut self,
300 fs: Arc<dyn Fs>,
301 abs_path: impl AsRef<Path>,
302 visible: bool,
303 cx: &mut ModelContext<Self>,
304 ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
305 let next_entry_id = self.next_entry_id.clone();
306 let path: Arc<Path> = abs_path.as_ref().into();
307
308 cx.spawn(move |this, mut cx| async move {
309 let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;
310
311 let worktree = worktree?;
312 this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;
313
314 if visible {
315 cx.update(|cx| {
316 cx.add_recent_document(&path);
317 })
318 .log_err();
319 }
320
321 Ok(worktree)
322 })
323 }
324
325 fn create_dev_server_worktree(
326 &mut self,
327 client: AnyProtoClient,
328 dev_server_project_id: DevServerProjectId,
329 abs_path: impl AsRef<Path>,
330 cx: &mut ModelContext<Self>,
331 ) -> Task<Result<Model<Worktree>, Arc<anyhow::Error>>> {
332 let path: Arc<Path> = abs_path.as_ref().into();
333 let mut paths: Vec<String> = self
334 .visible_worktrees(cx)
335 .map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
336 .collect();
337 paths.push(path.to_string_lossy().to_string());
338 let request = client.request(proto::UpdateDevServerProject {
339 dev_server_project_id: dev_server_project_id.0,
340 paths,
341 });
342
343 let abs_path = abs_path.as_ref().to_path_buf();
344 cx.spawn(move |project, cx| async move {
345 let (tx, rx) = futures::channel::oneshot::channel();
346 let tx = RefCell::new(Some(tx));
347 let Some(project) = project.upgrade() else {
348 return Err(anyhow!("project dropped"))?;
349 };
350 let observer = cx.update(|cx| {
351 cx.observe(&project, move |project, cx| {
352 let abs_path = abs_path.clone();
353 project.update(cx, |project, cx| {
354 if let Some((worktree, _)) = project.find_worktree(&abs_path, cx) {
355 if let Some(tx) = tx.borrow_mut().take() {
356 tx.send(worktree).ok();
357 }
358 }
359 })
360 })
361 })?;
362
363 request.await?;
364 let worktree = rx.await.map_err(|e| anyhow!(e))?;
365 drop(observer);
366 Ok(worktree)
367 })
368 }
369
370 pub fn add(&mut self, worktree: &Model<Worktree>, cx: &mut ModelContext<Self>) {
371 let worktree_id = worktree.read(cx).id();
372 debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id));
373
374 let push_strong_handle = self.retain_worktrees || worktree.read(cx).is_visible();
375 let handle = if push_strong_handle {
376 WorktreeHandle::Strong(worktree.clone())
377 } else {
378 WorktreeHandle::Weak(worktree.downgrade())
379 };
380 if self.worktrees_reordered {
381 self.worktrees.push(handle);
382 } else {
383 let i = match self
384 .worktrees
385 .binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {
386 other.upgrade().map(|worktree| worktree.read(cx).abs_path())
387 }) {
388 Ok(i) | Err(i) => i,
389 };
390 self.worktrees.insert(i, handle);
391 }
392
393 cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));
394 self.send_project_updates(cx);
395
396 let handle_id = worktree.entity_id();
397 cx.observe_release(worktree, move |this, worktree, cx| {
398 cx.emit(WorktreeStoreEvent::WorktreeReleased(
399 handle_id,
400 worktree.id(),
401 ));
402 cx.emit(WorktreeStoreEvent::WorktreeRemoved(
403 handle_id,
404 worktree.id(),
405 ));
406 this.send_project_updates(cx);
407 })
408 .detach();
409 }
410
411 pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut ModelContext<Self>) {
412 self.worktrees.retain(|worktree| {
413 if let Some(worktree) = worktree.upgrade() {
414 if worktree.read(cx).id() == id_to_remove {
415 cx.emit(WorktreeStoreEvent::WorktreeRemoved(
416 worktree.entity_id(),
417 id_to_remove,
418 ));
419 false
420 } else {
421 true
422 }
423 } else {
424 false
425 }
426 });
427 self.send_project_updates(cx);
428 }
429
430 pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {
431 self.worktrees_reordered = worktrees_reordered;
432 }
433
434 fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {
435 match &self.state {
436 WorktreeStoreState::Remote {
437 upstream_client,
438 upstream_project_id,
439 ..
440 } => Some((upstream_client.clone(), *upstream_project_id)),
441 WorktreeStoreState::Local { .. } => None,
442 }
443 }
444
445 pub fn set_worktrees_from_proto(
446 &mut self,
447 worktrees: Vec<proto::WorktreeMetadata>,
448 replica_id: ReplicaId,
449 cx: &mut ModelContext<Self>,
450 ) -> Result<()> {
451 let mut old_worktrees_by_id = self
452 .worktrees
453 .drain(..)
454 .filter_map(|worktree| {
455 let worktree = worktree.upgrade()?;
456 Some((worktree.read(cx).id(), worktree))
457 })
458 .collect::<HashMap<_, _>>();
459
460 let (client, project_id) = self
461 .upstream_client()
462 .clone()
463 .ok_or_else(|| anyhow!("invalid project"))?;
464
465 for worktree in worktrees {
466 if let Some(old_worktree) =
467 old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))
468 {
469 let push_strong_handle =
470 self.retain_worktrees || old_worktree.read(cx).is_visible();
471 let handle = if push_strong_handle {
472 WorktreeHandle::Strong(old_worktree.clone())
473 } else {
474 WorktreeHandle::Weak(old_worktree.downgrade())
475 };
476 self.worktrees.push(handle);
477 } else {
478 self.add(
479 &Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),
480 cx,
481 );
482 }
483 }
484 self.send_project_updates(cx);
485
486 Ok(())
487 }
488
489 pub fn move_worktree(
490 &mut self,
491 source: WorktreeId,
492 destination: WorktreeId,
493 cx: &mut ModelContext<Self>,
494 ) -> Result<()> {
495 if source == destination {
496 return Ok(());
497 }
498
499 let mut source_index = None;
500 let mut destination_index = None;
501 for (i, worktree) in self.worktrees.iter().enumerate() {
502 if let Some(worktree) = worktree.upgrade() {
503 let worktree_id = worktree.read(cx).id();
504 if worktree_id == source {
505 source_index = Some(i);
506 if destination_index.is_some() {
507 break;
508 }
509 } else if worktree_id == destination {
510 destination_index = Some(i);
511 if source_index.is_some() {
512 break;
513 }
514 }
515 }
516 }
517
518 let source_index =
519 source_index.with_context(|| format!("Missing worktree for id {source}"))?;
520 let destination_index =
521 destination_index.with_context(|| format!("Missing worktree for id {destination}"))?;
522
523 if source_index == destination_index {
524 return Ok(());
525 }
526
527 let worktree_to_move = self.worktrees.remove(source_index);
528 self.worktrees.insert(destination_index, worktree_to_move);
529 self.worktrees_reordered = true;
530 cx.emit(WorktreeStoreEvent::WorktreeOrderChanged);
531 cx.notify();
532 Ok(())
533 }
534
535 pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
536 for worktree in &self.worktrees {
537 if let Some(worktree) = worktree.upgrade() {
538 worktree.update(cx, |worktree, _| {
539 if let Some(worktree) = worktree.as_remote_mut() {
540 worktree.disconnected_from_host();
541 }
542 });
543 }
544 }
545 }
546
547 pub fn send_project_updates(&mut self, cx: &mut ModelContext<Self>) {
548 let Some((downstream_client, project_id)) = self.downstream_client.clone() else {
549 return;
550 };
551
552 let update = proto::UpdateProject {
553 project_id,
554 worktrees: self.worktree_metadata_protos(cx),
555 };
556
557 // collab has bad concurrency guarantees, so we send requests in serial.
558 let update_project = if downstream_client.is_via_collab() {
559 Some(downstream_client.request(update))
560 } else {
561 downstream_client.send(update).log_err();
562 None
563 };
564 cx.spawn(|this, mut cx| async move {
565 if let Some(update_project) = update_project {
566 update_project.await?;
567 }
568
569 this.update(&mut cx, |this, cx| {
570 let worktrees = this.worktrees().collect::<Vec<_>>();
571
572 for worktree in worktrees {
573 worktree.update(cx, |worktree, cx| {
574 let client = downstream_client.clone();
575 worktree.observe_updates(project_id, cx, {
576 move |update| {
577 let client = client.clone();
578 async move {
579 if client.is_via_collab() {
580 client
581 .request(update)
582 .map(|result| result.log_err().is_some())
583 .await
584 } else {
585 client.send(update).log_err().is_some()
586 }
587 }
588 }
589 });
590 });
591
592 cx.emit(WorktreeStoreEvent::WorktreeUpdateSent(worktree.clone()))
593 }
594
595 anyhow::Ok(())
596 })
597 })
598 .detach_and_log_err(cx);
599 }
600
601 pub fn worktree_metadata_protos(&self, cx: &AppContext) -> Vec<proto::WorktreeMetadata> {
602 self.worktrees()
603 .map(|worktree| {
604 let worktree = worktree.read(cx);
605 proto::WorktreeMetadata {
606 id: worktree.id().to_proto(),
607 root_name: worktree.root_name().into(),
608 visible: worktree.is_visible(),
609 abs_path: worktree.abs_path().to_string_lossy().into(),
610 }
611 })
612 .collect()
613 }
614
615 pub fn shared(
616 &mut self,
617 remote_id: u64,
618 downsteam_client: AnyProtoClient,
619 cx: &mut ModelContext<Self>,
620 ) {
621 self.retain_worktrees = true;
622 self.downstream_client = Some((downsteam_client, remote_id));
623
624 // When shared, retain all worktrees
625 for worktree_handle in self.worktrees.iter_mut() {
626 match worktree_handle {
627 WorktreeHandle::Strong(_) => {}
628 WorktreeHandle::Weak(worktree) => {
629 if let Some(worktree) = worktree.upgrade() {
630 *worktree_handle = WorktreeHandle::Strong(worktree);
631 }
632 }
633 }
634 }
635 self.send_project_updates(cx);
636 }
637
638 pub fn unshared(&mut self, cx: &mut ModelContext<Self>) {
639 self.retain_worktrees = false;
640 self.downstream_client.take();
641
642 // When not shared, only retain the visible worktrees
643 for worktree_handle in self.worktrees.iter_mut() {
644 if let WorktreeHandle::Strong(worktree) = worktree_handle {
645 let is_visible = worktree.update(cx, |worktree, _| {
646 worktree.stop_observing_updates();
647 worktree.is_visible()
648 });
649 if !is_visible {
650 *worktree_handle = WorktreeHandle::Weak(worktree.downgrade());
651 }
652 }
653 }
654 }
655
656 /// search over all worktrees and return buffers that *might* match the search.
657 pub fn find_search_candidates(
658 &self,
659 query: SearchQuery,
660 limit: usize,
661 open_entries: HashSet<ProjectEntryId>,
662 fs: Arc<dyn Fs>,
663 cx: &ModelContext<Self>,
664 ) -> Receiver<ProjectPath> {
665 let snapshots = self
666 .visible_worktrees(cx)
667 .filter_map(|tree| {
668 let tree = tree.read(cx);
669 Some((tree.snapshot(), tree.as_local()?.settings()))
670 })
671 .collect::<Vec<_>>();
672
673 let executor = cx.background_executor().clone();
674
675 // We want to return entries in the order they are in the worktrees, so we have one
676 // thread that iterates over the worktrees (and ignored directories) as necessary,
677 // and pushes a oneshot::Receiver to the output channel and a oneshot::Sender to the filter
678 // channel.
679 // We spawn a number of workers that take items from the filter channel and check the query
680 // against the version of the file on disk.
681 let (filter_tx, filter_rx) = smol::channel::bounded(64);
682 let (output_tx, mut output_rx) = smol::channel::bounded(64);
683 let (matching_paths_tx, matching_paths_rx) = smol::channel::unbounded();
684
685 let input = cx.background_executor().spawn({
686 let fs = fs.clone();
687 let query = query.clone();
688 async move {
689 Self::find_candidate_paths(
690 fs,
691 snapshots,
692 open_entries,
693 query,
694 filter_tx,
695 output_tx,
696 )
697 .await
698 .log_err();
699 }
700 });
701 const MAX_CONCURRENT_FILE_SCANS: usize = 64;
702 let filters = cx.background_executor().spawn(async move {
703 let fs = &fs;
704 let query = &query;
705 executor
706 .scoped(move |scope| {
707 for _ in 0..MAX_CONCURRENT_FILE_SCANS {
708 let filter_rx = filter_rx.clone();
709 scope.spawn(async move {
710 Self::filter_paths(fs, filter_rx, query).await.log_err();
711 })
712 }
713 })
714 .await;
715 });
716 cx.background_executor()
717 .spawn(async move {
718 let mut matched = 0;
719 while let Some(mut receiver) = output_rx.next().await {
720 let Some(path) = receiver.next().await else {
721 continue;
722 };
723 let Ok(_) = matching_paths_tx.send(path).await else {
724 break;
725 };
726 matched += 1;
727 if matched == limit {
728 break;
729 }
730 }
731 drop(input);
732 drop(filters);
733 })
734 .detach();
735 matching_paths_rx
736 }
737
738 fn scan_ignored_dir<'a>(
739 fs: &'a Arc<dyn Fs>,
740 snapshot: &'a worktree::Snapshot,
741 path: &'a Path,
742 query: &'a SearchQuery,
743 include_root: bool,
744 filter_tx: &'a Sender<MatchingEntry>,
745 output_tx: &'a Sender<oneshot::Receiver<ProjectPath>>,
746 ) -> BoxFuture<'a, Result<()>> {
747 async move {
748 let abs_path = snapshot.abs_path().join(path);
749 let Some(mut files) = fs
750 .read_dir(&abs_path)
751 .await
752 .with_context(|| format!("listing ignored path {abs_path:?}"))
753 .log_err()
754 else {
755 return Ok(());
756 };
757
758 let mut results = Vec::new();
759
760 while let Some(Ok(file)) = files.next().await {
761 let Some(metadata) = fs
762 .metadata(&file)
763 .await
764 .with_context(|| format!("fetching fs metadata for {abs_path:?}"))
765 .log_err()
766 .flatten()
767 else {
768 continue;
769 };
770 if metadata.is_symlink || metadata.is_fifo {
771 continue;
772 }
773 results.push((
774 file.strip_prefix(snapshot.abs_path())?.to_path_buf(),
775 !metadata.is_dir,
776 ))
777 }
778 results.sort_by(|(a_path, a_is_file), (b_path, b_is_file)| {
779 compare_paths((a_path, *a_is_file), (b_path, *b_is_file))
780 });
781 for (path, is_file) in results {
782 if is_file {
783 if query.filters_path() {
784 let matched_path = if include_root {
785 let mut full_path = PathBuf::from(snapshot.root_name());
786 full_path.push(&path);
787 query.file_matches(&full_path)
788 } else {
789 query.file_matches(&path)
790 };
791 if !matched_path {
792 continue;
793 }
794 }
795 let (tx, rx) = oneshot::channel();
796 output_tx.send(rx).await?;
797 filter_tx
798 .send(MatchingEntry {
799 respond: tx,
800 worktree_path: snapshot.abs_path().clone(),
801 path: ProjectPath {
802 worktree_id: snapshot.id(),
803 path: Arc::from(path),
804 },
805 })
806 .await?;
807 } else {
808 Self::scan_ignored_dir(
809 fs,
810 snapshot,
811 &path,
812 query,
813 include_root,
814 filter_tx,
815 output_tx,
816 )
817 .await?;
818 }
819 }
820 Ok(())
821 }
822 .boxed()
823 }
824
825 async fn find_candidate_paths(
826 fs: Arc<dyn Fs>,
827 snapshots: Vec<(worktree::Snapshot, WorktreeSettings)>,
828 open_entries: HashSet<ProjectEntryId>,
829 query: SearchQuery,
830 filter_tx: Sender<MatchingEntry>,
831 output_tx: Sender<oneshot::Receiver<ProjectPath>>,
832 ) -> Result<()> {
833 let include_root = snapshots.len() > 1;
834 for (snapshot, settings) in snapshots {
835 let mut entries: Vec<_> = snapshot.entries(query.include_ignored(), 0).collect();
836 entries.sort_by(|a, b| compare_paths((&a.path, a.is_file()), (&b.path, b.is_file())));
837 for entry in entries {
838 if entry.is_dir() && entry.is_ignored {
839 if !settings.is_path_excluded(&entry.path) {
840 Self::scan_ignored_dir(
841 &fs,
842 &snapshot,
843 &entry.path,
844 &query,
845 include_root,
846 &filter_tx,
847 &output_tx,
848 )
849 .await?;
850 }
851 continue;
852 }
853
854 if entry.is_fifo || !entry.is_file() {
855 continue;
856 }
857
858 if query.filters_path() {
859 let matched_path = if include_root {
860 let mut full_path = PathBuf::from(snapshot.root_name());
861 full_path.push(&entry.path);
862 query.file_matches(&full_path)
863 } else {
864 query.file_matches(&entry.path)
865 };
866 if !matched_path {
867 continue;
868 }
869 }
870
871 let (mut tx, rx) = oneshot::channel();
872
873 if open_entries.contains(&entry.id) {
874 tx.send(ProjectPath {
875 worktree_id: snapshot.id(),
876 path: entry.path.clone(),
877 })
878 .await?;
879 } else {
880 filter_tx
881 .send(MatchingEntry {
882 respond: tx,
883 worktree_path: snapshot.abs_path().clone(),
884 path: ProjectPath {
885 worktree_id: snapshot.id(),
886 path: entry.path.clone(),
887 },
888 })
889 .await?;
890 }
891
892 output_tx.send(rx).await?;
893 }
894 }
895 Ok(())
896 }
897
898 async fn filter_paths(
899 fs: &Arc<dyn Fs>,
900 mut input: Receiver<MatchingEntry>,
901 query: &SearchQuery,
902 ) -> Result<()> {
903 while let Some(mut entry) = input.next().await {
904 let abs_path = entry.worktree_path.join(&entry.path.path);
905 let Some(file) = fs.open_sync(&abs_path).await.log_err() else {
906 continue;
907 };
908 if query.detect(file).unwrap_or(false) {
909 entry.respond.send(entry.path).await?
910 }
911 }
912
913 Ok(())
914 }
915
916 pub async fn handle_create_project_entry(
917 this: Model<Self>,
918 envelope: TypedEnvelope<proto::CreateProjectEntry>,
919 mut cx: AsyncAppContext,
920 ) -> Result<proto::ProjectEntryResponse> {
921 let worktree = this.update(&mut cx, |this, cx| {
922 let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id);
923 this.worktree_for_id(worktree_id, cx)
924 .ok_or_else(|| anyhow!("worktree not found"))
925 })??;
926 Worktree::handle_create_entry(worktree, envelope.payload, cx).await
927 }
928
929 pub async fn handle_rename_project_entry(
930 this: Model<Self>,
931 envelope: TypedEnvelope<proto::RenameProjectEntry>,
932 mut cx: AsyncAppContext,
933 ) -> Result<proto::ProjectEntryResponse> {
934 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
935 let worktree = this.update(&mut cx, |this, cx| {
936 this.worktree_for_entry(entry_id, cx)
937 .ok_or_else(|| anyhow!("worktree not found"))
938 })??;
939 Worktree::handle_rename_entry(worktree, envelope.payload, cx).await
940 }
941
942 pub async fn handle_copy_project_entry(
943 this: Model<Self>,
944 envelope: TypedEnvelope<proto::CopyProjectEntry>,
945 mut cx: AsyncAppContext,
946 ) -> Result<proto::ProjectEntryResponse> {
947 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
948 let worktree = this.update(&mut cx, |this, cx| {
949 this.worktree_for_entry(entry_id, cx)
950 .ok_or_else(|| anyhow!("worktree not found"))
951 })??;
952 Worktree::handle_copy_entry(worktree, envelope.payload, cx).await
953 }
954
955 pub async fn handle_delete_project_entry(
956 this: Model<Self>,
957 envelope: TypedEnvelope<proto::DeleteProjectEntry>,
958 mut cx: AsyncAppContext,
959 ) -> Result<proto::ProjectEntryResponse> {
960 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
961 let worktree = this.update(&mut cx, |this, cx| {
962 this.worktree_for_entry(entry_id, cx)
963 .ok_or_else(|| anyhow!("worktree not found"))
964 })??;
965 Worktree::handle_delete_entry(worktree, envelope.payload, cx).await
966 }
967
968 pub async fn handle_expand_project_entry(
969 this: Model<Self>,
970 envelope: TypedEnvelope<proto::ExpandProjectEntry>,
971 mut cx: AsyncAppContext,
972 ) -> Result<proto::ExpandProjectEntryResponse> {
973 let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id);
974 let worktree = this
975 .update(&mut cx, |this, cx| this.worktree_for_entry(entry_id, cx))?
976 .ok_or_else(|| anyhow!("invalid request"))?;
977 Worktree::handle_expand_entry(worktree, envelope.payload, cx).await
978 }
979}
980
981#[derive(Clone, Debug)]
982enum WorktreeHandle {
983 Strong(Model<Worktree>),
984 Weak(WeakModel<Worktree>),
985}
986
987impl WorktreeHandle {
988 fn upgrade(&self) -> Option<Model<Worktree>> {
989 match self {
990 WorktreeHandle::Strong(handle) => Some(handle.clone()),
991 WorktreeHandle::Weak(handle) => handle.upgrade(),
992 }
993 }
994}