From 2deafd87066ee3e03b52105ed32391a8483c02e8 Mon Sep 17 00:00:00 2001 From: Nia Date: Wed, 22 Oct 2025 02:24:38 +0200 Subject: [PATCH] project_panel: Don't show trash dialog on remote connections (#40838) In remote connections, we don't detect whether trashing is possible and generally shouldn't assume it is. This also fixes up some incomplete logic that was attempting to do that. Closes #39212 Release Notes: - No longer show trash option in remote projects --- crates/project/src/project.rs | 68 ++++++++++++++++++++++- crates/project_panel/src/project_panel.rs | 15 +++-- 2 files changed, 77 insertions(+), 6 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index d167434c52abc161f81d92e2a51c992d52fb9872..f301c7800a5b098ddc93a7badc1617f7842e62d1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1825,10 +1825,12 @@ impl Project { project } + #[inline] pub fn dap_store(&self) -> Entity { self.dap_store.clone() } + #[inline] pub fn breakpoint_store(&self) -> Entity { self.breakpoint_store.clone() } @@ -1842,50 +1844,62 @@ impl Project { Some((session, active_position.clone())) } + #[inline] pub fn lsp_store(&self) -> Entity { self.lsp_store.clone() } + #[inline] pub fn worktree_store(&self) -> Entity { self.worktree_store.clone() } + #[inline] pub fn context_server_store(&self) -> Entity { self.context_server_store.clone() } + #[inline] pub fn buffer_for_id(&self, remote_id: BufferId, cx: &App) -> Option> { self.buffer_store.read(cx).get(remote_id) } + #[inline] pub fn languages(&self) -> &Arc { &self.languages } + #[inline] pub fn client(&self) -> Arc { self.collab_client.clone() } + #[inline] pub fn remote_client(&self) -> Option> { self.remote_client.clone() } + #[inline] pub fn user_store(&self) -> Entity { self.user_store.clone() } + #[inline] pub fn node_runtime(&self) -> Option<&NodeRuntime> { self.node.as_ref() } + #[inline] pub fn opened_buffers(&self, cx: &App) -> Vec> { self.buffer_store.read(cx).buffers().collect() } + #[inline] pub fn environment(&self) -> &Entity { &self.environment } + #[inline] pub fn cli_environment(&self, cx: &App) -> Option> { self.environment.read(cx).get_cli_environment() } @@ -1916,6 +1930,7 @@ impl Project { }) } + #[inline] pub fn peek_environment_error<'a>( &'a self, cx: &'a App, @@ -1923,6 +1938,7 @@ impl Project { self.environment.read(cx).peek_environment_error() } + #[inline] pub fn pop_environment_error(&mut self, cx: &mut Context) { self.environment.update(cx, |environment, _| { environment.pop_environment_error(); @@ -1930,6 +1946,7 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] + #[inline] pub fn has_open_buffer(&self, path: impl Into, cx: &App) -> bool { self.buffer_store .read(cx) @@ -1937,10 +1954,12 @@ impl Project { .is_some() } + #[inline] pub fn fs(&self) -> &Arc { &self.fs } + #[inline] pub fn remote_id(&self) -> Option { match self.client_state { ProjectClientState::Local => None, @@ -1949,6 +1968,7 @@ impl Project { } } + #[inline] pub fn supports_terminal(&self, _cx: &App) -> bool { if self.is_local() { return true; @@ -1960,18 +1980,21 @@ impl Project { false } + #[inline] pub fn remote_connection_state(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_state()) } + #[inline] pub fn remote_connection_options(&self, cx: &App) -> Option { self.remote_client .as_ref() .map(|remote| remote.read(cx).connection_options()) } + #[inline] pub fn replica_id(&self) -> ReplicaId { match self.client_state { ProjectClientState::Remote { replica_id, .. } => replica_id, @@ -1985,14 +2008,17 @@ impl Project { } } + #[inline] pub fn task_store(&self) -> &Entity { &self.task_store } + #[inline] pub fn snippets(&self) -> &Entity { &self.snippets } + #[inline] pub fn search_history(&self, kind: SearchInputKind) -> &SearchHistory { match kind { SearchInputKind::Query => &self.search_history, @@ -2001,6 +2027,7 @@ impl Project { } } + #[inline] pub fn search_history_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistory { match kind { SearchInputKind::Query => &mut self.search_history, @@ -2009,14 +2036,17 @@ impl Project { } } + #[inline] pub fn collaborators(&self) -> &HashMap { &self.collaborators } + #[inline] pub fn host(&self) -> Option<&Collaborator> { self.collaborators.values().find(|c| c.is_host) } + #[inline] pub fn set_worktrees_reordered(&mut self, worktrees_reordered: bool, cx: &mut App) { self.worktree_store.update(cx, |store, _| { store.set_worktrees_reordered(worktrees_reordered); @@ -2024,6 +2054,7 @@ impl Project { } /// Collect all worktrees, including ones that don't appear in the project panel + #[inline] pub fn worktrees<'a>( &self, cx: &'a App, @@ -2032,6 +2063,7 @@ impl Project { } /// Collect all user-visible worktrees, the ones that appear in the project panel. + #[inline] pub fn visible_worktrees<'a>( &'a self, cx: &'a App, @@ -2039,16 +2071,19 @@ impl Project { self.worktree_store.read(cx).visible_worktrees(cx) } + #[inline] pub fn worktree_for_root_name(&self, root_name: &str, cx: &App) -> Option> { self.visible_worktrees(cx) .find(|tree| tree.read(cx).root_name() == root_name) } + #[inline] pub fn worktree_root_names<'a>(&'a self, cx: &'a App) -> impl Iterator { self.visible_worktrees(cx) .map(|tree| tree.read(cx).root_name().as_unix_str()) } + #[inline] pub fn worktree_for_id(&self, id: WorktreeId, cx: &App) -> Option> { self.worktree_store.read(cx).worktree_for_id(id, cx) } @@ -2063,12 +2098,14 @@ impl Project { .worktree_for_entry(entry_id, cx) } + #[inline] pub fn worktree_id_for_entry(&self, entry_id: ProjectEntryId, cx: &App) -> Option { self.worktree_for_entry(entry_id, cx) .map(|worktree| worktree.read(cx).id()) } /// Checks if the entry is the root of a worktree. + #[inline] pub fn entry_is_worktree_root(&self, entry_id: ProjectEntryId, cx: &App) -> bool { self.worktree_for_entry(entry_id, cx) .map(|worktree| { @@ -2080,6 +2117,7 @@ impl Project { .unwrap_or(false) } + #[inline] pub fn project_path_git_status( &self, project_path: &ProjectPath, @@ -2090,6 +2128,7 @@ impl Project { .project_path_git_status(project_path, cx) } + #[inline] pub fn visibility_for_paths( &self, paths: &[PathBuf], @@ -2141,6 +2180,7 @@ impl Project { }) } + #[inline] pub fn copy_entry( &mut self, entry_id: ProjectEntryId, @@ -2219,6 +2259,7 @@ impl Project { }) } + #[inline] pub fn delete_file( &mut self, path: ProjectPath, @@ -2229,6 +2270,7 @@ impl Project { self.delete_entry(entry.id, trash, cx) } + #[inline] pub fn delete_entry( &mut self, entry_id: ProjectEntryId, @@ -2242,6 +2284,7 @@ impl Project { }) } + #[inline] pub fn expand_entry( &mut self, worktree_id: WorktreeId, @@ -2393,6 +2436,7 @@ impl Project { Ok(()) } + #[inline] pub fn unshare(&mut self, cx: &mut Context) -> Result<()> { self.unshare_internal(cx)?; cx.emit(Event::RemoteIdChanged(None)); @@ -2489,10 +2533,12 @@ impl Project { } } + #[inline] pub fn close(&mut self, cx: &mut Context) { cx.emit(Event::Closed); } + #[inline] pub fn is_disconnected(&self, cx: &App) -> bool { match &self.client_state { ProjectClientState::Remote { @@ -2506,6 +2552,7 @@ impl Project { } } + #[inline] fn remote_client_is_disconnected(&self, cx: &App) -> bool { self.remote_client .as_ref() @@ -2513,6 +2560,7 @@ impl Project { .unwrap_or(false) } + #[inline] pub fn capability(&self) -> Capability { match &self.client_state { ProjectClientState::Remote { capability, .. } => *capability, @@ -2520,10 +2568,12 @@ impl Project { } } + #[inline] pub fn is_read_only(&self, cx: &App) -> bool { self.is_disconnected(cx) || self.capability() == Capability::ReadOnly } + #[inline] pub fn is_local(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { @@ -2533,6 +2583,8 @@ impl Project { } } + /// Whether this project is a remote server (not counting collab). + #[inline] pub fn is_via_remote_server(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => { @@ -2542,6 +2594,8 @@ impl Project { } } + /// Whether this project is from collab (not counting remote servers). + #[inline] pub fn is_via_collab(&self) -> bool { match &self.client_state { ProjectClientState::Local | ProjectClientState::Shared { .. } => false, @@ -2549,6 +2603,17 @@ impl Project { } } + /// `!self.is_local()` + #[inline] + pub fn is_remote(&self) -> bool { + debug_assert_eq!( + !self.is_local(), + self.is_via_collab() || self.is_via_remote_server() + ); + !self.is_local() + } + + #[inline] pub fn create_buffer( &mut self, searchable: bool, @@ -2559,6 +2624,7 @@ impl Project { }) } + #[inline] pub fn create_local_buffer( &mut self, text: &str, @@ -2566,7 +2632,7 @@ impl Project { project_searchable: bool, cx: &mut Context, ) -> Entity { - if self.is_via_collab() || self.is_via_remote_server() { + if self.is_remote() { panic!("called create_local_buffer on a remote project") } self.buffer_store.update(cx, |buffer_store, cx| { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 98ff619588d42f80ec53e9e91133d47e63cfcbee..67d01a4459bde943e1bdcbaf3d15b3db0f56ce3e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -615,8 +615,11 @@ impl ProjectPanel { .detach(); let trash_action = [TypeId::of::()]; - let is_remote = project.read(cx).is_via_collab(); + let is_remote = project.read(cx).is_remote(); + // Make sure the trash option is never displayed anywhere on remote + // hosts since they may not support trashing. May want to dynamically + // detect this in the future. if is_remote { CommandPaletteFilter::update_global(cx, |filter, _cx| { filter.hide_action_types(&trash_action); @@ -981,7 +984,7 @@ impl ProjectPanel { let is_foldable = auto_fold_dirs && self.is_foldable(entry, worktree); let is_unfoldable = auto_fold_dirs && self.is_unfoldable(entry, worktree); let is_read_only = project.is_read_only(cx); - let is_remote = project.is_via_collab(); + let is_remote = project.is_remote(); let is_local = project.is_local(); let settings = ProjectPanelSettings::get_global(cx); @@ -1045,13 +1048,13 @@ impl ProjectPanel { .when(!should_hide_rename, |menu| { menu.action("Rename", Box::new(Rename)) }) - .when(!is_root & !is_remote, |menu| { + .when(!is_root && !is_remote, |menu| { menu.action("Trash", Box::new(Trash { skip_prompt: false })) }) .when(!is_root, |menu| { menu.action("Delete", Box::new(Delete { skip_prompt: false })) }) - .when(!is_remote & is_root, |menu| { + .when(!is_remote && is_root, |menu| { menu.separator() .action( "Add Folder to Project…", @@ -5458,11 +5461,13 @@ impl Render for ProjectPanel { .on_action(cx.listener(Self::new_directory)) .on_action(cx.listener(Self::rename)) .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::trash)) .on_action(cx.listener(Self::cut)) .on_action(cx.listener(Self::copy)) .on_action(cx.listener(Self::paste)) .on_action(cx.listener(Self::duplicate)) + .when(!project.is_remote(), |el| { + el.on_action(cx.listener(Self::trash)) + }) }) .when(project.is_local(), |el| { el.on_action(cx.listener(Self::reveal_in_finder))