4862 lines · 167.0 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 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.