diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index cb3d3ab659..20393d63e1 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -268,7 +268,7 @@ impl PickerDelegate for RecentProjectsDelegate { .as_ref() .map(|port| port.to_string()) .unwrap_or_default(), - ssh_project.path, + ssh_project.paths.join(","), ssh_project .user .as_ref() @@ -403,7 +403,7 @@ impl PickerDelegate for RecentProjectsDelegate { password: None, }; - let paths = vec![PathBuf::from(ssh_project.path.clone())]; + let paths = ssh_project.paths.iter().map(PathBuf::from).collect(); cx.spawn(|_, mut cx| async move { open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await @@ -460,9 +460,7 @@ impl PickerDelegate for RecentProjectsDelegate { .filter_map(|i| paths.paths().get(*i).cloned()) .collect(), ), - SerializedWorkspaceLocation::Ssh(ssh_project) => { - Arc::new(vec![PathBuf::from(ssh_project.ssh_url())]) - } + SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()), SerializedWorkspaceLocation::DevServer(dev_server_project) => { Arc::new(vec![PathBuf::from(format!( "{}:{}", diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 034328a30b..3a0b8eabb9 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -366,6 +366,9 @@ define_connection! { ); ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE; ), + sql!( + ALTER TABLE ssh_projects RENAME COLUMN path TO paths; + ), ]; } @@ -769,39 +772,40 @@ impl WorkspaceDb { &self, host: String, port: Option, - path: String, + paths: Vec, user: Option, ) -> Result { + let paths = serde_json::to_string(&paths)?; if let Some(project) = self - .get_ssh_project(host.clone(), port, path.clone(), user.clone()) + .get_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await? { Ok(project) } else { - self.insert_ssh_project(host, port, path, user) + self.insert_ssh_project(host, port, paths, user) .await? .ok_or_else(|| anyhow!("failed to insert ssh project")) } } query! { - async fn get_ssh_project(host: String, port: Option, path: String, user: Option) -> Result> { - SELECT id, host, port, path, user + async fn get_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { + SELECT id, host, port, paths, user FROM ssh_projects - WHERE host IS ? AND port IS ? AND path IS ? AND user IS ? + WHERE host IS ? AND port IS ? AND paths IS ? AND user IS ? LIMIT 1 } } query! { - async fn insert_ssh_project(host: String, port: Option, path: String, user: Option) -> Result> { + async fn insert_ssh_project(host: String, port: Option, paths: String, user: Option) -> Result> { INSERT INTO ssh_projects( host, port, - path, + paths, user ) VALUES (?1, ?2, ?3, ?4) - RETURNING id, host, port, path, user + RETURNING id, host, port, paths, user } } @@ -840,7 +844,7 @@ impl WorkspaceDb { query! { fn ssh_projects() -> Result> { - SELECT id, host, port, path, user + SELECT id, host, port, paths, user FROM ssh_projects } } @@ -1656,45 +1660,45 @@ mod tests { async fn test_get_or_create_ssh_project() { let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project").await); - let (host, port, path, user) = ( + let (host, port, paths, user) = ( "example.com".to_string(), Some(22_u16), - "/home/user".to_string(), + vec!["/home/user".to_string(), "/etc/nginx".to_string()], Some("user".to_string()), ); let project = db - .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone()) + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); assert_eq!(project.host, host); - assert_eq!(project.path, path); + assert_eq!(project.paths, paths); assert_eq!(project.user, user); // Test that calling the function again with the same parameters returns the same project let same_project = db - .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone()) + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); assert_eq!(project.id, same_project.id); // Test with different parameters - let (host2, path2, user2) = ( + let (host2, paths2, user2) = ( "otherexample.com".to_string(), - "/home/otheruser".to_string(), + vec!["/home/otheruser".to_string()], Some("otheruser".to_string()), ); let different_project = db - .get_or_create_ssh_project(host2.clone(), None, path2.clone(), user2.clone()) + .get_or_create_ssh_project(host2.clone(), None, paths2.clone(), user2.clone()) .await .unwrap(); assert_ne!(project.id, different_project.id); assert_eq!(different_project.host, host2); - assert_eq!(different_project.path, path2); + assert_eq!(different_project.paths, paths2); assert_eq!(different_project.user, user2); } @@ -1702,25 +1706,25 @@ mod tests { async fn test_get_or_create_ssh_project_with_null_user() { let db = WorkspaceDb(open_test_db("test_get_or_create_ssh_project_with_null_user").await); - let (host, port, path, user) = ( + let (host, port, paths, user) = ( "example.com".to_string(), None, - "/home/user".to_string(), + vec!["/home/user".to_string()], None, ); let project = db - .get_or_create_ssh_project(host.clone(), port, path.clone(), None) + .get_or_create_ssh_project(host.clone(), port, paths.clone(), None) .await .unwrap(); assert_eq!(project.host, host); - assert_eq!(project.path, path); + assert_eq!(project.paths, paths); assert_eq!(project.user, None); // Test that calling the function again with the same parameters returns the same project let same_project = db - .get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone()) + .get_or_create_ssh_project(host.clone(), port, paths.clone(), user.clone()) .await .unwrap(); @@ -1735,32 +1739,32 @@ mod tests { ( "example.com".to_string(), None, - "/home/user".to_string(), + vec!["/home/user".to_string()], None, ), ( "anotherexample.com".to_string(), Some(123_u16), - "/home/user2".to_string(), + vec!["/home/user2".to_string()], Some("user2".to_string()), ), ( "yetanother.com".to_string(), Some(345_u16), - "/home/user3".to_string(), + vec!["/home/user3".to_string(), "/proc/1234/exe".to_string()], None, ), ]; - for (host, port, path, user) in projects.iter() { + for (host, port, paths, user) in projects.iter() { let project = db - .get_or_create_ssh_project(host.clone(), *port, path.clone(), user.clone()) + .get_or_create_ssh_project(host.clone(), *port, paths.clone(), user.clone()) .await .unwrap(); assert_eq!(&project.host, host); assert_eq!(&project.port, port); - assert_eq!(&project.path, path); + assert_eq!(&project.paths, paths); assert_eq!(&project.user, user); } diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 0ad3fa5e60..7528e4c393 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -26,24 +26,29 @@ pub struct SerializedSshProject { pub id: SshProjectId, pub host: String, pub port: Option, - pub path: String, + pub paths: Vec, pub user: Option, } impl SerializedSshProject { - pub fn ssh_url(&self) -> String { - let mut result = String::from("ssh://"); - if let Some(user) = &self.user { - result.push_str(user); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = &self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result.push_str(&self.path); - result + pub fn ssh_urls(&self) -> Vec { + self.paths + .iter() + .map(|path| { + let mut result = String::new(); + if let Some(user) = &self.user { + result.push_str(user); + result.push('@'); + } + result.push_str(&self.host); + if let Some(port) = &self.port { + result.push(':'); + result.push_str(&port.to_string()); + } + result.push_str(path); + PathBuf::from(result) + }) + .collect() } } @@ -58,7 +63,8 @@ impl Bind for &SerializedSshProject { let next_index = statement.bind(&self.id.0, start_index)?; let next_index = statement.bind(&self.host, next_index)?; let next_index = statement.bind(&self.port, next_index)?; - let next_index = statement.bind(&self.path, next_index)?; + let raw_paths = serde_json::to_string(&self.paths)?; + let next_index = statement.bind(&raw_paths, next_index)?; statement.bind(&self.user, next_index) } } @@ -68,7 +74,9 @@ impl Column for SerializedSshProject { let id = statement.column_int64(start_index)?; let host = statement.column_text(start_index + 1)?.to_string(); let (port, _) = Option::::column(statement, start_index + 2)?; - let path = statement.column_text(start_index + 3)?.to_string(); + let raw_paths = statement.column_text(start_index + 3)?.to_string(); + let paths: Vec = serde_json::from_str(&raw_paths)?; + let (user, _) = Option::::column(statement, start_index + 4)?; Ok(( @@ -76,7 +84,7 @@ impl Column for SerializedSshProject { id: SshProjectId(id as u64), host, port, - path, + paths, user, }, start_index + 5, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b732eb5bc7..98f793c234 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5516,14 +5516,14 @@ pub fn open_ssh_project( cx: &mut AppContext, ) -> Task> { cx.spawn(|mut cx| async move { - // TODO: Handle multiple paths - let path = paths.iter().next().cloned().unwrap_or_default(); - let serialized_ssh_project = persistence::DB .get_or_create_ssh_project( connection_options.host.clone(), connection_options.port, - path.to_string_lossy().to_string(), + paths + .iter() + .map(|path| path.to_string_lossy().to_string()) + .collect::>(), connection_options.username.clone(), ) .await?;