Add initial support for WSL (#37035)
Closes #36188 ## Todo * [x] CLI * [x] terminals * [x] tasks ## For future PRs * debugging * UI for opening WSL projects * fixing workspace state restoration Release Notes: - Windows alpha: Zed now supports editing folders in WSL. --------- Co-authored-by: Junkui Zhang <364772080@qq.com>
This commit is contained in:
co-authored by
Junkui Zhang
parent
1c2e2a00fe
commit
f78f3e7729
+310
-128
@@ -20,6 +20,7 @@ use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
|
||||
|
||||
use language::{LanguageName, Toolchain};
|
||||
use project::WorktreeId;
|
||||
use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions};
|
||||
use sqlez::{
|
||||
bindable::{Bind, Column, StaticColumnCount},
|
||||
statement::{SqlType, Statement},
|
||||
@@ -33,11 +34,12 @@ use uuid::Uuid;
|
||||
use crate::{
|
||||
WorkspaceId,
|
||||
path_list::{PathList, SerializedPathList},
|
||||
persistence::model::RemoteConnectionKind,
|
||||
};
|
||||
|
||||
use model::{
|
||||
GroupId, ItemId, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
||||
SerializedSshConnection, SerializedWorkspace, SshConnectionId,
|
||||
GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane,
|
||||
SerializedPaneGroup, SerializedWorkspace,
|
||||
};
|
||||
|
||||
use self::model::{DockStructure, SerializedWorkspaceLocation};
|
||||
@@ -627,6 +629,88 @@ impl Domain for WorkspaceDb {
|
||||
END
|
||||
WHERE paths IS NOT NULL
|
||||
),
|
||||
sql!(
|
||||
CREATE TABLE remote_connections(
|
||||
id INTEGER PRIMARY KEY,
|
||||
kind TEXT NOT NULL,
|
||||
host TEXT,
|
||||
port INTEGER,
|
||||
user TEXT,
|
||||
distro TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE workspaces_2(
|
||||
workspace_id INTEGER PRIMARY KEY,
|
||||
paths TEXT,
|
||||
paths_order TEXT,
|
||||
remote_connection_id INTEGER REFERENCES remote_connections(id),
|
||||
timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
window_state TEXT,
|
||||
window_x REAL,
|
||||
window_y REAL,
|
||||
window_width REAL,
|
||||
window_height REAL,
|
||||
display BLOB,
|
||||
left_dock_visible INTEGER,
|
||||
left_dock_active_panel TEXT,
|
||||
right_dock_visible INTEGER,
|
||||
right_dock_active_panel TEXT,
|
||||
bottom_dock_visible INTEGER,
|
||||
bottom_dock_active_panel TEXT,
|
||||
left_dock_zoom INTEGER,
|
||||
right_dock_zoom INTEGER,
|
||||
bottom_dock_zoom INTEGER,
|
||||
fullscreen INTEGER,
|
||||
centered_layout INTEGER,
|
||||
session_id TEXT,
|
||||
window_id INTEGER
|
||||
) STRICT;
|
||||
|
||||
INSERT INTO remote_connections
|
||||
SELECT
|
||||
id,
|
||||
"ssh" as kind,
|
||||
host,
|
||||
port,
|
||||
user,
|
||||
NULL as distro
|
||||
FROM ssh_connections;
|
||||
|
||||
INSERT
|
||||
INTO workspaces_2
|
||||
SELECT
|
||||
workspace_id,
|
||||
paths,
|
||||
paths_order,
|
||||
ssh_connection_id as remote_connection_id,
|
||||
timestamp,
|
||||
window_state,
|
||||
window_x,
|
||||
window_y,
|
||||
window_width,
|
||||
window_height,
|
||||
display,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
right_dock_visible,
|
||||
right_dock_active_panel,
|
||||
bottom_dock_visible,
|
||||
bottom_dock_active_panel,
|
||||
left_dock_zoom,
|
||||
right_dock_zoom,
|
||||
bottom_dock_zoom,
|
||||
fullscreen,
|
||||
centered_layout,
|
||||
session_id,
|
||||
window_id
|
||||
FROM
|
||||
workspaces;
|
||||
|
||||
DROP TABLE workspaces;
|
||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||
|
||||
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
|
||||
),
|
||||
];
|
||||
|
||||
// Allow recovering from bad migration that was initially shipped to nightly
|
||||
@@ -650,10 +734,10 @@ impl WorkspaceDb {
|
||||
self.workspace_for_roots_internal(worktree_roots, None)
|
||||
}
|
||||
|
||||
pub(crate) fn ssh_workspace_for_roots<P: AsRef<Path>>(
|
||||
pub(crate) fn remote_workspace_for_roots<P: AsRef<Path>>(
|
||||
&self,
|
||||
worktree_roots: &[P],
|
||||
ssh_project_id: SshConnectionId,
|
||||
ssh_project_id: RemoteConnectionId,
|
||||
) -> Option<SerializedWorkspace> {
|
||||
self.workspace_for_roots_internal(worktree_roots, Some(ssh_project_id))
|
||||
}
|
||||
@@ -661,7 +745,7 @@ impl WorkspaceDb {
|
||||
pub(crate) fn workspace_for_roots_internal<P: AsRef<Path>>(
|
||||
&self,
|
||||
worktree_roots: &[P],
|
||||
ssh_connection_id: Option<SshConnectionId>,
|
||||
remote_connection_id: Option<RemoteConnectionId>,
|
||||
) -> Option<SerializedWorkspace> {
|
||||
// paths are sorted before db interactions to ensure that the order of the paths
|
||||
// doesn't affect the workspace selection for existing workspaces
|
||||
@@ -713,13 +797,13 @@ impl WorkspaceDb {
|
||||
FROM workspaces
|
||||
WHERE
|
||||
paths IS ? AND
|
||||
ssh_connection_id IS ?
|
||||
remote_connection_id IS ?
|
||||
LIMIT 1
|
||||
})
|
||||
.map(|mut prepared_statement| {
|
||||
(prepared_statement)((
|
||||
root_paths.serialize().paths,
|
||||
ssh_connection_id.map(|id| id.0 as i32),
|
||||
remote_connection_id.map(|id| id.0 as i32),
|
||||
))
|
||||
.unwrap()
|
||||
})
|
||||
@@ -803,14 +887,12 @@ impl WorkspaceDb {
|
||||
log::debug!("Saving workspace at location: {:?}", workspace.location);
|
||||
self.write(move |conn| {
|
||||
conn.with_savepoint("update_worktrees", || {
|
||||
let ssh_connection_id = match &workspace.location {
|
||||
let remote_connection_id = match workspace.location.clone() {
|
||||
SerializedWorkspaceLocation::Local => None,
|
||||
SerializedWorkspaceLocation::Ssh(connection) => {
|
||||
Some(Self::get_or_create_ssh_connection_query(
|
||||
SerializedWorkspaceLocation::Remote(connection_options) => {
|
||||
Some(Self::get_or_create_remote_connection_internal(
|
||||
conn,
|
||||
connection.host.clone(),
|
||||
connection.port,
|
||||
connection.user.clone(),
|
||||
connection_options
|
||||
)?.0)
|
||||
}
|
||||
};
|
||||
@@ -860,11 +942,11 @@ impl WorkspaceDb {
|
||||
WHERE
|
||||
workspace_id != ?1 AND
|
||||
paths IS ?2 AND
|
||||
ssh_connection_id IS ?3
|
||||
remote_connection_id IS ?3
|
||||
))?((
|
||||
workspace.id,
|
||||
paths.paths.clone(),
|
||||
ssh_connection_id,
|
||||
remote_connection_id,
|
||||
))
|
||||
.context("clearing out old locations")?;
|
||||
|
||||
@@ -874,7 +956,7 @@ impl WorkspaceDb {
|
||||
workspace_id,
|
||||
paths,
|
||||
paths_order,
|
||||
ssh_connection_id,
|
||||
remote_connection_id,
|
||||
left_dock_visible,
|
||||
left_dock_active_panel,
|
||||
left_dock_zoom,
|
||||
@@ -893,7 +975,7 @@ impl WorkspaceDb {
|
||||
UPDATE SET
|
||||
paths = ?2,
|
||||
paths_order = ?3,
|
||||
ssh_connection_id = ?4,
|
||||
remote_connection_id = ?4,
|
||||
left_dock_visible = ?5,
|
||||
left_dock_active_panel = ?6,
|
||||
left_dock_zoom = ?7,
|
||||
@@ -912,7 +994,7 @@ impl WorkspaceDb {
|
||||
workspace.id,
|
||||
paths.paths.clone(),
|
||||
paths.order.clone(),
|
||||
ssh_connection_id,
|
||||
remote_connection_id,
|
||||
workspace.docks,
|
||||
workspace.session_id,
|
||||
workspace.window_id,
|
||||
@@ -931,39 +1013,78 @@ impl WorkspaceDb {
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn get_or_create_ssh_connection(
|
||||
pub(crate) async fn get_or_create_remote_connection(
|
||||
&self,
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
user: Option<String>,
|
||||
) -> Result<SshConnectionId> {
|
||||
self.write(move |conn| Self::get_or_create_ssh_connection_query(conn, host, port, user))
|
||||
options: RemoteConnectionOptions,
|
||||
) -> Result<RemoteConnectionId> {
|
||||
self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options))
|
||||
.await
|
||||
}
|
||||
|
||||
fn get_or_create_ssh_connection_query(
|
||||
fn get_or_create_remote_connection_internal(
|
||||
this: &Connection,
|
||||
host: String,
|
||||
options: RemoteConnectionOptions,
|
||||
) -> Result<RemoteConnectionId> {
|
||||
let kind;
|
||||
let user;
|
||||
let mut host = None;
|
||||
let mut port = None;
|
||||
let mut distro = None;
|
||||
match options {
|
||||
RemoteConnectionOptions::Ssh(options) => {
|
||||
kind = RemoteConnectionKind::Ssh;
|
||||
host = Some(options.host);
|
||||
port = options.port;
|
||||
user = options.username;
|
||||
}
|
||||
RemoteConnectionOptions::Wsl(options) => {
|
||||
kind = RemoteConnectionKind::Wsl;
|
||||
distro = Some(options.distro_name);
|
||||
user = options.user;
|
||||
}
|
||||
}
|
||||
Self::get_or_create_remote_connection_query(this, kind, host, port, user, distro)
|
||||
}
|
||||
|
||||
fn get_or_create_remote_connection_query(
|
||||
this: &Connection,
|
||||
kind: RemoteConnectionKind,
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
user: Option<String>,
|
||||
) -> Result<SshConnectionId> {
|
||||
distro: Option<String>,
|
||||
) -> Result<RemoteConnectionId> {
|
||||
if let Some(id) = this.select_row_bound(sql!(
|
||||
SELECT id FROM ssh_connections WHERE host IS ? AND port IS ? AND user IS ? LIMIT 1
|
||||
))?((host.clone(), port, user.clone()))?
|
||||
{
|
||||
Ok(SshConnectionId(id))
|
||||
SELECT id
|
||||
FROM remote_connections
|
||||
WHERE
|
||||
kind IS ? AND
|
||||
host IS ? AND
|
||||
port IS ? AND
|
||||
user IS ? AND
|
||||
distro IS ?
|
||||
LIMIT 1
|
||||
))?((
|
||||
kind.serialize(),
|
||||
host.clone(),
|
||||
port,
|
||||
user.clone(),
|
||||
distro.clone(),
|
||||
))? {
|
||||
Ok(RemoteConnectionId(id))
|
||||
} else {
|
||||
log::debug!("Inserting SSH project at host {host}");
|
||||
let id = this.select_row_bound(sql!(
|
||||
INSERT INTO ssh_connections (
|
||||
INSERT INTO remote_connections (
|
||||
kind,
|
||||
host,
|
||||
port,
|
||||
user
|
||||
) VALUES (?1, ?2, ?3)
|
||||
user,
|
||||
distro
|
||||
) VALUES (?1, ?2, ?3, ?4, ?5)
|
||||
RETURNING id
|
||||
))?((host, port, user))?
|
||||
.context("failed to insert ssh project")?;
|
||||
Ok(SshConnectionId(id))
|
||||
))?((kind.serialize(), host, port, user, distro))?
|
||||
.context("failed to insert remote project")?;
|
||||
Ok(RemoteConnectionId(id))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -973,15 +1094,17 @@ impl WorkspaceDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn recent_workspaces(&self) -> Result<Vec<(WorkspaceId, PathList, Option<u64>)>> {
|
||||
fn recent_workspaces(
|
||||
&self,
|
||||
) -> Result<Vec<(WorkspaceId, PathList, Option<RemoteConnectionId>)>> {
|
||||
Ok(self
|
||||
.recent_workspaces_query()?
|
||||
.into_iter()
|
||||
.map(|(id, paths, order, ssh_connection_id)| {
|
||||
.map(|(id, paths, order, remote_connection_id)| {
|
||||
(
|
||||
id,
|
||||
PathList::deserialize(&SerializedPathList { paths, order }),
|
||||
ssh_connection_id,
|
||||
remote_connection_id.map(RemoteConnectionId),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
@@ -1001,7 +1124,7 @@ impl WorkspaceDb {
|
||||
fn session_workspaces(
|
||||
&self,
|
||||
session_id: String,
|
||||
) -> Result<Vec<(PathList, Option<u64>, Option<SshConnectionId>)>> {
|
||||
) -> Result<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
|
||||
Ok(self
|
||||
.session_workspaces_query(session_id)?
|
||||
.into_iter()
|
||||
@@ -1009,7 +1132,7 @@ impl WorkspaceDb {
|
||||
(
|
||||
PathList::deserialize(&SerializedPathList { paths, order }),
|
||||
window_id,
|
||||
ssh_connection_id.map(SshConnectionId),
|
||||
ssh_connection_id.map(RemoteConnectionId),
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
@@ -1017,7 +1140,7 @@ impl WorkspaceDb {
|
||||
|
||||
query! {
|
||||
fn session_workspaces_query(session_id: String) -> Result<Vec<(String, String, Option<u64>, Option<u64>)>> {
|
||||
SELECT paths, paths_order, window_id, ssh_connection_id
|
||||
SELECT paths, paths_order, window_id, remote_connection_id
|
||||
FROM workspaces
|
||||
WHERE session_id = ?1
|
||||
ORDER BY timestamp DESC
|
||||
@@ -1039,40 +1162,55 @@ impl WorkspaceDb {
|
||||
}
|
||||
}
|
||||
|
||||
fn ssh_connections(&self) -> Result<HashMap<SshConnectionId, SerializedSshConnection>> {
|
||||
Ok(self
|
||||
.ssh_connections_query()?
|
||||
.into_iter()
|
||||
.map(|(id, host, port, user)| {
|
||||
(
|
||||
SshConnectionId(id),
|
||||
SerializedSshConnection { host, port, user },
|
||||
)
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
query! {
|
||||
pub fn ssh_connections_query() -> Result<Vec<(u64, String, Option<u16>, Option<String>)>> {
|
||||
SELECT id, host, port, user
|
||||
FROM ssh_connections
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ssh_connection(&self, id: SshConnectionId) -> Result<SerializedSshConnection> {
|
||||
let row = self.ssh_connection_query(id.0)?;
|
||||
Ok(SerializedSshConnection {
|
||||
host: row.0,
|
||||
port: row.1,
|
||||
user: row.2,
|
||||
fn remote_connections(&self) -> Result<HashMap<RemoteConnectionId, RemoteConnectionOptions>> {
|
||||
Ok(self.select(sql!(
|
||||
SELECT
|
||||
id, kind, host, port, user, distro
|
||||
FROM
|
||||
remote_connections
|
||||
))?()?
|
||||
.into_iter()
|
||||
.filter_map(|(id, kind, host, port, user, distro)| {
|
||||
Some((
|
||||
RemoteConnectionId(id),
|
||||
Self::remote_connection_from_row(kind, host, port, user, distro)?,
|
||||
))
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
|
||||
query! {
|
||||
fn ssh_connection_query(id: u64) -> Result<(String, Option<u16>, Option<String>)> {
|
||||
SELECT host, port, user
|
||||
FROM ssh_connections
|
||||
pub(crate) fn remote_connection(
|
||||
&self,
|
||||
id: RemoteConnectionId,
|
||||
) -> Result<RemoteConnectionOptions> {
|
||||
let (kind, host, port, user, distro) = self.select_row_bound(sql!(
|
||||
SELECT kind, host, port, user, distro
|
||||
FROM remote_connections
|
||||
WHERE id = ?
|
||||
))?(id.0)?
|
||||
.context("no such remote connection")?;
|
||||
Self::remote_connection_from_row(kind, host, port, user, distro)
|
||||
.context("invalid remote_connection row")
|
||||
}
|
||||
|
||||
fn remote_connection_from_row(
|
||||
kind: String,
|
||||
host: Option<String>,
|
||||
port: Option<u16>,
|
||||
user: Option<String>,
|
||||
distro: Option<String>,
|
||||
) -> Option<RemoteConnectionOptions> {
|
||||
match RemoteConnectionKind::deserialize(&kind)? {
|
||||
RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions {
|
||||
distro_name: distro?,
|
||||
user: user,
|
||||
})),
|
||||
RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host?,
|
||||
port,
|
||||
username: user,
|
||||
..Default::default()
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1108,14 +1246,14 @@ impl WorkspaceDb {
|
||||
) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
|
||||
let mut result = Vec::new();
|
||||
let mut delete_tasks = Vec::new();
|
||||
let ssh_connections = self.ssh_connections()?;
|
||||
let remote_connections = self.remote_connections()?;
|
||||
|
||||
for (id, paths, ssh_connection_id) in self.recent_workspaces()? {
|
||||
if let Some(ssh_connection_id) = ssh_connection_id.map(SshConnectionId) {
|
||||
if let Some(ssh_connection) = ssh_connections.get(&ssh_connection_id) {
|
||||
for (id, paths, remote_connection_id) in self.recent_workspaces()? {
|
||||
if let Some(remote_connection_id) = remote_connection_id {
|
||||
if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
|
||||
result.push((
|
||||
id,
|
||||
SerializedWorkspaceLocation::Ssh(ssh_connection.clone()),
|
||||
SerializedWorkspaceLocation::Remote(connection_options.clone()),
|
||||
paths,
|
||||
));
|
||||
} else {
|
||||
@@ -1157,12 +1295,14 @@ impl WorkspaceDb {
|
||||
) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
|
||||
let mut workspaces = Vec::new();
|
||||
|
||||
for (paths, window_id, ssh_connection_id) in
|
||||
for (paths, window_id, remote_connection_id) in
|
||||
self.session_workspaces(last_session_id.to_owned())?
|
||||
{
|
||||
if let Some(ssh_connection_id) = ssh_connection_id {
|
||||
if let Some(remote_connection_id) = remote_connection_id {
|
||||
workspaces.push((
|
||||
SerializedWorkspaceLocation::Ssh(self.ssh_connection(ssh_connection_id)?),
|
||||
SerializedWorkspaceLocation::Remote(
|
||||
self.remote_connection(remote_connection_id)?,
|
||||
),
|
||||
paths,
|
||||
window_id.map(WindowId::from),
|
||||
));
|
||||
@@ -1545,6 +1685,7 @@ mod tests {
|
||||
};
|
||||
use gpui;
|
||||
use pretty_assertions::assert_eq;
|
||||
use remote::SshConnectionOptions;
|
||||
use std::{thread, time::Duration};
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2196,14 +2337,20 @@ mod tests {
|
||||
};
|
||||
|
||||
let connection_id = db
|
||||
.get_or_create_ssh_connection("my-host".to_string(), Some(1234), None)
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "my-host".to_string(),
|
||||
port: Some(1234),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let workspace_5 = SerializedWorkspace {
|
||||
id: WorkspaceId(5),
|
||||
paths: PathList::default(),
|
||||
location: SerializedWorkspaceLocation::Ssh(db.ssh_connection(connection_id).unwrap()),
|
||||
location: SerializedWorkspaceLocation::Remote(
|
||||
db.remote_connection(connection_id).unwrap(),
|
||||
),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@@ -2362,13 +2509,12 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_last_session_workspace_locations_ssh_projects() {
|
||||
let db = WorkspaceDb::open_test_db(
|
||||
"test_serializing_workspaces_last_session_workspaces_ssh_projects",
|
||||
)
|
||||
.await;
|
||||
async fn test_last_session_workspace_locations_remote() {
|
||||
let db =
|
||||
WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote")
|
||||
.await;
|
||||
|
||||
let ssh_connections = [
|
||||
let remote_connections = [
|
||||
("host-1", "my-user-1"),
|
||||
("host-2", "my-user-2"),
|
||||
("host-3", "my-user-3"),
|
||||
@@ -2376,30 +2522,31 @@ mod tests {
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(host, user)| async {
|
||||
db.get_or_create_ssh_connection(host.to_string(), None, Some(user.to_string()))
|
||||
let options = RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host.to_string(),
|
||||
username: Some(user.to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
db.get_or_create_remote_connection(options.clone())
|
||||
.await
|
||||
.unwrap();
|
||||
SerializedSshConnection {
|
||||
host: host.into(),
|
||||
port: None,
|
||||
user: Some(user.into()),
|
||||
}
|
||||
options
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let ssh_connections = futures::future::join_all(ssh_connections).await;
|
||||
let remote_connections = futures::future::join_all(remote_connections).await;
|
||||
|
||||
let workspaces = [
|
||||
(1, ssh_connections[0].clone(), 9),
|
||||
(2, ssh_connections[1].clone(), 5),
|
||||
(3, ssh_connections[2].clone(), 8),
|
||||
(4, ssh_connections[3].clone(), 2),
|
||||
(1, remote_connections[0].clone(), 9),
|
||||
(2, remote_connections[1].clone(), 5),
|
||||
(3, remote_connections[2].clone(), 8),
|
||||
(4, remote_connections[3].clone(), 2),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(id, ssh_connection, window_id)| SerializedWorkspace {
|
||||
.map(|(id, remote_connection, window_id)| SerializedWorkspace {
|
||||
id: WorkspaceId(id),
|
||||
paths: PathList::default(),
|
||||
location: SerializedWorkspaceLocation::Ssh(ssh_connection),
|
||||
location: SerializedWorkspaceLocation::Remote(remote_connection),
|
||||
center_group: Default::default(),
|
||||
window_bounds: Default::default(),
|
||||
display: Default::default(),
|
||||
@@ -2429,28 +2576,28 @@ mod tests {
|
||||
assert_eq!(
|
||||
have[0],
|
||||
(
|
||||
SerializedWorkspaceLocation::Ssh(ssh_connections[3].clone()),
|
||||
SerializedWorkspaceLocation::Remote(remote_connections[3].clone()),
|
||||
PathList::default()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
have[1],
|
||||
(
|
||||
SerializedWorkspaceLocation::Ssh(ssh_connections[2].clone()),
|
||||
SerializedWorkspaceLocation::Remote(remote_connections[2].clone()),
|
||||
PathList::default()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
have[2],
|
||||
(
|
||||
SerializedWorkspaceLocation::Ssh(ssh_connections[1].clone()),
|
||||
SerializedWorkspaceLocation::Remote(remote_connections[1].clone()),
|
||||
PathList::default()
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
have[3],
|
||||
(
|
||||
SerializedWorkspaceLocation::Ssh(ssh_connections[0].clone()),
|
||||
SerializedWorkspaceLocation::Remote(remote_connections[0].clone()),
|
||||
PathList::default()
|
||||
)
|
||||
);
|
||||
@@ -2465,13 +2612,23 @@ mod tests {
|
||||
let user = Some("user".to_string());
|
||||
|
||||
let connection_id = db
|
||||
.get_or_create_ssh_connection(host.clone(), port, user.clone())
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host.clone(),
|
||||
port,
|
||||
username: user.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Test that calling the function again with the same parameters returns the same project
|
||||
let same_connection = db
|
||||
.get_or_create_ssh_connection(host.clone(), port, user.clone())
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host.clone(),
|
||||
port,
|
||||
username: user.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2483,7 +2640,12 @@ mod tests {
|
||||
let user2 = Some("otheruser".to_string());
|
||||
|
||||
let different_connection = db
|
||||
.get_or_create_ssh_connection(host2.clone(), port2, user2.clone())
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host2.clone(),
|
||||
port: port2,
|
||||
username: user2.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2497,12 +2659,22 @@ mod tests {
|
||||
let (host, port, user) = ("example.com".to_string(), None, None);
|
||||
|
||||
let connection_id = db
|
||||
.get_or_create_ssh_connection(host.clone(), port, None)
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host.clone(),
|
||||
port,
|
||||
username: None,
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let same_connection_id = db
|
||||
.get_or_create_ssh_connection(host.clone(), port, user.clone())
|
||||
.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: host.clone(),
|
||||
port,
|
||||
username: user.clone(),
|
||||
..Default::default()
|
||||
}))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -2510,8 +2682,8 @@ mod tests {
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_get_ssh_connections() {
|
||||
let db = WorkspaceDb::open_test_db("test_get_ssh_connections").await;
|
||||
async fn test_get_remote_connections() {
|
||||
let db = WorkspaceDb::open_test_db("test_get_remote_connections").await;
|
||||
|
||||
let connections = [
|
||||
("example.com".to_string(), None, None),
|
||||
@@ -2526,39 +2698,49 @@ mod tests {
|
||||
let mut ids = Vec::new();
|
||||
for (host, port, user) in connections.iter() {
|
||||
ids.push(
|
||||
db.get_or_create_ssh_connection(host.clone(), *port, user.clone())
|
||||
.await
|
||||
.unwrap(),
|
||||
db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh(
|
||||
SshConnectionOptions {
|
||||
host: host.clone(),
|
||||
port: *port,
|
||||
username: user.clone(),
|
||||
..Default::default()
|
||||
},
|
||||
))
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let stored_projects = db.ssh_connections().unwrap();
|
||||
let stored_connections = db.remote_connections().unwrap();
|
||||
assert_eq!(
|
||||
stored_projects,
|
||||
stored_connections,
|
||||
[
|
||||
(
|
||||
ids[0],
|
||||
SerializedSshConnection {
|
||||
RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "example.com".into(),
|
||||
port: None,
|
||||
user: None,
|
||||
}
|
||||
username: None,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
ids[1],
|
||||
SerializedSshConnection {
|
||||
RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "anotherexample.com".into(),
|
||||
port: Some(123),
|
||||
user: Some("user2".into()),
|
||||
}
|
||||
username: Some("user2".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
(
|
||||
ids[2],
|
||||
SerializedSshConnection {
|
||||
RemoteConnectionOptions::Ssh(SshConnectionOptions {
|
||||
host: "yetanother.com".into(),
|
||||
port: Some(345),
|
||||
user: None,
|
||||
}
|
||||
username: None,
|
||||
..Default::default()
|
||||
}),
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
|
||||
@@ -12,7 +12,7 @@ use db::sqlez::{
|
||||
use gpui::{AsyncWindowContext, Entity, WeakEntity};
|
||||
|
||||
use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use remote::RemoteConnectionOptions;
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
@@ -24,19 +24,18 @@ use uuid::Uuid;
|
||||
#[derive(
|
||||
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||
)]
|
||||
pub(crate) struct SshConnectionId(pub u64);
|
||||
pub(crate) struct RemoteConnectionId(pub u64);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct SerializedSshConnection {
|
||||
pub host: String,
|
||||
pub port: Option<u16>,
|
||||
pub user: Option<String>,
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
pub(crate) enum RemoteConnectionKind {
|
||||
Ssh,
|
||||
Wsl,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
pub enum SerializedWorkspaceLocation {
|
||||
Local,
|
||||
Ssh(SerializedSshConnection),
|
||||
Remote(RemoteConnectionOptions),
|
||||
}
|
||||
|
||||
impl SerializedWorkspaceLocation {
|
||||
@@ -68,6 +67,23 @@ pub struct DockStructure {
|
||||
pub(crate) bottom: DockData,
|
||||
}
|
||||
|
||||
impl RemoteConnectionKind {
|
||||
pub(crate) fn serialize(&self) -> &'static str {
|
||||
match self {
|
||||
RemoteConnectionKind::Ssh => "ssh",
|
||||
RemoteConnectionKind::Wsl => "wsl",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn deserialize(text: &str) -> Option<Self> {
|
||||
match text {
|
||||
"ssh" => Some(Self::Ssh),
|
||||
"wsl" => Some(Self::Wsl),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Column for DockStructure {
|
||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||
let (left, next_index) = DockData::column(statement, start_index)?;
|
||||
|
||||
@@ -67,14 +67,14 @@ pub use pane_group::*;
|
||||
use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
|
||||
pub use persistence::{
|
||||
DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
|
||||
model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation},
|
||||
model::{ItemId, SerializedWorkspaceLocation},
|
||||
};
|
||||
use postage::stream::Stream;
|
||||
use project::{
|
||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
|
||||
};
|
||||
use remote::{RemoteClientDelegate, SshConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use schemars::JsonSchema;
|
||||
use serde::Deserialize;
|
||||
use session::AppSession;
|
||||
@@ -5262,14 +5262,7 @@ impl Workspace {
|
||||
fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation {
|
||||
let paths = PathList::new(&self.root_paths(cx));
|
||||
if let Some(connection) = self.project.read(cx).remote_connection_options(cx) {
|
||||
WorkspaceLocation::Location(
|
||||
SerializedWorkspaceLocation::Ssh(SerializedSshConnection {
|
||||
host: connection.host,
|
||||
port: connection.port,
|
||||
user: connection.username,
|
||||
}),
|
||||
paths,
|
||||
)
|
||||
WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths)
|
||||
} else if self.project.read(cx).is_local() {
|
||||
if !paths.is_empty() {
|
||||
WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths)
|
||||
@@ -7282,9 +7275,9 @@ pub fn create_and_open_local_file(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_ssh_project_with_new_connection(
|
||||
pub fn open_remote_project_with_new_connection(
|
||||
window: WindowHandle<Workspace>,
|
||||
connection_options: SshConnectionOptions,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
cancel_rx: oneshot::Receiver<()>,
|
||||
delegate: Arc<dyn RemoteClientDelegate>,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -7293,11 +7286,11 @@ pub fn open_ssh_project_with_new_connection(
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let (workspace_id, serialized_workspace) =
|
||||
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
|
||||
let session = match cx
|
||||
.update(|cx| {
|
||||
remote::RemoteClient::ssh(
|
||||
remote::RemoteClient::new(
|
||||
ConnectionIdentifier::Workspace(workspace_id.0),
|
||||
connection_options,
|
||||
cancel_rx,
|
||||
@@ -7323,7 +7316,7 @@ pub fn open_ssh_project_with_new_connection(
|
||||
)
|
||||
})?;
|
||||
|
||||
open_ssh_project_inner(
|
||||
open_remote_project_inner(
|
||||
project,
|
||||
paths,
|
||||
workspace_id,
|
||||
@@ -7336,8 +7329,8 @@ pub fn open_ssh_project_with_new_connection(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn open_ssh_project_with_existing_connection(
|
||||
connection_options: SshConnectionOptions,
|
||||
pub fn open_remote_project_with_existing_connection(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
project: Entity<Project>,
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
@@ -7346,9 +7339,9 @@ pub fn open_ssh_project_with_existing_connection(
|
||||
) -> Task<Result<()>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let (workspace_id, serialized_workspace) =
|
||||
serialize_ssh_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
serialize_remote_project(connection_options.clone(), paths.clone(), cx).await?;
|
||||
|
||||
open_ssh_project_inner(
|
||||
open_remote_project_inner(
|
||||
project,
|
||||
paths,
|
||||
workspace_id,
|
||||
@@ -7361,7 +7354,7 @@ pub fn open_ssh_project_with_existing_connection(
|
||||
})
|
||||
}
|
||||
|
||||
async fn open_ssh_project_inner(
|
||||
async fn open_remote_project_inner(
|
||||
project: Entity<Project>,
|
||||
paths: Vec<PathBuf>,
|
||||
workspace_id: WorkspaceId,
|
||||
@@ -7448,22 +7441,18 @@ async fn open_ssh_project_inner(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn serialize_ssh_project(
|
||||
connection_options: SshConnectionOptions,
|
||||
fn serialize_remote_project(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
cx: &AsyncApp,
|
||||
) -> Task<Result<(WorkspaceId, Option<SerializedWorkspace>)>> {
|
||||
cx.background_spawn(async move {
|
||||
let ssh_connection_id = persistence::DB
|
||||
.get_or_create_ssh_connection(
|
||||
connection_options.host.clone(),
|
||||
connection_options.port,
|
||||
connection_options.username.clone(),
|
||||
)
|
||||
let remote_connection_id = persistence::DB
|
||||
.get_or_create_remote_connection(connection_options)
|
||||
.await?;
|
||||
|
||||
let serialized_workspace =
|
||||
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
|
||||
persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
|
||||
|
||||
let workspace_id = if let Some(workspace_id) =
|
||||
serialized_workspace.as_ref().map(|workspace| workspace.id)
|
||||
@@ -8013,22 +8002,20 @@ pub struct WorkspacePosition {
|
||||
pub centered_layout: bool,
|
||||
}
|
||||
|
||||
pub fn ssh_workspace_position_from_db(
|
||||
host: String,
|
||||
port: Option<u16>,
|
||||
user: Option<String>,
|
||||
pub fn remote_workspace_position_from_db(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths_to_open: &[PathBuf],
|
||||
cx: &App,
|
||||
) -> Task<Result<WorkspacePosition>> {
|
||||
let paths = paths_to_open.to_vec();
|
||||
|
||||
cx.background_spawn(async move {
|
||||
let ssh_connection_id = persistence::DB
|
||||
.get_or_create_ssh_connection(host, port, user)
|
||||
let remote_connection_id = persistence::DB
|
||||
.get_or_create_remote_connection(connection_options)
|
||||
.await
|
||||
.context("fetching serialized ssh project")?;
|
||||
let serialized_workspace =
|
||||
persistence::DB.ssh_workspace_for_roots(&paths, ssh_connection_id);
|
||||
persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id);
|
||||
|
||||
let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() {
|
||||
(Some(WindowBounds::Windowed(bounds)), None)
|
||||
|
||||
Reference in New Issue
Block a user