999 lines · 34.6 KB · Rust
RawDownload
use std::{    io::{BufRead, BufReader},    path::{Path, PathBuf},    pin::pin,    sync::{atomic::AtomicUsize, Arc},}; use anyhow::{anyhow, Context as _, Result};use collections::{HashMap, HashSet};use fs::Fs;use futures::{    future::{BoxFuture, Shared},    FutureExt, SinkExt,};use git::repository::Branch;use gpui::{    App, AppContext as _, AsyncApp, Context, Entity, EntityId, EventEmitter, Task, WeakEntity,};use postage::oneshot;use rpc::{    proto::{self, FromProto, ToProto, SSH_PROJECT_ID},    AnyProtoClient, ErrorExt, TypedEnvelope,};use smol::{    channel::{Receiver, Sender},    stream::StreamExt,};use text::ReplicaId;use util::{paths::SanitizedPath, ResultExt};use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings}; use crate::{search::SearchQuery, ProjectPath}; struct MatchingEntry {    worktree_path: Arc<Path>,    path: ProjectPath,    respond: oneshot::Sender<ProjectPath>,} enum WorktreeStoreState {    Local {        fs: Arc<dyn Fs>,    },    Remote {        upstream_client: AnyProtoClient,        upstream_project_id: u64,    },} pub struct WorktreeStore {    next_entry_id: Arc<AtomicUsize>,    downstream_client: Option<(AnyProtoClient, u64)>,    retain_worktrees: bool,    worktrees: Vec<WorktreeHandle>,    worktrees_reordered: bool,    #[allow(clippy::type_complexity)]    loading_worktrees:        HashMap<SanitizedPath, Shared<Task<Result<Entity<Worktree>, Arc<anyhow::Error>>>>>,    state: WorktreeStoreState,} #[derive(Debug)]pub enum WorktreeStoreEvent {    WorktreeAdded(Entity<Worktree>),    WorktreeRemoved(EntityId, WorktreeId),    WorktreeReleased(EntityId, WorktreeId),    WorktreeOrderChanged,    WorktreeUpdateSent(Entity<Worktree>),    WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),    WorktreeUpdatedGitRepositories(WorktreeId),    WorktreeDeletedEntry(WorktreeId, ProjectEntryId),} impl EventEmitter<WorktreeStoreEvent> for WorktreeStore {} impl WorktreeStore {    pub fn init(client: &AnyProtoClient) {        client.add_entity_request_handler(Self::handle_create_project_entry);        client.add_entity_request_handler(Self::handle_copy_project_entry);        client.add_entity_request_handler(Self::handle_delete_project_entry);        client.add_entity_request_handler(Self::handle_expand_project_entry);        client.add_entity_request_handler(Self::handle_expand_all_for_project_entry);    }     pub fn local(retain_worktrees: bool, fs: Arc<dyn Fs>) -> Self {        Self {            next_entry_id: Default::default(),            loading_worktrees: Default::default(),            downstream_client: None,            worktrees: Vec::new(),            worktrees_reordered: false,            retain_worktrees,            state: WorktreeStoreState::Local { fs },        }    }     pub fn remote(        retain_worktrees: bool,        upstream_client: AnyProtoClient,        upstream_project_id: u64,    ) -> Self {        Self {            next_entry_id: Default::default(),            loading_worktrees: Default::default(),            downstream_client: None,            worktrees: Vec::new(),            worktrees_reordered: false,            retain_worktrees,            state: WorktreeStoreState::Remote {                upstream_client,                upstream_project_id,            },        }    }     /// Iterates through all worktrees, including ones that don't appear in the project panel    pub fn worktrees(&self) -> impl '_ + DoubleEndedIterator<Item = Entity<Worktree>> {        self.worktrees            .iter()            .filter_map(move |worktree| worktree.upgrade())    }     /// Iterates through all user-visible worktrees, the ones that appear in the project panel.    pub fn visible_worktrees<'a>(        &'a self,        cx: &'a App,    ) -> impl 'a + DoubleEndedIterator<Item = Entity<Worktree>> {        self.worktrees()            .filter(|worktree| worktree.read(cx).is_visible())    }     pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option<Entity<Worktree>> {        self.worktrees()            .find(|worktree| worktree.read(cx).id() == id)    }     pub fn current_branch(&self, repository: ProjectPath, cx: &App) -> Option<Branch> {        self.worktree_for_id(repository.worktree_id, cx)?            .read(cx)            .git_entry(repository.path)?            .branch()            .cloned()    }     pub fn worktree_for_entry(        &self,        entry_id: ProjectEntryId,        cx: &App,    ) -> Option<Entity<Worktree>> {        self.worktrees()            .find(|worktree| worktree.read(cx).contains_entry(entry_id))    }     pub fn find_worktree(        &self,        abs_path: impl Into<SanitizedPath>,        cx: &App,    ) -> Option<(Entity<Worktree>, PathBuf)> {        let abs_path: SanitizedPath = abs_path.into();        for tree in self.worktrees() {            if let Ok(relative_path) = abs_path.as_path().strip_prefix(tree.read(cx).abs_path()) {                return Some((tree.clone(), relative_path.into()));            }        }        None    }     pub fn find_or_create_worktree(        &mut self,        abs_path: impl AsRef<Path>,        visible: bool,        cx: &mut Context<Self>,    ) -> Task<Result<(Entity<Worktree>, PathBuf)>> {        let abs_path = abs_path.as_ref();        if let Some((tree, relative_path)) = self.find_worktree(abs_path, cx) {            Task::ready(Ok((tree, relative_path)))        } else {            let worktree = self.create_worktree(abs_path, visible, cx);            cx.background_spawn(async move { Ok((worktree.await?, PathBuf::new())) })        }    }     pub fn entry_for_id<'a>(&'a self, entry_id: ProjectEntryId, cx: &'a App) -> Option<&'a Entry> {        self.worktrees()            .find_map(|worktree| worktree.read(cx).entry_for_id(entry_id))    }     pub fn worktree_and_entry_for_id<'a>(        &'a self,        entry_id: ProjectEntryId,        cx: &'a App,    ) -> Option<(Entity<Worktree>, &'a Entry)> {        self.worktrees().find_map(|worktree| {            worktree                .read(cx)                .entry_for_id(entry_id)                .map(|e| (worktree.clone(), e))        })    }     pub fn entry_for_path(&self, path: &ProjectPath, cx: &App) -> Option<Entry> {        self.worktree_for_id(path.worktree_id, cx)?            .read(cx)            .entry_for_path(&path.path)            .cloned()    }     pub fn create_worktree(        &mut self,        abs_path: impl Into<SanitizedPath>,        visible: bool,        cx: &mut Context<Self>,    ) -> Task<Result<Entity<Worktree>>> {        let abs_path: SanitizedPath = abs_path.into();        if !self.loading_worktrees.contains_key(&abs_path) {            let task = match &self.state {                WorktreeStoreState::Remote {                    upstream_client, ..                } => {                    if upstream_client.is_via_collab() {                        Task::ready(Err(Arc::new(anyhow!("cannot create worktrees via collab"))))                    } else {                        self.create_ssh_worktree(                            upstream_client.clone(),                            abs_path.clone(),                            visible,                            cx,                        )                    }                }                WorktreeStoreState::Local { fs } => {                    self.create_local_worktree(fs.clone(), abs_path.clone(), visible, cx)                }            };             self.loading_worktrees                .insert(abs_path.clone(), task.shared());        }        let task = self.loading_worktrees.get(&abs_path).unwrap().clone();        cx.spawn(|this, mut cx| async move {            let result = task.await;            this.update(&mut cx, |this, _| this.loading_worktrees.remove(&abs_path))                .ok();            match result {                Ok(worktree) => Ok(worktree),                Err(err) => Err((*err).cloned()),            }        })    }     fn create_ssh_worktree(        &mut self,        client: AnyProtoClient,        abs_path: impl Into<SanitizedPath>,        visible: bool,        cx: &mut Context<Self>,    ) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {        let mut abs_path = Into::<SanitizedPath>::into(abs_path).to_string();        // If we start with `/~` that means the ssh path was something like `ssh://user@host/~/home-dir-folder/`        // in which case want to strip the leading the `/`.        // On the host-side, the `~` will get expanded.        // That's what git does too: https://github.com/libgit2/libgit2/issues/3345#issuecomment-127050850        if abs_path.starts_with("/~") {            abs_path = abs_path[1..].to_string();        }        if abs_path.is_empty() || abs_path == "/" {            abs_path = "~/".to_string();        }        cx.spawn(|this, mut cx| async move {            let this = this.upgrade().context("Dropped worktree store")?;             let path = Path::new(abs_path.as_str());            let response = client                .request(proto::AddWorktree {                    project_id: SSH_PROJECT_ID,                    path: path.to_proto(),                    visible,                })                .await?;             if let Some(existing_worktree) = this.read_with(&cx, |this, cx| {                this.worktree_for_id(WorktreeId::from_proto(response.worktree_id), cx)            })? {                return Ok(existing_worktree);            }             let root_path_buf = PathBuf::from_proto(response.canonicalized_path.clone());            let root_name = root_path_buf                .file_name()                .map(|n| n.to_string_lossy().to_string())                .unwrap_or(root_path_buf.to_string_lossy().to_string());             let worktree = cx.update(|cx| {                Worktree::remote(                    SSH_PROJECT_ID,                    0,                    proto::WorktreeMetadata {                        id: response.worktree_id,                        root_name,                        visible,                        abs_path: response.canonicalized_path,                    },                    client,                    cx,                )            })?;             this.update(&mut cx, |this, cx| {                this.add(&worktree, cx);            })?;            Ok(worktree)        })    }     fn create_local_worktree(        &mut self,        fs: Arc<dyn Fs>,        abs_path: impl Into<SanitizedPath>,        visible: bool,        cx: &mut Context<Self>,    ) -> Task<Result<Entity<Worktree>, Arc<anyhow::Error>>> {        let next_entry_id = self.next_entry_id.clone();        let path: SanitizedPath = abs_path.into();         cx.spawn(move |this, mut cx| async move {            let worktree = Worktree::local(path.clone(), visible, fs, next_entry_id, &mut cx).await;             let worktree = worktree?;             this.update(&mut cx, |this, cx| this.add(&worktree, cx))?;             if visible {                cx.update(|cx| {                    cx.add_recent_document(path.as_path());                })                .log_err();            }             Ok(worktree)        })    }     pub fn add(&mut self, worktree: &Entity<Worktree>, cx: &mut Context<Self>) {        let worktree_id = worktree.read(cx).id();        debug_assert!(self.worktrees().all(|w| w.read(cx).id() != worktree_id));         let push_strong_handle = self.retain_worktrees || worktree.read(cx).is_visible();        let handle = if push_strong_handle {            WorktreeHandle::Strong(worktree.clone())        } else {            WorktreeHandle::Weak(worktree.downgrade())        };        if self.worktrees_reordered {            self.worktrees.push(handle);        } else {            let i = match self                .worktrees                .binary_search_by_key(&Some(worktree.read(cx).abs_path()), |other| {                    other.upgrade().map(|worktree| worktree.read(cx).abs_path())                }) {                Ok(i) | Err(i) => i,            };            self.worktrees.insert(i, handle);        }         cx.emit(WorktreeStoreEvent::WorktreeAdded(worktree.clone()));        self.send_project_updates(cx);         let handle_id = worktree.entity_id();        cx.subscribe(worktree, |_, worktree, event, cx| {            let worktree_id = worktree.update(cx, |worktree, _| worktree.id());            match event {                worktree::Event::UpdatedEntries(changes) => {                    cx.emit(WorktreeStoreEvent::WorktreeUpdatedEntries(                        worktree_id,                        changes.clone(),                    ));                }                worktree::Event::UpdatedGitRepositories(_) => {                    cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(                        worktree_id,                    ));                }                worktree::Event::DeletedEntry(id) => {                    cx.emit(WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, *id))                }            }        })        .detach();        cx.observe_release(worktree, move |this, worktree, cx| {            cx.emit(WorktreeStoreEvent::WorktreeReleased(                handle_id,                worktree.id(),            ));            cx.emit(WorktreeStoreEvent::WorktreeRemoved(                handle_id,                worktree.id(),            ));            this.send_project_updates(cx);        })        .detach();    }     pub fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut Context<Self>) {        self.worktrees.retain(|worktree| {            if let Some(worktree) = worktree.upgrade() {                if worktree.read(cx).id() == id_to_remove {                    cx.emit(WorktreeStoreEvent::WorktreeRemoved(                        worktree.entity_id(),                        id_to_remove,                    ));                    false                } else {                    true                }            } else {                false            }        });        self.send_project_updates(cx);    }     pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool) {        self.worktrees_reordered = worktrees_reordered;    }     fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> {        match &self.state {            WorktreeStoreState::Remote {                upstream_client,                upstream_project_id,                ..            } => Some((upstream_client.clone(), *upstream_project_id)),            WorktreeStoreState::Local { .. } => None,        }    }     pub fn set_worktrees_from_proto(        &mut self,        worktrees: Vec<proto::WorktreeMetadata>,        replica_id: ReplicaId,        cx: &mut Context<Self>,    ) -> Result<()> {        let mut old_worktrees_by_id = self            .worktrees            .drain(..)            .filter_map(|worktree| {                let worktree = worktree.upgrade()?;                Some((worktree.read(cx).id(), worktree))            })            .collect::<HashMap<_, _>>();         let (client, project_id) = self            .upstream_client()            .clone()            .ok_or_else(|| anyhow!("invalid project"))?;         for worktree in worktrees {            if let Some(old_worktree) =                old_worktrees_by_id.remove(&WorktreeId::from_proto(worktree.id))            {                let push_strong_handle =                    self.retain_worktrees || old_worktree.read(cx).is_visible();                let handle = if push_strong_handle {                    WorktreeHandle::Strong(old_worktree.clone())                } else {                    WorktreeHandle::Weak(old_worktree.downgrade())                };                self.worktrees.push(handle);            } else {                self.add(                    &Worktree::remote(project_id, replica_id, worktree, client.clone(), cx),                    cx,                );            }        }        self.send_project_updates(cx);         Ok(())    }     pub fn move_worktree(        &mut self,        source: WorktreeId,        destination: WorktreeId,        cx: &mut Context<Self>,    ) -> Result<()> {        if source == destination {            return Ok(());        }         let mut source_index = None;        let mut destination_index = None;        for (i, worktree) in self.worktrees.iter().enumerate() {            if let Some(worktree) = worktree.upgrade() {                let worktree_id = worktree.read(cx).id();                if worktree_id == source {                    source_index = Some(i);                    if destination_index.is_some() {                        break;

Lines 1–500 of 999.