mod ignore;mod worktree_settings;#[cfg(test)]mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};use anyhow::{anyhow, Context as _, Result};use clock::ReplicaId;use collections::{HashMap, HashSet, VecDeque};use fs::{copy_recursive, Fs, PathEvent, RemoveOptions, Watcher};use futures::{ channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, stream::select, task::Poll, FutureExt as _, Stream, StreamExt,};use fuzzy::CharBag;use git::{ repository::{GitFileStatus, GitRepository, RepoPath}, status::GitStatus, DOT_GIT, GITIGNORE,};use gpui::{ AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, Task,};use ignore::IgnoreStack;use parking_lot::Mutex;use paths::local_settings_folder_relative_path;use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch,};use rpc::{proto, AnyProtoClient};pub use settings::WorktreeId;use settings::{Settings, SettingsLocation, SettingsStore};use smallvec::{smallvec, SmallVec};use smol::channel::{self, Sender};use std::{ any::Any, cmp::Ordering, collections::hash_map, 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, Instant, SystemTime},};use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};use text::{LineEnding, Rope};use util::{paths::home_dir, 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); /// A set of local or remote files that are being opened as part of a project./// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates./// Stores git repositories data and the diagnostics for the file(s).////// Has an absolute path, and may be set to be visible in Zed UI or not./// May correspond to a directory or a single file./// Possible examples:/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree/// * a directory opened in Zed — may be added as a visible entry to the current worktree////// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries.pub enum Worktree { Local(LocalWorktree), Remote(RemoteWorktree),} /// An entry, created in the worktree.#[derive(Debug)]pub enum CreatedEntry { /// Got created and indexed by the worktree, receiving a corresponding entry. Included(Entry), /// Got created, but not indexed due to falling under exclusion filters. Excluded { abs_path: PathBuf },} pub struct LoadedFile { pub file: Arc<File>, pub text: String, pub diff_base: Option<String>,} 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<()>>, update_observer: Option<UpdateObservationState>, fs: Arc<dyn Fs>, fs_case_sensitive: bool, visible: bool, next_entry_id: Arc<AtomicUsize>, settings: WorktreeSettings, share_private_files: bool,} struct ScanRequest { relative_paths: Vec<Arc<Path>>, done: SmallVec<[barrier::Sender; 1]>,} pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>, project_id: u64, client: AnyProtoClient, updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>, update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, 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>>, /// If location_in_repo is set, it means the .git folder is external /// and in a parent folder of the project root. /// In that case, the work_directory field will point to the /// project-root and location_in_repo contains the location of the /// project-root in the repository. /// /// Example: /// /// my_root_folder/ <-- repository root /// .git /// my_sub_folder_1/ /// project_root/ <-- Project root, Zed opened here /// ... /// /// For this setup, the attributes will have the following values: /// /// work_directory: pointing to "" entry /// location_in_repo: Some("my_sub_folder_1/project_root") pub(crate) location_in_repo: Option<Arc<Path>>,} 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 { self.into() } /// relativize returns the given project path relative to the root folder of the /// repository. /// If the root of the repository (and its .git folder) are located in a parent folder /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. pub fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> { let relativize_path = |path: &Path| { let entry = worktree .entry_for_id(self.work_directory.0) .ok_or_else(|| anyhow!("entry not found"))?; let relativized_path = path .strip_prefix(&entry.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?; Ok(relativized_path.into()) }; if let Some(location_in_repo) = &self.location_in_repo { relativize_path(&location_in_repo.join(path)) } else { relativize_path(path) } }} 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' of a repository in relation/// to Zed's project root./// In the majority of the cases, this is the folder that contains the .git folder./// But if a sub-folder of a git repository is opened, this corresponds to the/// project root and the .git folder is located in a parent directory.#[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 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>,} 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_entries: HashMap<u64, Entry>, 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<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 repo(&self) -> &Arc<dyn GitRepository> { &self.repo_ptr }} 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: SmallVec<[barrier::Sender; 1]>, scanning: bool, },} struct UpdateObservationState { 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), DeletedEntry(ProjectEntryId),} static EMPTY_PATH: &str = ""; impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncAppContext, ) -> Result<Model<Self>> { 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 }); cx.new_model(move |cx: &mut ModelContext<Worktree>| { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), git_repositories: Default::default(), snapshot: Snapshot::new( cx.entity_id().as_u64(), abs_path .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()), abs_path, ), }; if let Some(metadata) = metadata { snapshot.insert_entry( Entry::new( Arc::from(Path::new("")), &metadata, &next_entry_id, snapshot.root_char_bag, None, ), fs.as_ref(), ); } let worktree_id = snapshot.id(); let settings_location = Some(SettingsLocation { worktree_id, path: Path::new(EMPTY_PATH), }); let settings = WorktreeSettings::get(settings_location, cx).clone(); cx.observe_global::<SettingsStore>(move |this, cx| { if let Self::Local(this) = this { let settings = WorktreeSettings::get(settings_location, cx).clone(); if settings != this.settings { this.settings = settings; this.restart_background_scanners(cx); } } }) .detach(); let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); let mut worktree = LocalWorktree { share_private_files: false, next_entry_id, snapshot, is_scanning: watch::channel_with(true), update_observer: None, scan_requests_tx, path_prefixes_to_scan_tx, _background_scanner_tasks: Vec::new(), fs, fs_case_sensitive, visible, settings, }; worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); Worktree::Local(worktree) }) } pub fn remote( project_id: u64, replica_id: ReplicaId, worktree: proto::WorktreeMetadata, client: AnyProtoClient, cx: &mut AppContext, ) -> Model<Self> { cx.new_model(|cx: &mut ModelContext<Self>| { let snapshot = Snapshot::new( worktree.id, worktree.root_name, Arc::from(PathBuf::from(worktree.abs_path)), ); let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new()))); let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded(); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); let worktree = RemoteWorktree { client, project_id, replica_id, snapshot, background_snapshot: background_snapshot.clone(), updates_tx: Some(background_updates_tx), update_observer: None, snapshot_subscriptions: Default::default(), visible: worktree.visible, disconnected: false, }; // Apply updates to a separate snapshto in a background task, then // send them to a foreground task which updates the model. cx.background_executor() .spawn(async move { while let Some(update) = background_updates_rx.next().await { { let mut lock = background_snapshot.lock(); if let Err(error) = lock.0.apply_remote_update(update.clone()) { log::error!("error applying worktree update: {}", error); } lock.1.push(update); } snapshot_updated_tx.send(()).await.ok(); } }) .detach(); // On the foreground task, update to the latest snapshot and notify // any update observer of all updates that led to that snapshot. cx.spawn(|this, mut cx| async move { while (snapshot_updated_rx.recv().await).is_some() { this.update(&mut cx, |this, cx| { let this = this.as_remote_mut().unwrap(); { let mut lock = this.background_snapshot.lock(); this.snapshot = lock.0.clone();Lines 1–500 of 5328. Load more
mod ignore; mod worktree_settings; #[cfg(test)] mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; use anyhow::{anyhow, Context as _, Result}; use clock::ReplicaId; use collections::{HashMap, HashSet, VecDeque}; use fs::{copy_recursive, Fs, PathEvent, RemoveOptions, Watcher}; use futures::{ channel::{ mpsc::{self, UnboundedSender}, oneshot, }, select_biased, stream::select, task::Poll, FutureExt as _, Stream, StreamExt, }; use fuzzy::CharBag; use git::{ repository::{GitFileStatus, GitRepository, RepoPath}, status::GitStatus, DOT_GIT, GITIGNORE, }; use gpui::{ AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext, Task, }; use ignore::IgnoreStack; use parking_lot::Mutex; use paths::local_settings_folder_relative_path; use postage::{ barrier, prelude::{Sink as _, Stream as _}, watch, }; use rpc::{proto, AnyProtoClient}; pub use settings::WorktreeId; use settings::{Settings, SettingsLocation, SettingsStore}; use smallvec::{smallvec, SmallVec}; use smol::channel::{self, Sender}; use std::{ any::Any, cmp::Ordering, collections::hash_map, 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, Instant, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet}; use text::{LineEnding, Rope}; use util::{paths::home_dir, 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); /// A set of local or remote files that are being opened as part of a project. /// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates. /// Stores git repositories data and the diagnostics for the file(s). /// /// Has an absolute path, and may be set to be visible in Zed UI or not. /// May correspond to a directory or a single file. /// Possible examples: /// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree /// * a directory opened in Zed — may be added as a visible entry to the current worktree /// /// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries. pub enum Worktree { Local(LocalWorktree), Remote(RemoteWorktree), } /// An entry, created in the worktree. #[derive(Debug)] pub enum CreatedEntry { /// Got created and indexed by the worktree, receiving a corresponding entry. Included(Entry), /// Got created, but not indexed due to falling under exclusion filters. Excluded { abs_path: PathBuf }, } pub struct LoadedFile { pub file: Arc<File>, pub text: String, pub diff_base: Option<String>, } 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<()>>, update_observer: Option<UpdateObservationState>, fs: Arc<dyn Fs>, fs_case_sensitive: bool, visible: bool, next_entry_id: Arc<AtomicUsize>, settings: WorktreeSettings, share_private_files: bool, } struct ScanRequest { relative_paths: Vec<Arc<Path>>, done: SmallVec<[barrier::Sender; 1]>, } pub struct RemoteWorktree { snapshot: Snapshot, background_snapshot: Arc<Mutex<(Snapshot, Vec<proto::UpdateWorktree>)>>, project_id: u64, client: AnyProtoClient, updates_tx: Option<UnboundedSender<proto::UpdateWorktree>>, update_observer: Option<mpsc::UnboundedSender<proto::UpdateWorktree>>, snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, replica_id: ReplicaId, 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>>, /// If location_in_repo is set, it means the .git folder is external /// and in a parent folder of the project root. /// In that case, the work_directory field will point to the /// project-root and location_in_repo contains the location of the /// project-root in the repository. /// /// Example: /// /// my_root_folder/ <-- repository root /// .git /// my_sub_folder_1/ /// project_root/ <-- Project root, Zed opened here /// ... /// /// For this setup, the attributes will have the following values: /// /// work_directory: pointing to "" entry /// location_in_repo: Some("my_sub_folder_1/project_root") pub(crate) location_in_repo: Option<Arc<Path>>, } 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 { self.into() } /// relativize returns the given project path relative to the root folder of the /// repository. /// If the root of the repository (and its .git folder) are located in a parent folder /// of the project root folder, then the returned RepoPath is relative to the root /// of the repository and not a valid path inside the project. pub fn relativize(&self, worktree: &Snapshot, path: &Path) -> Result<RepoPath> { let relativize_path = |path: &Path| { let entry = worktree .entry_for_id(self.work_directory.0) .ok_or_else(|| anyhow!("entry not found"))?; let relativized_path = path .strip_prefix(&entry.path) .map_err(|_| anyhow!("could not relativize {:?} against {:?}", path, entry.path))?; Ok(relativized_path.into()) }; if let Some(location_in_repo) = &self.location_in_repo { relativize_path(&location_in_repo.join(path)) } else { relativize_path(path) } } } 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' of a repository in relation /// to Zed's project root. /// In the majority of the cases, this is the folder that contains the .git folder. /// But if a sub-folder of a git repository is opened, this corresponds to the /// project root and the .git folder is located in a parent directory. #[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 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>, } 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_entries: HashMap<u64, Entry>, 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<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 repo(&self) -> &Arc<dyn GitRepository> { &self.repo_ptr } } 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: SmallVec<[barrier::Sender; 1]>, scanning: bool, }, } struct UpdateObservationState { 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), DeletedEntry(ProjectEntryId), } static EMPTY_PATH: &str = ""; impl EventEmitter<Event> for Worktree {} impl Worktree { pub async fn local( path: impl Into<Arc<Path>>, visible: bool, fs: Arc<dyn Fs>, next_entry_id: Arc<AtomicUsize>, cx: &mut AsyncAppContext, ) -> Result<Model<Self>> { 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 }); cx.new_model(move |cx: &mut ModelContext<Worktree>| { let mut snapshot = LocalSnapshot { ignores_by_parent_abs_path: Default::default(), git_repositories: Default::default(), snapshot: Snapshot::new( cx.entity_id().as_u64(), abs_path .file_name() .map_or(String::new(), |f| f.to_string_lossy().to_string()), abs_path, ), }; if let Some(metadata) = metadata { snapshot.insert_entry( Entry::new( Arc::from(Path::new("")), &metadata, &next_entry_id, snapshot.root_char_bag, None, ), fs.as_ref(), ); } let worktree_id = snapshot.id(); let settings_location = Some(SettingsLocation { worktree_id, path: Path::new(EMPTY_PATH), }); let settings = WorktreeSettings::get(settings_location, cx).clone(); cx.observe_global::<SettingsStore>(move |this, cx| { if let Self::Local(this) = this { let settings = WorktreeSettings::get(settings_location, cx).clone(); if settings != this.settings { this.settings = settings; this.restart_background_scanners(cx); } } }) .detach(); let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); let mut worktree = LocalWorktree { share_private_files: false, next_entry_id, snapshot, is_scanning: watch::channel_with(true), update_observer: None, scan_requests_tx, path_prefixes_to_scan_tx, _background_scanner_tasks: Vec::new(), fs, fs_case_sensitive, visible, settings, }; worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); Worktree::Local(worktree) }) } pub fn remote( project_id: u64, replica_id: ReplicaId, worktree: proto::WorktreeMetadata, client: AnyProtoClient, cx: &mut AppContext, ) -> Model<Self> { cx.new_model(|cx: &mut ModelContext<Self>| { let snapshot = Snapshot::new( worktree.id, worktree.root_name, Arc::from(PathBuf::from(worktree.abs_path)), ); let background_snapshot = Arc::new(Mutex::new((snapshot.clone(), Vec::new()))); let (background_updates_tx, mut background_updates_rx) = mpsc::unbounded(); let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); let worktree = RemoteWorktree { client, project_id, replica_id, snapshot, background_snapshot: background_snapshot.clone(), updates_tx: Some(background_updates_tx), update_observer: None, snapshot_subscriptions: Default::default(), visible: worktree.visible, disconnected: false, }; // Apply updates to a separate snapshto in a background task, then // send them to a foreground task which updates the model. cx.background_executor() .spawn(async move { while let Some(update) = background_updates_rx.next().await { { let mut lock = background_snapshot.lock(); if let Err(error) = lock.0.apply_remote_update(update.clone()) { log::error!("error applying worktree update: {}", error); } lock.1.push(update); } snapshot_updated_tx.send(()).await.ok(); } }) .detach(); // On the foreground task, update to the latest snapshot and notify // any update observer of all updates that led to that snapshot. cx.spawn(|this, mut cx| async move { while (snapshot_updated_rx.recv().await).is_some() { this.update(&mut cx, |this, cx| { let this = this.as_remote_mut().unwrap(); { let mut lock = this.background_snapshot.lock(); this.snapshot = lock.0.clone();