Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:31:24.647Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

worktree.rs

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