5328 lines · 185.9 KB · Rust
RawDownload
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.