Fix duplicate SshProject's in Remote Projects menu (#20271)

AidanV created

Closes #20269

Release Notes:

- Changes SshConnection to use a BTreeSet of SshProject's instead of a
Vec of SshProject's in order to remove duplicate remote projects from
"settings.json" and the Remote Projects menu.

Change summary

crates/recent_projects/src/remote_servers.rs  | 24 ++++++++++++++-------
crates/recent_projects/src/ssh_connections.rs |  5 ++-
2 files changed, 19 insertions(+), 10 deletions(-)

Detailed changes

crates/recent_projects/src/remote_servers.rs 🔗

@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
 use std::path::PathBuf;
 use std::sync::Arc;
 
@@ -173,7 +174,7 @@ impl ProjectPicker {
                                     .as_mut()
                                     .and_then(|connections| connections.get_mut(ix))
                                 {
-                                    server.projects.push(SshProject { paths })
+                                    server.projects.insert(SshProject { paths });
                                 }
                             }
                         });
@@ -784,7 +785,8 @@ impl RemoteServerProjects {
                     .end_hover_slot::<AnyElement>(Some(
                         div()
                             .mr_2()
-                            .child(
+                            .child({
+                                let project = project.clone();
                                 // Right-margin to offset it from the Scrollbar
                                 IconButton::new("remove-remote-project", IconName::TrashAlt)
                                     .icon_size(IconSize::Small)
@@ -792,9 +794,9 @@ impl RemoteServerProjects {
                                     .size(ButtonSize::Large)
                                     .tooltip(|cx| Tooltip::text("Delete Remote Project", cx))
                                     .on_click(cx.listener(move |this, _, cx| {
-                                        this.delete_ssh_project(server_ix, ix, cx)
-                                    })),
-                            )
+                                        this.delete_ssh_project(server_ix, &project, cx)
+                                    }))
+                            })
                             .into_any_element(),
                     )),
             )
@@ -823,14 +825,20 @@ impl RemoteServerProjects {
         });
     }
 
-    fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
+    fn delete_ssh_project(
+        &mut self,
+        server: usize,
+        project: &SshProject,
+        cx: &mut ViewContext<Self>,
+    ) {
+        let project = project.clone();
         self.update_settings_file(cx, move |setting, _| {
             if let Some(server) = setting
                 .ssh_connections
                 .as_mut()
                 .and_then(|connections| connections.get_mut(server))
             {
-                server.projects.remove(project);
+                server.projects.remove(&project);
             }
         });
     }
@@ -848,7 +856,7 @@ impl RemoteServerProjects {
                     host: SharedString::from(connection_options.host),
                     username: connection_options.username,
                     port: connection_options.port,
-                    projects: vec![],
+                    projects: BTreeSet::<SshProject>::new(),
                     nickname: None,
                     args: connection_options.args.unwrap_or_default(),
                     upload_binary_over_ssh: None,

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
 use std::{path::PathBuf, sync::Arc, time::Duration};
 
 use anyhow::{anyhow, Result};
@@ -75,7 +76,7 @@ pub struct SshConnection {
     #[serde(default)]
     pub args: Vec<String>,
     #[serde(default)]
-    pub projects: Vec<SshProject>,
+    pub projects: BTreeSet<SshProject>,
     /// Name to use for this server in UI.
     #[serde(skip_serializing_if = "Option::is_none")]
     pub nickname: Option<String>,
@@ -101,7 +102,7 @@ impl From<SshConnection> for SshConnectionOptions {
     }
 }
 
-#[derive(Clone, Default, Serialize, PartialEq, Deserialize, JsonSchema)]
+#[derive(Clone, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema)]
 pub struct SshProject {
     pub paths: Vec<String>,
 }