mod ignore;mod worktree_settings;#[cfg(test)]mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};use anyhow::{anyhow, Context as _, Result};use client::{proto, Client};use clock::ReplicaId;use collections::{HashMap, HashSet, VecDeque};use fs::{copy_recursive, RemoveOptions};use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, Fs,};use futures::{ channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, task::Poll, FutureExt as _, Stream, StreamExt,};use fuzzy::CharBag;use git::{DOT_GIT, GITIGNORE};use gpui::{ AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, Task,};use ignore::IgnoreStack;use itertools::Itertools;use language::{ proto::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped,};use lsp::{DiagnosticSeverity, LanguageServerId};use parking_lot::Mutex;use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch,};use serde::Serialize;use settings::{Settings, SettingsLocation, SettingsStore};use smol::channel::{self, Sender};use std::{ any::Any, cmp::{self, Ordering}, convert::TryFrom, ffi::OsStr, fmt, future::Future, mem, ops::{AddAssign, Deref, DerefMut, Sub}, path::{Path, PathBuf}, pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, SystemTime},};use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};use text::BufferId;use util::{ paths::{PathMatcher, HOME}, ResultExt,}; pub use worktree_settings::WorktreeSettings; #[cfg(feature = "test-support")]pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100);#[cfg(not(feature = "test-support"))]pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]pub struct WorktreeId(usize); pub enum Worktree { Local(LocalWorktree), Remote(RemoteWorktree),} pub struct LocalWorktree { snapshot: LocalSnapshot, scan_requests_tx: channel::Sender<ScanRequest>, path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>, is_scanning: (watch::Sender<bool>, watch::Receiver<bool>), _background_scanner_tasks: Vec<Task<()>>, share: Option<ShareState>, diagnostics: HashMap< Arc<Path>, Vec<( LanguageServerId, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>, )>, >, diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>, client: Arc<Client>, fs: Arc<dyn Fs>, fs_case_sensitive: bool, visible: bool,} struct ScanRequest { relative_paths: Vec<Arc<Path>>, done: barrier::Sender,} pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc<Mutex<Snapshot>>, project_id: u64, client: Arc<Client>, updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>, visible: bool, disconnected: bool,} #[derive(Clone)]pub struct Snapshot { id: WorktreeId, abs_path: Arc<Path>, root_name: String, root_char_bag: CharBag, entries_by_path: SumTree<Entry>, entries_by_id: SumTree<PathEntry>, repository_entries: TreeMap<RepositoryWorkDirectory, RepositoryEntry>, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused /// by some operation performed on the worktree, such as reading or /// writing a file, or by an event reported by the filesystem. scan_id: usize, /// The latest scan id that has completed, and whose preceding scans /// have all completed. The current `scan_id` could be more than one /// greater than the `completed_scan_id` if operations are performed /// on the worktree while it is processing a file-system event. completed_scan_id: usize,} #[derive(Clone, Debug, PartialEq, Eq)]pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option<Arc<str>>,} impl RepositoryEntry { pub fn branch(&self) -> Option<Arc<str>> { self.branch.clone() } pub fn work_directory_id(&self) -> ProjectEntryId { *self.work_directory } pub fn work_directory(&self, snapshot: &Snapshot) -> Option<RepositoryWorkDirectory> { snapshot .entry_for_id(self.work_directory_id()) .map(|entry| RepositoryWorkDirectory(entry.path.clone())) } pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id().to_proto(), branch: self.branch.as_ref().map(|str| str.to_string()), } }} impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } }} /// This path corresponds to the 'content path' (the folder that contains the .git)#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]pub struct RepositoryWorkDirectory(pub(crate) Arc<Path>); impl Default for RepositoryWorkDirectory { fn default() -> Self { RepositoryWorkDirectory(Arc::from(Path::new(""))) }} impl AsRef<Path> for RepositoryWorkDirectory { fn as_ref(&self) -> &Path { self.0.as_ref() }} #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]pub struct WorkDirectoryEntry(ProjectEntryId); impl WorkDirectoryEntry { pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> { let entry = worktree .entry_for_id(self.0) .ok_or_else(|| anyhow!("entry not found"))?; let path = path .strip_prefix(&entry.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?; Ok(path.into()) }} impl Deref for WorkDirectoryEntry { type Target = ProjectEntryId; fn deref(&self) -> &Self::Target { &self.0 }} impl From<ProjectEntryId> for WorkDirectoryEntry { fn from(value: ProjectEntryId) -> Self { WorkDirectoryEntry(value) }} #[derive(Debug, Clone)]pub struct LocalSnapshot { snapshot: Snapshot, /// All of the gitignore files in the worktree, indexed by their relative path. /// The boolean indicates whether the gitignore needs to be updated. ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>, /// All of the git repositories in the worktree, indexed by the project entry /// id of their parent directory. git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, file_scan_exclusions: Vec<PathMatcher>, private_files: Vec<PathMatcher>,} struct BackgroundScannerState { snapshot: LocalSnapshot, scanned_dirs: HashSet<ProjectEntryId>, path_prefixes_to_scan: HashSet<Arc<Path>>, paths_to_scan: HashSet<Arc<Path>>, /// The ids of all of the entries that were removed from the snapshot /// as part of the current update. These entry ids may be re-used /// if the same inode is discovered at a new path, or if the given /// path is re-created after being deleted. removed_entry_ids: HashMap<u64, ProjectEntryId>, changed_paths: Vec<Arc<Path>>, prev_snapshot: Snapshot,} #[derive(Debug, Clone)]pub struct LocalRepositoryEntry { pub(crate) git_dir_scan_id: usize, pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file pub(crate) git_dir_path: Arc<Path>,} impl LocalRepositoryEntry { pub fn load_index_text(&self, relative_file_path: &Path) -> Option<String> { self.repo_ptr.lock().load_index_text(relative_file_path) }} impl Deref for LocalSnapshot { type Target = Snapshot; fn deref(&self) -> &Self::Target { &self.snapshot }} impl DerefMut for LocalSnapshot { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.snapshot }} enum ScanState { Started, Updated { snapshot: LocalSnapshot, changes: UpdatedEntriesSet, barrier: Option<barrier::Sender>, scanning: bool, },} struct ShareState { project_id: u64, snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>, resume_updates: watch::Sender<()>, _maintain_remote_snapshot: Task<Option<()>>,} #[derive(Clone)]pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet),} impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( client: Arc<Client>, path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncAppContext, ) -> Result<Model<Self>> { // After determining whether the root entry is a file or a directory, populate the // snapshot's "root name", which will be used for the purpose of fuzzy matching. let abs_path = path.into(); let metadata = fs .metadata(&abs_path) .await .context("failed to stat worktree path")?; let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" ); true }); let closure_fs = Arc::clone(&fs); let closure_next_entry_id = Arc::clone(&next_entry_id); let closure_abs_path = abs_path.to_path_buf(); cx.new_model(move |cx: &mut ModelContext<Worktree>| { cx.observe_global::<SettingsStore>(move |this, cx| { if let Self::Local(this) = this { let new_file_scan_exclusions = path_matchers( WorktreeSettings::get_global(cx) .file_scan_exclusions .as_deref(), "file_scan_exclusions", ); let new_private_files = path_matchers( WorktreeSettings::get(Some(settings::SettingsLocation { worktree_id: cx.handle().entity_id().as_u64() as usize, path: Path::new("") }), cx).private_files.as_deref(), "private_files", ); if new_file_scan_exclusions != this.snapshot.file_scan_exclusions || new_private_files != this.snapshot.private_files { this.snapshot.file_scan_exclusions = new_file_scan_exclusions; this.snapshot.private_files = new_private_files; log::info!( "Re-scanning directories, new scan exclude files: {:?}, new dotenv files: {:?}", this.snapshot .file_scan_exclusions .iter() .map(ToString::to_string) .collect::<Vec<_>>(), this.snapshot .private_files .iter() .map(ToString::to_string) .collect::<Vec<_>>() ); let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); this.scan_requests_tx = scan_requests_tx; this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; this._background_scanner_tasks = start_background_scan_tasks( &closure_abs_path, this.snapshot(), scan_requests_rx, path_prefixes_to_scan_rx, Arc::clone(&closure_next_entry_id), Arc::clone(&closure_fs), cx, ); this.is_scanning = watch::channel_with(true); } } }) .detach(); let root_name = abs_path .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()); let mut snapshot = LocalSnapshot { file_scan_exclusions: path_matchers( WorktreeSettings::get_global(cx) .file_scan_exclusions .as_deref(), "file_scan_exclusions", ), private_files: path_matchers( WorktreeSettings::get(Some(SettingsLocation { worktree_id: cx.handle().entity_id().as_u64() as usize, path: Path::new(""), }), cx).private_files.as_deref(), "private_files", ), ignores_by_parent_abs_path: Default::default(), git_repositories: Default::default(), snapshot: Snapshot { id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize), abs_path: abs_path.to_path_buf().into(), root_name: root_name.clone(), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }, }; if let Some(metadata) = metadata { snapshot.insert_entry( Entry::new( Arc::from(Path::new("")), &metadata, &next_entry_id, snapshot.root_char_bag, ), fs.as_ref(), ); } let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); let task_snapshot = snapshot.clone(); Worktree::Local(LocalWorktree { snapshot, is_scanning: watch::channel_with(true), share: None, scan_requests_tx, path_prefixes_to_scan_tx, _background_scanner_tasks: start_background_scan_tasks( &abs_path, task_snapshot, scan_requests_rx, path_prefixes_to_scan_rx, Arc::clone(&next_entry_id), Arc::clone(&fs), cx, ), diagnostics: Default::default(), diagnostic_summaries: Default::default(), client, fs, fs_case_sensitive, visible, }) }) } pub fn remote( project_remote_id: u64, replica_id: ReplicaId, worktree: proto::WorktreeMetadata, client: Arc<Client>, cx: &mut AppContext, ) -> Model<Self> { cx.new_model(|cx: &mut ModelContext<Self>| { let snapshot = Snapshot { id: WorktreeId(worktree.id as usize), abs_path: Arc::from(PathBuf::from(worktree.abs_path)), root_name: worktree.root_name.clone(), root_char_bag: worktree .root_name .chars() .map(|c| c.to_ascii_lowercase()) .collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));Lines 1–500 of 4862. Load more
mod ignore; mod worktree_settings; #[cfg(test)] mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use client::{proto, Client}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{copy_recursive, RemoveOptions}; use fs::{ repository::{GitFileStatus, GitRepository, RepoPath}, Fs, }; use futures::{ channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, task::Poll, FutureExt as _, Stream, StreamExt, }; use fuzzy::CharBag; use git::{DOT_GIT, GITIGNORE}; use gpui::{ AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, Task, }; use ignore::IgnoreStack; use itertools::Itertools; use language::{ proto::{ deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending, serialize_version, }, Buffer, Capability, DiagnosticEntry, File as _, LineEnding, PointUtf16, Rope, RopeFingerprint, Unclipped, }; use lsp::{DiagnosticSeverity, LanguageServerId}; use parking_lot::Mutex; use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch, }; use serde::Serialize; use settings::{Settings, SettingsLocation, SettingsStore}; use smol::channel::{self, Sender}; use std::{ any::Any, cmp::{self, Ordering}, convert::TryFrom, ffi::OsStr, fmt, future::Future, mem, ops::{AddAssign, Deref, DerefMut, Sub}, path::{Path, PathBuf}, pin::Pin, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, Arc, }, time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::BufferId; use util::{ paths::{PathMatcher, HOME}, ResultExt, }; pub use worktree_settings::WorktreeSettings; #[cfg(feature = "test-support")] pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); #[cfg(not(feature = "test-support"))] pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); #[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)] pub struct WorktreeId(usize); pub enum Worktree { Local(LocalWorktree), Remote(RemoteWorktree), } pub struct LocalWorktree { snapshot: LocalSnapshot, scan_requests_tx: channel::Sender<ScanRequest>, path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>, is_scanning: (watch::Sender<bool>, watch::Receiver<bool>), _background_scanner_tasks: Vec<Task<()>>, share: Option<ShareState>, diagnostics: HashMap< Arc<Path>, Vec<( LanguageServerId, Vec<DiagnosticEntry<Unclipped<PointUtf16>>>, )>, >, diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>, client: Arc<Client>, fs: Arc<dyn Fs>, fs_case_sensitive: bool, visible: bool, } struct ScanRequest { relative_paths: Vec<Arc<Path>>, done: barrier::Sender, } pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc<Mutex<Snapshot>>, project_id: u64, client: Arc<Client>, updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, diagnostic_summaries: HashMap<Arc<Path>, HashMap<LanguageServerId, DiagnosticSummary>>, visible: bool, disconnected: bool, } #[derive(Clone)] pub struct Snapshot { id: WorktreeId, abs_path: Arc<Path>, root_name: String, root_char_bag: CharBag, entries_by_path: SumTree<Entry>, entries_by_id: SumTree<PathEntry>, repository_entries: TreeMap<RepositoryWorkDirectory, RepositoryEntry>, /// A number that increases every time the worktree begins scanning /// a set of paths from the filesystem. This scanning could be caused /// by some operation performed on the worktree, such as reading or /// writing a file, or by an event reported by the filesystem. scan_id: usize, /// The latest scan id that has completed, and whose preceding scans /// have all completed. The current `scan_id` could be more than one /// greater than the `completed_scan_id` if operations are performed /// on the worktree while it is processing a file-system event. completed_scan_id: usize, } #[derive(Clone, Debug, PartialEq, Eq)] pub struct RepositoryEntry { pub(crate) work_directory: WorkDirectoryEntry, pub(crate) branch: Option<Arc<str>>, } impl RepositoryEntry { pub fn branch(&self) -> Option<Arc<str>> { self.branch.clone() } pub fn work_directory_id(&self) -> ProjectEntryId { *self.work_directory } pub fn work_directory(&self, snapshot: &Snapshot) -> Option<RepositoryWorkDirectory> { snapshot .entry_for_id(self.work_directory_id()) .map(|entry| RepositoryWorkDirectory(entry.path.clone())) } pub fn build_update(&self, _: &Self) -> proto::RepositoryEntry { proto::RepositoryEntry { work_directory_id: self.work_directory_id().to_proto(), branch: self.branch.as_ref().map(|str| str.to_string()), } } } impl From<&RepositoryEntry> for proto::RepositoryEntry { fn from(value: &RepositoryEntry) -> Self { proto::RepositoryEntry { work_directory_id: value.work_directory.to_proto(), branch: value.branch.as_ref().map(|str| str.to_string()), } } } /// This path corresponds to the 'content path' (the folder that contains the .git) #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct RepositoryWorkDirectory(pub(crate) Arc<Path>); impl Default for RepositoryWorkDirectory { fn default() -> Self { RepositoryWorkDirectory(Arc::from(Path::new(""))) } } impl AsRef<Path> for RepositoryWorkDirectory { fn as_ref(&self) -> &Path { self.0.as_ref() } } #[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] pub struct WorkDirectoryEntry(ProjectEntryId); impl WorkDirectoryEntry { pub(crate) fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> { let entry = worktree .entry_for_id(self.0) .ok_or_else(|| anyhow!("entry not found"))?; let path = path .strip_prefix(&entry.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?; Ok(path.into()) } } impl Deref for WorkDirectoryEntry { type Target = ProjectEntryId; fn deref(&self) -> &Self::Target { &self.0 } } impl From<ProjectEntryId> for WorkDirectoryEntry { fn from(value: ProjectEntryId) -> Self { WorkDirectoryEntry(value) } } #[derive(Debug, Clone)] pub struct LocalSnapshot { snapshot: Snapshot, /// All of the gitignore files in the worktree, indexed by their relative path. /// The boolean indicates whether the gitignore needs to be updated. ignores_by_parent_abs_path: HashMap<Arc<Path>, (Arc<Gitignore>, bool)>, /// All of the git repositories in the worktree, indexed by the project entry /// id of their parent directory. git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>, file_scan_exclusions: Vec<PathMatcher>, private_files: Vec<PathMatcher>, } struct BackgroundScannerState { snapshot: LocalSnapshot, scanned_dirs: HashSet<ProjectEntryId>, path_prefixes_to_scan: HashSet<Arc<Path>>, paths_to_scan: HashSet<Arc<Path>>, /// The ids of all of the entries that were removed from the snapshot /// as part of the current update. These entry ids may be re-used /// if the same inode is discovered at a new path, or if the given /// path is re-created after being deleted. removed_entry_ids: HashMap<u64, ProjectEntryId>, changed_paths: Vec<Arc<Path>>, prev_snapshot: Snapshot, } #[derive(Debug, Clone)] pub struct LocalRepositoryEntry { pub(crate) git_dir_scan_id: usize, pub(crate) repo_ptr: Arc<Mutex<dyn GitRepository>>, /// Path to the actual .git folder. /// Note: if .git is a file, this points to the folder indicated by the .git file pub(crate) git_dir_path: Arc<Path>, } impl LocalRepositoryEntry { pub fn load_index_text(&self, relative_file_path: &Path) -> Option<String> { self.repo_ptr.lock().load_index_text(relative_file_path) } } impl Deref for LocalSnapshot { type Target = Snapshot; fn deref(&self) -> &Self::Target { &self.snapshot } } impl DerefMut for LocalSnapshot { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.snapshot } } enum ScanState { Started, Updated { snapshot: LocalSnapshot, changes: UpdatedEntriesSet, barrier: Option<barrier::Sender>, scanning: bool, }, } struct ShareState { project_id: u64, snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet, UpdatedGitRepositoriesSet)>, resume_updates: watch::Sender<()>, _maintain_remote_snapshot: Task<Option<()>>, } #[derive(Clone)] pub enum Event { UpdatedEntries(UpdatedEntriesSet), UpdatedGitRepositories(UpdatedGitRepositoriesSet), } impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( client: Arc<Client>, path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncAppContext, ) -> Result<Model<Self>> { // After determining whether the root entry is a file or a directory, populate the // snapshot's "root name", which will be used for the purpose of fuzzy matching. let abs_path = path.into(); let metadata = fs .metadata(&abs_path) .await .context("failed to stat worktree path")?; let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { log::error!( "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" ); true }); let closure_fs = Arc::clone(&fs); let closure_next_entry_id = Arc::clone(&next_entry_id); let closure_abs_path = abs_path.to_path_buf(); cx.new_model(move |cx: &mut ModelContext<Worktree>| { cx.observe_global::<SettingsStore>(move |this, cx| { if let Self::Local(this) = this { let new_file_scan_exclusions = path_matchers( WorktreeSettings::get_global(cx) .file_scan_exclusions .as_deref(), "file_scan_exclusions", ); let new_private_files = path_matchers( WorktreeSettings::get(Some(settings::SettingsLocation { worktree_id: cx.handle().entity_id().as_u64() as usize, path: Path::new("") }), cx).private_files.as_deref(), "private_files", ); if new_file_scan_exclusions != this.snapshot.file_scan_exclusions || new_private_files != this.snapshot.private_files { this.snapshot.file_scan_exclusions = new_file_scan_exclusions; this.snapshot.private_files = new_private_files; log::info!( "Re-scanning directories, new scan exclude files: {:?}, new dotenv files: {:?}", this.snapshot .file_scan_exclusions .iter() .map(ToString::to_string) .collect::<Vec<_>>(), this.snapshot .private_files .iter() .map(ToString::to_string) .collect::<Vec<_>>() ); let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); this.scan_requests_tx = scan_requests_tx; this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; this._background_scanner_tasks = start_background_scan_tasks( &closure_abs_path, this.snapshot(), scan_requests_rx, path_prefixes_to_scan_rx, Arc::clone(&closure_next_entry_id), Arc::clone(&closure_fs), cx, ); this.is_scanning = watch::channel_with(true); } } }) .detach(); let root_name = abs_path .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()); let mut snapshot = LocalSnapshot { file_scan_exclusions: path_matchers( WorktreeSettings::get_global(cx) .file_scan_exclusions .as_deref(), "file_scan_exclusions", ), private_files: path_matchers( WorktreeSettings::get(Some(SettingsLocation { worktree_id: cx.handle().entity_id().as_u64() as usize, path: Path::new(""), }), cx).private_files.as_deref(), "private_files", ), ignores_by_parent_abs_path: Default::default(), git_repositories: Default::default(), snapshot: Snapshot { id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize), abs_path: abs_path.to_path_buf().into(), root_name: root_name.clone(), root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }, }; if let Some(metadata) = metadata { snapshot.insert_entry( Entry::new( Arc::from(Path::new("")), &metadata, &next_entry_id, snapshot.root_char_bag, ), fs.as_ref(), ); } let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); let task_snapshot = snapshot.clone(); Worktree::Local(LocalWorktree { snapshot, is_scanning: watch::channel_with(true), share: None, scan_requests_tx, path_prefixes_to_scan_tx, _background_scanner_tasks: start_background_scan_tasks( &abs_path, task_snapshot, scan_requests_rx, path_prefixes_to_scan_rx, Arc::clone(&next_entry_id), Arc::clone(&fs), cx, ), diagnostics: Default::default(), diagnostic_summaries: Default::default(), client, fs, fs_case_sensitive, visible, }) }) } pub fn remote( project_remote_id: u64, replica_id: ReplicaId, worktree: proto::WorktreeMetadata, client: Arc<Client>, cx: &mut AppContext, ) -> Model<Self> { cx.new_model(|cx: &mut ModelContext<Self>| { let snapshot = Snapshot { id: WorktreeId(worktree.id as usize), abs_path: Arc::from(PathBuf::from(worktree.abs_path)), root_name: worktree.root_name.clone(), root_char_bag: worktree .root_name .chars() .map(|c| c.to_ascii_lowercase()) .collect(), entries_by_path: Default::default(), entries_by_id: Default::default(), repository_entries: Default::default(), scan_id: 1, completed_scan_id: 0, }; let (updates_tx, mut updates_rx) = mpsc::unbounded(); let background_snapshot = Arc::new(Mutex::new(snapshot.clone()));