5594 lines · 194.5 KB · Rust
RawDownload
mod ignore;mod worktree_settings;#[cfg(test)]mod worktree_tests; use ::ignore::gitignore::{Gitignore, GitignoreBuilder};use anyhow::{Context as _, Result, anyhow};use clock::ReplicaId;use collections::{HashMap, HashSet, VecDeque};use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items};use futures::{    FutureExt as _, Stream, StreamExt,    channel::{        mpsc::{self, UnboundedSender},        oneshot,    },    select_biased,    task::Poll,};use fuzzy::CharBag;use git::{    COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR,    repository::RepoPath, status::GitSummary,};use gpui::{    App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Task,};use ignore::IgnoreStack;use language::DiskState; use parking_lot::Mutex;use paths::{local_settings_folder_relative_path, local_vscode_folder_relative_path};use postage::{    barrier,    prelude::{Sink as _, Stream as _},    watch,};use rpc::{    AnyProtoClient,    proto::{self, FromProto, ToProto, split_worktree_update},};pub use settings::WorktreeId;use settings::{Settings, SettingsLocation, SettingsStore};use smallvec::{SmallVec, smallvec};use smol::channel::{self, Sender};use std::{    any::Any,    borrow::Borrow as _,    cmp::Ordering,    collections::hash_map,    convert::TryFrom,    ffi::OsStr,    fmt,    future::Future,    mem::{self},    ops::{Deref, DerefMut},    path::{Component, Path, PathBuf},    pin::Pin,    sync::{        Arc,        atomic::{AtomicUsize, Ordering::SeqCst},    },    time::{Duration, Instant},};use sum_tree::{Bias, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet, Unit};use text::{LineEnding, Rope};use util::{    ResultExt,    paths::{PathMatcher, SanitizedPath, home_dir},};pub use worktree_settings::WorktreeSettings; 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 struct LoadedBinaryFile {    pub file: Arc<File>,    pub content: Vec<u8>,} impl fmt::Debug for LoadedBinaryFile {    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {        f.debug_struct("LoadedBinaryFile")            .field("file", &self.file)            .field("content_bytes", &self.content.len())            .finish()    }} pub struct LocalWorktree {    snapshot: LocalSnapshot,    scan_requests_tx: channel::Sender<ScanRequest>,    path_prefixes_to_scan_tx: channel::Sender<PathPrefixScanRequest>,    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,} pub struct PathPrefixScanRequest {    path: Arc<Path>,    done: SmallVec<[barrier::Sender; 1]>,} 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,    file_scan_inclusions: PathMatcher,    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: SanitizedPath,    root_name: String,    root_char_bag: CharBag,    entries_by_path: SumTree<Entry>,    entries_by_id: SumTree<PathEntry>,    always_included_entries: Vec<Arc<Path>>,     /// 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,} /// 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, Hash)]pub enum WorkDirectory {    InProject {        relative_path: Arc<Path>,    },    AboveProject {        absolute_path: Arc<Path>,        location_in_repo: Arc<Path>,    },} impl WorkDirectory {    #[cfg(test)]    fn in_project(path: &str) -> Self {        let path = Path::new(path);        Self::InProject {            relative_path: path.into(),        }    }     //#[cfg(test)]    //fn canonicalize(&self) -> Self {    //    match self {    //        WorkDirectory::InProject { relative_path } => WorkDirectory::InProject {    //            relative_path: relative_path.clone(),    //        },    //        WorkDirectory::AboveProject {    //            absolute_path,    //            location_in_repo,    //        } => WorkDirectory::AboveProject {    //            absolute_path: absolute_path.canonicalize().unwrap().into(),    //            location_in_repo: location_in_repo.clone(),    //        },    //    }    //}     fn path_key(&self) -> PathKey {        match self {            WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()),            WorkDirectory::AboveProject { .. } => PathKey(Path::new("").into()),        }    }     /// Returns true if the given path is a child of the work directory.    ///    /// Note that the path may not be a member of this repository, if there    /// is a repository in a directory between these two paths    /// external .git folder in a parent folder of the project root.    #[track_caller]    pub fn directory_contains(&self, path: impl AsRef<Path>) -> bool {        let path = path.as_ref();        debug_assert!(path.is_relative());        match self {            WorkDirectory::InProject { relative_path } => path.starts_with(relative_path),            WorkDirectory::AboveProject { .. } => true,        }    }     /// 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, path: &Path) -> Result<RepoPath> {        // path is assumed to be relative to worktree root.        debug_assert!(path.is_relative());        match self {            WorkDirectory::InProject { relative_path } => Ok(path                .strip_prefix(relative_path)                .map_err(|_| {                    anyhow!(                        "could not relativize {:?} against {:?}",                        path,                        relative_path                    )                })?                .into()),            WorkDirectory::AboveProject {                location_in_repo, ..            } => {                // Avoid joining a `/` to location_in_repo in the case of a single-file worktree.                if path == Path::new("") {                    Ok(RepoPath(location_in_repo.clone()))                } else {                    Ok(location_in_repo.join(path).into())                }            }        }    }     /// This is the opposite operation to `relativize` above    pub fn try_unrelativize(&self, path: &RepoPath) -> Option<Arc<Path>> {        match self {            WorkDirectory::InProject { relative_path } => Some(relative_path.join(path).into()),            WorkDirectory::AboveProject {                location_in_repo, ..            } => {                // If we fail to strip the prefix, that means this status entry is                // external to this worktree, and we definitely won't have an entry_id                path.strip_prefix(location_in_repo).ok().map(Into::into)            }        }    }     pub fn unrelativize(&self, path: &RepoPath) -> Arc<Path> {        match self {            WorkDirectory::InProject { relative_path } => relative_path.join(path).into(),            WorkDirectory::AboveProject {                location_in_repo, ..            } => {                if &path.0 == location_in_repo {                    // Single-file worktree                    return location_in_repo                        .file_name()                        .map(Path::new)                        .unwrap_or(Path::new(""))                        .into();                }                let mut location_in_repo = &**location_in_repo;                let mut parents = PathBuf::new();                loop {                    if let Ok(segment) = path.strip_prefix(location_in_repo) {                        return parents.join(segment).into();                    }                    location_in_repo = location_in_repo.parent().unwrap_or(Path::new(""));                    parents.push(Component::ParentDir);                }            }        }    }     pub fn display_name(&self) -> String {        match self {            WorkDirectory::InProject { relative_path } => relative_path.display().to_string(),            WorkDirectory::AboveProject {                absolute_path,                location_in_repo,            } => {                let num_of_dots = location_in_repo.components().count();                 "../".repeat(num_of_dots)                    + &absolute_path                        .file_name()                        .map(|s| s.to_string_lossy())                        .unwrap_or_default()                    + "/"            }        }    }} impl Default for WorkDirectory {    fn default() -> Self {        Self::InProject {            relative_path: Arc::from(Path::new("")),        }    }} #[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>,    /// The file handle of the root dir    /// (so we can find it after it's been moved)    root_file_handle: Option<Arc<dyn fs::FileHandle>>,} 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)]struct LocalRepositoryEntry {    work_directory_id: ProjectEntryId,    work_directory: WorkDirectory,    work_directory_abs_path: Arc<Path>,    git_dir_scan_id: usize,    /// Absolute path to the original .git entry that caused us to create this repository.    ///    /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere    /// (whose path we then store in `repository_dir_abs_path`).    dot_git_abs_path: Arc<Path>,    /// Absolute path to the "commondir" for this repository.    ///    /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path,    /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory    /// from which the submodule/worktree was derived.    common_dir_abs_path: Arc<Path>,    /// Absolute path to the directory holding the repository's state.    ///    /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and    /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the    /// commondir like `/project/.git/modules/foo`.    repository_dir_abs_path: Arc<Path>,} impl sum_tree::Item for LocalRepositoryEntry {    type Summary = PathSummary<Unit>;     fn summary(&self, _: &<Self::Summary as Summary>::Context) -> Self::Summary {        PathSummary {            max_path: self.work_directory.path_key().0,            item_summary: Unit,        }    }} impl KeyedItem for LocalRepositoryEntry {    type Key = PathKey;     fn key(&self) -> Self::Key {        self.work_directory.path_key()    }} //impl LocalRepositoryEntry {//    pub fn repo(&self) -> &Arc<dyn GitRepository> {//        &self.repo_ptr//    }//} impl Deref for LocalRepositoryEntry {    type Target = WorkDirectory;     fn deref(&self) -> &Self::Target {        &self.work_directory    }} 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    }} #[derive(Debug)]enum ScanState {    Started,    Updated {        snapshot: LocalSnapshot,        changes: UpdatedEntriesSet,        barrier: SmallVec<[barrier::Sender; 1]>,        scanning: bool,    },    RootUpdated {        new_path: Option<SanitizedPath>,    },} struct UpdateObservationState {    snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>,    resume_updates: watch::Sender<()>,    _maintain_remote_snapshot: Task<Option<()>>,} #[derive(Clone)]pub enum Event {    UpdatedEntries(UpdatedEntriesSet),    UpdatedGitRepositories(UpdatedGitRepositoriesSet),    DeletedEntry(ProjectEntryId),} const 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 AsyncApp,    ) -> Result<Entity<Self>> {

Lines 1–500 of 5594.