1mod ignore;
2mod worktree_settings;
3#[cfg(test)]
4mod worktree_tests;
5
6use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
7use anyhow::{Context as _, Result, anyhow};
8use chardetng::EncodingDetector;
9use clock::ReplicaId;
10use collections::{HashMap, HashSet, VecDeque};
11use encoding_rs::Encoding;
12use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};
13use futures::{
14 FutureExt as _, Stream, StreamExt,
15 channel::{
16 mpsc::{self, UnboundedSender},
17 oneshot,
18 },
19 select_biased, stream,
20 task::Poll,
21};
22use fuzzy::CharBag;
23use git::{
24 COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, REPO_EXCLUDE,
25 status::GitSummary,
26};
27use gpui::{
28 App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority,
29 Task,
30};
31use ignore::IgnoreStack;
32use language::DiskState;
33
34use parking_lot::Mutex;
35use paths::{local_settings_folder_name, local_vscode_folder_name};
36use postage::{
37 barrier,
38 prelude::{Sink as _, Stream as _},
39 watch,
40};
41use rpc::{
42 AnyProtoClient,
43 proto::{self, split_worktree_update},
44};
45pub use settings::WorktreeId;
46use settings::{Settings, SettingsLocation, SettingsStore};
47use smallvec::{SmallVec, smallvec};
48use smol::channel::{self, Sender};
49use std::{
50 any::Any,
51 borrow::Borrow as _,
52 cmp::Ordering,
53 collections::hash_map,
54 convert::TryFrom,
55 ffi::OsStr,
56 fmt,
57 future::Future,
58 mem::{self},
59 ops::{Deref, DerefMut, Range},
60 path::{Path, PathBuf},
61 pin::Pin,
62 sync::{
63 Arc,
64 atomic::{AtomicUsize, Ordering::SeqCst},
65 },
66 time::{Duration, Instant},
67};
68use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet};
69use text::{LineEnding, Rope};
70use util::{
71 ResultExt, debug_panic, maybe,
72 paths::{PathMatcher, PathStyle, SanitizedPath, home_dir},
73 rel_path::RelPath,
74};
75pub use worktree_settings::WorktreeSettings;
76
77use crate::ignore::IgnoreKind;
78
79pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);
80
81/// A set of local or remote files that are being opened as part of a project.
82/// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates.
83/// Stores git repositories data and the diagnostics for the file(s).
84///
85/// Has an absolute path, and may be set to be visible in Zed UI or not.
86/// May correspond to a directory or a single file.
87/// Possible examples:
88/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree
89/// * a directory opened in Zed — may be added as a visible entry to the current worktree
90///
91/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.
92pub enum Worktree {
93 Local(LocalWorktree),
94 Remote(RemoteWorktree),
95}
96
97/// An entry, created in the worktree.
98#[derive(Debug)]
99pub enum CreatedEntry {
100 /// Got created and indexed by the worktree, receiving a corresponding entry.
101 Included(Entry),
102 /// Got created, but not indexed due to falling under exclusion filters.
103 Excluded { abs_path: PathBuf },
104}
105
106#[derive(Debug)]
107pub struct LoadedFile {
108 pub file: Arc<File>,
109 pub text: String,
110 pub encoding: &'static Encoding,
111 pub has_bom: bool,
112}
113
114pub struct LoadedBinaryFile {
115 pub file: Arc<File>,
116 pub content: Vec<u8>,
117}
118
119impl fmt::Debug for LoadedBinaryFile {
120 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121 f.debug_struct("LoadedBinaryFile")
122 .field("file", &self.file)
123 .field("content_bytes", &self.content.len())
124 .finish()
125 }
126}
127
128pub struct LocalWorktree {
129 snapshot: LocalSnapshot,
130 scan_requests_tx: channel::Sender<ScanRequest>,
131 path_prefixes_to_scan_tx: channel::Sender<PathPrefixScanRequest>,
132 is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
133 _background_scanner_tasks: Vec<Task<()>>,
134 update_observer: Option<UpdateObservationState>,
135 fs: Arc<dyn Fs>,
136 fs_case_sensitive: bool,
137 visible: bool,
138 next_entry_id: Arc<AtomicUsize>,
139 settings: WorktreeSettings,
140 share_private_files: bool,
141 scanning_enabled: bool,
142}
143
144pub struct PathPrefixScanRequest {
145 path: Arc<RelPath>,
146 done: SmallVec<[barrier::Sender; 1]>,
147}
148
149struct ScanRequest {
150 relative_paths: Vec<Arc<RelPath>>,
151 done: SmallVec<[barrier::Sender; 1]>,
152}
153
154pub struct RemoteWorktree {
155 snapshot: Snapshot,
156 background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>,
157 project_id: u64,
158 client: AnyProtoClient,
159 file_scan_inclusions: PathMatcher,
160 updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>,
161 update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>,
162 snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>,
163 replica_id: ReplicaId,
164 visible: bool,
165 disconnected: bool,
166}
167
168#[derive(Clone)]
169pub struct Snapshot {
170 id: WorktreeId,
171 /// The absolute path of the worktree root.
172 abs_path: Arc<SanitizedPath>,
173 path_style: PathStyle,
174 root_name: Arc<RelPath>,
175 root_char_bag: CharBag,
176 entries_by_path: SumTree<Entry>,
177 entries_by_id: SumTree<PathEntry>,
178 always_included_entries: Vec<Arc<RelPath>>,
179
180 /// A number that increases every time the worktree begins scanning
181 /// a set of paths from the filesystem. This scanning could be caused
182 /// by some operation performed on the worktree, such as reading or
183 /// writing a file, or by an event reported by the filesystem.
184 scan_id: usize,
185
186 /// The latest scan id that has completed, and whose preceding scans
187 /// have all completed. The current `scan_id` could be more than one
188 /// greater than the `completed_scan_id` if operations are performed
189 /// on the worktree while it is processing a file-system event.
190 completed_scan_id: usize,
191}
192
193/// This path corresponds to the 'content path' of a repository in relation
194/// to Zed's project root.
195/// In the majority of the cases, this is the folder that contains the .git folder.
196/// But if a sub-folder of a git repository is opened, this corresponds to the
197/// project root and the .git folder is located in a parent directory.
198#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
199pub enum WorkDirectory {
200 InProject {
201 relative_path: Arc<RelPath>,
202 },
203 AboveProject {
204 absolute_path: Arc<Path>,
205 location_in_repo: Arc<Path>,
206 },
207}
208
209impl WorkDirectory {
210 fn path_key(&self) -> PathKey {
211 match self {
212 WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),
213 WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()),
214 }
215 }
216
217 /// Returns true if the given path is a child of the work directory.
218 ///
219 /// Note that the path may not be a member of this repository, if there
220 /// is a repository in a directory between these two paths
221 /// external .git folder in a parent folder of the project root.
222 #[track_caller]
223 pub fn directory_contains(&self, path: &RelPath) -> bool {
224 match self {
225 WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),
226 WorkDirectory::AboveProject { .. } => true,
227 }
228 }
229}
230
231impl Default for WorkDirectory {
232 fn default() -> Self {
233 Self::InProject {
234 relative_path: Arc::from(RelPath::empty()),
235 }
236 }
237}
238
239#[derive(Clone)]
240pub struct LocalSnapshot {
241 snapshot: Snapshot,
242 global_gitignore: Option<Arc<Gitignore>>,
243 /// Exclude files for all git repositories in the worktree, indexed by their absolute path.
244 /// The boolean indicates whether the gitignore needs to be updated.
245 repo_exclude_by_work_dir_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
246 /// All of the gitignore files in the worktree, indexed by their absolute path.
247 /// The boolean indicates whether the gitignore needs to be updated.
248 ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
249 /// All of the git repositories in the worktree, indexed by the project entry
250 /// id of their parent directory.
251 git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
252 /// The file handle of the worktree root
253 /// (so we can find it after it's been moved)
254 root_file_handle: Option<Arc<dyn fs::FileHandle>>,
255}
256
257struct BackgroundScannerState {
258 snapshot: LocalSnapshot,
259 scanned_dirs: HashSet<ProjectEntryId>,
260 path_prefixes_to_scan: HashSet<Arc<RelPath>>,
261 paths_to_scan: HashSet<Arc<RelPath>>,
262 /// The ids of all of the entries that were removed from the snapshot
263 /// as part of the current update. These entry ids may be re-used
264 /// if the same inode is discovered at a new path, or if the given
265 /// path is re-created after being deleted.
266 removed_entries: HashMap<u64, Entry>,
267 changed_paths: Vec<Arc<RelPath>>,
268 prev_snapshot: Snapshot,
269 scanning_enabled: bool,
270}
271
272#[derive(Debug, Clone)]
273struct LocalRepositoryEntry {
274 work_directory_id: ProjectEntryId,
275 work_directory: WorkDirectory,
276 work_directory_abs_path: Arc<Path>,
277 git_dir_scan_id: usize,
278 /// Absolute path to the original .git entry that caused us to create this repository.
279 ///
280 /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere
281 /// (whose path we then store in `repository_dir_abs_path`).
282 dot_git_abs_path: Arc<Path>,
283 /// Absolute path to the "commondir" for this repository.
284 ///
285 /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path,
286 /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory
287 /// from which the submodule/worktree was derived.
288 common_dir_abs_path: Arc<Path>,
289 /// Absolute path to the directory holding the repository's state.
290 ///
291 /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and
292 /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the
293 /// commondir like `/project/.git/modules/foo`.
294 repository_dir_abs_path: Arc<Path>,
295}
296
297impl sum_tree::Item for LocalRepositoryEntry {
298 type Summary = PathSummary<sum_tree::NoSummary>;
299
300 fn summary(&self, _: <Self::Summary as Summary>::Context<'_>) -> Self::Summary {
301 PathSummary {
302 max_path: self.work_directory.path_key().0,
303 item_summary: sum_tree::NoSummary,
304 }
305 }
306}
307
308impl KeyedItem for LocalRepositoryEntry {
309 type Key = PathKey;
310
311 fn key(&self) -> Self::Key {
312 self.work_directory.path_key()
313 }
314}
315
316impl Deref for LocalRepositoryEntry {
317 type Target = WorkDirectory;
318
319 fn deref(&self) -> &Self::Target {
320 &self.work_directory
321 }
322}
323
324impl Deref for LocalSnapshot {
325 type Target = Snapshot;
326
327 fn deref(&self) -> &Self::Target {
328 &self.snapshot
329 }
330}
331
332impl DerefMut for LocalSnapshot {
333 fn deref_mut(&mut self) -> &mut Self::Target {
334 &mut self.snapshot
335 }
336}
337
338enum ScanState {
339 Started,
340 Updated {
341 snapshot: LocalSnapshot,
342 changes: UpdatedEntriesSet,
343 barrier: SmallVec<[barrier::Sender; 1]>,
344 scanning: bool,
345 },
346 RootUpdated {
347 new_path: Arc<SanitizedPath>,
348 },
349}
350
351struct UpdateObservationState {
352 snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>,
353 resume_updates: watch::Sender<()>,
354 _maintain_remote_snapshot: Task<Option<()>>,
355}
356
357#[derive(Debug, Clone)]
358pub enum Event {
359 UpdatedEntries(UpdatedEntriesSet),
360 UpdatedGitRepositories(UpdatedGitRepositoriesSet),
361 DeletedEntry(ProjectEntryId),
362}
363
364impl EventEmitter<Event> for Worktree {}
365
366impl Worktree {
367 pub async fn local(
368 path: impl Into<Arc<Path>>,
369 visible: bool,
370 fs: Arc<dyn Fs>,
371 next_entry_id: Arc<AtomicUsize>,
372 scanning_enabled: bool,
373 cx: &mut AsyncApp,
374 ) -> Result<Entity<Self>> {
375 let abs_path = path.into();
376 let metadata = fs
377 .metadata(&abs_path)
378 .await
379 .context("failed to stat worktree path")?;
380
381 let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| {
382 log::error!(
383 "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}"
384 );
385 true
386 });
387
388 let root_file_handle = if metadata.as_ref().is_some() {
389 fs.open_handle(&abs_path)
390 .await
391 .with_context(|| {
392 format!(
393 "failed to open local worktree root at {}",
394 abs_path.display()
395 )
396 })
397 .log_err()
398 } else {
399 None
400 };
401
402 Ok(cx.new(move |cx: &mut Context<Worktree>| {
403 let mut snapshot = LocalSnapshot {
404 ignores_by_parent_abs_path: Default::default(),
405 global_gitignore: Default::default(),
406 repo_exclude_by_work_dir_abs_path: Default::default(),
407 git_repositories: Default::default(),
408 snapshot: Snapshot::new(
409 cx.entity_id().as_u64(),
410 abs_path
411 .file_name()
412 .and_then(|f| f.to_str())
413 .map_or(RelPath::empty().into(), |f| {
414 RelPath::unix(f).unwrap().into()
415 }),
416 abs_path.clone(),
417 PathStyle::local(),
418 ),
419 root_file_handle,
420 };
421
422 let worktree_id = snapshot.id();
423 let settings_location = Some(SettingsLocation {
424 worktree_id,
425 path: RelPath::empty(),
426 });
427
428 let settings = WorktreeSettings::get(settings_location, cx).clone();
429 cx.observe_global::<SettingsStore>(move |this, cx| {
430 if let Self::Local(this) = this {
431 let settings = WorktreeSettings::get(settings_location, cx).clone();
432 if this.settings != settings {
433 this.settings = settings;
434 this.restart_background_scanners(cx);
435 }
436 }
437 })
438 .detach();
439
440 let share_private_files = false;
441 if let Some(metadata) = metadata {
442 let mut entry = Entry::new(
443 RelPath::empty().into(),
444 &metadata,
445 ProjectEntryId::new(&next_entry_id),
446 snapshot.root_char_bag,
447 None,
448 );
449 if metadata.is_dir {
450 if !scanning_enabled {
451 entry.kind = EntryKind::UnloadedDir;
452 }
453 } else {
454 if let Some(file_name) = abs_path.file_name()
455 && let Some(file_name) = file_name.to_str()
456 && let Ok(path) = RelPath::unix(file_name)
457 {
458 entry.is_private = !share_private_files && settings.is_path_private(path);
459 entry.is_hidden = settings.is_path_hidden(path);
460 }
461 }
462 cx.foreground_executor()
463 .block_on(snapshot.insert_entry(entry, fs.as_ref()));
464 }
465
466 let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
467 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
468 let mut worktree = LocalWorktree {
469 share_private_files,
470 next_entry_id,
471 snapshot,
472 is_scanning: watch::channel_with(true),
473 update_observer: None,
474 scan_requests_tx,
475 path_prefixes_to_scan_tx,
476 _background_scanner_tasks: Vec::new(),
477 fs,
478 fs_case_sensitive,
479 visible,
480 settings,
481 scanning_enabled,
482 };
483 worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
484 Worktree::Local(worktree)
485 }))
486 }
487
488 pub fn remote(
489 project_id: u64,
490 replica_id: ReplicaId,
491 worktree: proto::WorktreeMetadata,
492 client: AnyProtoClient,
493 path_style: PathStyle,
494 cx: &mut App,
495 ) -> Entity<Self> {
496 cx.new(|cx: &mut Context<Self>| {
497 let snapshot = Snapshot::new(
498 worktree.id,
499 RelPath::from_proto(&worktree.root_name)
500 .unwrap_or_else(|_| RelPath::empty().into()),
501 Path::new(&worktree.abs_path).into(),
502 path_style,
503 );
504
505 let background_snapshot = Arc::new(Mutex::new((
506 snapshot.clone(),
507 Vec::<proto::UpdateWorktree>::new(),
508 )));
509 let (background_updates_tx, mut background_updates_rx) =
510 mpsc::unbounded::<proto::UpdateWorktree>();
511 let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel();
512
513 let worktree_id = snapshot.id();
514 let settings_location = Some(SettingsLocation {
515 worktree_id,
516 path: RelPath::empty(),
517 });
518
519 let settings = WorktreeSettings::get(settings_location, cx).clone();
520 let worktree = RemoteWorktree {
521 client,
522 project_id,
523 replica_id,
524 snapshot,
525 file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(),
526 background_snapshot: background_snapshot.clone(),
527 updates_tx: Some(background_updates_tx),
528 update_observer: None,
529 snapshot_subscriptions: Default::default(),
530 visible: worktree.visible,
531 disconnected: false,
532 };
533
534 // Apply updates to a separate snapshot in a background task, then
535 // send them to a foreground task which updates the model.
536 cx.background_spawn(async move {
537 while let Some(update) = background_updates_rx.next().await {
538 {
539 let mut lock = background_snapshot.lock();
540 lock.0.apply_remote_update(
541 update.clone(),
542 &settings.parent_dir_scan_inclusions,
543 );
544 lock.1.push(update);
545 }
546 snapshot_updated_tx.send(()).await.ok();
547 }
548 })
549 .detach();
550
551 // On the foreground task, update to the latest snapshot and notify
552 // any update observer of all updates that led to that snapshot.
553 cx.spawn(async move |this, cx| {
554 while (snapshot_updated_rx.recv().await).is_some() {
555 this.update(cx, |this, cx| {
556 let mut entries_changed = false;
557 let this = this.as_remote_mut().unwrap();
558 {
559 let mut lock = this.background_snapshot.lock();
560 this.snapshot = lock.0.clone();
561 for update in lock.1.drain(..) {
562 entries_changed |= !update.updated_entries.is_empty()
563 || !update.removed_entries.is_empty();
564 if let Some(tx) = &this.update_observer {
565 tx.unbounded_send(update).ok();
566 }
567 }
568 };
569
570 if entries_changed {
571 cx.emit(Event::UpdatedEntries(Arc::default()));
572 }
573 cx.notify();
574 while let Some((scan_id, _)) = this.snapshot_subscriptions.front() {
575 if this.observed_snapshot(*scan_id) {
576 let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap();
577 let _ = tx.send(());
578 } else {
579 break;
580 }
581 }
582 })?;
583 }
584 anyhow::Ok(())
585 })
586 .detach();
587
588 Worktree::Remote(worktree)
589 })
590 }
591
592 pub fn as_local(&self) -> Option<&LocalWorktree> {
593 if let Worktree::Local(worktree) = self {
594 Some(worktree)
595 } else {
596 None
597 }
598 }
599
600 pub fn as_remote(&self) -> Option<&RemoteWorktree> {
601 if let Worktree::Remote(worktree) = self {
602 Some(worktree)
603 } else {
604 None
605 }
606 }
607
608 pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> {
609 if let Worktree::Local(worktree) = self {
610 Some(worktree)
611 } else {
612 None
613 }
614 }
615
616 pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> {
617 if let Worktree::Remote(worktree) = self {
618 Some(worktree)
619 } else {
620 None
621 }
622 }
623
624 pub fn is_local(&self) -> bool {
625 matches!(self, Worktree::Local(_))
626 }
627
628 pub fn is_remote(&self) -> bool {
629 !self.is_local()
630 }
631
632 pub fn settings_location(&self, _: &Context<Self>) -> SettingsLocation<'static> {
633 SettingsLocation {
634 worktree_id: self.id(),
635 path: RelPath::empty(),
636 }
637 }
638
639 pub fn snapshot(&self) -> Snapshot {
640 match self {
641 Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(),
642 Worktree::Remote(worktree) => worktree.snapshot.clone(),
643 }
644 }
645
646 pub fn scan_id(&self) -> usize {
647 match self {
648 Worktree::Local(worktree) => worktree.snapshot.scan_id,
649 Worktree::Remote(worktree) => worktree.snapshot.scan_id,
650 }
651 }
652
653 pub fn metadata_proto(&self) -> proto::WorktreeMetadata {
654 proto::WorktreeMetadata {
655 id: self.id().to_proto(),
656 root_name: self.root_name().to_proto(),
657 visible: self.is_visible(),
658 abs_path: self.abs_path().to_string_lossy().into_owned(),
659 }
660 }
661
662 pub fn completed_scan_id(&self) -> usize {
663 match self {
664 Worktree::Local(worktree) => worktree.snapshot.completed_scan_id,
665 Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id,
666 }
667 }
668
669 pub fn is_visible(&self) -> bool {
670 match self {
671 Worktree::Local(worktree) => worktree.visible,
672 Worktree::Remote(worktree) => worktree.visible,
673 }
674 }
675
676 pub fn replica_id(&self) -> ReplicaId {
677 match self {
678 Worktree::Local(_) => ReplicaId::LOCAL,
679 Worktree::Remote(worktree) => worktree.replica_id,
680 }
681 }
682
683 pub fn abs_path(&self) -> Arc<Path> {
684 match self {
685 Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
686 Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()),
687 }
688 }
689
690 pub fn root_file(&self, cx: &Context<Self>) -> Option<Arc<File>> {
691 let entry = self.root_entry()?;
692 Some(File::for_entry(entry.clone(), cx.entity()))
693 }
694
695 pub fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
696 where
697 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
698 Fut: 'static + Send + Future<Output = bool>,
699 {
700 match self {
701 Worktree::Local(this) => this.observe_updates(project_id, cx, callback),
702 Worktree::Remote(this) => this.observe_updates(project_id, cx, callback),
703 }
704 }
705
706 pub fn stop_observing_updates(&mut self) {
707 match self {
708 Worktree::Local(this) => {
709 this.update_observer.take();
710 }
711 Worktree::Remote(this) => {
712 this.update_observer.take();
713 }
714 }
715 }
716
717 #[cfg(any(test, feature = "test-support"))]
718 pub fn has_update_observer(&self) -> bool {
719 match self {
720 Worktree::Local(this) => this.update_observer.is_some(),
721 Worktree::Remote(this) => this.update_observer.is_some(),
722 }
723 }
724
725 pub fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
726 match self {
727 Worktree::Local(this) => this.load_file(path, cx),
728 Worktree::Remote(_) => {
729 Task::ready(Err(anyhow!("remote worktrees can't yet load files")))
730 }
731 }
732 }
733
734 pub fn load_binary_file(
735 &self,
736 path: &RelPath,
737 cx: &Context<Worktree>,
738 ) -> Task<Result<LoadedBinaryFile>> {
739 match self {
740 Worktree::Local(this) => this.load_binary_file(path, cx),
741 Worktree::Remote(_) => {
742 Task::ready(Err(anyhow!("remote worktrees can't yet load binary files")))
743 }
744 }
745 }
746
747 pub fn write_file(
748 &self,
749 path: Arc<RelPath>,
750 text: Rope,
751 line_ending: LineEnding,
752 encoding: &'static Encoding,
753 has_bom: bool,
754 cx: &Context<Worktree>,
755 ) -> Task<Result<Arc<File>>> {
756 match self {
757 Worktree::Local(this) => {
758 this.write_file(path, text, line_ending, encoding, has_bom, cx)
759 }
760 Worktree::Remote(_) => {
761 Task::ready(Err(anyhow!("remote worktree can't yet write files")))
762 }
763 }
764 }
765
766 pub fn create_entry(
767 &mut self,
768 path: Arc<RelPath>,
769 is_directory: bool,
770 content: Option<Vec<u8>>,
771 cx: &Context<Worktree>,
772 ) -> Task<Result<CreatedEntry>> {
773 let worktree_id = self.id();
774 match self {
775 Worktree::Local(this) => this.create_entry(path, is_directory, content, cx),
776 Worktree::Remote(this) => {
777 let project_id = this.project_id;
778 let request = this.client.request(proto::CreateProjectEntry {
779 worktree_id: worktree_id.to_proto(),
780 project_id,
781 path: path.as_ref().to_proto(),
782 content,
783 is_directory,
784 });
785 cx.spawn(async move |this, cx| {
786 let response = request.await?;
787 match response.entry {
788 Some(entry) => this
789 .update(cx, |worktree, cx| {
790 worktree.as_remote_mut().unwrap().insert_entry(
791 entry,
792 response.worktree_scan_id as usize,
793 cx,
794 )
795 })?
796 .await
797 .map(CreatedEntry::Included),
798 None => {
799 let abs_path =
800 this.read_with(cx, |worktree, _| worktree.absolutize(&path))?;
801 Ok(CreatedEntry::Excluded { abs_path })
802 }
803 }
804 })
805 }
806 }
807 }
808
809 pub fn delete_entry(
810 &mut self,
811 entry_id: ProjectEntryId,
812 trash: bool,
813 cx: &mut Context<Worktree>,
814 ) -> Option<Task<Result<()>>> {
815 let task = match self {
816 Worktree::Local(this) => this.delete_entry(entry_id, trash, cx),
817 Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx),
818 }?;
819
820 let entry = match &*self {
821 Worktree::Local(this) => this.entry_for_id(entry_id),
822 Worktree::Remote(this) => this.entry_for_id(entry_id),
823 }?;
824
825 let mut ids = vec![entry_id];
826 let path = &*entry.path;
827
828 self.get_children_ids_recursive(path, &mut ids);
829
830 for id in ids {
831 cx.emit(Event::DeletedEntry(id));
832 }
833 Some(task)
834 }
835
836 fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec<ProjectEntryId>) {
837 let children_iter = self.child_entries(path);
838 for child in children_iter {
839 ids.push(child.id);
840 self.get_children_ids_recursive(&child.path, ids);
841 }
842 }
843
844 // pub fn rename_entry(
845 // &mut self,
846 // entry_id: ProjectEntryId,
847 // new_path: Arc<RelPath>,
848 // cx: &Context<Self>,
849 // ) -> Task<Result<CreatedEntry>> {
850 // match self {
851 // Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx),
852 // Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx),
853 // }
854 // }
855
856 pub fn copy_external_entries(
857 &mut self,
858 target_directory: Arc<RelPath>,
859 paths: Vec<Arc<Path>>,
860 fs: Arc<dyn Fs>,
861 cx: &Context<Worktree>,
862 ) -> Task<Result<Vec<ProjectEntryId>>> {
863 match self {
864 Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx),
865 Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx),
866 }
867 }
868
869 pub fn expand_entry(
870 &mut self,
871 entry_id: ProjectEntryId,
872 cx: &Context<Worktree>,
873 ) -> Option<Task<Result<()>>> {
874 match self {
875 Worktree::Local(this) => this.expand_entry(entry_id, cx),
876 Worktree::Remote(this) => {
877 let response = this.client.request(proto::ExpandProjectEntry {
878 project_id: this.project_id,
879 entry_id: entry_id.to_proto(),
880 });
881 Some(cx.spawn(async move |this, cx| {
882 let response = response.await?;
883 this.update(cx, |this, _| {
884 this.as_remote_mut()
885 .unwrap()
886 .wait_for_snapshot(response.worktree_scan_id as usize)
887 })?
888 .await?;
889 Ok(())
890 }))
891 }
892 }
893 }
894
895 pub fn expand_all_for_entry(
896 &mut self,
897 entry_id: ProjectEntryId,
898 cx: &Context<Worktree>,
899 ) -> Option<Task<Result<()>>> {
900 match self {
901 Worktree::Local(this) => this.expand_all_for_entry(entry_id, cx),
902 Worktree::Remote(this) => {
903 let response = this.client.request(proto::ExpandAllForProjectEntry {
904 project_id: this.project_id,
905 entry_id: entry_id.to_proto(),
906 });
907 Some(cx.spawn(async move |this, cx| {
908 let response = response.await?;
909 this.update(cx, |this, _| {
910 this.as_remote_mut()
911 .unwrap()
912 .wait_for_snapshot(response.worktree_scan_id as usize)
913 })?
914 .await?;
915 Ok(())
916 }))
917 }
918 }
919 }
920
921 pub async fn handle_create_entry(
922 this: Entity<Self>,
923 request: proto::CreateProjectEntry,
924 mut cx: AsyncApp,
925 ) -> Result<proto::ProjectEntryResponse> {
926 let (scan_id, entry) = this.update(&mut cx, |this, cx| {
927 anyhow::Ok((
928 this.scan_id(),
929 this.create_entry(
930 RelPath::from_proto(&request.path).with_context(|| {
931 format!("received invalid relative path {:?}", request.path)
932 })?,
933 request.is_directory,
934 request.content,
935 cx,
936 ),
937 ))
938 })?;
939 Ok(proto::ProjectEntryResponse {
940 entry: match &entry.await? {
941 CreatedEntry::Included(entry) => Some(entry.into()),
942 CreatedEntry::Excluded { .. } => None,
943 },
944 worktree_scan_id: scan_id as u64,
945 })
946 }
947
948 pub async fn handle_delete_entry(
949 this: Entity<Self>,
950 request: proto::DeleteProjectEntry,
951 mut cx: AsyncApp,
952 ) -> Result<proto::ProjectEntryResponse> {
953 let (scan_id, task) = this.update(&mut cx, |this, cx| {
954 (
955 this.scan_id(),
956 this.delete_entry(
957 ProjectEntryId::from_proto(request.entry_id),
958 request.use_trash,
959 cx,
960 ),
961 )
962 });
963 task.ok_or_else(|| anyhow::anyhow!("invalid entry"))?
964 .await?;
965 Ok(proto::ProjectEntryResponse {
966 entry: None,
967 worktree_scan_id: scan_id as u64,
968 })
969 }
970
971 pub async fn handle_expand_entry(
972 this: Entity<Self>,
973 request: proto::ExpandProjectEntry,
974 mut cx: AsyncApp,
975 ) -> Result<proto::ExpandProjectEntryResponse> {
976 let task = this.update(&mut cx, |this, cx| {
977 this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx)
978 });
979 task.ok_or_else(|| anyhow::anyhow!("no such entry"))?
980 .await?;
981 let scan_id = this.read_with(&cx, |this, _| this.scan_id());
982 Ok(proto::ExpandProjectEntryResponse {
983 worktree_scan_id: scan_id as u64,
984 })
985 }
986
987 pub async fn handle_expand_all_for_entry(
988 this: Entity<Self>,
989 request: proto::ExpandAllForProjectEntry,
990 mut cx: AsyncApp,
991 ) -> Result<proto::ExpandAllForProjectEntryResponse> {
992 let task = this.update(&mut cx, |this, cx| {
993 this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx)
994 });
995 task.ok_or_else(|| anyhow::anyhow!("no such entry"))?
996 .await?;
997 let scan_id = this.read_with(&cx, |this, _| this.scan_id());
998 Ok(proto::ExpandAllForProjectEntryResponse {
999 worktree_scan_id: scan_id as u64,
1000 })
1001 }
1002
1003 pub fn is_single_file(&self) -> bool {
1004 self.root_dir().is_none()
1005 }
1006
1007 /// For visible worktrees, returns the path with the worktree name as the first component.
1008 /// Otherwise, returns an absolute path.
1009 pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf {
1010 if self.is_visible() {
1011 self.root_name()
1012 .join(worktree_relative_path)
1013 .display(self.path_style)
1014 .to_string()
1015 .into()
1016 } else {
1017 let full_path = self.abs_path();
1018 let mut full_path_string = if self.is_local()
1019 && let Ok(stripped) = full_path.strip_prefix(home_dir())
1020 {
1021 self.path_style
1022 .join("~", &*stripped.to_string_lossy())
1023 .unwrap()
1024 } else {
1025 full_path.to_string_lossy().into_owned()
1026 };
1027
1028 if worktree_relative_path.components().next().is_some() {
1029 full_path_string.push_str(self.path_style.primary_separator());
1030 full_path_string.push_str(&worktree_relative_path.display(self.path_style));
1031 }
1032
1033 full_path_string.into()
1034 }
1035 }
1036}
1037
1038impl LocalWorktree {
1039 pub fn fs(&self) -> &Arc<dyn Fs> {
1040 &self.fs
1041 }
1042
1043 pub fn is_path_private(&self, path: &RelPath) -> bool {
1044 !self.share_private_files && self.settings.is_path_private(path)
1045 }
1046
1047 pub fn fs_is_case_sensitive(&self) -> bool {
1048 self.fs_case_sensitive
1049 }
1050
1051 fn restart_background_scanners(&mut self, cx: &Context<Worktree>) {
1052 let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
1053 let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
1054 self.scan_requests_tx = scan_requests_tx;
1055 self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
1056
1057 self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx);
1058 let always_included_entries = mem::take(&mut self.snapshot.always_included_entries);
1059 log::debug!(
1060 "refreshing entries for the following always included paths: {:?}",
1061 always_included_entries
1062 );
1063
1064 // Cleans up old always included entries to ensure they get updated properly. Otherwise,
1065 // nested always included entries may not get updated and will result in out-of-date info.
1066 self.refresh_entries_for_paths(always_included_entries);
1067 }
1068
1069 fn start_background_scanner(
1070 &mut self,
1071 scan_requests_rx: channel::Receiver<ScanRequest>,
1072 path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
1073 cx: &Context<Worktree>,
1074 ) {
1075 let snapshot = self.snapshot();
1076 let share_private_files = self.share_private_files;
1077 let next_entry_id = self.next_entry_id.clone();
1078 let fs = self.fs.clone();
1079 let scanning_enabled = self.scanning_enabled;
1080 let settings = self.settings.clone();
1081 let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
1082 let background_scanner = cx.background_spawn({
1083 let abs_path = snapshot.abs_path.as_path().to_path_buf();
1084 let background = cx.background_executor().clone();
1085 async move {
1086 let (events, watcher) = if scanning_enabled {
1087 fs.watch(&abs_path, FS_WATCH_LATENCY).await
1088 } else {
1089 (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _)
1090 };
1091 let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| {
1092 log::error!("Failed to determine whether filesystem is case sensitive: {e:#}");
1093 true
1094 });
1095
1096 let mut scanner = BackgroundScanner {
1097 fs,
1098 fs_case_sensitive,
1099 status_updates_tx: scan_states_tx,
1100 executor: background,
1101 scan_requests_rx,
1102 path_prefixes_to_scan_rx,
1103 next_entry_id,
1104 state: async_lock::Mutex::new(BackgroundScannerState {
1105 prev_snapshot: snapshot.snapshot.clone(),
1106 snapshot,
1107 scanned_dirs: Default::default(),
1108 scanning_enabled,
1109 path_prefixes_to_scan: Default::default(),
1110 paths_to_scan: Default::default(),
1111 removed_entries: Default::default(),
1112 changed_paths: Default::default(),
1113 }),
1114 phase: BackgroundScannerPhase::InitialScan,
1115 share_private_files,
1116 settings,
1117 watcher,
1118 };
1119
1120 scanner
1121 .run(Box::pin(events.map(|events| events.into_iter().collect())))
1122 .await;
1123 }
1124 });
1125 let scan_state_updater = cx.spawn(async move |this, cx| {
1126 while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
1127 this.update(cx, |this, cx| {
1128 let this = this.as_local_mut().unwrap();
1129 match state {
1130 ScanState::Started => {
1131 *this.is_scanning.0.borrow_mut() = true;
1132 }
1133 ScanState::Updated {
1134 snapshot,
1135 changes,
1136 barrier,
1137 scanning,
1138 } => {
1139 *this.is_scanning.0.borrow_mut() = scanning;
1140 this.set_snapshot(snapshot, changes, cx);
1141 drop(barrier);
1142 }
1143 ScanState::RootUpdated { new_path } => {
1144 this.update_abs_path_and_refresh(new_path, cx);
1145 }
1146 }
1147 });
1148 }
1149 });
1150 self._background_scanner_tasks = vec![background_scanner, scan_state_updater];
1151 *self.is_scanning.0.borrow_mut() = true;
1152 }
1153
1154 fn set_snapshot(
1155 &mut self,
1156 mut new_snapshot: LocalSnapshot,
1157 entry_changes: UpdatedEntriesSet,
1158 cx: &mut Context<Worktree>,
1159 ) {
1160 let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot);
1161 self.snapshot = new_snapshot;
1162
1163 if let Some(share) = self.update_observer.as_mut() {
1164 share
1165 .snapshots_tx
1166 .unbounded_send((self.snapshot.clone(), entry_changes.clone()))
1167 .ok();
1168 }
1169
1170 if !entry_changes.is_empty() {
1171 cx.emit(Event::UpdatedEntries(entry_changes));
1172 }
1173 if !repo_changes.is_empty() {
1174 cx.emit(Event::UpdatedGitRepositories(repo_changes));
1175 }
1176 }
1177
1178 fn changed_repos(
1179 &self,
1180 old_snapshot: &LocalSnapshot,
1181 new_snapshot: &mut LocalSnapshot,
1182 ) -> UpdatedGitRepositoriesSet {
1183 let mut changes = Vec::new();
1184 let mut old_repos = old_snapshot.git_repositories.iter().peekable();
1185 let new_repos = new_snapshot.git_repositories.clone();
1186 let mut new_repos = new_repos.iter().peekable();
1187
1188 loop {
1189 match (new_repos.peek().map(clone), old_repos.peek().map(clone)) {
1190 (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => {
1191 match Ord::cmp(&new_entry_id, &old_entry_id) {
1192 Ordering::Less => {
1193 changes.push(UpdatedGitRepository {
1194 work_directory_id: new_entry_id,
1195 old_work_directory_abs_path: None,
1196 new_work_directory_abs_path: Some(
1197 new_repo.work_directory_abs_path.clone(),
1198 ),
1199 dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
1200 repository_dir_abs_path: Some(
1201 new_repo.repository_dir_abs_path.clone(),
1202 ),
1203 common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
1204 });
1205 new_repos.next();
1206 }
1207 Ordering::Equal => {
1208 if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id
1209 || new_repo.work_directory_abs_path
1210 != old_repo.work_directory_abs_path
1211 {
1212 changes.push(UpdatedGitRepository {
1213 work_directory_id: new_entry_id,
1214 old_work_directory_abs_path: Some(
1215 old_repo.work_directory_abs_path.clone(),
1216 ),
1217 new_work_directory_abs_path: Some(
1218 new_repo.work_directory_abs_path.clone(),
1219 ),
1220 dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()),
1221 repository_dir_abs_path: Some(
1222 new_repo.repository_dir_abs_path.clone(),
1223 ),
1224 common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()),
1225 });
1226 }
1227 new_repos.next();
1228 old_repos.next();
1229 }
1230 Ordering::Greater => {
1231 changes.push(UpdatedGitRepository {
1232 work_directory_id: old_entry_id,
1233 old_work_directory_abs_path: Some(
1234 old_repo.work_directory_abs_path.clone(),
1235 ),
1236 new_work_directory_abs_path: None,
1237 dot_git_abs_path: None,
1238 repository_dir_abs_path: None,
1239 common_dir_abs_path: None,
1240 });
1241 old_repos.next();
1242 }
1243 }
1244 }
1245 (Some((entry_id, repo)), None) => {
1246 changes.push(UpdatedGitRepository {
1247 work_directory_id: entry_id,
1248 old_work_directory_abs_path: None,
1249 new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
1250 dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
1251 repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
1252 common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
1253 });
1254 new_repos.next();
1255 }
1256 (None, Some((entry_id, repo))) => {
1257 changes.push(UpdatedGitRepository {
1258 work_directory_id: entry_id,
1259 old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()),
1260 new_work_directory_abs_path: None,
1261 dot_git_abs_path: Some(repo.dot_git_abs_path.clone()),
1262 repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()),
1263 common_dir_abs_path: Some(repo.common_dir_abs_path.clone()),
1264 });
1265 old_repos.next();
1266 }
1267 (None, None) => break,
1268 }
1269 }
1270
1271 fn clone<T: Clone, U: Clone>(value: &(&T, &U)) -> (T, U) {
1272 (value.0.clone(), value.1.clone())
1273 }
1274
1275 changes.into()
1276 }
1277
1278 pub fn scan_complete(&self) -> impl Future<Output = ()> + use<> {
1279 let mut is_scanning_rx = self.is_scanning.1.clone();
1280 async move {
1281 let mut is_scanning = *is_scanning_rx.borrow();
1282 while is_scanning {
1283 if let Some(value) = is_scanning_rx.recv().await {
1284 is_scanning = value;
1285 } else {
1286 break;
1287 }
1288 }
1289 }
1290 }
1291
1292 pub fn snapshot(&self) -> LocalSnapshot {
1293 self.snapshot.clone()
1294 }
1295
1296 pub fn settings(&self) -> WorktreeSettings {
1297 self.settings.clone()
1298 }
1299
1300 fn load_binary_file(
1301 &self,
1302 path: &RelPath,
1303 cx: &Context<Worktree>,
1304 ) -> Task<Result<LoadedBinaryFile>> {
1305 let path = Arc::from(path);
1306 let abs_path = self.absolutize(&path);
1307 let fs = self.fs.clone();
1308 let entry = self.refresh_entry(path.clone(), None, cx);
1309 let is_private = self.is_path_private(&path);
1310
1311 let worktree = cx.weak_entity();
1312 cx.background_spawn(async move {
1313 let content = fs.load_bytes(&abs_path).await?;
1314
1315 let worktree = worktree.upgrade().context("worktree was dropped")?;
1316 let file = match entry.await? {
1317 Some(entry) => File::for_entry(entry, worktree),
1318 None => {
1319 let metadata = fs
1320 .metadata(&abs_path)
1321 .await
1322 .with_context(|| {
1323 format!("Loading metadata for excluded file {abs_path:?}")
1324 })?
1325 .with_context(|| {
1326 format!("Excluded file {abs_path:?} got removed during loading")
1327 })?;
1328 Arc::new(File {
1329 entry_id: None,
1330 worktree,
1331 path,
1332 disk_state: DiskState::Present {
1333 mtime: metadata.mtime,
1334 },
1335 is_local: true,
1336 is_private,
1337 })
1338 }
1339 };
1340
1341 Ok(LoadedBinaryFile { file, content })
1342 })
1343 }
1344
1345 fn load_file(&self, path: &RelPath, cx: &Context<Worktree>) -> Task<Result<LoadedFile>> {
1346 let path = Arc::from(path);
1347 let abs_path = self.absolutize(&path);
1348 let fs = self.fs.clone();
1349 let entry = self.refresh_entry(path.clone(), None, cx);
1350 let is_private = self.is_path_private(path.as_ref());
1351
1352 let this = cx.weak_entity();
1353 cx.background_spawn(async move {
1354 // WARN: Temporary workaround for #27283.
1355 // We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file
1356 // Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file
1357 // if it is too large
1358 // 5GB seems to be more reasonable, peaking at ~16GB, while 6GB jumps up to >24GB which seems like a
1359 // reasonable limit
1360 {
1361 const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB
1362 if let Ok(Some(metadata)) = fs.metadata(&abs_path).await
1363 && metadata.len >= FILE_SIZE_MAX
1364 {
1365 anyhow::bail!("File is too large to load");
1366 }
1367 }
1368 let (text, encoding, has_bom) = decode_file_text(fs.as_ref(), &abs_path).await?;
1369
1370 let worktree = this.upgrade().context("worktree was dropped")?;
1371 let file = match entry.await? {
1372 Some(entry) => File::for_entry(entry, worktree),
1373 None => {
1374 let metadata = fs
1375 .metadata(&abs_path)
1376 .await
1377 .with_context(|| {
1378 format!("Loading metadata for excluded file {abs_path:?}")
1379 })?
1380 .with_context(|| {
1381 format!("Excluded file {abs_path:?} got removed during loading")
1382 })?;
1383 Arc::new(File {
1384 entry_id: None,
1385 worktree,
1386 path,
1387 disk_state: DiskState::Present {
1388 mtime: metadata.mtime,
1389 },
1390 is_local: true,
1391 is_private,
1392 })
1393 }
1394 };
1395
1396 Ok(LoadedFile {
1397 file,
1398 text,
1399 encoding,
1400 has_bom,
1401 })
1402 })
1403 }
1404
1405 /// Find the lowest path in the worktree's datastructures that is an ancestor
1406 fn lowest_ancestor(&self, path: &RelPath) -> Arc<RelPath> {
1407 let mut lowest_ancestor = None;
1408 for path in path.ancestors() {
1409 if self.entry_for_path(path).is_some() {
1410 lowest_ancestor = Some(path.into());
1411 break;
1412 }
1413 }
1414
1415 lowest_ancestor.unwrap_or_else(|| RelPath::empty().into())
1416 }
1417
1418 fn create_entry(
1419 &self,
1420 path: Arc<RelPath>,
1421 is_dir: bool,
1422 content: Option<Vec<u8>>,
1423 cx: &Context<Worktree>,
1424 ) -> Task<Result<CreatedEntry>> {
1425 let abs_path = self.absolutize(&path);
1426 let path_excluded = self.settings.is_path_excluded(&path);
1427 let fs = self.fs.clone();
1428 let task_abs_path = abs_path.clone();
1429 let write = cx.background_spawn(async move {
1430 if is_dir {
1431 fs.create_dir(&task_abs_path)
1432 .await
1433 .with_context(|| format!("creating directory {task_abs_path:?}"))
1434 } else {
1435 fs.write(&task_abs_path, content.as_deref().unwrap_or(&[]))
1436 .await
1437 .with_context(|| format!("creating file {task_abs_path:?}"))
1438 }
1439 });
1440
1441 let lowest_ancestor = self.lowest_ancestor(&path);
1442 cx.spawn(async move |this, cx| {
1443 write.await?;
1444 if path_excluded {
1445 return Ok(CreatedEntry::Excluded { abs_path });
1446 }
1447
1448 let (result, refreshes) = this.update(cx, |this, cx| {
1449 let mut refreshes = Vec::new();
1450 let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap();
1451 for refresh_path in refresh_paths.ancestors() {
1452 if refresh_path == RelPath::empty() {
1453 continue;
1454 }
1455 let refresh_full_path = lowest_ancestor.join(refresh_path);
1456
1457 refreshes.push(this.as_local_mut().unwrap().refresh_entry(
1458 refresh_full_path,
1459 None,
1460 cx,
1461 ));
1462 }
1463 (
1464 this.as_local_mut().unwrap().refresh_entry(path, None, cx),
1465 refreshes,
1466 )
1467 })?;
1468 for refresh in refreshes {
1469 refresh.await.log_err();
1470 }
1471
1472 Ok(result
1473 .await?
1474 .map(CreatedEntry::Included)
1475 .unwrap_or_else(|| CreatedEntry::Excluded { abs_path }))
1476 })
1477 }
1478
1479 fn write_file(
1480 &self,
1481 path: Arc<RelPath>,
1482 text: Rope,
1483 line_ending: LineEnding,
1484 encoding: &'static Encoding,
1485 has_bom: bool,
1486 cx: &Context<Worktree>,
1487 ) -> Task<Result<Arc<File>>> {
1488 let fs = self.fs.clone();
1489 let is_private = self.is_path_private(&path);
1490 let abs_path = self.absolutize(&path);
1491
1492 let write = cx.background_spawn({
1493 let fs = fs.clone();
1494 let abs_path = abs_path.clone();
1495 async move {
1496 // For UTF-8, use the optimized `fs.save` which writes Rope chunks directly to disk
1497 // without allocating a contiguous string.
1498 if encoding == encoding_rs::UTF_8 && !has_bom {
1499 return fs.save(&abs_path, &text, line_ending).await;
1500 }
1501
1502 // For legacy encodings (e.g. Shift-JIS), we fall back to converting the entire Rope
1503 // to a String/Bytes in memory before writing.
1504 //
1505 // Note: This is inefficient for very large files compared to the streaming approach above,
1506 // but supporting streaming writes for arbitrary encodings would require a significant
1507 // refactor of the `fs` crate to expose a Writer interface.
1508 let text_string = text.to_string();
1509 let normalized_text = match line_ending {
1510 LineEnding::Unix => text_string,
1511 LineEnding::Windows => text_string.replace('\n', "\r\n"),
1512 };
1513
1514 // Create the byte vector manually for UTF-16 encodings because encoding_rs encodes to UTF-8 by default (per WHATWG standards),
1515 // which is not what we want for saving files.
1516 let bytes = if encoding == encoding_rs::UTF_16BE {
1517 let mut data = Vec::with_capacity(normalized_text.len() * 2 + 2);
1518 if has_bom {
1519 data.extend_from_slice(&[0xFE, 0xFF]); // BOM
1520 }
1521 let utf16be_bytes =
1522 normalized_text.encode_utf16().flat_map(|u| u.to_be_bytes());
1523 data.extend(utf16be_bytes);
1524 data.into()
1525 } else if encoding == encoding_rs::UTF_16LE {
1526 let mut data = Vec::with_capacity(normalized_text.len() * 2 + 2);
1527 if has_bom {
1528 data.extend_from_slice(&[0xFF, 0xFE]); // BOM
1529 }
1530 let utf16le_bytes =
1531 normalized_text.encode_utf16().flat_map(|u| u.to_le_bytes());
1532 data.extend(utf16le_bytes);
1533 data.into()
1534 } else {
1535 // For other encodings (Shift-JIS, UTF-8 with BOM, etc.), delegate to encoding_rs.
1536 let bom_bytes = if has_bom {
1537 if encoding == encoding_rs::UTF_8 {
1538 vec![0xEF, 0xBB, 0xBF]
1539 } else {
1540 vec![]
1541 }
1542 } else {
1543 vec![]
1544 };
1545 let (cow, _, _) = encoding.encode(&normalized_text);
1546 if !bom_bytes.is_empty() {
1547 let mut bytes = bom_bytes;
1548 bytes.extend_from_slice(&cow);
1549 bytes.into()
1550 } else {
1551 cow
1552 }
1553 };
1554
1555 fs.write(&abs_path, &bytes).await
1556 }
1557 });
1558
1559 cx.spawn(async move |this, cx| {
1560 write.await?;
1561 let entry = this
1562 .update(cx, |this, cx| {
1563 this.as_local_mut()
1564 .unwrap()
1565 .refresh_entry(path.clone(), None, cx)
1566 })?
1567 .await?;
1568 let worktree = this.upgrade().context("worktree dropped")?;
1569 if let Some(entry) = entry {
1570 Ok(File::for_entry(entry, worktree))
1571 } else {
1572 let metadata = fs
1573 .metadata(&abs_path)
1574 .await
1575 .with_context(|| {
1576 format!("Fetching metadata after saving the excluded buffer {abs_path:?}")
1577 })?
1578 .with_context(|| {
1579 format!("Excluded buffer {path:?} got removed during saving")
1580 })?;
1581 Ok(Arc::new(File {
1582 worktree,
1583 path,
1584 disk_state: DiskState::Present {
1585 mtime: metadata.mtime,
1586 },
1587 entry_id: None,
1588 is_local: true,
1589 is_private,
1590 }))
1591 }
1592 })
1593 }
1594
1595 fn delete_entry(
1596 &self,
1597 entry_id: ProjectEntryId,
1598 trash: bool,
1599 cx: &Context<Worktree>,
1600 ) -> Option<Task<Result<()>>> {
1601 let entry = self.entry_for_id(entry_id)?.clone();
1602 let abs_path = self.absolutize(&entry.path);
1603 let fs = self.fs.clone();
1604
1605 let delete = cx.background_spawn(async move {
1606 if entry.is_file() {
1607 if trash {
1608 fs.trash_file(&abs_path, Default::default()).await?;
1609 } else {
1610 fs.remove_file(&abs_path, Default::default()).await?;
1611 }
1612 } else if trash {
1613 fs.trash_dir(
1614 &abs_path,
1615 RemoveOptions {
1616 recursive: true,
1617 ignore_if_not_exists: false,
1618 },
1619 )
1620 .await?;
1621 } else {
1622 fs.remove_dir(
1623 &abs_path,
1624 RemoveOptions {
1625 recursive: true,
1626 ignore_if_not_exists: false,
1627 },
1628 )
1629 .await?;
1630 }
1631 anyhow::Ok(entry.path)
1632 });
1633
1634 Some(cx.spawn(async move |this, cx| {
1635 let path = delete.await?;
1636 this.update(cx, |this, _| {
1637 this.as_local_mut()
1638 .unwrap()
1639 .refresh_entries_for_paths(vec![path])
1640 })?
1641 .recv()
1642 .await;
1643 Ok(())
1644 }))
1645 }
1646
1647 pub fn copy_external_entries(
1648 &self,
1649 target_directory: Arc<RelPath>,
1650 paths: Vec<Arc<Path>>,
1651 cx: &Context<Worktree>,
1652 ) -> Task<Result<Vec<ProjectEntryId>>> {
1653 let target_directory = self.absolutize(&target_directory);
1654 let worktree_path = self.abs_path().clone();
1655 let fs = self.fs.clone();
1656 let paths = paths
1657 .into_iter()
1658 .filter_map(|source| {
1659 let file_name = source.file_name()?;
1660 let mut target = target_directory.clone();
1661 target.push(file_name);
1662
1663 // Do not allow copying the same file to itself.
1664 if source.as_ref() != target.as_path() {
1665 Some((source, target))
1666 } else {
1667 None
1668 }
1669 })
1670 .collect::<Vec<_>>();
1671
1672 let paths_to_refresh = paths
1673 .iter()
1674 .filter_map(|(_, target)| {
1675 RelPath::new(
1676 target.strip_prefix(&worktree_path).ok()?,
1677 PathStyle::local(),
1678 )
1679 .ok()
1680 .map(|path| path.into_arc())
1681 })
1682 .collect::<Vec<_>>();
1683
1684 cx.spawn(async move |this, cx| {
1685 cx.background_spawn(async move {
1686 for (source, target) in paths {
1687 copy_recursive(
1688 fs.as_ref(),
1689 &source,
1690 &target,
1691 fs::CopyOptions {
1692 overwrite: true,
1693 ..Default::default()
1694 },
1695 )
1696 .await
1697 .with_context(|| {
1698 format!("Failed to copy file from {source:?} to {target:?}")
1699 })?;
1700 }
1701 anyhow::Ok(())
1702 })
1703 .await
1704 .log_err();
1705 let mut refresh = cx.read_entity(
1706 &this.upgrade().with_context(|| "Dropped worktree")?,
1707 |this, _| {
1708 anyhow::Ok::<postage::barrier::Receiver>(
1709 this.as_local()
1710 .with_context(|| "Worktree is not local")?
1711 .refresh_entries_for_paths(paths_to_refresh.clone()),
1712 )
1713 },
1714 )?;
1715
1716 cx.background_spawn(async move {
1717 refresh.next().await;
1718 anyhow::Ok(())
1719 })
1720 .await
1721 .log_err();
1722
1723 let this = this.upgrade().with_context(|| "Dropped worktree")?;
1724 Ok(cx.read_entity(&this, |this, _| {
1725 paths_to_refresh
1726 .iter()
1727 .filter_map(|path| Some(this.entry_for_path(path)?.id))
1728 .collect()
1729 }))
1730 })
1731 }
1732
1733 fn expand_entry(
1734 &self,
1735 entry_id: ProjectEntryId,
1736 cx: &Context<Worktree>,
1737 ) -> Option<Task<Result<()>>> {
1738 let path = self.entry_for_id(entry_id)?.path.clone();
1739 let mut refresh = self.refresh_entries_for_paths(vec![path]);
1740 Some(cx.background_spawn(async move {
1741 refresh.next().await;
1742 Ok(())
1743 }))
1744 }
1745
1746 fn expand_all_for_entry(
1747 &self,
1748 entry_id: ProjectEntryId,
1749 cx: &Context<Worktree>,
1750 ) -> Option<Task<Result<()>>> {
1751 let path = self.entry_for_id(entry_id).unwrap().path.clone();
1752 let mut rx = self.add_path_prefix_to_scan(path);
1753 Some(cx.background_spawn(async move {
1754 rx.next().await;
1755 Ok(())
1756 }))
1757 }
1758
1759 pub fn refresh_entries_for_paths(&self, paths: Vec<Arc<RelPath>>) -> barrier::Receiver {
1760 let (tx, rx) = barrier::channel();
1761 self.scan_requests_tx
1762 .try_send(ScanRequest {
1763 relative_paths: paths,
1764 done: smallvec![tx],
1765 })
1766 .ok();
1767 rx
1768 }
1769
1770 #[cfg(feature = "test-support")]
1771 pub fn manually_refresh_entries_for_paths(
1772 &self,
1773 paths: Vec<Arc<RelPath>>,
1774 ) -> barrier::Receiver {
1775 self.refresh_entries_for_paths(paths)
1776 }
1777
1778 pub fn add_path_prefix_to_scan(&self, path_prefix: Arc<RelPath>) -> barrier::Receiver {
1779 let (tx, rx) = barrier::channel();
1780 self.path_prefixes_to_scan_tx
1781 .try_send(PathPrefixScanRequest {
1782 path: path_prefix,
1783 done: smallvec![tx],
1784 })
1785 .ok();
1786 rx
1787 }
1788
1789 pub fn refresh_entry(
1790 &self,
1791 path: Arc<RelPath>,
1792 old_path: Option<Arc<RelPath>>,
1793 cx: &Context<Worktree>,
1794 ) -> Task<Result<Option<Entry>>> {
1795 if self.settings.is_path_excluded(&path) {
1796 return Task::ready(Ok(None));
1797 }
1798 let paths = if let Some(old_path) = old_path.as_ref() {
1799 vec![old_path.clone(), path.clone()]
1800 } else {
1801 vec![path.clone()]
1802 };
1803 let t0 = Instant::now();
1804 let mut refresh = self.refresh_entries_for_paths(paths);
1805 // todo(lw): Hot foreground spawn
1806 cx.spawn(async move |this, cx| {
1807 refresh.recv().await;
1808 log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed());
1809 let new_entry = this.read_with(cx, |this, _| {
1810 this.entry_for_path(&path).cloned().with_context(|| {
1811 format!("Could not find entry in worktree for {path:?} after refresh")
1812 })
1813 })??;
1814 Ok(Some(new_entry))
1815 })
1816 }
1817
1818 fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
1819 where
1820 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
1821 Fut: 'static + Send + Future<Output = bool>,
1822 {
1823 if let Some(observer) = self.update_observer.as_mut() {
1824 *observer.resume_updates.borrow_mut() = ();
1825 return;
1826 }
1827
1828 let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>();
1829 let (snapshots_tx, mut snapshots_rx) =
1830 mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet)>();
1831 snapshots_tx
1832 .unbounded_send((self.snapshot(), Arc::default()))
1833 .ok();
1834
1835 let worktree_id = cx.entity_id().as_u64();
1836 let _maintain_remote_snapshot = cx.background_spawn(async move {
1837 let mut is_first = true;
1838 while let Some((snapshot, entry_changes)) = snapshots_rx.next().await {
1839 let update = if is_first {
1840 is_first = false;
1841 snapshot.build_initial_update(project_id, worktree_id)
1842 } else {
1843 snapshot.build_update(project_id, worktree_id, entry_changes)
1844 };
1845
1846 for update in proto::split_worktree_update(update) {
1847 let _ = resume_updates_rx.try_recv();
1848 loop {
1849 let result = callback(update.clone());
1850 if result.await {
1851 break;
1852 } else {
1853 log::info!("waiting to resume updates");
1854 if resume_updates_rx.next().await.is_none() {
1855 return Some(());
1856 }
1857 }
1858 }
1859 }
1860 }
1861 Some(())
1862 });
1863
1864 self.update_observer = Some(UpdateObservationState {
1865 snapshots_tx,
1866 resume_updates: resume_updates_tx,
1867 _maintain_remote_snapshot,
1868 });
1869 }
1870
1871 pub fn share_private_files(&mut self, cx: &Context<Worktree>) {
1872 self.share_private_files = true;
1873 self.restart_background_scanners(cx);
1874 }
1875
1876 pub fn update_abs_path_and_refresh(
1877 &mut self,
1878 new_path: Arc<SanitizedPath>,
1879 cx: &Context<Worktree>,
1880 ) {
1881 self.snapshot.git_repositories = Default::default();
1882 self.snapshot.ignores_by_parent_abs_path = Default::default();
1883 let root_name = new_path
1884 .as_path()
1885 .file_name()
1886 .and_then(|f| f.to_str())
1887 .map_or(RelPath::empty().into(), |f| {
1888 RelPath::unix(f).unwrap().into()
1889 });
1890 self.snapshot.update_abs_path(new_path, root_name);
1891 self.restart_background_scanners(cx);
1892 }
1893}
1894
1895impl RemoteWorktree {
1896 pub fn project_id(&self) -> u64 {
1897 self.project_id
1898 }
1899
1900 pub fn client(&self) -> AnyProtoClient {
1901 self.client.clone()
1902 }
1903
1904 pub fn disconnected_from_host(&mut self) {
1905 self.updates_tx.take();
1906 self.snapshot_subscriptions.clear();
1907 self.disconnected = true;
1908 }
1909
1910 pub fn update_from_remote(&self, update: proto::UpdateWorktree) {
1911 if let Some(updates_tx) = &self.updates_tx {
1912 updates_tx
1913 .unbounded_send(update)
1914 .expect("consumer runs to completion");
1915 }
1916 }
1917
1918 fn observe_updates<F, Fut>(&mut self, project_id: u64, cx: &Context<Worktree>, callback: F)
1919 where
1920 F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut,
1921 Fut: 'static + Send + Future<Output = bool>,
1922 {
1923 let (tx, mut rx) = mpsc::unbounded();
1924 let initial_update = self
1925 .snapshot
1926 .build_initial_update(project_id, self.id().to_proto());
1927 self.update_observer = Some(tx);
1928 cx.spawn(async move |this, cx| {
1929 let mut update = initial_update;
1930 'outer: loop {
1931 // SSH projects use a special project ID of 0, and we need to
1932 // remap it to the correct one here.
1933 update.project_id = project_id;
1934
1935 for chunk in split_worktree_update(update) {
1936 if !callback(chunk).await {
1937 break 'outer;
1938 }
1939 }
1940
1941 if let Some(next_update) = rx.next().await {
1942 update = next_update;
1943 } else {
1944 break;
1945 }
1946 }
1947 this.update(cx, |this, _| {
1948 let this = this.as_remote_mut().unwrap();
1949 this.update_observer.take();
1950 })
1951 })
1952 .detach();
1953 }
1954
1955 fn observed_snapshot(&self, scan_id: usize) -> bool {
1956 self.completed_scan_id >= scan_id
1957 }
1958
1959 pub fn wait_for_snapshot(
1960 &mut self,
1961 scan_id: usize,
1962 ) -> impl Future<Output = Result<()>> + use<> {
1963 let (tx, rx) = oneshot::channel();
1964 if self.observed_snapshot(scan_id) {
1965 let _ = tx.send(());
1966 } else if self.disconnected {
1967 drop(tx);
1968 } else {
1969 match self
1970 .snapshot_subscriptions
1971 .binary_search_by_key(&scan_id, |probe| probe.0)
1972 {
1973 Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)),
1974 }
1975 }
1976
1977 async move {
1978 rx.await?;
1979 Ok(())
1980 }
1981 }
1982
1983 pub fn insert_entry(
1984 &mut self,
1985 entry: proto::Entry,
1986 scan_id: usize,
1987 cx: &Context<Worktree>,
1988 ) -> Task<Result<Entry>> {
1989 let wait_for_snapshot = self.wait_for_snapshot(scan_id);
1990 cx.spawn(async move |this, cx| {
1991 wait_for_snapshot.await?;
1992 this.update(cx, |worktree, _| {
1993 let worktree = worktree.as_remote_mut().unwrap();
1994 let snapshot = &mut worktree.background_snapshot.lock().0;
1995 let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions);
1996 worktree.snapshot = snapshot.clone();
1997 entry
1998 })?
1999 })
2000 }
2001
2002 fn delete_entry(
2003 &self,
2004 entry_id: ProjectEntryId,
2005 trash: bool,
2006 cx: &Context<Worktree>,
2007 ) -> Option<Task<Result<()>>> {
2008 let response = self.client.request(proto::DeleteProjectEntry {
2009 project_id: self.project_id,
2010 entry_id: entry_id.to_proto(),
2011 use_trash: trash,
2012 });
2013 Some(cx.spawn(async move |this, cx| {
2014 let response = response.await?;
2015 let scan_id = response.worktree_scan_id as usize;
2016
2017 this.update(cx, move |this, _| {
2018 this.as_remote_mut().unwrap().wait_for_snapshot(scan_id)
2019 })?
2020 .await?;
2021
2022 this.update(cx, |this, _| {
2023 let this = this.as_remote_mut().unwrap();
2024 let snapshot = &mut this.background_snapshot.lock().0;
2025 snapshot.delete_entry(entry_id);
2026 this.snapshot = snapshot.clone();
2027 })
2028 }))
2029 }
2030
2031 // fn rename_entry(
2032 // &self,
2033 // entry_id: ProjectEntryId,
2034 // new_path: impl Into<Arc<RelPath>>,
2035 // cx: &Context<Worktree>,
2036 // ) -> Task<Result<CreatedEntry>> {
2037 // let new_path: Arc<RelPath> = new_path.into();
2038 // let response = self.client.request(proto::RenameProjectEntry {
2039 // project_id: self.project_id,
2040 // entry_id: entry_id.to_proto(),
2041 // new_worktree_id: new_path.worktree_id,
2042 // new_path: new_path.as_ref().to_proto(),
2043 // });
2044 // cx.spawn(async move |this, cx| {
2045 // let response = response.await?;
2046 // match response.entry {
2047 // Some(entry) => this
2048 // .update(cx, |this, cx| {
2049 // this.as_remote_mut().unwrap().insert_entry(
2050 // entry,
2051 // response.worktree_scan_id as usize,
2052 // cx,
2053 // )
2054 // })?
2055 // .await
2056 // .map(CreatedEntry::Included),
2057 // None => {
2058 // let abs_path =
2059 // this.read_with(cx, |worktree, _| worktree.absolutize(&new_path))?;
2060 // Ok(CreatedEntry::Excluded { abs_path })
2061 // }
2062 // }
2063 // })
2064 // }
2065
2066 fn copy_external_entries(
2067 &self,
2068 target_directory: Arc<RelPath>,
2069 paths_to_copy: Vec<Arc<Path>>,
2070 local_fs: Arc<dyn Fs>,
2071 cx: &Context<Worktree>,
2072 ) -> Task<anyhow::Result<Vec<ProjectEntryId>>> {
2073 let client = self.client.clone();
2074 let worktree_id = self.id().to_proto();
2075 let project_id = self.project_id;
2076
2077 cx.background_spawn(async move {
2078 let mut requests = Vec::new();
2079 for root_path_to_copy in paths_to_copy {
2080 let Some(filename) = root_path_to_copy
2081 .file_name()
2082 .and_then(|name| name.to_str())
2083 .and_then(|filename| RelPath::unix(filename).ok())
2084 else {
2085 continue;
2086 };
2087 for (abs_path, is_directory) in
2088 read_dir_items(local_fs.as_ref(), &root_path_to_copy).await?
2089 {
2090 let Some(relative_path) = abs_path
2091 .strip_prefix(&root_path_to_copy)
2092 .map_err(|e| anyhow::Error::from(e))
2093 .and_then(|relative_path| RelPath::new(relative_path, PathStyle::local()))
2094 .log_err()
2095 else {
2096 continue;
2097 };
2098 let content = if is_directory {
2099 None
2100 } else {
2101 Some(local_fs.load_bytes(&abs_path).await?)
2102 };
2103
2104 let mut target_path = target_directory.join(filename);
2105 if relative_path.file_name().is_some() {
2106 target_path = target_path.join(&relative_path);
2107 }
2108
2109 requests.push(proto::CreateProjectEntry {
2110 project_id,
2111 worktree_id,
2112 path: target_path.to_proto(),
2113 is_directory,
2114 content,
2115 });
2116 }
2117 }
2118 requests.sort_unstable_by(|a, b| a.path.cmp(&b.path));
2119 requests.dedup();
2120
2121 let mut copied_entry_ids = Vec::new();
2122 for request in requests {
2123 let response = client.request(request).await?;
2124 copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id)));
2125 }
2126
2127 Ok(copied_entry_ids)
2128 })
2129 }
2130}
2131
2132impl Snapshot {
2133 pub fn new(
2134 id: u64,
2135 root_name: Arc<RelPath>,
2136 abs_path: Arc<Path>,
2137 path_style: PathStyle,
2138 ) -> Self {
2139 Snapshot {
2140 id: WorktreeId::from_usize(id as usize),
2141 abs_path: SanitizedPath::from_arc(abs_path),
2142 path_style,
2143 root_char_bag: root_name
2144 .as_unix_str()
2145 .chars()
2146 .map(|c| c.to_ascii_lowercase())
2147 .collect(),
2148 root_name,
2149 always_included_entries: Default::default(),
2150 entries_by_path: Default::default(),
2151 entries_by_id: Default::default(),
2152 scan_id: 1,
2153 completed_scan_id: 0,
2154 }
2155 }
2156
2157 pub fn id(&self) -> WorktreeId {
2158 self.id
2159 }
2160
2161 // TODO:
2162 // Consider the following:
2163 //
2164 // ```rust
2165 // let abs_path: Arc<Path> = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project"
2166 // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs");
2167 // // The caller perform some actions here:
2168 // some_non_trimmed_path.strip_prefix(abs_path); // This fails
2169 // some_non_trimmed_path.starts_with(abs_path); // This fails too
2170 // ```
2171 //
2172 // This is definitely a bug, but it's not clear if we should handle it here or not.
2173 pub fn abs_path(&self) -> &Arc<Path> {
2174 SanitizedPath::cast_arc_ref(&self.abs_path)
2175 }
2176
2177 fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree {
2178 let mut updated_entries = self
2179 .entries_by_path
2180 .iter()
2181 .map(proto::Entry::from)
2182 .collect::<Vec<_>>();
2183 updated_entries.sort_unstable_by_key(|e| e.id);
2184
2185 proto::UpdateWorktree {
2186 project_id,
2187 worktree_id,
2188 abs_path: self.abs_path().to_string_lossy().into_owned(),
2189 root_name: self.root_name().to_proto(),
2190 updated_entries,
2191 removed_entries: Vec::new(),
2192 scan_id: self.scan_id as u64,
2193 is_last_update: self.completed_scan_id == self.scan_id,
2194 // Sent in separate messages.
2195 updated_repositories: Vec::new(),
2196 removed_repositories: Vec::new(),
2197 }
2198 }
2199
2200 pub fn work_directory_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf {
2201 match work_directory {
2202 WorkDirectory::InProject { relative_path } => self.absolutize(relative_path),
2203 WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(),
2204 }
2205 }
2206
2207 pub fn absolutize(&self, path: &RelPath) -> PathBuf {
2208 if path.file_name().is_some() {
2209 let mut abs_path = self.abs_path.to_string();
2210 for component in path.components() {
2211 if !abs_path.ends_with(self.path_style.primary_separator()) {
2212 abs_path.push_str(self.path_style.primary_separator());
2213 }
2214 abs_path.push_str(component);
2215 }
2216 PathBuf::from(abs_path)
2217 } else {
2218 self.abs_path.as_path().to_path_buf()
2219 }
2220 }
2221
2222 pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool {
2223 self.entries_by_id.get(&entry_id, ()).is_some()
2224 }
2225
2226 fn insert_entry(
2227 &mut self,
2228 entry: proto::Entry,
2229 always_included_paths: &PathMatcher,
2230 ) -> Result<Entry> {
2231 let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?;
2232 let old_entry = self.entries_by_id.insert_or_replace(
2233 PathEntry {
2234 id: entry.id,
2235 path: entry.path.clone(),
2236 is_ignored: entry.is_ignored,
2237 scan_id: 0,
2238 },
2239 (),
2240 );
2241 if let Some(old_entry) = old_entry {
2242 self.entries_by_path.remove(&PathKey(old_entry.path), ());
2243 }
2244 self.entries_by_path.insert_or_replace(entry.clone(), ());
2245 Ok(entry)
2246 }
2247
2248 fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option<Arc<RelPath>> {
2249 let removed_entry = self.entries_by_id.remove(&entry_id, ())?;
2250 self.entries_by_path = {
2251 let mut cursor = self.entries_by_path.cursor::<TraversalProgress>(());
2252 let mut new_entries_by_path =
2253 cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left);
2254 while let Some(entry) = cursor.item() {
2255 if entry.path.starts_with(&removed_entry.path) {
2256 self.entries_by_id.remove(&entry.id, ());
2257 cursor.next();
2258 } else {
2259 break;
2260 }
2261 }
2262 new_entries_by_path.append(cursor.suffix(), ());
2263 new_entries_by_path
2264 };
2265
2266 Some(removed_entry.path)
2267 }
2268
2269 fn update_abs_path(&mut self, abs_path: Arc<SanitizedPath>, root_name: Arc<RelPath>) {
2270 self.abs_path = abs_path;
2271 if root_name != self.root_name {
2272 self.root_char_bag = root_name
2273 .as_unix_str()
2274 .chars()
2275 .map(|c| c.to_ascii_lowercase())
2276 .collect();
2277 self.root_name = root_name;
2278 }
2279 }
2280
2281 fn apply_remote_update(
2282 &mut self,
2283 update: proto::UpdateWorktree,
2284 always_included_paths: &PathMatcher,
2285 ) {
2286 log::debug!(
2287 "applying remote worktree update. {} entries updated, {} removed",
2288 update.updated_entries.len(),
2289 update.removed_entries.len()
2290 );
2291 if let Some(root_name) = RelPath::from_proto(&update.root_name).log_err() {
2292 self.update_abs_path(
2293 SanitizedPath::new_arc(&Path::new(&update.abs_path)),
2294 root_name,
2295 );
2296 }
2297
2298 let mut entries_by_path_edits = Vec::new();
2299 let mut entries_by_id_edits = Vec::new();
2300
2301 for entry_id in update.removed_entries {
2302 let entry_id = ProjectEntryId::from_proto(entry_id);
2303 entries_by_id_edits.push(Edit::Remove(entry_id));
2304 if let Some(entry) = self.entry_for_id(entry_id) {
2305 entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone())));
2306 }
2307 }
2308
2309 for entry in update.updated_entries {
2310 let Some(entry) =
2311 Entry::try_from((&self.root_char_bag, always_included_paths, entry)).log_err()
2312 else {
2313 continue;
2314 };
2315 if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, ()) {
2316 entries_by_path_edits.push(Edit::Remove(PathKey(path.clone())));
2317 }
2318 if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ())
2319 && old_entry.id != entry.id
2320 {
2321 entries_by_id_edits.push(Edit::Remove(old_entry.id));
2322 }
2323 entries_by_id_edits.push(Edit::Insert(PathEntry {
2324 id: entry.id,
2325 path: entry.path.clone(),
2326 is_ignored: entry.is_ignored,
2327 scan_id: 0,
2328 }));
2329 entries_by_path_edits.push(Edit::Insert(entry));
2330 }
2331
2332 self.entries_by_path.edit(entries_by_path_edits, ());
2333 self.entries_by_id.edit(entries_by_id_edits, ());
2334
2335 self.scan_id = update.scan_id as usize;
2336 if update.is_last_update {
2337 self.completed_scan_id = update.scan_id as usize;
2338 }
2339 }
2340
2341 pub fn entry_count(&self) -> usize {
2342 self.entries_by_path.summary().count
2343 }
2344
2345 pub fn visible_entry_count(&self) -> usize {
2346 self.entries_by_path.summary().non_ignored_count
2347 }
2348
2349 pub fn dir_count(&self) -> usize {
2350 let summary = self.entries_by_path.summary();
2351 summary.count - summary.file_count
2352 }
2353
2354 pub fn visible_dir_count(&self) -> usize {
2355 let summary = self.entries_by_path.summary();
2356 summary.non_ignored_count - summary.non_ignored_file_count
2357 }
2358
2359 pub fn file_count(&self) -> usize {
2360 self.entries_by_path.summary().file_count
2361 }
2362
2363 pub fn visible_file_count(&self) -> usize {
2364 self.entries_by_path.summary().non_ignored_file_count
2365 }
2366
2367 fn traverse_from_offset(
2368 &self,
2369 include_files: bool,
2370 include_dirs: bool,
2371 include_ignored: bool,
2372 start_offset: usize,
2373 ) -> Traversal<'_> {
2374 let mut cursor = self.entries_by_path.cursor(());
2375 cursor.seek(
2376 &TraversalTarget::Count {
2377 count: start_offset,
2378 include_files,
2379 include_dirs,
2380 include_ignored,
2381 },
2382 Bias::Right,
2383 );
2384 Traversal {
2385 snapshot: self,
2386 cursor,
2387 include_files,
2388 include_dirs,
2389 include_ignored,
2390 }
2391 }
2392
2393 pub fn traverse_from_path(
2394 &self,
2395 include_files: bool,
2396 include_dirs: bool,
2397 include_ignored: bool,
2398 path: &RelPath,
2399 ) -> Traversal<'_> {
2400 Traversal::new(self, include_files, include_dirs, include_ignored, path)
2401 }
2402
2403 pub fn files(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2404 self.traverse_from_offset(true, false, include_ignored, start)
2405 }
2406
2407 pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2408 self.traverse_from_offset(false, true, include_ignored, start)
2409 }
2410
2411 pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal<'_> {
2412 self.traverse_from_offset(true, true, include_ignored, start)
2413 }
2414
2415 pub fn paths(&self) -> impl Iterator<Item = &RelPath> {
2416 self.entries_by_path
2417 .cursor::<()>(())
2418 .filter(move |entry| !entry.path.is_empty())
2419 .map(|entry| entry.path.as_ref())
2420 }
2421
2422 pub fn child_entries<'a>(&'a self, parent_path: &'a RelPath) -> ChildEntriesIter<'a> {
2423 let options = ChildEntriesOptions {
2424 include_files: true,
2425 include_dirs: true,
2426 include_ignored: true,
2427 };
2428 self.child_entries_with_options(parent_path, options)
2429 }
2430
2431 pub fn child_entries_with_options<'a>(
2432 &'a self,
2433 parent_path: &'a RelPath,
2434 options: ChildEntriesOptions,
2435 ) -> ChildEntriesIter<'a> {
2436 let mut cursor = self.entries_by_path.cursor(());
2437 cursor.seek(&TraversalTarget::path(parent_path), Bias::Right);
2438 let traversal = Traversal {
2439 snapshot: self,
2440 cursor,
2441 include_files: options.include_files,
2442 include_dirs: options.include_dirs,
2443 include_ignored: options.include_ignored,
2444 };
2445 ChildEntriesIter {
2446 traversal,
2447 parent_path,
2448 }
2449 }
2450
2451 pub fn root_entry(&self) -> Option<&Entry> {
2452 self.entries_by_path.first()
2453 }
2454
2455 /// Returns `None` for a single file worktree, or `Some(self.abs_path())` if
2456 /// it is a directory.
2457 pub fn root_dir(&self) -> Option<Arc<Path>> {
2458 self.root_entry()
2459 .filter(|entry| entry.is_dir())
2460 .map(|_| self.abs_path().clone())
2461 }
2462
2463 pub fn root_name(&self) -> &RelPath {
2464 &self.root_name
2465 }
2466
2467 pub fn root_name_str(&self) -> &str {
2468 self.root_name.as_unix_str()
2469 }
2470
2471 pub fn scan_id(&self) -> usize {
2472 self.scan_id
2473 }
2474
2475 pub fn entry_for_path(&self, path: &RelPath) -> Option<&Entry> {
2476 self.traverse_from_path(true, true, true, path)
2477 .entry()
2478 .and_then(|entry| {
2479 if entry.path.as_ref() == path {
2480 Some(entry)
2481 } else {
2482 None
2483 }
2484 })
2485 }
2486
2487 /// Resolves a path to an executable using the following heuristics:
2488 ///
2489 /// 1. If the path starts with `~`, it is expanded to the user's home directory.
2490 /// 2. If the path is relative and contains more than one component,
2491 /// it is joined to the worktree root path.
2492 /// 3. If the path is relative and exists in the worktree
2493 /// (even if falls under an exclusion filter),
2494 /// it is joined to the worktree root path.
2495 /// 4. Otherwise the path is returned unmodified.
2496 ///
2497 /// Relative paths that do not exist in the worktree may
2498 /// still be found using the `PATH` environment variable.
2499 pub fn resolve_executable_path(&self, path: PathBuf) -> PathBuf {
2500 if let Some(path_str) = path.to_str() {
2501 if let Some(remaining_path) = path_str.strip_prefix("~/") {
2502 return home_dir().join(remaining_path);
2503 } else if path_str == "~" {
2504 return home_dir().to_path_buf();
2505 }
2506 }
2507
2508 if let Ok(rel_path) = RelPath::new(&path, self.path_style)
2509 && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some())
2510 {
2511 self.abs_path().join(path)
2512 } else {
2513 path
2514 }
2515 }
2516
2517 pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> {
2518 let entry = self.entries_by_id.get(&id, ())?;
2519 self.entry_for_path(&entry.path)
2520 }
2521
2522 pub fn path_style(&self) -> PathStyle {
2523 self.path_style
2524 }
2525}
2526
2527impl LocalSnapshot {
2528 fn local_repo_for_work_directory_path(&self, path: &RelPath) -> Option<&LocalRepositoryEntry> {
2529 self.git_repositories
2530 .iter()
2531 .map(|(_, entry)| entry)
2532 .find(|entry| entry.work_directory.path_key() == PathKey(path.into()))
2533 }
2534
2535 fn build_update(
2536 &self,
2537 project_id: u64,
2538 worktree_id: u64,
2539 entry_changes: UpdatedEntriesSet,
2540 ) -> proto::UpdateWorktree {
2541 let mut updated_entries = Vec::new();
2542 let mut removed_entries = Vec::new();
2543
2544 for (_, entry_id, path_change) in entry_changes.iter() {
2545 if let PathChange::Removed = path_change {
2546 removed_entries.push(entry_id.0 as u64);
2547 } else if let Some(entry) = self.entry_for_id(*entry_id) {
2548 updated_entries.push(proto::Entry::from(entry));
2549 }
2550 }
2551
2552 removed_entries.sort_unstable();
2553 updated_entries.sort_unstable_by_key(|e| e.id);
2554
2555 // TODO - optimize, knowing that removed_entries are sorted.
2556 removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err());
2557
2558 proto::UpdateWorktree {
2559 project_id,
2560 worktree_id,
2561 abs_path: self.abs_path().to_string_lossy().into_owned(),
2562 root_name: self.root_name().to_proto(),
2563 updated_entries,
2564 removed_entries,
2565 scan_id: self.scan_id as u64,
2566 is_last_update: self.completed_scan_id == self.scan_id,
2567 // Sent in separate messages.
2568 updated_repositories: Vec::new(),
2569 removed_repositories: Vec::new(),
2570 }
2571 }
2572
2573 async fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry {
2574 log::trace!("insert entry {:?}", entry.path);
2575 if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) {
2576 let abs_path = self.absolutize(&entry.path);
2577 match build_gitignore(&abs_path, fs).await {
2578 Ok(ignore) => {
2579 self.ignores_by_parent_abs_path
2580 .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true));
2581 }
2582 Err(error) => {
2583 log::error!(
2584 "error loading .gitignore file {:?} - {:?}",
2585 &entry.path,
2586 error
2587 );
2588 }
2589 }
2590 }
2591
2592 if entry.kind == EntryKind::PendingDir
2593 && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ())
2594 {
2595 entry.kind = existing_entry.kind;
2596 }
2597
2598 let scan_id = self.scan_id;
2599 let removed = self.entries_by_path.insert_or_replace(entry.clone(), ());
2600 if let Some(removed) = removed
2601 && removed.id != entry.id
2602 {
2603 self.entries_by_id.remove(&removed.id, ());
2604 }
2605 self.entries_by_id.insert_or_replace(
2606 PathEntry {
2607 id: entry.id,
2608 path: entry.path.clone(),
2609 is_ignored: entry.is_ignored,
2610 scan_id,
2611 },
2612 (),
2613 );
2614
2615 entry
2616 }
2617
2618 fn ancestor_inodes_for_path(&self, path: &RelPath) -> TreeSet<u64> {
2619 let mut inodes = TreeSet::default();
2620 for ancestor in path.ancestors().skip(1) {
2621 if let Some(entry) = self.entry_for_path(ancestor) {
2622 inodes.insert(entry.inode);
2623 }
2624 }
2625 inodes
2626 }
2627
2628 async fn ignore_stack_for_abs_path(
2629 &self,
2630 abs_path: &Path,
2631 is_dir: bool,
2632 fs: &dyn Fs,
2633 ) -> IgnoreStack {
2634 let mut new_ignores = Vec::new();
2635 let mut repo_root = None;
2636 for (index, ancestor) in abs_path.ancestors().enumerate() {
2637 if index > 0 {
2638 if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) {
2639 new_ignores.push((ancestor, Some(ignore.clone())));
2640 } else {
2641 new_ignores.push((ancestor, None));
2642 }
2643 }
2644
2645 let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten();
2646 if metadata.is_some() {
2647 repo_root = Some(Arc::from(ancestor));
2648 break;
2649 }
2650 }
2651
2652 let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() {
2653 IgnoreStack::global(global_gitignore)
2654 } else {
2655 IgnoreStack::none()
2656 };
2657
2658 if let Some((repo_exclude, _)) = repo_root
2659 .as_ref()
2660 .and_then(|abs_path| self.repo_exclude_by_work_dir_abs_path.get(abs_path))
2661 {
2662 ignore_stack = ignore_stack.append(IgnoreKind::RepoExclude, repo_exclude.clone());
2663 }
2664 ignore_stack.repo_root = repo_root;
2665 for (parent_abs_path, ignore) in new_ignores.into_iter().rev() {
2666 if ignore_stack.is_abs_path_ignored(parent_abs_path, true) {
2667 ignore_stack = IgnoreStack::all();
2668 break;
2669 } else if let Some(ignore) = ignore {
2670 ignore_stack =
2671 ignore_stack.append(IgnoreKind::Gitignore(parent_abs_path.into()), ignore);
2672 }
2673 }
2674
2675 if ignore_stack.is_abs_path_ignored(abs_path, is_dir) {
2676 ignore_stack = IgnoreStack::all();
2677 }
2678
2679 ignore_stack
2680 }
2681
2682 #[cfg(test)]
2683 fn expanded_entries(&self) -> impl Iterator<Item = &Entry> {
2684 self.entries_by_path
2685 .cursor::<()>(())
2686 .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored))
2687 }
2688
2689 #[cfg(test)]
2690 pub fn check_invariants(&self, git_state: bool) {
2691 use pretty_assertions::assert_eq;
2692
2693 assert_eq!(
2694 self.entries_by_path
2695 .cursor::<()>(())
2696 .map(|e| (&e.path, e.id))
2697 .collect::<Vec<_>>(),
2698 self.entries_by_id
2699 .cursor::<()>(())
2700 .map(|e| (&e.path, e.id))
2701 .collect::<collections::BTreeSet<_>>()
2702 .into_iter()
2703 .collect::<Vec<_>>(),
2704 "entries_by_path and entries_by_id are inconsistent"
2705 );
2706
2707 let mut files = self.files(true, 0);
2708 let mut visible_files = self.files(false, 0);
2709 for entry in self.entries_by_path.cursor::<()>(()) {
2710 if entry.is_file() {
2711 assert_eq!(files.next().unwrap().inode, entry.inode);
2712 if (!entry.is_ignored && !entry.is_external) || entry.is_always_included {
2713 assert_eq!(visible_files.next().unwrap().inode, entry.inode);
2714 }
2715 }
2716 }
2717
2718 assert!(files.next().is_none());
2719 assert!(visible_files.next().is_none());
2720
2721 let mut bfs_paths = Vec::new();
2722 let mut stack = self
2723 .root_entry()
2724 .map(|e| e.path.as_ref())
2725 .into_iter()
2726 .collect::<Vec<_>>();
2727 while let Some(path) = stack.pop() {
2728 bfs_paths.push(path);
2729 let ix = stack.len();
2730 for child_entry in self.child_entries(path) {
2731 stack.insert(ix, &child_entry.path);
2732 }
2733 }
2734
2735 let dfs_paths_via_iter = self
2736 .entries_by_path
2737 .cursor::<()>(())
2738 .map(|e| e.path.as_ref())
2739 .collect::<Vec<_>>();
2740 assert_eq!(bfs_paths, dfs_paths_via_iter);
2741
2742 let dfs_paths_via_traversal = self
2743 .entries(true, 0)
2744 .map(|e| e.path.as_ref())
2745 .collect::<Vec<_>>();
2746
2747 assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter);
2748
2749 if git_state {
2750 for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() {
2751 let ignore_parent_path = &RelPath::new(
2752 ignore_parent_abs_path
2753 .strip_prefix(self.abs_path.as_path())
2754 .unwrap(),
2755 PathStyle::local(),
2756 )
2757 .unwrap();
2758 assert!(self.entry_for_path(ignore_parent_path).is_some());
2759 assert!(
2760 self.entry_for_path(
2761 &ignore_parent_path.join(RelPath::unix(GITIGNORE).unwrap())
2762 )
2763 .is_some()
2764 );
2765 }
2766 }
2767 }
2768
2769 #[cfg(test)]
2770 pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&RelPath, u64, bool)> {
2771 let mut paths = Vec::new();
2772 for entry in self.entries_by_path.cursor::<()>(()) {
2773 if include_ignored || !entry.is_ignored {
2774 paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored));
2775 }
2776 }
2777 paths.sort_by(|a, b| a.0.cmp(b.0));
2778 paths
2779 }
2780}
2781
2782impl BackgroundScannerState {
2783 fn should_scan_directory(&self, entry: &Entry) -> bool {
2784 (self.scanning_enabled && !entry.is_external && (!entry.is_ignored || entry.is_always_included))
2785 || entry.path.file_name() == Some(DOT_GIT)
2786 || entry.path.file_name() == Some(local_settings_folder_name())
2787 || entry.path.file_name() == Some(local_vscode_folder_name())
2788 || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning
2789 || self
2790 .paths_to_scan
2791 .iter()
2792 .any(|p| p.starts_with(&entry.path))
2793 || self
2794 .path_prefixes_to_scan
2795 .iter()
2796 .any(|p| entry.path.starts_with(p))
2797 }
2798
2799 async fn enqueue_scan_dir(
2800 &self,
2801 abs_path: Arc<Path>,
2802 entry: &Entry,
2803 scan_job_tx: &Sender<ScanJob>,
2804 fs: &dyn Fs,
2805 ) {
2806 let path = entry.path.clone();
2807 let ignore_stack = self
2808 .snapshot
2809 .ignore_stack_for_abs_path(&abs_path, true, fs)
2810 .await;
2811 let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
2812
2813 if !ancestor_inodes.contains(&entry.inode) {
2814 ancestor_inodes.insert(entry.inode);
2815 scan_job_tx
2816 .try_send(ScanJob {
2817 abs_path,
2818 path,
2819 ignore_stack,
2820 scan_queue: scan_job_tx.clone(),
2821 ancestor_inodes,
2822 is_external: entry.is_external,
2823 })
2824 .unwrap();
2825 }
2826 }
2827
2828 fn reuse_entry_id(&mut self, entry: &mut Entry) {
2829 if let Some(mtime) = entry.mtime {
2830 // If an entry with the same inode was removed from the worktree during this scan,
2831 // then it *might* represent the same file or directory. But the OS might also have
2832 // re-used the inode for a completely different file or directory.
2833 //
2834 // Conditionally reuse the old entry's id:
2835 // * if the mtime is the same, the file was probably been renamed.
2836 // * if the path is the same, the file may just have been updated
2837 if let Some(removed_entry) = self.removed_entries.remove(&entry.inode) {
2838 if removed_entry.mtime == Some(mtime) || removed_entry.path == entry.path {
2839 entry.id = removed_entry.id;
2840 }
2841 } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) {
2842 entry.id = existing_entry.id;
2843 }
2844 }
2845 }
2846
2847 fn entry_id_for(
2848 &mut self,
2849 next_entry_id: &AtomicUsize,
2850 path: &RelPath,
2851 metadata: &fs::Metadata,
2852 ) -> ProjectEntryId {
2853 // If an entry with the same inode was removed from the worktree during this scan,
2854 // then it *might* represent the same file or directory. But the OS might also have
2855 // re-used the inode for a completely different file or directory.
2856 //
2857 // Conditionally reuse the old entry's id:
2858 // * if the mtime is the same, the file was probably been renamed.
2859 // * if the path is the same, the file may just have been updated
2860 if let Some(removed_entry) = self.removed_entries.remove(&metadata.inode) {
2861 if removed_entry.mtime == Some(metadata.mtime) || *removed_entry.path == *path {
2862 return removed_entry.id;
2863 }
2864 } else if let Some(existing_entry) = self.snapshot.entry_for_path(path) {
2865 return existing_entry.id;
2866 }
2867 ProjectEntryId::new(next_entry_id)
2868 }
2869
2870 async fn insert_entry(&mut self, entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry {
2871 let entry = self.snapshot.insert_entry(entry, fs).await;
2872 if entry.path.file_name() == Some(&DOT_GIT) {
2873 self.insert_git_repository(entry.path.clone(), fs, watcher)
2874 .await;
2875 }
2876
2877 #[cfg(test)]
2878 self.snapshot.check_invariants(false);
2879
2880 entry
2881 }
2882
2883 fn populate_dir(
2884 &mut self,
2885 parent_path: Arc<RelPath>,
2886 entries: impl IntoIterator<Item = Entry>,
2887 ignore: Option<Arc<Gitignore>>,
2888 ) {
2889 let mut parent_entry = if let Some(parent_entry) = self
2890 .snapshot
2891 .entries_by_path
2892 .get(&PathKey(parent_path.clone()), ())
2893 {
2894 parent_entry.clone()
2895 } else {
2896 log::warn!(
2897 "populating a directory {:?} that has been removed",
2898 parent_path
2899 );
2900 return;
2901 };
2902
2903 match parent_entry.kind {
2904 EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir,
2905 EntryKind::Dir => {}
2906 _ => return,
2907 }
2908
2909 if let Some(ignore) = ignore {
2910 let abs_parent_path = self
2911 .snapshot
2912 .abs_path
2913 .as_path()
2914 .join(parent_path.as_std_path())
2915 .into();
2916 self.snapshot
2917 .ignores_by_parent_abs_path
2918 .insert(abs_parent_path, (ignore, false));
2919 }
2920
2921 let parent_entry_id = parent_entry.id;
2922 self.scanned_dirs.insert(parent_entry_id);
2923 let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)];
2924 let mut entries_by_id_edits = Vec::new();
2925
2926 for entry in entries {
2927 entries_by_id_edits.push(Edit::Insert(PathEntry {
2928 id: entry.id,
2929 path: entry.path.clone(),
2930 is_ignored: entry.is_ignored,
2931 scan_id: self.snapshot.scan_id,
2932 }));
2933 entries_by_path_edits.push(Edit::Insert(entry));
2934 }
2935
2936 self.snapshot
2937 .entries_by_path
2938 .edit(entries_by_path_edits, ());
2939 self.snapshot.entries_by_id.edit(entries_by_id_edits, ());
2940
2941 if let Err(ix) = self.changed_paths.binary_search(&parent_path) {
2942 self.changed_paths.insert(ix, parent_path.clone());
2943 }
2944
2945 #[cfg(test)]
2946 self.snapshot.check_invariants(false);
2947 }
2948
2949 fn remove_path(&mut self, path: &RelPath) {
2950 log::trace!("background scanner removing path {path:?}");
2951 let mut new_entries;
2952 let removed_entries;
2953 {
2954 let mut cursor = self
2955 .snapshot
2956 .entries_by_path
2957 .cursor::<TraversalProgress>(());
2958 new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left);
2959 removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left);
2960 new_entries.append(cursor.suffix(), ());
2961 }
2962 self.snapshot.entries_by_path = new_entries;
2963
2964 let mut removed_ids = Vec::with_capacity(removed_entries.summary().count);
2965 for entry in removed_entries.cursor::<()>(()) {
2966 match self.removed_entries.entry(entry.inode) {
2967 hash_map::Entry::Occupied(mut e) => {
2968 let prev_removed_entry = e.get_mut();
2969 if entry.id > prev_removed_entry.id {
2970 *prev_removed_entry = entry.clone();
2971 }
2972 }
2973 hash_map::Entry::Vacant(e) => {
2974 e.insert(entry.clone());
2975 }
2976 }
2977
2978 if entry.path.file_name() == Some(GITIGNORE) {
2979 let abs_parent_path = self.snapshot.absolutize(&entry.path.parent().unwrap());
2980 if let Some((_, needs_update)) = self
2981 .snapshot
2982 .ignores_by_parent_abs_path
2983 .get_mut(abs_parent_path.as_path())
2984 {
2985 *needs_update = true;
2986 }
2987 }
2988
2989 if let Err(ix) = removed_ids.binary_search(&entry.id) {
2990 removed_ids.insert(ix, entry.id);
2991 }
2992 }
2993
2994 self.snapshot
2995 .entries_by_id
2996 .edit(removed_ids.iter().map(|&id| Edit::Remove(id)).collect(), ());
2997 self.snapshot
2998 .git_repositories
2999 .retain(|id, _| removed_ids.binary_search(id).is_err());
3000
3001 #[cfg(test)]
3002 self.snapshot.check_invariants(false);
3003 }
3004
3005 async fn insert_git_repository(
3006 &mut self,
3007 dot_git_path: Arc<RelPath>,
3008 fs: &dyn Fs,
3009 watcher: &dyn Watcher,
3010 ) {
3011 let work_dir_path: Arc<RelPath> = match dot_git_path.parent() {
3012 Some(parent_dir) => {
3013 // Guard against repositories inside the repository metadata
3014 if parent_dir
3015 .components()
3016 .any(|component| component == DOT_GIT)
3017 {
3018 log::debug!(
3019 "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}"
3020 );
3021 return;
3022 };
3023
3024 parent_dir.into()
3025 }
3026 None => {
3027 // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself,
3028 // no files inside that directory are tracked by git, so no need to build the repo around it
3029 log::debug!(
3030 "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}"
3031 );
3032 return;
3033 }
3034 };
3035
3036 let dot_git_abs_path = Arc::from(self.snapshot.absolutize(&dot_git_path).as_ref());
3037
3038 self.insert_git_repository_for_path(
3039 WorkDirectory::InProject {
3040 relative_path: work_dir_path,
3041 },
3042 dot_git_abs_path,
3043 fs,
3044 watcher,
3045 )
3046 .await
3047 .log_err();
3048 }
3049
3050 async fn insert_git_repository_for_path(
3051 &mut self,
3052 work_directory: WorkDirectory,
3053 dot_git_abs_path: Arc<Path>,
3054 fs: &dyn Fs,
3055 watcher: &dyn Watcher,
3056 ) -> Result<LocalRepositoryEntry> {
3057 let work_dir_entry = self
3058 .snapshot
3059 .entry_for_path(&work_directory.path_key().0)
3060 .with_context(|| {
3061 format!(
3062 "working directory `{}` not indexed",
3063 work_directory
3064 .path_key()
3065 .0
3066 .display(self.snapshot.path_style)
3067 )
3068 })?;
3069 let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory);
3070
3071 let (repository_dir_abs_path, common_dir_abs_path) =
3072 discover_git_paths(&dot_git_abs_path, fs).await;
3073 watcher
3074 .add(&common_dir_abs_path)
3075 .context("failed to add common directory to watcher")
3076 .log_err();
3077 if !repository_dir_abs_path.starts_with(&common_dir_abs_path) {
3078 watcher
3079 .add(&repository_dir_abs_path)
3080 .context("failed to add repository directory to watcher")
3081 .log_err();
3082 }
3083
3084 let work_directory_id = work_dir_entry.id;
3085
3086 let local_repository = LocalRepositoryEntry {
3087 work_directory_id,
3088 work_directory,
3089 work_directory_abs_path: work_directory_abs_path.as_path().into(),
3090 git_dir_scan_id: 0,
3091 dot_git_abs_path,
3092 common_dir_abs_path,
3093 repository_dir_abs_path,
3094 };
3095
3096 self.snapshot
3097 .git_repositories
3098 .insert(work_directory_id, local_repository.clone());
3099
3100 log::trace!("inserting new local git repository");
3101 Ok(local_repository)
3102 }
3103}
3104
3105async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool {
3106 if let Some(file_name) = path.file_name()
3107 && file_name == DOT_GIT
3108 {
3109 return true;
3110 }
3111
3112 // If we're in a bare repository, we are not inside a `.git` folder. In a
3113 // bare repository, the root folder contains what would normally be in the
3114 // `.git` folder.
3115 let head_metadata = fs.metadata(&path.join("HEAD")).await;
3116 if !matches!(head_metadata, Ok(Some(_))) {
3117 return false;
3118 }
3119 let config_metadata = fs.metadata(&path.join("config")).await;
3120 matches!(config_metadata, Ok(Some(_)))
3121}
3122
3123async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result<Gitignore> {
3124 let contents = fs
3125 .load(abs_path)
3126 .await
3127 .with_context(|| format!("failed to load gitignore file at {}", abs_path.display()))?;
3128 let parent = abs_path.parent().unwrap_or_else(|| Path::new("/"));
3129 let mut builder = GitignoreBuilder::new(parent);
3130 for line in contents.lines() {
3131 builder.add_line(Some(abs_path.into()), line)?;
3132 }
3133 Ok(builder.build()?)
3134}
3135
3136impl Deref for Worktree {
3137 type Target = Snapshot;
3138
3139 fn deref(&self) -> &Self::Target {
3140 match self {
3141 Worktree::Local(worktree) => &worktree.snapshot,
3142 Worktree::Remote(worktree) => &worktree.snapshot,
3143 }
3144 }
3145}
3146
3147impl Deref for LocalWorktree {
3148 type Target = LocalSnapshot;
3149
3150 fn deref(&self) -> &Self::Target {
3151 &self.snapshot
3152 }
3153}
3154
3155impl Deref for RemoteWorktree {
3156 type Target = Snapshot;
3157
3158 fn deref(&self) -> &Self::Target {
3159 &self.snapshot
3160 }
3161}
3162
3163impl fmt::Debug for LocalWorktree {
3164 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3165 self.snapshot.fmt(f)
3166 }
3167}
3168
3169impl fmt::Debug for Snapshot {
3170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3171 struct EntriesById<'a>(&'a SumTree<PathEntry>);
3172 struct EntriesByPath<'a>(&'a SumTree<Entry>);
3173
3174 impl fmt::Debug for EntriesByPath<'_> {
3175 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3176 f.debug_map()
3177 .entries(self.0.iter().map(|entry| (&entry.path, entry.id)))
3178 .finish()
3179 }
3180 }
3181
3182 impl fmt::Debug for EntriesById<'_> {
3183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
3184 f.debug_list().entries(self.0.iter()).finish()
3185 }
3186 }
3187
3188 f.debug_struct("Snapshot")
3189 .field("id", &self.id)
3190 .field("root_name", &self.root_name)
3191 .field("entries_by_path", &EntriesByPath(&self.entries_by_path))
3192 .field("entries_by_id", &EntriesById(&self.entries_by_id))
3193 .finish()
3194 }
3195}
3196
3197#[derive(Debug, Clone, PartialEq)]
3198pub struct File {
3199 pub worktree: Entity<Worktree>,
3200 pub path: Arc<RelPath>,
3201 pub disk_state: DiskState,
3202 pub entry_id: Option<ProjectEntryId>,
3203 pub is_local: bool,
3204 pub is_private: bool,
3205}
3206
3207impl language::File for File {
3208 fn as_local(&self) -> Option<&dyn language::LocalFile> {
3209 if self.is_local { Some(self) } else { None }
3210 }
3211
3212 fn disk_state(&self) -> DiskState {
3213 self.disk_state
3214 }
3215
3216 fn path(&self) -> &Arc<RelPath> {
3217 &self.path
3218 }
3219
3220 fn full_path(&self, cx: &App) -> PathBuf {
3221 self.worktree.read(cx).full_path(&self.path)
3222 }
3223
3224 /// Returns the last component of this handle's absolute path. If this handle refers to the root
3225 /// of its worktree, then this method will return the name of the worktree itself.
3226 fn file_name<'a>(&'a self, cx: &'a App) -> &'a str {
3227 self.path
3228 .file_name()
3229 .unwrap_or_else(|| self.worktree.read(cx).root_name_str())
3230 }
3231
3232 fn worktree_id(&self, cx: &App) -> WorktreeId {
3233 self.worktree.read(cx).id()
3234 }
3235
3236 fn to_proto(&self, cx: &App) -> rpc::proto::File {
3237 rpc::proto::File {
3238 worktree_id: self.worktree.read(cx).id().to_proto(),
3239 entry_id: self.entry_id.map(|id| id.to_proto()),
3240 path: self.path.as_ref().to_proto(),
3241 mtime: self.disk_state.mtime().map(|time| time.into()),
3242 is_deleted: self.disk_state.is_deleted(),
3243 is_historic: matches!(self.disk_state, DiskState::Historic { .. }),
3244 }
3245 }
3246
3247 fn is_private(&self) -> bool {
3248 self.is_private
3249 }
3250
3251 fn path_style(&self, cx: &App) -> PathStyle {
3252 self.worktree.read(cx).path_style()
3253 }
3254
3255 fn can_open(&self) -> bool {
3256 true
3257 }
3258}
3259
3260impl language::LocalFile for File {
3261 fn abs_path(&self, cx: &App) -> PathBuf {
3262 self.worktree.read(cx).absolutize(&self.path)
3263 }
3264
3265 fn load(&self, cx: &App) -> Task<Result<String>> {
3266 let worktree = self.worktree.read(cx).as_local().unwrap();
3267 let abs_path = worktree.absolutize(&self.path);
3268 let fs = worktree.fs.clone();
3269 cx.background_spawn(async move { fs.load(&abs_path).await })
3270 }
3271
3272 fn load_bytes(&self, cx: &App) -> Task<Result<Vec<u8>>> {
3273 let worktree = self.worktree.read(cx).as_local().unwrap();
3274 let abs_path = worktree.absolutize(&self.path);
3275 let fs = worktree.fs.clone();
3276 cx.background_spawn(async move { fs.load_bytes(&abs_path).await })
3277 }
3278}
3279
3280impl File {
3281 pub fn for_entry(entry: Entry, worktree: Entity<Worktree>) -> Arc<Self> {
3282 Arc::new(Self {
3283 worktree,
3284 path: entry.path.clone(),
3285 disk_state: if let Some(mtime) = entry.mtime {
3286 DiskState::Present { mtime }
3287 } else {
3288 DiskState::New
3289 },
3290 entry_id: Some(entry.id),
3291 is_local: true,
3292 is_private: entry.is_private,
3293 })
3294 }
3295
3296 pub fn from_proto(
3297 proto: rpc::proto::File,
3298 worktree: Entity<Worktree>,
3299 cx: &App,
3300 ) -> Result<Self> {
3301 let worktree_id = worktree.read(cx).as_remote().context("not remote")?.id();
3302
3303 anyhow::ensure!(
3304 worktree_id.to_proto() == proto.worktree_id,
3305 "worktree id does not match file"
3306 );
3307
3308 let disk_state = if proto.is_historic {
3309 DiskState::Historic {
3310 was_deleted: proto.is_deleted,
3311 }
3312 } else if proto.is_deleted {
3313 DiskState::Deleted
3314 } else if let Some(mtime) = proto.mtime.map(&Into::into) {
3315 DiskState::Present { mtime }
3316 } else {
3317 DiskState::New
3318 };
3319
3320 Ok(Self {
3321 worktree,
3322 path: RelPath::from_proto(&proto.path).context("invalid path in file protobuf")?,
3323 disk_state,
3324 entry_id: proto.entry_id.map(ProjectEntryId::from_proto),
3325 is_local: false,
3326 is_private: false,
3327 })
3328 }
3329
3330 pub fn from_dyn(file: Option<&Arc<dyn language::File>>) -> Option<&Self> {
3331 file.and_then(|f| {
3332 let f: &dyn language::File = f.borrow();
3333 let f: &dyn Any = f;
3334 f.downcast_ref()
3335 })
3336 }
3337
3338 pub fn worktree_id(&self, cx: &App) -> WorktreeId {
3339 self.worktree.read(cx).id()
3340 }
3341
3342 pub fn project_entry_id(&self) -> Option<ProjectEntryId> {
3343 match self.disk_state {
3344 DiskState::Deleted => None,
3345 _ => self.entry_id,
3346 }
3347 }
3348}
3349
3350#[derive(Clone, Debug, PartialEq, Eq)]
3351pub struct Entry {
3352 pub id: ProjectEntryId,
3353 pub kind: EntryKind,
3354 pub path: Arc<RelPath>,
3355 pub inode: u64,
3356 pub mtime: Option<MTime>,
3357
3358 pub canonical_path: Option<Arc<Path>>,
3359 /// Whether this entry is ignored by Git.
3360 ///
3361 /// We only scan ignored entries once the directory is expanded and
3362 /// exclude them from searches.
3363 pub is_ignored: bool,
3364
3365 /// Whether this entry is hidden or inside hidden directory.
3366 ///
3367 /// We only scan hidden entries once the directory is expanded.
3368 pub is_hidden: bool,
3369
3370 /// Whether this entry is always included in searches.
3371 ///
3372 /// This is used for entries that are always included in searches, even
3373 /// if they are ignored by git. Overridden by file_scan_exclusions.
3374 pub is_always_included: bool,
3375
3376 /// Whether this entry's canonical path is outside of the worktree.
3377 /// This means the entry is only accessible from the worktree root via a
3378 /// symlink.
3379 ///
3380 /// We only scan entries outside of the worktree once the symlinked
3381 /// directory is expanded. External entries are treated like gitignored
3382 /// entries in that they are not included in searches.
3383 pub is_external: bool,
3384
3385 /// Whether this entry is considered to be a `.env` file.
3386 pub is_private: bool,
3387 /// The entry's size on disk, in bytes.
3388 pub size: u64,
3389 pub char_bag: CharBag,
3390 pub is_fifo: bool,
3391}
3392
3393#[derive(Clone, Copy, Debug, PartialEq, Eq)]
3394pub enum EntryKind {
3395 UnloadedDir,
3396 PendingDir,
3397 Dir,
3398 File,
3399}
3400
3401#[derive(Clone, Copy, Debug, PartialEq)]
3402pub enum PathChange {
3403 /// A filesystem entry was was created.
3404 Added,
3405 /// A filesystem entry was removed.
3406 Removed,
3407 /// A filesystem entry was updated.
3408 Updated,
3409 /// A filesystem entry was either updated or added. We don't know
3410 /// whether or not it already existed, because the path had not
3411 /// been loaded before the event.
3412 AddedOrUpdated,
3413 /// A filesystem entry was found during the initial scan of the worktree.
3414 Loaded,
3415}
3416
3417#[derive(Clone, Debug, PartialEq, Eq)]
3418pub struct UpdatedGitRepository {
3419 /// ID of the repository's working directory.
3420 ///
3421 /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique.
3422 /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed.
3423 pub work_directory_id: ProjectEntryId,
3424 pub old_work_directory_abs_path: Option<Arc<Path>>,
3425 pub new_work_directory_abs_path: Option<Arc<Path>>,
3426 /// For a normal git repository checkout, the absolute path to the .git directory.
3427 /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory.
3428 pub dot_git_abs_path: Option<Arc<Path>>,
3429 pub repository_dir_abs_path: Option<Arc<Path>>,
3430 pub common_dir_abs_path: Option<Arc<Path>>,
3431}
3432
3433pub type UpdatedEntriesSet = Arc<[(Arc<RelPath>, ProjectEntryId, PathChange)]>;
3434pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>;
3435
3436#[derive(Clone, Debug)]
3437pub struct PathProgress<'a> {
3438 pub max_path: &'a RelPath,
3439}
3440
3441#[derive(Clone, Debug)]
3442pub struct PathSummary<S> {
3443 pub max_path: Arc<RelPath>,
3444 pub item_summary: S,
3445}
3446
3447impl<S: Summary> Summary for PathSummary<S> {
3448 type Context<'a> = S::Context<'a>;
3449
3450 fn zero(cx: Self::Context<'_>) -> Self {
3451 Self {
3452 max_path: RelPath::empty().into(),
3453 item_summary: S::zero(cx),
3454 }
3455 }
3456
3457 fn add_summary(&mut self, rhs: &Self, cx: Self::Context<'_>) {
3458 self.max_path = rhs.max_path.clone();
3459 self.item_summary.add_summary(&rhs.item_summary, cx);
3460 }
3461}
3462
3463impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathProgress<'a> {
3464 fn zero(_: <PathSummary<S> as Summary>::Context<'_>) -> Self {
3465 Self {
3466 max_path: RelPath::empty(),
3467 }
3468 }
3469
3470 fn add_summary(
3471 &mut self,
3472 summary: &'a PathSummary<S>,
3473 _: <PathSummary<S> as Summary>::Context<'_>,
3474 ) {
3475 self.max_path = summary.max_path.as_ref()
3476 }
3477}
3478
3479impl<'a> sum_tree::Dimension<'a, PathSummary<GitSummary>> for GitSummary {
3480 fn zero(_cx: ()) -> Self {
3481 Default::default()
3482 }
3483
3484 fn add_summary(&mut self, summary: &'a PathSummary<GitSummary>, _: ()) {
3485 *self += summary.item_summary
3486 }
3487}
3488
3489impl<'a>
3490 sum_tree::SeekTarget<'a, PathSummary<GitSummary>, Dimensions<TraversalProgress<'a>, GitSummary>>
3491 for PathTarget<'_>
3492{
3493 fn cmp(
3494 &self,
3495 cursor_location: &Dimensions<TraversalProgress<'a>, GitSummary>,
3496 _: (),
3497 ) -> Ordering {
3498 self.cmp_path(cursor_location.0.max_path)
3499 }
3500}
3501
3502impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for PathKey {
3503 fn zero(_: S::Context<'_>) -> Self {
3504 Default::default()
3505 }
3506
3507 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: S::Context<'_>) {
3508 self.0 = summary.max_path.clone();
3509 }
3510}
3511
3512impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary<S>> for TraversalProgress<'a> {
3513 fn zero(_cx: S::Context<'_>) -> Self {
3514 Default::default()
3515 }
3516
3517 fn add_summary(&mut self, summary: &'a PathSummary<S>, _: S::Context<'_>) {
3518 self.max_path = summary.max_path.as_ref();
3519 }
3520}
3521
3522impl Entry {
3523 fn new(
3524 path: Arc<RelPath>,
3525 metadata: &fs::Metadata,
3526 id: ProjectEntryId,
3527 root_char_bag: CharBag,
3528 canonical_path: Option<Arc<Path>>,
3529 ) -> Self {
3530 let char_bag = char_bag_for_path(root_char_bag, &path);
3531 Self {
3532 id,
3533 kind: if metadata.is_dir {
3534 EntryKind::PendingDir
3535 } else {
3536 EntryKind::File
3537 },
3538 path,
3539 inode: metadata.inode,
3540 mtime: Some(metadata.mtime),
3541 size: metadata.len,
3542 canonical_path,
3543 is_ignored: false,
3544 is_hidden: false,
3545 is_always_included: false,
3546 is_external: false,
3547 is_private: false,
3548 char_bag,
3549 is_fifo: metadata.is_fifo,
3550 }
3551 }
3552
3553 pub fn is_created(&self) -> bool {
3554 self.mtime.is_some()
3555 }
3556
3557 pub fn is_dir(&self) -> bool {
3558 self.kind.is_dir()
3559 }
3560
3561 pub fn is_file(&self) -> bool {
3562 self.kind.is_file()
3563 }
3564}
3565
3566impl EntryKind {
3567 pub fn is_dir(&self) -> bool {
3568 matches!(
3569 self,
3570 EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir
3571 )
3572 }
3573
3574 pub fn is_unloaded(&self) -> bool {
3575 matches!(self, EntryKind::UnloadedDir)
3576 }
3577
3578 pub fn is_file(&self) -> bool {
3579 matches!(self, EntryKind::File)
3580 }
3581}
3582
3583impl sum_tree::Item for Entry {
3584 type Summary = EntrySummary;
3585
3586 fn summary(&self, _cx: ()) -> Self::Summary {
3587 let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included
3588 {
3589 0
3590 } else {
3591 1
3592 };
3593 let file_count;
3594 let non_ignored_file_count;
3595 if self.is_file() {
3596 file_count = 1;
3597 non_ignored_file_count = non_ignored_count;
3598 } else {
3599 file_count = 0;
3600 non_ignored_file_count = 0;
3601 }
3602
3603 EntrySummary {
3604 max_path: self.path.clone(),
3605 count: 1,
3606 non_ignored_count,
3607 file_count,
3608 non_ignored_file_count,
3609 }
3610 }
3611}
3612
3613impl sum_tree::KeyedItem for Entry {
3614 type Key = PathKey;
3615
3616 fn key(&self) -> Self::Key {
3617 PathKey(self.path.clone())
3618 }
3619}
3620
3621#[derive(Clone, Debug)]
3622pub struct EntrySummary {
3623 max_path: Arc<RelPath>,
3624 count: usize,
3625 non_ignored_count: usize,
3626 file_count: usize,
3627 non_ignored_file_count: usize,
3628}
3629
3630impl Default for EntrySummary {
3631 fn default() -> Self {
3632 Self {
3633 max_path: Arc::from(RelPath::empty()),
3634 count: 0,
3635 non_ignored_count: 0,
3636 file_count: 0,
3637 non_ignored_file_count: 0,
3638 }
3639 }
3640}
3641
3642impl sum_tree::ContextLessSummary for EntrySummary {
3643 fn zero() -> Self {
3644 Default::default()
3645 }
3646
3647 fn add_summary(&mut self, rhs: &Self) {
3648 self.max_path = rhs.max_path.clone();
3649 self.count += rhs.count;
3650 self.non_ignored_count += rhs.non_ignored_count;
3651 self.file_count += rhs.file_count;
3652 self.non_ignored_file_count += rhs.non_ignored_file_count;
3653 }
3654}
3655
3656#[derive(Clone, Debug)]
3657struct PathEntry {
3658 id: ProjectEntryId,
3659 path: Arc<RelPath>,
3660 is_ignored: bool,
3661 scan_id: usize,
3662}
3663
3664impl sum_tree::Item for PathEntry {
3665 type Summary = PathEntrySummary;
3666
3667 fn summary(&self, _cx: ()) -> Self::Summary {
3668 PathEntrySummary { max_id: self.id }
3669 }
3670}
3671
3672impl sum_tree::KeyedItem for PathEntry {
3673 type Key = ProjectEntryId;
3674
3675 fn key(&self) -> Self::Key {
3676 self.id
3677 }
3678}
3679
3680#[derive(Clone, Debug, Default)]
3681struct PathEntrySummary {
3682 max_id: ProjectEntryId,
3683}
3684
3685impl sum_tree::ContextLessSummary for PathEntrySummary {
3686 fn zero() -> Self {
3687 Default::default()
3688 }
3689
3690 fn add_summary(&mut self, summary: &Self) {
3691 self.max_id = summary.max_id;
3692 }
3693}
3694
3695impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId {
3696 fn zero(_cx: ()) -> Self {
3697 Default::default()
3698 }
3699
3700 fn add_summary(&mut self, summary: &'a PathEntrySummary, _: ()) {
3701 *self = summary.max_id;
3702 }
3703}
3704
3705#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
3706pub struct PathKey(pub Arc<RelPath>);
3707
3708impl Default for PathKey {
3709 fn default() -> Self {
3710 Self(RelPath::empty().into())
3711 }
3712}
3713
3714impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey {
3715 fn zero(_cx: ()) -> Self {
3716 Default::default()
3717 }
3718
3719 fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) {
3720 self.0 = summary.max_path.clone();
3721 }
3722}
3723
3724struct BackgroundScanner {
3725 state: async_lock::Mutex<BackgroundScannerState>,
3726 fs: Arc<dyn Fs>,
3727 fs_case_sensitive: bool,
3728 status_updates_tx: UnboundedSender<ScanState>,
3729 executor: BackgroundExecutor,
3730 scan_requests_rx: channel::Receiver<ScanRequest>,
3731 path_prefixes_to_scan_rx: channel::Receiver<PathPrefixScanRequest>,
3732 next_entry_id: Arc<AtomicUsize>,
3733 phase: BackgroundScannerPhase,
3734 watcher: Arc<dyn Watcher>,
3735 settings: WorktreeSettings,
3736 share_private_files: bool,
3737}
3738
3739#[derive(Copy, Clone, PartialEq)]
3740enum BackgroundScannerPhase {
3741 InitialScan,
3742 EventsReceivedDuringInitialScan,
3743 Events,
3744}
3745
3746impl BackgroundScanner {
3747 async fn run(&mut self, mut fs_events_rx: Pin<Box<dyn Send + Stream<Item = Vec<PathEvent>>>>) {
3748 let root_abs_path;
3749 let scanning_enabled;
3750 {
3751 let state = self.state.lock().await;
3752 root_abs_path = state.snapshot.abs_path.clone();
3753 scanning_enabled = state.scanning_enabled;
3754 }
3755
3756 // If the worktree root does not contain a git repository, then find
3757 // the git repository in an ancestor directory. Find any gitignore files
3758 // in ancestor directories.
3759 let repo = if scanning_enabled {
3760 let (ignores, exclude, repo) =
3761 discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await;
3762 self.state
3763 .lock()
3764 .await
3765 .snapshot
3766 .ignores_by_parent_abs_path
3767 .extend(ignores);
3768 if let Some(exclude) = exclude {
3769 self.state
3770 .lock()
3771 .await
3772 .snapshot
3773 .repo_exclude_by_work_dir_abs_path
3774 .insert(root_abs_path.as_path().into(), (exclude, false));
3775 }
3776
3777 repo
3778 } else {
3779 None
3780 };
3781
3782 let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo
3783 && scanning_enabled
3784 {
3785 maybe!(async {
3786 self.state
3787 .lock()
3788 .await
3789 .insert_git_repository_for_path(
3790 work_directory,
3791 ancestor_dot_git.clone().into(),
3792 self.fs.as_ref(),
3793 self.watcher.as_ref(),
3794 )
3795 .await
3796 .log_err()?;
3797 Some(ancestor_dot_git)
3798 })
3799 .await
3800 } else {
3801 None
3802 };
3803
3804 log::trace!("containing git repository: {containing_git_repository:?}");
3805
3806 let mut global_gitignore_events = if let Some(global_gitignore_path) =
3807 &paths::global_gitignore_path()
3808 && scanning_enabled
3809 {
3810 let is_file = self.fs.is_file(&global_gitignore_path).await;
3811 self.state.lock().await.snapshot.global_gitignore = if is_file {
3812 build_gitignore(global_gitignore_path, self.fs.as_ref())
3813 .await
3814 .ok()
3815 .map(Arc::new)
3816 } else {
3817 None
3818 };
3819 if is_file
3820 || matches!(global_gitignore_path.parent(), Some(path) if self.fs.is_dir(path).await)
3821 {
3822 self.fs
3823 .watch(global_gitignore_path, FS_WATCH_LATENCY)
3824 .await
3825 .0
3826 } else {
3827 Box::pin(futures::stream::pending())
3828 }
3829 } else {
3830 self.state.lock().await.snapshot.global_gitignore = None;
3831 Box::pin(futures::stream::pending())
3832 };
3833
3834 let (scan_job_tx, scan_job_rx) = channel::unbounded();
3835 {
3836 let mut state = self.state.lock().await;
3837 state.snapshot.scan_id += 1;
3838 if let Some(mut root_entry) = state.snapshot.root_entry().cloned() {
3839 let ignore_stack = state
3840 .snapshot
3841 .ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref())
3842 .await;
3843 if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) {
3844 root_entry.is_ignored = true;
3845 let mut root_entry = root_entry.clone();
3846 state.reuse_entry_id(&mut root_entry);
3847 state
3848 .insert_entry(root_entry, self.fs.as_ref(), self.watcher.as_ref())
3849 .await;
3850 }
3851 if root_entry.is_dir() && state.scanning_enabled {
3852 state
3853 .enqueue_scan_dir(
3854 root_abs_path.as_path().into(),
3855 &root_entry,
3856 &scan_job_tx,
3857 self.fs.as_ref(),
3858 )
3859 .await;
3860 }
3861 }
3862 };
3863
3864 // Perform an initial scan of the directory.
3865 drop(scan_job_tx);
3866 self.scan_dirs(true, scan_job_rx).await;
3867 {
3868 let mut state = self.state.lock().await;
3869 state.snapshot.completed_scan_id = state.snapshot.scan_id;
3870 }
3871
3872 self.send_status_update(false, SmallVec::new()).await;
3873
3874 // Process any any FS events that occurred while performing the initial scan.
3875 // For these events, update events cannot be as precise, because we didn't
3876 // have the previous state loaded yet.
3877 self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan;
3878 if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) {
3879 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
3880 paths.extend(more_paths);
3881 }
3882 self.process_events(
3883 paths
3884 .into_iter()
3885 .filter(|e| e.kind.is_some())
3886 .map(Into::into)
3887 .collect(),
3888 )
3889 .await;
3890 }
3891 if let Some(abs_path) = containing_git_repository {
3892 self.process_events(vec![abs_path]).await;
3893 }
3894
3895 // Continue processing events until the worktree is dropped.
3896 self.phase = BackgroundScannerPhase::Events;
3897
3898 loop {
3899 select_biased! {
3900 // Process any path refresh requests from the worktree. Prioritize
3901 // these before handling changes reported by the filesystem.
3902 request = self.next_scan_request().fuse() => {
3903 let Ok(request) = request else { break };
3904 if !self.process_scan_request(request, false).await {
3905 return;
3906 }
3907 }
3908
3909 path_prefix_request = self.path_prefixes_to_scan_rx.recv().fuse() => {
3910 let Ok(request) = path_prefix_request else { break };
3911 log::trace!("adding path prefix {:?}", request.path);
3912
3913 let did_scan = self.forcibly_load_paths(std::slice::from_ref(&request.path)).await;
3914 if did_scan {
3915 let abs_path =
3916 {
3917 let mut state = self.state.lock().await;
3918 state.path_prefixes_to_scan.insert(request.path.clone());
3919 state.snapshot.absolutize(&request.path)
3920 };
3921
3922 if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() {
3923 self.process_events(vec![abs_path]).await;
3924 }
3925 }
3926 self.send_status_update(false, request.done).await;
3927 }
3928
3929 paths = fs_events_rx.next().fuse() => {
3930 let Some(mut paths) = paths else { break };
3931 while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) {
3932 paths.extend(more_paths);
3933 }
3934 self.process_events(paths.into_iter().filter(|e| e.kind.is_some()).map(Into::into).collect()).await;
3935 }
3936
3937 paths = global_gitignore_events.next().fuse() => {
3938 match paths.as_deref() {
3939 Some([event, ..]) => {
3940 self.update_global_gitignore(&event.path).await;
3941 }
3942 _ => (),
3943 }
3944 }
3945 }
3946 }
3947 }
3948
3949 async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool {
3950 log::debug!("rescanning paths {:?}", request.relative_paths);
3951
3952 request.relative_paths.sort_unstable();
3953 self.forcibly_load_paths(&request.relative_paths).await;
3954
3955 let root_path = self.state.lock().await.snapshot.abs_path.clone();
3956 let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
3957 let root_canonical_path = match &root_canonical_path {
3958 Ok(path) => SanitizedPath::new(path),
3959 Err(err) => {
3960 log::error!("failed to canonicalize root path {root_path:?}: {err:#}");
3961 return true;
3962 }
3963 };
3964 let abs_paths = request
3965 .relative_paths
3966 .iter()
3967 .map(|path| {
3968 if path.file_name().is_some() {
3969 root_canonical_path.as_path().join(path.as_std_path())
3970 } else {
3971 root_canonical_path.as_path().to_path_buf()
3972 }
3973 })
3974 .collect::<Vec<_>>();
3975
3976 {
3977 let mut state = self.state.lock().await;
3978 let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id;
3979 state.snapshot.scan_id += 1;
3980 if is_idle {
3981 state.snapshot.completed_scan_id = state.snapshot.scan_id;
3982 }
3983 }
3984
3985 self.reload_entries_for_paths(
3986 &root_path,
3987 &root_canonical_path,
3988 &request.relative_paths,
3989 abs_paths,
3990 None,
3991 )
3992 .await;
3993
3994 self.send_status_update(scanning, request.done).await
3995 }
3996
3997 async fn process_events(&self, mut abs_paths: Vec<PathBuf>) {
3998 log::trace!("process events: {abs_paths:?}");
3999 let root_path = self.state.lock().await.snapshot.abs_path.clone();
4000 let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await;
4001 let root_canonical_path = match &root_canonical_path {
4002 Ok(path) => SanitizedPath::new(path),
4003 Err(err) => {
4004 let new_path = self
4005 .state
4006 .lock()
4007 .await
4008 .snapshot
4009 .root_file_handle
4010 .clone()
4011 .and_then(|handle| match handle.current_path(&self.fs) {
4012 Ok(new_path) => Some(new_path),
4013 Err(e) => {
4014 log::error!("Failed to refresh worktree root path: {e:#}");
4015 None
4016 }
4017 })
4018 .map(|path| SanitizedPath::new_arc(&path))
4019 .filter(|new_path| *new_path != root_path);
4020
4021 if let Some(new_path) = new_path {
4022 log::info!(
4023 "root renamed from {:?} to {:?}",
4024 root_path.as_path(),
4025 new_path.as_path(),
4026 );
4027 self.status_updates_tx
4028 .unbounded_send(ScanState::RootUpdated { new_path })
4029 .ok();
4030 } else {
4031 log::error!("root path could not be canonicalized: {err:#}");
4032 }
4033 return;
4034 }
4035 };
4036
4037 // Certain directories may have FS changes, but do not lead to git data changes that Zed cares about.
4038 // Ignore these, to avoid Zed unnecessarily rescanning git metadata.
4039 let skipped_files_in_dot_git = [COMMIT_MESSAGE, INDEX_LOCK];
4040 let skipped_dirs_in_dot_git = [FSMONITOR_DAEMON, LFS_DIR];
4041
4042 let mut relative_paths = Vec::with_capacity(abs_paths.len());
4043 let mut dot_git_abs_paths = Vec::new();
4044 let mut work_dirs_needing_exclude_update = Vec::new();
4045 abs_paths.sort_unstable();
4046 abs_paths.dedup_by(|a, b| a.starts_with(b));
4047 {
4048 let snapshot = &self.state.lock().await.snapshot;
4049
4050 let mut ranges_to_drop = SmallVec::<[Range<usize>; 4]>::new();
4051
4052 fn skip_ix(ranges: &mut SmallVec<[Range<usize>; 4]>, ix: usize) {
4053 if let Some(last_range) = ranges.last_mut()
4054 && last_range.end == ix
4055 {
4056 last_range.end += 1;
4057 } else {
4058 ranges.push(ix..ix + 1);
4059 }
4060 }
4061
4062 for (ix, abs_path) in abs_paths.iter().enumerate() {
4063 let abs_path = &SanitizedPath::new(&abs_path);
4064
4065 let mut is_git_related = false;
4066 let mut dot_git_paths = None;
4067
4068 for ancestor in abs_path.as_path().ancestors() {
4069 if is_git_dir(ancestor, self.fs.as_ref()).await {
4070 let path_in_git_dir = abs_path
4071 .as_path()
4072 .strip_prefix(ancestor)
4073 .expect("stripping off the ancestor");
4074 dot_git_paths = Some((ancestor.to_owned(), path_in_git_dir.to_owned()));
4075 break;
4076 }
4077 }
4078
4079 if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths {
4080 if skipped_files_in_dot_git
4081 .iter()
4082 .any(|skipped| OsStr::new(skipped) == path_in_git_dir.as_path().as_os_str())
4083 || skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| {
4084 path_in_git_dir.starts_with(skipped_git_subdir)
4085 })
4086 {
4087 log::debug!(
4088 "ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories"
4089 );
4090 skip_ix(&mut ranges_to_drop, ix);
4091 continue;
4092 }
4093
4094 is_git_related = true;
4095 if !dot_git_abs_paths.contains(&dot_git_abs_path) {
4096 dot_git_abs_paths.push(dot_git_abs_path);
4097 }
4098 }
4099
4100 let relative_path = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path)
4101 && let Ok(path) = RelPath::new(path, PathStyle::local())
4102 {
4103 path
4104 } else {
4105 if is_git_related {
4106 log::debug!(
4107 "ignoring event {abs_path:?}, since it's in git dir outside of root path {root_canonical_path:?}",
4108 );
4109 } else {
4110 log::error!(
4111 "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}",
4112 );
4113 }
4114 skip_ix(&mut ranges_to_drop, ix);
4115 continue;
4116 };
4117
4118 let absolute_path = abs_path.to_path_buf();
4119 if absolute_path.ends_with(Path::new(DOT_GIT).join(REPO_EXCLUDE)) {
4120 if let Some(repository) = snapshot
4121 .git_repositories
4122 .values()
4123 .find(|repo| repo.common_dir_abs_path.join(REPO_EXCLUDE) == absolute_path)
4124 {
4125 work_dirs_needing_exclude_update
4126 .push(repository.work_directory_abs_path.clone());
4127 }
4128 }
4129
4130 if abs_path.file_name() == Some(OsStr::new(GITIGNORE)) {
4131 for (_, repo) in snapshot
4132 .git_repositories
4133 .iter()
4134 .filter(|(_, repo)| repo.directory_contains(&relative_path))
4135 {
4136 if !dot_git_abs_paths.iter().any(|dot_git_abs_path| {
4137 dot_git_abs_path == repo.common_dir_abs_path.as_ref()
4138 }) {
4139 dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf());
4140 }
4141 }
4142 }
4143
4144 let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| {
4145 snapshot
4146 .entry_for_path(parent)
4147 .is_some_and(|entry| entry.kind == EntryKind::Dir)
4148 });
4149 if !parent_dir_is_loaded {
4150 log::debug!("ignoring event {relative_path:?} within unloaded directory");
4151 skip_ix(&mut ranges_to_drop, ix);
4152 continue;
4153 }
4154
4155 if self.settings.is_path_excluded(&relative_path) {
4156 if !is_git_related {
4157 log::debug!("ignoring FS event for excluded path {relative_path:?}");
4158 }
4159 skip_ix(&mut ranges_to_drop, ix);
4160 continue;
4161 }
4162
4163 relative_paths.push(relative_path.into_arc());
4164 }
4165
4166 for range_to_drop in ranges_to_drop.into_iter().rev() {
4167 abs_paths.drain(range_to_drop);
4168 }
4169 }
4170
4171 if relative_paths.is_empty() && dot_git_abs_paths.is_empty() {
4172 return;
4173 }
4174
4175 if !work_dirs_needing_exclude_update.is_empty() {
4176 let mut state = self.state.lock().await;
4177 for work_dir_abs_path in work_dirs_needing_exclude_update {
4178 if let Some((_, needs_update)) = state
4179 .snapshot
4180 .repo_exclude_by_work_dir_abs_path
4181 .get_mut(&work_dir_abs_path)
4182 {
4183 *needs_update = true;
4184 }
4185 }
4186 }
4187
4188 self.state.lock().await.snapshot.scan_id += 1;
4189
4190 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4191 log::debug!("received fs events {:?}", relative_paths);
4192 self.reload_entries_for_paths(
4193 &root_path,
4194 &root_canonical_path,
4195 &relative_paths,
4196 abs_paths,
4197 Some(scan_job_tx.clone()),
4198 )
4199 .await;
4200
4201 let affected_repo_roots = if !dot_git_abs_paths.is_empty() {
4202 self.update_git_repositories(dot_git_abs_paths).await
4203 } else {
4204 Vec::new()
4205 };
4206
4207 {
4208 let mut ignores_to_update = self.ignores_needing_update().await;
4209 ignores_to_update.extend(affected_repo_roots);
4210 let ignores_to_update = self.order_ignores(ignores_to_update).await;
4211 let snapshot = self.state.lock().await.snapshot.clone();
4212 self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update)
4213 .await;
4214 self.scan_dirs(false, scan_job_rx).await;
4215 }
4216
4217 {
4218 let mut state = self.state.lock().await;
4219 state.snapshot.completed_scan_id = state.snapshot.scan_id;
4220 for (_, entry) in mem::take(&mut state.removed_entries) {
4221 state.scanned_dirs.remove(&entry.id);
4222 }
4223 }
4224 self.send_status_update(false, SmallVec::new()).await;
4225 }
4226
4227 async fn update_global_gitignore(&self, abs_path: &Path) {
4228 let ignore = build_gitignore(abs_path, self.fs.as_ref())
4229 .await
4230 .log_err()
4231 .map(Arc::new);
4232 let (prev_snapshot, ignore_stack, abs_path) = {
4233 let mut state = self.state.lock().await;
4234 state.snapshot.global_gitignore = ignore;
4235 let abs_path = state.snapshot.abs_path().clone();
4236 let ignore_stack = state
4237 .snapshot
4238 .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref())
4239 .await;
4240 (state.snapshot.clone(), ignore_stack, abs_path)
4241 };
4242 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4243 self.update_ignore_statuses_for_paths(
4244 scan_job_tx,
4245 prev_snapshot,
4246 vec![(abs_path, ignore_stack)],
4247 )
4248 .await;
4249 self.scan_dirs(false, scan_job_rx).await;
4250 self.send_status_update(false, SmallVec::new()).await;
4251 }
4252
4253 async fn forcibly_load_paths(&self, paths: &[Arc<RelPath>]) -> bool {
4254 let (scan_job_tx, scan_job_rx) = channel::unbounded();
4255 {
4256 let mut state = self.state.lock().await;
4257 let root_path = state.snapshot.abs_path.clone();
4258 for path in paths {
4259 for ancestor in path.ancestors() {
4260 if let Some(entry) = state.snapshot.entry_for_path(ancestor)
4261 && entry.kind == EntryKind::UnloadedDir
4262 {
4263 let abs_path = root_path.join(ancestor.as_std_path());
4264 state
4265 .enqueue_scan_dir(
4266 abs_path.into(),
4267 entry,
4268 &scan_job_tx,
4269 self.fs.as_ref(),
4270 )
4271 .await;
4272 state.paths_to_scan.insert(path.clone());
4273 break;
4274 }
4275 }
4276 }
4277 drop(scan_job_tx);
4278 }
4279 while let Ok(job) = scan_job_rx.recv().await {
4280 self.scan_dir(&job).await.log_err();
4281 }
4282
4283 !mem::take(&mut self.state.lock().await.paths_to_scan).is_empty()
4284 }
4285
4286 async fn scan_dirs(
4287 &self,
4288 enable_progress_updates: bool,
4289 scan_jobs_rx: channel::Receiver<ScanJob>,
4290 ) {
4291 if self
4292 .status_updates_tx
4293 .unbounded_send(ScanState::Started)
4294 .is_err()
4295 {
4296 return;
4297 }
4298
4299 let progress_update_count = AtomicUsize::new(0);
4300 self.executor
4301 .scoped_priority(Priority::Low, |scope| {
4302 for _ in 0..self.executor.num_cpus() {
4303 scope.spawn(async {
4304 let mut last_progress_update_count = 0;
4305 let progress_update_timer = self.progress_timer(enable_progress_updates).fuse();
4306 futures::pin_mut!(progress_update_timer);
4307
4308 loop {
4309 select_biased! {
4310 // Process any path refresh requests before moving on to process
4311 // the scan queue, so that user operations are prioritized.
4312 request = self.next_scan_request().fuse() => {
4313 let Ok(request) = request else { break };
4314 if !self.process_scan_request(request, true).await {
4315 return;
4316 }
4317 }
4318
4319 // Send periodic progress updates to the worktree. Use an atomic counter
4320 // to ensure that only one of the workers sends a progress update after
4321 // the update interval elapses.
4322 _ = progress_update_timer => {
4323 match progress_update_count.compare_exchange(
4324 last_progress_update_count,
4325 last_progress_update_count + 1,
4326 SeqCst,
4327 SeqCst
4328 ) {
4329 Ok(_) => {
4330 last_progress_update_count += 1;
4331 self.send_status_update(true, SmallVec::new()).await;
4332 }
4333 Err(count) => {
4334 last_progress_update_count = count;
4335 }
4336 }
4337 progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse());
4338 }
4339
4340 // Recursively load directories from the file system.
4341 job = scan_jobs_rx.recv().fuse() => {
4342 let Ok(job) = job else { break };
4343 if let Err(err) = self.scan_dir(&job).await
4344 && job.path.is_empty() {
4345 log::error!("error scanning directory {:?}: {}", job.abs_path, err);
4346 }
4347 }
4348 }
4349 }
4350 });
4351 }
4352 })
4353 .await;
4354 }
4355
4356 async fn send_status_update(
4357 &self,
4358 scanning: bool,
4359 barrier: SmallVec<[barrier::Sender; 1]>,
4360 ) -> bool {
4361 let mut state = self.state.lock().await;
4362 if state.changed_paths.is_empty() && scanning {
4363 return true;
4364 }
4365
4366 let new_snapshot = state.snapshot.clone();
4367 let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone());
4368 let changes = build_diff(
4369 self.phase,
4370 &old_snapshot,
4371 &new_snapshot,
4372 &state.changed_paths,
4373 );
4374 state.changed_paths.clear();
4375
4376 self.status_updates_tx
4377 .unbounded_send(ScanState::Updated {
4378 snapshot: new_snapshot,
4379 changes,
4380 scanning,
4381 barrier,
4382 })
4383 .is_ok()
4384 }
4385
4386 async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
4387 let root_abs_path;
4388 let root_char_bag;
4389 {
4390 let snapshot = &self.state.lock().await.snapshot;
4391 if self.settings.is_path_excluded(&job.path) {
4392 log::error!("skipping excluded directory {:?}", job.path);
4393 return Ok(());
4394 }
4395 log::trace!("scanning directory {:?}", job.path);
4396 root_abs_path = snapshot.abs_path().clone();
4397 root_char_bag = snapshot.root_char_bag;
4398 }
4399
4400 let next_entry_id = self.next_entry_id.clone();
4401 let mut ignore_stack = job.ignore_stack.clone();
4402 let mut new_ignore = None;
4403 let mut root_canonical_path = None;
4404 let mut new_entries: Vec<Entry> = Vec::new();
4405 let mut new_jobs: Vec<Option<ScanJob>> = Vec::new();
4406 let mut child_paths = self
4407 .fs
4408 .read_dir(&job.abs_path)
4409 .await?
4410 .filter_map(|entry| async {
4411 match entry {
4412 Ok(entry) => Some(entry),
4413 Err(error) => {
4414 log::error!("error processing entry {:?}", error);
4415 None
4416 }
4417 }
4418 })
4419 .collect::<Vec<_>>()
4420 .await;
4421
4422 // Ensure that .git and .gitignore are processed first.
4423 swap_to_front(&mut child_paths, GITIGNORE);
4424 swap_to_front(&mut child_paths, DOT_GIT);
4425
4426 if let Some(path) = child_paths.first()
4427 && path.ends_with(DOT_GIT)
4428 {
4429 ignore_stack.repo_root = Some(job.abs_path.clone());
4430 }
4431
4432 for child_abs_path in child_paths {
4433 let child_abs_path: Arc<Path> = child_abs_path.into();
4434 let child_name = child_abs_path.file_name().unwrap();
4435 let Some(child_path) = child_name
4436 .to_str()
4437 .and_then(|name| Some(job.path.join(RelPath::unix(name).ok()?)))
4438 else {
4439 continue;
4440 };
4441
4442 if child_name == DOT_GIT {
4443 let mut state = self.state.lock().await;
4444 state
4445 .insert_git_repository(
4446 child_path.clone(),
4447 self.fs.as_ref(),
4448 self.watcher.as_ref(),
4449 )
4450 .await;
4451 } else if child_name == GITIGNORE {
4452 match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
4453 Ok(ignore) => {
4454 let ignore = Arc::new(ignore);
4455 ignore_stack = ignore_stack
4456 .append(IgnoreKind::Gitignore(job.abs_path.clone()), ignore.clone());
4457 new_ignore = Some(ignore);
4458 }
4459 Err(error) => {
4460 log::error!(
4461 "error loading .gitignore file {:?} - {:?}",
4462 child_name,
4463 error
4464 );
4465 }
4466 }
4467 }
4468
4469 if self.settings.is_path_excluded(&child_path) {
4470 log::debug!("skipping excluded child entry {child_path:?}");
4471 self.state.lock().await.remove_path(&child_path);
4472 continue;
4473 }
4474
4475 let child_metadata = match self.fs.metadata(&child_abs_path).await {
4476 Ok(Some(metadata)) => metadata,
4477 Ok(None) => continue,
4478 Err(err) => {
4479 log::error!("error processing {child_abs_path:?}: {err:#}");
4480 continue;
4481 }
4482 };
4483
4484 let mut child_entry = Entry::new(
4485 child_path.clone(),
4486 &child_metadata,
4487 ProjectEntryId::new(&next_entry_id),
4488 root_char_bag,
4489 None,
4490 );
4491
4492 if job.is_external {
4493 child_entry.is_external = true;
4494 } else if child_metadata.is_symlink {
4495 let canonical_path = match self.fs.canonicalize(&child_abs_path).await {
4496 Ok(path) => path,
4497 Err(err) => {
4498 log::error!("error reading target of symlink {child_abs_path:?}: {err:#}",);
4499 continue;
4500 }
4501 };
4502
4503 // lazily canonicalize the root path in order to determine if
4504 // symlinks point outside of the worktree.
4505 let root_canonical_path = match &root_canonical_path {
4506 Some(path) => path,
4507 None => match self.fs.canonicalize(&root_abs_path).await {
4508 Ok(path) => root_canonical_path.insert(path),
4509 Err(err) => {
4510 log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err);
4511 continue;
4512 }
4513 },
4514 };
4515
4516 if !canonical_path.starts_with(root_canonical_path) {
4517 child_entry.is_external = true;
4518 }
4519
4520 child_entry.canonical_path = Some(canonical_path.into());
4521 }
4522
4523 if child_entry.is_dir() {
4524 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true);
4525 child_entry.is_always_included =
4526 self.settings.is_path_always_included(&child_path, true);
4527
4528 // Avoid recursing until crash in the case of a recursive symlink
4529 if job.ancestor_inodes.contains(&child_entry.inode) {
4530 new_jobs.push(None);
4531 } else {
4532 let mut ancestor_inodes = job.ancestor_inodes.clone();
4533 ancestor_inodes.insert(child_entry.inode);
4534
4535 new_jobs.push(Some(ScanJob {
4536 abs_path: child_abs_path.clone(),
4537 path: child_path,
4538 is_external: child_entry.is_external,
4539 ignore_stack: if child_entry.is_ignored {
4540 IgnoreStack::all()
4541 } else {
4542 ignore_stack.clone()
4543 },
4544 ancestor_inodes,
4545 scan_queue: job.scan_queue.clone(),
4546 }));
4547 }
4548 } else {
4549 child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false);
4550 child_entry.is_always_included =
4551 self.settings.is_path_always_included(&child_path, false);
4552 }
4553
4554 {
4555 let relative_path = job
4556 .path
4557 .join(RelPath::unix(child_name.to_str().unwrap()).unwrap());
4558 if self.is_path_private(&relative_path) {
4559 log::debug!("detected private file: {relative_path:?}");
4560 child_entry.is_private = true;
4561 }
4562 if self.settings.is_path_hidden(&relative_path) {
4563 log::debug!("detected hidden file: {relative_path:?}");
4564 child_entry.is_hidden = true;
4565 }
4566 }
4567
4568 new_entries.push(child_entry);
4569 }
4570
4571 let mut state = self.state.lock().await;
4572
4573 // Identify any subdirectories that should not be scanned.
4574 let mut job_ix = 0;
4575 for entry in &mut new_entries {
4576 state.reuse_entry_id(entry);
4577 if entry.is_dir() {
4578 if state.should_scan_directory(entry) {
4579 job_ix += 1;
4580 } else {
4581 log::debug!("defer scanning directory {:?}", entry.path);
4582 entry.kind = EntryKind::UnloadedDir;
4583 new_jobs.remove(job_ix);
4584 }
4585 }
4586 if entry.is_always_included {
4587 state
4588 .snapshot
4589 .always_included_entries
4590 .push(entry.path.clone());
4591 }
4592 }
4593
4594 state.populate_dir(job.path.clone(), new_entries, new_ignore);
4595 self.watcher.add(job.abs_path.as_ref()).log_err();
4596
4597 for new_job in new_jobs.into_iter().flatten() {
4598 job.scan_queue
4599 .try_send(new_job)
4600 .expect("channel is unbounded");
4601 }
4602
4603 Ok(())
4604 }
4605
4606 /// All list arguments should be sorted before calling this function
4607 async fn reload_entries_for_paths(
4608 &self,
4609 root_abs_path: &SanitizedPath,
4610 root_canonical_path: &SanitizedPath,
4611 relative_paths: &[Arc<RelPath>],
4612 abs_paths: Vec<PathBuf>,
4613 scan_queue_tx: Option<Sender<ScanJob>>,
4614 ) {
4615 // grab metadata for all requested paths
4616 let metadata = futures::future::join_all(
4617 abs_paths
4618 .iter()
4619 .map(|abs_path| async move {
4620 let metadata = self.fs.metadata(abs_path).await?;
4621 if let Some(metadata) = metadata {
4622 let canonical_path = self.fs.canonicalize(abs_path).await?;
4623
4624 // If we're on a case-insensitive filesystem (default on macOS), we want
4625 // to only ignore metadata for non-symlink files if their absolute-path matches
4626 // the canonical-path.
4627 // Because if not, this might be a case-only-renaming (`mv test.txt TEST.TXT`)
4628 // and we want to ignore the metadata for the old path (`test.txt`) so it's
4629 // treated as removed.
4630 if !self.fs_case_sensitive && !metadata.is_symlink {
4631 let canonical_file_name = canonical_path.file_name();
4632 let file_name = abs_path.file_name();
4633 if canonical_file_name != file_name {
4634 return Ok(None);
4635 }
4636 }
4637
4638 anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path))))
4639 } else {
4640 Ok(None)
4641 }
4642 })
4643 .collect::<Vec<_>>(),
4644 )
4645 .await;
4646
4647 let mut new_ancestor_repo = if relative_paths.iter().any(|path| path.is_empty()) {
4648 Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await)
4649 } else {
4650 None
4651 };
4652
4653 let mut state = self.state.lock().await;
4654 let doing_recursive_update = scan_queue_tx.is_some();
4655
4656 // Remove any entries for paths that no longer exist or are being recursively
4657 // refreshed. Do this before adding any new entries, so that renames can be
4658 // detected regardless of the order of the paths.
4659 for (path, metadata) in relative_paths.iter().zip(metadata.iter()) {
4660 if matches!(metadata, Ok(None)) || doing_recursive_update {
4661 state.remove_path(path);
4662 }
4663 }
4664
4665 for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) {
4666 let abs_path: Arc<Path> = root_abs_path.join(path.as_std_path()).into();
4667 match metadata {
4668 Ok(Some((metadata, canonical_path))) => {
4669 let ignore_stack = state
4670 .snapshot
4671 .ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref())
4672 .await;
4673 let is_external = !canonical_path.starts_with(&root_canonical_path);
4674 let entry_id = state.entry_id_for(self.next_entry_id.as_ref(), path, &metadata);
4675 let mut fs_entry = Entry::new(
4676 path.clone(),
4677 &metadata,
4678 entry_id,
4679 state.snapshot.root_char_bag,
4680 if metadata.is_symlink {
4681 Some(canonical_path.as_path().to_path_buf().into())
4682 } else {
4683 None
4684 },
4685 );
4686
4687 let is_dir = fs_entry.is_dir();
4688 fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
4689 fs_entry.is_external = is_external;
4690 fs_entry.is_private = self.is_path_private(path);
4691 fs_entry.is_always_included =
4692 self.settings.is_path_always_included(path, is_dir);
4693 fs_entry.is_hidden = self.settings.is_path_hidden(path);
4694
4695 if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) {
4696 if state.should_scan_directory(&fs_entry)
4697 || (fs_entry.path.is_empty()
4698 && abs_path.file_name() == Some(OsStr::new(DOT_GIT)))
4699 {
4700 state
4701 .enqueue_scan_dir(
4702 abs_path,
4703 &fs_entry,
4704 scan_queue_tx,
4705 self.fs.as_ref(),
4706 )
4707 .await;
4708 } else {
4709 fs_entry.kind = EntryKind::UnloadedDir;
4710 }
4711 }
4712
4713 state
4714 .insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref())
4715 .await;
4716
4717 if path.is_empty()
4718 && let Some((ignores, exclude, repo)) = new_ancestor_repo.take()
4719 {
4720 log::trace!("updating ancestor git repository");
4721 state.snapshot.ignores_by_parent_abs_path.extend(ignores);
4722 if let Some((ancestor_dot_git, work_directory)) = repo {
4723 if let Some(exclude) = exclude {
4724 let work_directory_abs_path = self
4725 .state
4726 .lock()
4727 .await
4728 .snapshot
4729 .work_directory_abs_path(&work_directory);
4730
4731 state
4732 .snapshot
4733 .repo_exclude_by_work_dir_abs_path
4734 .insert(work_directory_abs_path.into(), (exclude, false));
4735 }
4736 state
4737 .insert_git_repository_for_path(
4738 work_directory,
4739 ancestor_dot_git.into(),
4740 self.fs.as_ref(),
4741 self.watcher.as_ref(),
4742 )
4743 .await
4744 .log_err();
4745 }
4746 }
4747 }
4748 Ok(None) => {
4749 self.remove_repo_path(path.clone(), &mut state.snapshot);
4750 }
4751 Err(err) => {
4752 log::error!("error reading file {abs_path:?} on event: {err:#}");
4753 }
4754 }
4755 }
4756
4757 util::extend_sorted(
4758 &mut state.changed_paths,
4759 relative_paths.iter().cloned(),
4760 usize::MAX,
4761 Ord::cmp,
4762 );
4763 }
4764
4765 fn remove_repo_path(&self, path: Arc<RelPath>, snapshot: &mut LocalSnapshot) -> Option<()> {
4766 if !path.components().any(|component| component == DOT_GIT)
4767 && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(&path)
4768 {
4769 let id = local_repo.work_directory_id;
4770 log::debug!("remove repo path: {:?}", path);
4771 snapshot.git_repositories.remove(&id);
4772 return Some(());
4773 }
4774
4775 Some(())
4776 }
4777
4778 async fn update_ignore_statuses_for_paths(
4779 &self,
4780 scan_job_tx: Sender<ScanJob>,
4781 prev_snapshot: LocalSnapshot,
4782 ignores_to_update: Vec<(Arc<Path>, IgnoreStack)>,
4783 ) {
4784 let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded();
4785 {
4786 for (parent_abs_path, ignore_stack) in ignores_to_update {
4787 ignore_queue_tx
4788 .send_blocking(UpdateIgnoreStatusJob {
4789 abs_path: parent_abs_path,
4790 ignore_stack,
4791 ignore_queue: ignore_queue_tx.clone(),
4792 scan_queue: scan_job_tx.clone(),
4793 })
4794 .unwrap();
4795 }
4796 }
4797 drop(ignore_queue_tx);
4798
4799 self.executor
4800 .scoped(|scope| {
4801 for _ in 0..self.executor.num_cpus() {
4802 scope.spawn(async {
4803 loop {
4804 select_biased! {
4805 // Process any path refresh requests before moving on to process
4806 // the queue of ignore statuses.
4807 request = self.next_scan_request().fuse() => {
4808 let Ok(request) = request else { break };
4809 if !self.process_scan_request(request, true).await {
4810 return;
4811 }
4812 }
4813
4814 // Recursively process directories whose ignores have changed.
4815 job = ignore_queue_rx.recv().fuse() => {
4816 let Ok(job) = job else { break };
4817 self.update_ignore_status(job, &prev_snapshot).await;
4818 }
4819 }
4820 }
4821 });
4822 }
4823 })
4824 .await;
4825 }
4826
4827 async fn ignores_needing_update(&self) -> Vec<Arc<Path>> {
4828 let mut ignores_to_update = Vec::new();
4829 let mut excludes_to_load: Vec<(Arc<Path>, PathBuf)> = Vec::new();
4830
4831 // First pass: collect updates and drop stale entries without awaiting.
4832 {
4833 let snapshot = &mut self.state.lock().await.snapshot;
4834 let abs_path = snapshot.abs_path.clone();
4835 let mut repo_exclude_keys_to_remove: Vec<Arc<Path>> = Vec::new();
4836
4837 for (work_dir_abs_path, (_, needs_update)) in
4838 snapshot.repo_exclude_by_work_dir_abs_path.iter_mut()
4839 {
4840 let repository = snapshot
4841 .git_repositories
4842 .iter()
4843 .find(|(_, repo)| &repo.work_directory_abs_path == work_dir_abs_path);
4844
4845 if *needs_update {
4846 *needs_update = false;
4847 ignores_to_update.push(work_dir_abs_path.clone());
4848
4849 if let Some((_, repository)) = repository {
4850 let exclude_abs_path = repository.common_dir_abs_path.join(REPO_EXCLUDE);
4851 excludes_to_load.push((work_dir_abs_path.clone(), exclude_abs_path));
4852 }
4853 }
4854
4855 if repository.is_none() {
4856 repo_exclude_keys_to_remove.push(work_dir_abs_path.clone());
4857 }
4858 }
4859
4860 for key in repo_exclude_keys_to_remove {
4861 snapshot.repo_exclude_by_work_dir_abs_path.remove(&key);
4862 }
4863
4864 snapshot
4865 .ignores_by_parent_abs_path
4866 .retain(|parent_abs_path, (_, needs_update)| {
4867 if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path())
4868 && let Some(parent_path) =
4869 RelPath::new(&parent_path, PathStyle::local()).log_err()
4870 {
4871 if *needs_update {
4872 *needs_update = false;
4873 if snapshot.snapshot.entry_for_path(&parent_path).is_some() {
4874 ignores_to_update.push(parent_abs_path.clone());
4875 }
4876 }
4877
4878 let ignore_path = parent_path.join(RelPath::unix(GITIGNORE).unwrap());
4879 if snapshot.snapshot.entry_for_path(&ignore_path).is_none() {
4880 return false;
4881 }
4882 }
4883 true
4884 });
4885 }
4886
4887 // Load gitignores asynchronously (outside the lock)
4888 let mut loaded_excludes: Vec<(Arc<Path>, Arc<Gitignore>)> = Vec::new();
4889 for (work_dir_abs_path, exclude_abs_path) in excludes_to_load {
4890 if let Ok(current_exclude) = build_gitignore(&exclude_abs_path, self.fs.as_ref()).await
4891 {
4892 loaded_excludes.push((work_dir_abs_path, Arc::new(current_exclude)));
4893 }
4894 }
4895
4896 // Second pass: apply updates.
4897 if !loaded_excludes.is_empty() {
4898 let snapshot = &mut self.state.lock().await.snapshot;
4899
4900 for (work_dir_abs_path, exclude) in loaded_excludes {
4901 if let Some((existing_exclude, _)) = snapshot
4902 .repo_exclude_by_work_dir_abs_path
4903 .get_mut(&work_dir_abs_path)
4904 {
4905 *existing_exclude = exclude;
4906 }
4907 }
4908 }
4909
4910 ignores_to_update
4911 }
4912
4913 async fn order_ignores(&self, mut ignores: Vec<Arc<Path>>) -> Vec<(Arc<Path>, IgnoreStack)> {
4914 let fs = self.fs.clone();
4915 let snapshot = self.state.lock().await.snapshot.clone();
4916 ignores.sort_unstable();
4917 let mut ignores_to_update = ignores.into_iter().peekable();
4918
4919 let mut result = vec![];
4920 while let Some(parent_abs_path) = ignores_to_update.next() {
4921 while ignores_to_update
4922 .peek()
4923 .map_or(false, |p| p.starts_with(&parent_abs_path))
4924 {
4925 ignores_to_update.next().unwrap();
4926 }
4927 let ignore_stack = snapshot
4928 .ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref())
4929 .await;
4930 result.push((parent_abs_path, ignore_stack));
4931 }
4932
4933 result
4934 }
4935
4936 async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) {
4937 log::trace!("update ignore status {:?}", job.abs_path);
4938
4939 let mut ignore_stack = job.ignore_stack;
4940 if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) {
4941 ignore_stack =
4942 ignore_stack.append(IgnoreKind::Gitignore(job.abs_path.clone()), ignore.clone());
4943 }
4944
4945 let mut entries_by_id_edits = Vec::new();
4946 let mut entries_by_path_edits = Vec::new();
4947 let Some(path) = job
4948 .abs_path
4949 .strip_prefix(snapshot.abs_path.as_path())
4950 .map_err(|_| {
4951 anyhow::anyhow!(
4952 "Failed to strip prefix '{}' from path '{}'",
4953 snapshot.abs_path.as_path().display(),
4954 job.abs_path.display()
4955 )
4956 })
4957 .log_err()
4958 else {
4959 return;
4960 };
4961
4962 let Some(path) = RelPath::new(&path, PathStyle::local()).log_err() else {
4963 return;
4964 };
4965
4966 if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await
4967 && metadata.is_dir
4968 {
4969 ignore_stack.repo_root = Some(job.abs_path.clone());
4970 }
4971
4972 for mut entry in snapshot.child_entries(&path).cloned() {
4973 let was_ignored = entry.is_ignored;
4974 let abs_path: Arc<Path> = snapshot.absolutize(&entry.path).into();
4975 entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir());
4976
4977 if entry.is_dir() {
4978 let child_ignore_stack = if entry.is_ignored {
4979 IgnoreStack::all()
4980 } else {
4981 ignore_stack.clone()
4982 };
4983
4984 // Scan any directories that were previously ignored and weren't previously scanned.
4985 if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
4986 let state = self.state.lock().await;
4987 if state.should_scan_directory(&entry) {
4988 state
4989 .enqueue_scan_dir(
4990 abs_path.clone(),
4991 &entry,
4992 &job.scan_queue,
4993 self.fs.as_ref(),
4994 )
4995 .await;
4996 }
4997 }
4998
4999 job.ignore_queue
5000 .send(UpdateIgnoreStatusJob {
5001 abs_path: abs_path.clone(),
5002 ignore_stack: child_ignore_stack,
5003 ignore_queue: job.ignore_queue.clone(),
5004 scan_queue: job.scan_queue.clone(),
5005 })
5006 .await
5007 .unwrap();
5008 }
5009
5010 if entry.is_ignored != was_ignored {
5011 let mut path_entry = snapshot.entries_by_id.get(&entry.id, ()).unwrap().clone();
5012 path_entry.scan_id = snapshot.scan_id;
5013 path_entry.is_ignored = entry.is_ignored;
5014 entries_by_id_edits.push(Edit::Insert(path_entry));
5015 entries_by_path_edits.push(Edit::Insert(entry));
5016 }
5017 }
5018
5019 let state = &mut self.state.lock().await;
5020 for edit in &entries_by_path_edits {
5021 if let Edit::Insert(entry) = edit
5022 && let Err(ix) = state.changed_paths.binary_search(&entry.path)
5023 {
5024 state.changed_paths.insert(ix, entry.path.clone());
5025 }
5026 }
5027
5028 state
5029 .snapshot
5030 .entries_by_path
5031 .edit(entries_by_path_edits, ());
5032 state.snapshot.entries_by_id.edit(entries_by_id_edits, ());
5033 }
5034
5035 async fn update_git_repositories(&self, dot_git_paths: Vec<PathBuf>) -> Vec<Arc<Path>> {
5036 log::trace!("reloading repositories: {dot_git_paths:?}");
5037 let mut state = self.state.lock().await;
5038 let scan_id = state.snapshot.scan_id;
5039 let mut affected_repo_roots = Vec::new();
5040 for dot_git_dir in dot_git_paths {
5041 let existing_repository_entry =
5042 state
5043 .snapshot
5044 .git_repositories
5045 .iter()
5046 .find_map(|(_, repo)| {
5047 let dot_git_dir = SanitizedPath::new(&dot_git_dir);
5048 if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir
5049 || SanitizedPath::new(repo.repository_dir_abs_path.as_ref())
5050 == dot_git_dir
5051 {
5052 Some(repo.clone())
5053 } else {
5054 None
5055 }
5056 });
5057
5058 match existing_repository_entry {
5059 None => {
5060 let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else {
5061 debug_panic!(
5062 "update_git_repositories called with .git directory outside the worktree root"
5063 );
5064 return Vec::new();
5065 };
5066 affected_repo_roots.push(dot_git_dir.parent().unwrap().into());
5067 state
5068 .insert_git_repository(
5069 RelPath::new(relative, PathStyle::local())
5070 .unwrap()
5071 .into_arc(),
5072 self.fs.as_ref(),
5073 self.watcher.as_ref(),
5074 )
5075 .await;
5076 }
5077 Some(local_repository) => {
5078 state.snapshot.git_repositories.update(
5079 &local_repository.work_directory_id,
5080 |entry| {
5081 entry.git_dir_scan_id = scan_id;
5082 },
5083 );
5084 }
5085 };
5086 }
5087
5088 // Remove any git repositories whose .git entry no longer exists.
5089 let snapshot = &mut state.snapshot;
5090 let mut ids_to_preserve = HashSet::default();
5091 for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
5092 let exists_in_snapshot =
5093 snapshot
5094 .entry_for_id(work_directory_id)
5095 .is_some_and(|entry| {
5096 snapshot
5097 .entry_for_path(&entry.path.join(RelPath::unix(DOT_GIT).unwrap()))
5098 .is_some()
5099 });
5100
5101 if exists_in_snapshot
5102 || matches!(
5103 self.fs.metadata(&entry.common_dir_abs_path).await,
5104 Ok(Some(_))
5105 )
5106 {
5107 ids_to_preserve.insert(work_directory_id);
5108 }
5109 }
5110
5111 snapshot
5112 .git_repositories
5113 .retain(|work_directory_id, entry| {
5114 let preserve = ids_to_preserve.contains(work_directory_id);
5115 if !preserve {
5116 affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into());
5117 snapshot
5118 .repo_exclude_by_work_dir_abs_path
5119 .remove(&entry.work_directory_abs_path);
5120 }
5121 preserve
5122 });
5123
5124 affected_repo_roots
5125 }
5126
5127 async fn progress_timer(&self, running: bool) {
5128 if !running {
5129 return futures::future::pending().await;
5130 }
5131
5132 #[cfg(any(test, feature = "test-support"))]
5133 if self.fs.is_fake() {
5134 return self.executor.simulate_random_delay().await;
5135 }
5136
5137 smol::Timer::after(FS_WATCH_LATENCY).await;
5138 }
5139
5140 fn is_path_private(&self, path: &RelPath) -> bool {
5141 !self.share_private_files && self.settings.is_path_private(path)
5142 }
5143
5144 async fn next_scan_request(&self) -> Result<ScanRequest> {
5145 let mut request = self.scan_requests_rx.recv().await?;
5146 while let Ok(next_request) = self.scan_requests_rx.try_recv() {
5147 request.relative_paths.extend(next_request.relative_paths);
5148 request.done.extend(next_request.done);
5149 }
5150 Ok(request)
5151 }
5152}
5153
5154async fn discover_ancestor_git_repo(
5155 fs: Arc<dyn Fs>,
5156 root_abs_path: &SanitizedPath,
5157) -> (
5158 HashMap<Arc<Path>, (Arc<Gitignore>, bool)>,
5159 Option<Arc<Gitignore>>,
5160 Option<(PathBuf, WorkDirectory)>,
5161) {
5162 let mut exclude = None;
5163 let mut ignores = HashMap::default();
5164 for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() {
5165 if index != 0 {
5166 if ancestor == paths::home_dir() {
5167 // Unless $HOME is itself the worktree root, don't consider it as a
5168 // containing git repository---expensive and likely unwanted.
5169 break;
5170 } else if let Ok(ignore) = build_gitignore(&ancestor.join(GITIGNORE), fs.as_ref()).await
5171 {
5172 ignores.insert(ancestor.into(), (ignore.into(), false));
5173 }
5174 }
5175
5176 let ancestor_dot_git = ancestor.join(DOT_GIT);
5177 log::trace!("considering ancestor: {ancestor_dot_git:?}");
5178 // Check whether the directory or file called `.git` exists (in the
5179 // case of worktrees it's a file.)
5180 if fs
5181 .metadata(&ancestor_dot_git)
5182 .await
5183 .is_ok_and(|metadata| metadata.is_some())
5184 {
5185 if index != 0 {
5186 // We canonicalize, since the FS events use the canonicalized path.
5187 if let Some(ancestor_dot_git) = fs.canonicalize(&ancestor_dot_git).await.log_err() {
5188 let location_in_repo = root_abs_path
5189 .as_path()
5190 .strip_prefix(ancestor)
5191 .unwrap()
5192 .into();
5193 log::info!("inserting parent git repo for this worktree: {location_in_repo:?}");
5194 // We associate the external git repo with our root folder and
5195 // also mark where in the git repo the root folder is located.
5196 return (
5197 ignores,
5198 exclude,
5199 Some((
5200 ancestor_dot_git,
5201 WorkDirectory::AboveProject {
5202 absolute_path: ancestor.into(),
5203 location_in_repo,
5204 },
5205 )),
5206 );
5207 };
5208 }
5209
5210 let repo_exclude_abs_path = ancestor_dot_git.join(REPO_EXCLUDE);
5211 if let Ok(repo_exclude) = build_gitignore(&repo_exclude_abs_path, fs.as_ref()).await {
5212 exclude = Some(Arc::new(repo_exclude));
5213 }
5214
5215 // Reached root of git repository.
5216 break;
5217 }
5218 }
5219
5220 (ignores, exclude, None)
5221}
5222
5223fn build_diff(
5224 phase: BackgroundScannerPhase,
5225 old_snapshot: &Snapshot,
5226 new_snapshot: &Snapshot,
5227 event_paths: &[Arc<RelPath>],
5228) -> UpdatedEntriesSet {
5229 use BackgroundScannerPhase::*;
5230 use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated};
5231
5232 // Identify which paths have changed. Use the known set of changed
5233 // parent paths to optimize the search.
5234 let mut changes = Vec::new();
5235 let mut old_paths = old_snapshot.entries_by_path.cursor::<PathKey>(());
5236 let mut new_paths = new_snapshot.entries_by_path.cursor::<PathKey>(());
5237 let mut last_newly_loaded_dir_path = None;
5238 old_paths.next();
5239 new_paths.next();
5240 for path in event_paths {
5241 let path = PathKey(path.clone());
5242 if old_paths.item().is_some_and(|e| e.path < path.0) {
5243 old_paths.seek_forward(&path, Bias::Left);
5244 }
5245 if new_paths.item().is_some_and(|e| e.path < path.0) {
5246 new_paths.seek_forward(&path, Bias::Left);
5247 }
5248 loop {
5249 match (old_paths.item(), new_paths.item()) {
5250 (Some(old_entry), Some(new_entry)) => {
5251 if old_entry.path > path.0
5252 && new_entry.path > path.0
5253 && !old_entry.path.starts_with(&path.0)
5254 && !new_entry.path.starts_with(&path.0)
5255 {
5256 break;
5257 }
5258
5259 match Ord::cmp(&old_entry.path, &new_entry.path) {
5260 Ordering::Less => {
5261 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5262 old_paths.next();
5263 }
5264 Ordering::Equal => {
5265 if phase == EventsReceivedDuringInitialScan {
5266 if old_entry.id != new_entry.id {
5267 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5268 }
5269 // If the worktree was not fully initialized when this event was generated,
5270 // we can't know whether this entry was added during the scan or whether
5271 // it was merely updated.
5272 changes.push((
5273 new_entry.path.clone(),
5274 new_entry.id,
5275 AddedOrUpdated,
5276 ));
5277 } else if old_entry.id != new_entry.id {
5278 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5279 changes.push((new_entry.path.clone(), new_entry.id, Added));
5280 } else if old_entry != new_entry {
5281 if old_entry.kind.is_unloaded() {
5282 last_newly_loaded_dir_path = Some(&new_entry.path);
5283 changes.push((new_entry.path.clone(), new_entry.id, Loaded));
5284 } else {
5285 changes.push((new_entry.path.clone(), new_entry.id, Updated));
5286 }
5287 }
5288 old_paths.next();
5289 new_paths.next();
5290 }
5291 Ordering::Greater => {
5292 let is_newly_loaded = phase == InitialScan
5293 || last_newly_loaded_dir_path
5294 .as_ref()
5295 .is_some_and(|dir| new_entry.path.starts_with(dir));
5296 changes.push((
5297 new_entry.path.clone(),
5298 new_entry.id,
5299 if is_newly_loaded { Loaded } else { Added },
5300 ));
5301 new_paths.next();
5302 }
5303 }
5304 }
5305 (Some(old_entry), None) => {
5306 changes.push((old_entry.path.clone(), old_entry.id, Removed));
5307 old_paths.next();
5308 }
5309 (None, Some(new_entry)) => {
5310 let is_newly_loaded = phase == InitialScan
5311 || last_newly_loaded_dir_path
5312 .as_ref()
5313 .is_some_and(|dir| new_entry.path.starts_with(dir));
5314 changes.push((
5315 new_entry.path.clone(),
5316 new_entry.id,
5317 if is_newly_loaded { Loaded } else { Added },
5318 ));
5319 new_paths.next();
5320 }
5321 (None, None) => break,
5322 }
5323 }
5324 }
5325
5326 changes.into()
5327}
5328
5329fn swap_to_front(child_paths: &mut Vec<PathBuf>, file: &str) {
5330 let position = child_paths
5331 .iter()
5332 .position(|path| path.file_name().unwrap() == file);
5333 if let Some(position) = position {
5334 let temp = child_paths.remove(position);
5335 child_paths.insert(0, temp);
5336 }
5337}
5338
5339fn char_bag_for_path(root_char_bag: CharBag, path: &RelPath) -> CharBag {
5340 let mut result = root_char_bag;
5341 result.extend(path.as_unix_str().chars().map(|c| c.to_ascii_lowercase()));
5342 result
5343}
5344
5345#[derive(Debug)]
5346struct ScanJob {
5347 abs_path: Arc<Path>,
5348 path: Arc<RelPath>,
5349 ignore_stack: IgnoreStack,
5350 scan_queue: Sender<ScanJob>,
5351 ancestor_inodes: TreeSet<u64>,
5352 is_external: bool,
5353}
5354
5355struct UpdateIgnoreStatusJob {
5356 abs_path: Arc<Path>,
5357 ignore_stack: IgnoreStack,
5358 ignore_queue: Sender<UpdateIgnoreStatusJob>,
5359 scan_queue: Sender<ScanJob>,
5360}
5361
5362pub trait WorktreeModelHandle {
5363 #[cfg(any(test, feature = "test-support"))]
5364 fn flush_fs_events<'a>(
5365 &self,
5366 cx: &'a mut gpui::TestAppContext,
5367 ) -> futures::future::LocalBoxFuture<'a, ()>;
5368
5369 #[cfg(any(test, feature = "test-support"))]
5370 fn flush_fs_events_in_root_git_repository<'a>(
5371 &self,
5372 cx: &'a mut gpui::TestAppContext,
5373 ) -> futures::future::LocalBoxFuture<'a, ()>;
5374}
5375
5376impl WorktreeModelHandle for Entity<Worktree> {
5377 // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that
5378 // occurred before the worktree was constructed. These events can cause the worktree to perform
5379 // extra directory scans, and emit extra scan-state notifications.
5380 //
5381 // This function mutates the worktree's directory and waits for those mutations to be picked up,
5382 // to ensure that all redundant FS events have already been processed.
5383 #[cfg(any(test, feature = "test-support"))]
5384 fn flush_fs_events<'a>(
5385 &self,
5386 cx: &'a mut gpui::TestAppContext,
5387 ) -> futures::future::LocalBoxFuture<'a, ()> {
5388 let file_name = "fs-event-sentinel";
5389
5390 let tree = self.clone();
5391 let (fs, root_path) = self.read_with(cx, |tree, _| {
5392 let tree = tree.as_local().unwrap();
5393 (tree.fs.clone(), tree.abs_path.clone())
5394 });
5395
5396 async move {
5397 fs.create_file(&root_path.join(file_name), Default::default())
5398 .await
5399 .unwrap();
5400
5401 let mut events = cx.events(&tree);
5402 while events.next().await.is_some() {
5403 if tree.read_with(cx, |tree, _| {
5404 tree.entry_for_path(RelPath::unix(file_name).unwrap())
5405 .is_some()
5406 }) {
5407 break;
5408 }
5409 }
5410
5411 fs.remove_file(&root_path.join(file_name), Default::default())
5412 .await
5413 .unwrap();
5414 while events.next().await.is_some() {
5415 if tree.read_with(cx, |tree, _| {
5416 tree.entry_for_path(RelPath::unix(file_name).unwrap())
5417 .is_none()
5418 }) {
5419 break;
5420 }
5421 }
5422
5423 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5424 .await;
5425 }
5426 .boxed_local()
5427 }
5428
5429 // This function is similar to flush_fs_events, except that it waits for events to be flushed in
5430 // the .git folder of the root repository.
5431 // The reason for its existence is that a repository's .git folder might live *outside* of the
5432 // worktree and thus its FS events might go through a different path.
5433 // In order to flush those, we need to create artificial events in the .git folder and wait
5434 // for the repository to be reloaded.
5435 #[cfg(any(test, feature = "test-support"))]
5436 fn flush_fs_events_in_root_git_repository<'a>(
5437 &self,
5438 cx: &'a mut gpui::TestAppContext,
5439 ) -> futures::future::LocalBoxFuture<'a, ()> {
5440 let file_name = "fs-event-sentinel";
5441
5442 let tree = self.clone();
5443 let (fs, root_path, mut git_dir_scan_id) = self.read_with(cx, |tree, _| {
5444 let tree = tree.as_local().unwrap();
5445 let local_repo_entry = tree
5446 .git_repositories
5447 .values()
5448 .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone())
5449 .unwrap();
5450 (
5451 tree.fs.clone(),
5452 local_repo_entry.common_dir_abs_path.clone(),
5453 local_repo_entry.git_dir_scan_id,
5454 )
5455 });
5456
5457 let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| {
5458 let tree = tree.as_local().unwrap();
5459 // let repository = tree.repositories.first().unwrap();
5460 let local_repo_entry = tree
5461 .git_repositories
5462 .values()
5463 .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone())
5464 .unwrap();
5465
5466 if local_repo_entry.git_dir_scan_id > *git_dir_scan_id {
5467 *git_dir_scan_id = local_repo_entry.git_dir_scan_id;
5468 true
5469 } else {
5470 false
5471 }
5472 };
5473
5474 async move {
5475 fs.create_file(&root_path.join(file_name), Default::default())
5476 .await
5477 .unwrap();
5478
5479 let mut events = cx.events(&tree);
5480 while events.next().await.is_some() {
5481 if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
5482 break;
5483 }
5484 }
5485
5486 fs.remove_file(&root_path.join(file_name), Default::default())
5487 .await
5488 .unwrap();
5489
5490 while events.next().await.is_some() {
5491 if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) {
5492 break;
5493 }
5494 }
5495
5496 cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete())
5497 .await;
5498 }
5499 .boxed_local()
5500 }
5501}
5502
5503#[derive(Clone, Debug)]
5504struct TraversalProgress<'a> {
5505 max_path: &'a RelPath,
5506 count: usize,
5507 non_ignored_count: usize,
5508 file_count: usize,
5509 non_ignored_file_count: usize,
5510}
5511
5512impl TraversalProgress<'_> {
5513 fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
5514 match (include_files, include_dirs, include_ignored) {
5515 (true, true, true) => self.count,
5516 (true, true, false) => self.non_ignored_count,
5517 (true, false, true) => self.file_count,
5518 (true, false, false) => self.non_ignored_file_count,
5519 (false, true, true) => self.count - self.file_count,
5520 (false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
5521 (false, false, _) => 0,
5522 }
5523 }
5524}
5525
5526impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> {
5527 fn zero(_cx: ()) -> Self {
5528 Default::default()
5529 }
5530
5531 fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) {
5532 self.max_path = summary.max_path.as_ref();
5533 self.count += summary.count;
5534 self.non_ignored_count += summary.non_ignored_count;
5535 self.file_count += summary.file_count;
5536 self.non_ignored_file_count += summary.non_ignored_file_count;
5537 }
5538}
5539
5540impl Default for TraversalProgress<'_> {
5541 fn default() -> Self {
5542 Self {
5543 max_path: RelPath::empty(),
5544 count: 0,
5545 non_ignored_count: 0,
5546 file_count: 0,
5547 non_ignored_file_count: 0,
5548 }
5549 }
5550}
5551
5552#[derive(Debug)]
5553pub struct Traversal<'a> {
5554 snapshot: &'a Snapshot,
5555 cursor: sum_tree::Cursor<'a, 'static, Entry, TraversalProgress<'a>>,
5556 include_ignored: bool,
5557 include_files: bool,
5558 include_dirs: bool,
5559}
5560
5561impl<'a> Traversal<'a> {
5562 fn new(
5563 snapshot: &'a Snapshot,
5564 include_files: bool,
5565 include_dirs: bool,
5566 include_ignored: bool,
5567 start_path: &RelPath,
5568 ) -> Self {
5569 let mut cursor = snapshot.entries_by_path.cursor(());
5570 cursor.seek(&TraversalTarget::path(start_path), Bias::Left);
5571 let mut traversal = Self {
5572 snapshot,
5573 cursor,
5574 include_files,
5575 include_dirs,
5576 include_ignored,
5577 };
5578 if traversal.end_offset() == traversal.start_offset() {
5579 traversal.next();
5580 }
5581 traversal
5582 }
5583
5584 pub fn advance(&mut self) -> bool {
5585 self.advance_by(1)
5586 }
5587
5588 pub fn advance_by(&mut self, count: usize) -> bool {
5589 self.cursor.seek_forward(
5590 &TraversalTarget::Count {
5591 count: self.end_offset() + count,
5592 include_dirs: self.include_dirs,
5593 include_files: self.include_files,
5594 include_ignored: self.include_ignored,
5595 },
5596 Bias::Left,
5597 )
5598 }
5599
5600 pub fn advance_to_sibling(&mut self) -> bool {
5601 while let Some(entry) = self.cursor.item() {
5602 self.cursor
5603 .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left);
5604 if let Some(entry) = self.cursor.item()
5605 && (self.include_files || !entry.is_file())
5606 && (self.include_dirs || !entry.is_dir())
5607 && (self.include_ignored || !entry.is_ignored || entry.is_always_included)
5608 {
5609 return true;
5610 }
5611 }
5612 false
5613 }
5614
5615 pub fn back_to_parent(&mut self) -> bool {
5616 let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else {
5617 return false;
5618 };
5619 self.cursor
5620 .seek(&TraversalTarget::path(parent_path), Bias::Left)
5621 }
5622
5623 pub fn entry(&self) -> Option<&'a Entry> {
5624 self.cursor.item()
5625 }
5626
5627 pub fn snapshot(&self) -> &'a Snapshot {
5628 self.snapshot
5629 }
5630
5631 pub fn start_offset(&self) -> usize {
5632 self.cursor
5633 .start()
5634 .count(self.include_files, self.include_dirs, self.include_ignored)
5635 }
5636
5637 pub fn end_offset(&self) -> usize {
5638 self.cursor
5639 .end()
5640 .count(self.include_files, self.include_dirs, self.include_ignored)
5641 }
5642}
5643
5644impl<'a> Iterator for Traversal<'a> {
5645 type Item = &'a Entry;
5646
5647 fn next(&mut self) -> Option<Self::Item> {
5648 if let Some(item) = self.entry() {
5649 self.advance();
5650 Some(item)
5651 } else {
5652 None
5653 }
5654 }
5655}
5656
5657#[derive(Debug, Clone, Copy)]
5658pub enum PathTarget<'a> {
5659 Path(&'a RelPath),
5660 Successor(&'a RelPath),
5661}
5662
5663impl PathTarget<'_> {
5664 fn cmp_path(&self, other: &RelPath) -> Ordering {
5665 match self {
5666 PathTarget::Path(path) => path.cmp(&other),
5667 PathTarget::Successor(path) => {
5668 if other.starts_with(path) {
5669 Ordering::Greater
5670 } else {
5671 Ordering::Equal
5672 }
5673 }
5674 }
5675 }
5676}
5677
5678impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, PathProgress<'a>> for PathTarget<'_> {
5679 fn cmp(&self, cursor_location: &PathProgress<'a>, _: S::Context<'_>) -> Ordering {
5680 self.cmp_path(cursor_location.max_path)
5681 }
5682}
5683
5684impl<'a, S: Summary> SeekTarget<'a, PathSummary<S>, TraversalProgress<'a>> for PathTarget<'_> {
5685 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: S::Context<'_>) -> Ordering {
5686 self.cmp_path(cursor_location.max_path)
5687 }
5688}
5689
5690#[derive(Debug)]
5691enum TraversalTarget<'a> {
5692 Path(PathTarget<'a>),
5693 Count {
5694 count: usize,
5695 include_files: bool,
5696 include_ignored: bool,
5697 include_dirs: bool,
5698 },
5699}
5700
5701impl<'a> TraversalTarget<'a> {
5702 fn path(path: &'a RelPath) -> Self {
5703 Self::Path(PathTarget::Path(path))
5704 }
5705
5706 fn successor(path: &'a RelPath) -> Self {
5707 Self::Path(PathTarget::Successor(path))
5708 }
5709
5710 fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering {
5711 match self {
5712 TraversalTarget::Path(path) => path.cmp_path(progress.max_path),
5713 TraversalTarget::Count {
5714 count,
5715 include_files,
5716 include_dirs,
5717 include_ignored,
5718 } => Ord::cmp(
5719 count,
5720 &progress.count(*include_files, *include_dirs, *include_ignored),
5721 ),
5722 }
5723 }
5724}
5725
5726impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'_> {
5727 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering {
5728 self.cmp_progress(cursor_location)
5729 }
5730}
5731
5732impl<'a> SeekTarget<'a, PathSummary<sum_tree::NoSummary>, TraversalProgress<'a>>
5733 for TraversalTarget<'_>
5734{
5735 fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering {
5736 self.cmp_progress(cursor_location)
5737 }
5738}
5739
5740pub struct ChildEntriesOptions {
5741 pub include_files: bool,
5742 pub include_dirs: bool,
5743 pub include_ignored: bool,
5744}
5745
5746pub struct ChildEntriesIter<'a> {
5747 parent_path: &'a RelPath,
5748 traversal: Traversal<'a>,
5749}
5750
5751impl<'a> Iterator for ChildEntriesIter<'a> {
5752 type Item = &'a Entry;
5753
5754 fn next(&mut self) -> Option<Self::Item> {
5755 if let Some(item) = self.traversal.entry()
5756 && item.path.starts_with(self.parent_path)
5757 {
5758 self.traversal.advance_to_sibling();
5759 return Some(item);
5760 }
5761 None
5762 }
5763}
5764
5765impl<'a> From<&'a Entry> for proto::Entry {
5766 fn from(entry: &'a Entry) -> Self {
5767 Self {
5768 id: entry.id.to_proto(),
5769 is_dir: entry.is_dir(),
5770 path: entry.path.as_ref().to_proto(),
5771 inode: entry.inode,
5772 mtime: entry.mtime.map(|time| time.into()),
5773 is_ignored: entry.is_ignored,
5774 is_hidden: entry.is_hidden,
5775 is_external: entry.is_external,
5776 is_fifo: entry.is_fifo,
5777 size: Some(entry.size),
5778 canonical_path: entry
5779 .canonical_path
5780 .as_ref()
5781 .map(|path| path.to_string_lossy().into_owned()),
5782 }
5783 }
5784}
5785
5786impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry {
5787 type Error = anyhow::Error;
5788
5789 fn try_from(
5790 (root_char_bag, always_included, entry): (&CharBag, &PathMatcher, proto::Entry),
5791 ) -> Result<Self> {
5792 let kind = if entry.is_dir {
5793 EntryKind::Dir
5794 } else {
5795 EntryKind::File
5796 };
5797
5798 let path =
5799 RelPath::from_proto(&entry.path).context("invalid relative path in proto message")?;
5800 let char_bag = char_bag_for_path(*root_char_bag, &path);
5801 let is_always_included = always_included.is_match(&path);
5802 Ok(Entry {
5803 id: ProjectEntryId::from_proto(entry.id),
5804 kind,
5805 path,
5806 inode: entry.inode,
5807 mtime: entry.mtime.map(|time| time.into()),
5808 size: entry.size.unwrap_or(0),
5809 canonical_path: entry
5810 .canonical_path
5811 .map(|path_string| Arc::from(PathBuf::from(path_string))),
5812 is_ignored: entry.is_ignored,
5813 is_hidden: entry.is_hidden,
5814 is_always_included,
5815 is_external: entry.is_external,
5816 is_private: false,
5817 char_bag,
5818 is_fifo: entry.is_fifo,
5819 })
5820 }
5821}
5822
5823#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
5824pub struct ProjectEntryId(usize);
5825
5826impl ProjectEntryId {
5827 pub const MAX: Self = Self(usize::MAX);
5828 pub const MIN: Self = Self(usize::MIN);
5829
5830 pub fn new(counter: &AtomicUsize) -> Self {
5831 Self(counter.fetch_add(1, SeqCst))
5832 }
5833
5834 pub fn from_proto(id: u64) -> Self {
5835 Self(id as usize)
5836 }
5837
5838 pub fn to_proto(self) -> u64 {
5839 self.0 as u64
5840 }
5841
5842 pub fn from_usize(id: usize) -> Self {
5843 ProjectEntryId(id)
5844 }
5845
5846 pub fn to_usize(self) -> usize {
5847 self.0
5848 }
5849}
5850
5851#[cfg(any(test, feature = "test-support"))]
5852impl CreatedEntry {
5853 pub fn into_included(self) -> Option<Entry> {
5854 match self {
5855 CreatedEntry::Included(entry) => Some(entry),
5856 CreatedEntry::Excluded { .. } => None,
5857 }
5858 }
5859}
5860
5861fn parse_gitfile(content: &str) -> anyhow::Result<&Path> {
5862 let path = content
5863 .strip_prefix("gitdir:")
5864 .with_context(|| format!("parsing gitfile content {content:?}"))?;
5865 Ok(Path::new(path.trim()))
5866}
5867
5868async fn discover_git_paths(dot_git_abs_path: &Arc<Path>, fs: &dyn Fs) -> (Arc<Path>, Arc<Path>) {
5869 let mut repository_dir_abs_path = dot_git_abs_path.clone();
5870 let mut common_dir_abs_path = dot_git_abs_path.clone();
5871
5872 if let Some(path) = fs
5873 .load(dot_git_abs_path)
5874 .await
5875 .ok()
5876 .as_ref()
5877 .and_then(|contents| parse_gitfile(contents).log_err())
5878 {
5879 let path = dot_git_abs_path
5880 .parent()
5881 .unwrap_or(Path::new(""))
5882 .join(path);
5883 if let Some(path) = fs.canonicalize(&path).await.log_err() {
5884 repository_dir_abs_path = Path::new(&path).into();
5885 common_dir_abs_path = repository_dir_abs_path.clone();
5886
5887 if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok()
5888 && let Some(commondir_path) = fs
5889 .canonicalize(&path.join(commondir_contents.trim()))
5890 .await
5891 .log_err()
5892 {
5893 common_dir_abs_path = commondir_path.as_path().into();
5894 }
5895 }
5896 };
5897 (repository_dir_abs_path, common_dir_abs_path)
5898}
5899
5900struct NullWatcher;
5901
5902impl fs::Watcher for NullWatcher {
5903 fn add(&self, _path: &Path) -> Result<()> {
5904 Ok(())
5905 }
5906
5907 fn remove(&self, _path: &Path) -> Result<()> {
5908 Ok(())
5909 }
5910}
5911
5912const FILE_ANALYSIS_BYTES: usize = 1024;
5913
5914async fn decode_file_text(
5915 fs: &dyn Fs,
5916 abs_path: &Path,
5917) -> Result<(String, &'static Encoding, bool)> {
5918 let mut file = fs
5919 .open_sync(&abs_path)
5920 .await
5921 .with_context(|| format!("opening file {abs_path:?}"))?;
5922
5923 // First, read the beginning of the file to determine its kind and encoding.
5924 // We do not want to load an entire large blob into memory only to discard it.
5925 let mut file_first_bytes = Vec::with_capacity(FILE_ANALYSIS_BYTES);
5926 let mut buf = [0u8; FILE_ANALYSIS_BYTES];
5927 let mut reached_eof = false;
5928 loop {
5929 if file_first_bytes.len() >= FILE_ANALYSIS_BYTES {
5930 break;
5931 }
5932 let n = file
5933 .read(&mut buf)
5934 .with_context(|| format!("reading bytes of the file {abs_path:?}"))?;
5935 if n == 0 {
5936 reached_eof = true;
5937 break;
5938 }
5939 file_first_bytes.extend_from_slice(&buf[..n]);
5940 }
5941 let (bom_encoding, byte_content) = decode_byte_header(&file_first_bytes);
5942 anyhow::ensure!(
5943 byte_content != ByteContent::Binary,
5944 "Binary files are not supported"
5945 );
5946
5947 // If the file is eligible for opening, read the rest of the file.
5948 let mut content = file_first_bytes;
5949 if !reached_eof {
5950 let mut buf = [0u8; 8 * 1024];
5951 loop {
5952 let n = file
5953 .read(&mut buf)
5954 .with_context(|| format!("reading remaining bytes of the file {abs_path:?}"))?;
5955 if n == 0 {
5956 break;
5957 }
5958 content.extend_from_slice(&buf[..n]);
5959 }
5960 }
5961 decode_byte_full(content, bom_encoding, byte_content)
5962}
5963
5964fn decode_byte_header(prefix: &[u8]) -> (Option<&'static Encoding>, ByteContent) {
5965 if let Some((encoding, _bom_len)) = Encoding::for_bom(prefix) {
5966 return (Some(encoding), ByteContent::Unknown);
5967 }
5968 (None, analyze_byte_content(prefix))
5969}
5970
5971fn decode_byte_full(
5972 bytes: Vec<u8>,
5973 bom_encoding: Option<&'static Encoding>,
5974 byte_content: ByteContent,
5975) -> Result<(String, &'static Encoding, bool)> {
5976 if let Some(encoding) = bom_encoding {
5977 let (cow, _) = encoding.decode_with_bom_removal(&bytes);
5978 return Ok((cow.into_owned(), encoding, true));
5979 }
5980
5981 match byte_content {
5982 ByteContent::Utf16Le => {
5983 let encoding = encoding_rs::UTF_16LE;
5984 let (cow, _, _) = encoding.decode(&bytes);
5985 return Ok((cow.into_owned(), encoding, false));
5986 }
5987 ByteContent::Utf16Be => {
5988 let encoding = encoding_rs::UTF_16BE;
5989 let (cow, _, _) = encoding.decode(&bytes);
5990 return Ok((cow.into_owned(), encoding, false));
5991 }
5992 ByteContent::Binary => {
5993 anyhow::bail!("Binary files are not supported");
5994 }
5995 ByteContent::Unknown => {}
5996 }
5997
5998 fn detect_encoding(bytes: Vec<u8>) -> (String, &'static Encoding) {
5999 let mut detector = EncodingDetector::new();
6000 detector.feed(&bytes, true);
6001
6002 let encoding = detector.guess(None, true); // Use None for TLD hint to ensure neutral detection logic.
6003
6004 let (cow, _, _) = encoding.decode(&bytes);
6005 (cow.into_owned(), encoding)
6006 }
6007
6008 match String::from_utf8(bytes) {
6009 Ok(text) => {
6010 // ISO-2022-JP (and other ISO-2022 variants) consists entirely of 7-bit ASCII bytes,
6011 // so it is valid UTF-8. However, it contains escape sequences starting with '\x1b'.
6012 // If we find an escape character, we double-check the encoding to prevent
6013 // displaying raw escape sequences instead of the correct characters.
6014 if text.contains('\x1b') {
6015 let (s, enc) = detect_encoding(text.into_bytes());
6016 Ok((s, enc, false))
6017 } else {
6018 Ok((text, encoding_rs::UTF_8, false))
6019 }
6020 }
6021 Err(e) => {
6022 let (s, enc) = detect_encoding(e.into_bytes());
6023 Ok((s, enc, false))
6024 }
6025 }
6026}
6027
6028#[derive(PartialEq)]
6029enum ByteContent {
6030 Utf16Le,
6031 Utf16Be,
6032 Binary,
6033 Unknown,
6034}
6035
6036// Heuristic check using null byte distribution plus a generic text-likeness
6037// heuristic. This prefers UTF-16 when many bytes are NUL and otherwise
6038// distinguishes between text-like and binary-like content.
6039fn analyze_byte_content(bytes: &[u8]) -> ByteContent {
6040 if bytes.len() < 2 {
6041 return ByteContent::Unknown;
6042 }
6043
6044 if is_known_binary_header(bytes) {
6045 return ByteContent::Binary;
6046 }
6047
6048 let limit = bytes.len().min(FILE_ANALYSIS_BYTES);
6049 let mut even_null_count = 0usize;
6050 let mut odd_null_count = 0usize;
6051 let mut non_text_like_count = 0usize;
6052
6053 for (i, &byte) in bytes[..limit].iter().enumerate() {
6054 if byte == 0 {
6055 if i % 2 == 0 {
6056 even_null_count += 1;
6057 } else {
6058 odd_null_count += 1;
6059 }
6060 non_text_like_count += 1;
6061 continue;
6062 }
6063
6064 let is_text_like = match byte {
6065 b'\t' | b'\n' | b'\r' | 0x0C => true,
6066 0x20..=0x7E => true,
6067 // Treat bytes that are likely part of UTF-8 or single-byte encodings as text-like.
6068 0x80..=0xBF | 0xC2..=0xF4 => true,
6069 _ => false,
6070 };
6071
6072 if !is_text_like {
6073 non_text_like_count += 1;
6074 }
6075 }
6076
6077 let total_null_count = even_null_count + odd_null_count;
6078
6079 // If there are no NUL bytes at all, this is overwhelmingly likely to be text.
6080 if total_null_count == 0 {
6081 return ByteContent::Unknown;
6082 }
6083
6084 if total_null_count >= limit / 16 {
6085 if even_null_count > odd_null_count * 4 {
6086 return ByteContent::Utf16Be;
6087 }
6088 if odd_null_count > even_null_count * 4 {
6089 return ByteContent::Utf16Le;
6090 }
6091 return ByteContent::Binary;
6092 }
6093
6094 if non_text_like_count * 100 < limit * 8 {
6095 ByteContent::Unknown
6096 } else {
6097 ByteContent::Binary
6098 }
6099}
6100
6101fn is_known_binary_header(bytes: &[u8]) -> bool {
6102 bytes.starts_with(b"%PDF-") // PDF
6103 || bytes.starts_with(b"PK\x03\x04") // ZIP local header
6104 || bytes.starts_with(b"PK\x05\x06") // ZIP end of central directory
6105 || bytes.starts_with(b"PK\x07\x08") // ZIP spanning/splitting
6106 || bytes.starts_with(b"\x89PNG\r\n\x1a\n") // PNG
6107 || bytes.starts_with(b"\xFF\xD8\xFF") // JPEG
6108 || bytes.starts_with(b"GIF87a") // GIF87a
6109 || bytes.starts_with(b"GIF89a") // GIF89a
6110}