project_panel: Don't show trash dialog on remote connections (#40838)

Nia created

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

Change summary

crates/project/src/project.rs             | 68 ++++++++++++++++++++++++
crates/project_panel/src/project_panel.rs | 15 +++-
2 files changed, 77 insertions(+), 6 deletions(-)

Detailed changes

crates/project/src/project.rs 🔗

@@ -1825,10 +1825,12 @@ impl Project {
         project
     }
 
+    #[inline]
     pub fn dap_store(&self) -> Entity<DapStore> {
         self.dap_store.clone()
     }
 
+    #[inline]
     pub fn breakpoint_store(&self) -> Entity<BreakpointStore> {
         self.breakpoint_store.clone()
     }
@@ -1842,50 +1844,62 @@ impl Project {
         Some((session, active_position.clone()))
     }
 
+    #[inline]
     pub fn lsp_store(&self) -> Entity<LspStore> {
         self.lsp_store.clone()
     }
 
+    #[inline]
     pub fn worktree_store(&self) -> Entity<WorktreeStore> {
         self.worktree_store.clone()
     }
 
+    #[inline]
     pub fn context_server_store(&self) -> Entity<ContextServerStore> {
         self.context_server_store.clone()
     }
 
+    #[inline]
     pub fn buffer_for_id(&self, remote_id: BufferId, cx: &App) -> Option<Entity<Buffer>> {
         self.buffer_store.read(cx).get(remote_id)
     }
 
+    #[inline]
     pub fn languages(&self) -> &Arc<LanguageRegistry> {
         &self.languages
     }
 
+    #[inline]
     pub fn client(&self) -> Arc<Client> {
         self.collab_client.clone()
     }
 
+    #[inline]
     pub fn remote_client(&self) -> Option<Entity<RemoteClient>> {
         self.remote_client.clone()
     }
 
+    #[inline]
     pub fn user_store(&self) -> Entity<UserStore> {
         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<Entity<Buffer>> {
         self.buffer_store.read(cx).buffers().collect()
     }
 
+    #[inline]
     pub fn environment(&self) -> &Entity<ProjectEnvironment> {
         &self.environment
     }
 
+    #[inline]
     pub fn cli_environment(&self, cx: &App) -> Option<HashMap<String, String>> {
         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>) {
         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<ProjectPath>, cx: &App) -> bool {
         self.buffer_store
             .read(cx)
@@ -1937,10 +1954,12 @@ impl Project {
             .is_some()
     }
 
+    #[inline]
     pub fn fs(&self) -> &Arc<dyn Fs> {
         &self.fs
     }
 
+    #[inline]
     pub fn remote_id(&self) -> Option<u64> {
         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<remote::ConnectionState> {
         self.remote_client
             .as_ref()
             .map(|remote| remote.read(cx).connection_state())
     }
 
+    #[inline]
     pub fn remote_connection_options(&self, cx: &App) -> Option<RemoteConnectionOptions> {
         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<TaskStore> {
         &self.task_store
     }
 
+    #[inline]
     pub fn snippets(&self) -> &Entity<SnippetProvider> {
         &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<proto::PeerId, Collaborator> {
         &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<Entity<Worktree>> {
         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<Item = &'a str> {
         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<Entity<Worktree>> {
         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<WorktreeId> {
         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<Self>) -> 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<Self>) {
         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<Self>,
     ) -> Entity<Buffer> {
-        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| {

crates/project_panel/src/project_panel.rs 🔗

@@ -615,8 +615,11 @@ impl ProjectPanel {
             .detach();
 
             let trash_action = [TypeId::of::<Trash>()];
-            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))