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::{ branch_to_proto, 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); client.add_entity_request_handler(Self::handle_git_branches); client.add_entity_request_handler(Self::handle_update_branch); } 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() {Lines 1–500 of 1196. Load more
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::{ branch_to_proto, 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); client.add_entity_request_handler(Self::handle_git_branches); client.add_entity_request_handler(Self::handle_update_branch); } 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() {