Workspace persistence for SSH projects (#17996)
TODOs: - [x] Add tests to `workspace/src/persistence.rs` - [x] Add a icon for ssh projects - [x] Fix all `TODO` comments - [x] Use `port` if it's passed in the ssh connection options In next PRs: - Make sure unsaved buffers are persisted/restored, along with other items/layout - Handle multiple paths/worktrees correctly Release Notes: - N/A --------- Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
This commit is contained in:
parent
7d0a7541bf
commit
e9f2e72ff0
12 changed files with 592 additions and 141 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -14096,6 +14096,7 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"remote",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
|
@ -39,7 +39,6 @@ use ui::{
|
||||||
RadioWithLabel, Tooltip,
|
RadioWithLabel, Tooltip,
|
||||||
};
|
};
|
||||||
use ui_input::{FieldLabelLayout, TextField};
|
use ui_input::{FieldLabelLayout, TextField};
|
||||||
use util::paths::PathWithPosition;
|
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::notifications::NotifyResultExt;
|
use workspace::notifications::NotifyResultExt;
|
||||||
use workspace::OpenOptions;
|
use workspace::OpenOptions;
|
||||||
|
@ -987,11 +986,7 @@ impl DevServerProjects {
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
let result = open_ssh_project(
|
let result = open_ssh_project(
|
||||||
server.into(),
|
server.into(),
|
||||||
project
|
project.paths.into_iter().map(PathBuf::from).collect(),
|
||||||
.paths
|
|
||||||
.into_iter()
|
|
||||||
.map(|path| PathWithPosition::from_path(PathBuf::from(path)))
|
|
||||||
.collect(),
|
|
||||||
app_state,
|
app_state,
|
||||||
OpenOptions::default(),
|
OpenOptions::default(),
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
|
|
@ -2,6 +2,7 @@ mod dev_servers;
|
||||||
pub mod disconnected_overlay;
|
pub mod disconnected_overlay;
|
||||||
mod ssh_connections;
|
mod ssh_connections;
|
||||||
mod ssh_remotes;
|
mod ssh_remotes;
|
||||||
|
use remote::SshConnectionOptions;
|
||||||
pub use ssh_connections::open_ssh_project;
|
pub use ssh_connections::open_ssh_project;
|
||||||
|
|
||||||
use client::{DevServerProjectId, ProjectId};
|
use client::{DevServerProjectId, ProjectId};
|
||||||
|
@ -32,8 +33,8 @@ use ui::{
|
||||||
};
|
};
|
||||||
use util::{paths::PathExt, ResultExt};
|
use util::{paths::PathExt, ResultExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
AppState, CloseIntent, ModalView, SerializedWorkspaceLocation, Workspace, WorkspaceId,
|
AppState, CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace,
|
||||||
WORKSPACE_DB,
|
WorkspaceId, WORKSPACE_DB,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(PartialEq, Clone, Deserialize, Default)]
|
#[derive(PartialEq, Clone, Deserialize, Default)]
|
||||||
|
@ -172,7 +173,7 @@ pub struct RecentProjectsDelegate {
|
||||||
create_new_window: bool,
|
create_new_window: bool,
|
||||||
// Flag to reset index when there is a new query vs not reset index when user delete an item
|
// Flag to reset index when there is a new query vs not reset index when user delete an item
|
||||||
reset_selected_match_index: bool,
|
reset_selected_match_index: bool,
|
||||||
has_any_dev_server_projects: bool,
|
has_any_non_local_projects: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentProjectsDelegate {
|
impl RecentProjectsDelegate {
|
||||||
|
@ -185,16 +186,16 @@ impl RecentProjectsDelegate {
|
||||||
create_new_window,
|
create_new_window,
|
||||||
render_paths,
|
render_paths,
|
||||||
reset_selected_match_index: true,
|
reset_selected_match_index: true,
|
||||||
has_any_dev_server_projects: false,
|
has_any_non_local_projects: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
|
pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
|
||||||
self.workspaces = workspaces;
|
self.workspaces = workspaces;
|
||||||
self.has_any_dev_server_projects = self
|
self.has_any_non_local_projects = !self
|
||||||
.workspaces
|
.workspaces
|
||||||
.iter()
|
.iter()
|
||||||
.any(|(_, location)| matches!(location, SerializedWorkspaceLocation::DevServer(_)));
|
.all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
|
||||||
|
@ -258,6 +259,23 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
dev_server_project.paths.join("")
|
dev_server_project.paths.join("")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
||||||
|
format!(
|
||||||
|
"{}{}{}{}",
|
||||||
|
ssh_project.host,
|
||||||
|
ssh_project
|
||||||
|
.port
|
||||||
|
.as_ref()
|
||||||
|
.map(|port| port.to_string())
|
||||||
|
.unwrap_or_default(),
|
||||||
|
ssh_project.path,
|
||||||
|
ssh_project
|
||||||
|
.user
|
||||||
|
.as_ref()
|
||||||
|
.map(|user| user.to_string())
|
||||||
|
.unwrap_or_default()
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
StringMatchCandidate::new(id, combined_string)
|
StringMatchCandidate::new(id, combined_string)
|
||||||
|
@ -364,6 +382,33 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
};
|
};
|
||||||
open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx)
|
open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx)
|
||||||
}
|
}
|
||||||
|
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
||||||
|
let app_state = workspace.app_state().clone();
|
||||||
|
|
||||||
|
let replace_window = if replace_current_window {
|
||||||
|
cx.window_handle().downcast::<Workspace>()
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
let open_options = OpenOptions {
|
||||||
|
replace_window,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
let connection_options = SshConnectionOptions {
|
||||||
|
host: ssh_project.host.clone(),
|
||||||
|
username: ssh_project.user.clone(),
|
||||||
|
port: ssh_project.port,
|
||||||
|
password: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let paths = vec![PathBuf::from(ssh_project.path.clone())];
|
||||||
|
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -392,7 +437,6 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
|
|
||||||
let (_, location) = self.workspaces.get(hit.candidate_id)?;
|
let (_, location) = self.workspaces.get(hit.candidate_id)?;
|
||||||
|
|
||||||
let is_remote = matches!(location, SerializedWorkspaceLocation::DevServer(_));
|
|
||||||
let dev_server_status =
|
let dev_server_status =
|
||||||
if let SerializedWorkspaceLocation::DevServer(dev_server_project) = location {
|
if let SerializedWorkspaceLocation::DevServer(dev_server_project) = location {
|
||||||
let store = dev_server_projects::Store::global(cx).read(cx);
|
let store = dev_server_projects::Store::global(cx).read(cx);
|
||||||
|
@ -416,6 +460,9 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
.filter_map(|i| paths.paths().get(*i).cloned())
|
.filter_map(|i| paths.paths().get(*i).cloned())
|
||||||
.collect(),
|
.collect(),
|
||||||
),
|
),
|
||||||
|
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
||||||
|
Arc::new(vec![PathBuf::from(ssh_project.ssh_url())])
|
||||||
|
}
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
|
||||||
Arc::new(vec![PathBuf::from(format!(
|
Arc::new(vec![PathBuf::from(format!(
|
||||||
"{}:{}",
|
"{}:{}",
|
||||||
|
@ -457,29 +504,34 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.gap_3()
|
.gap_3()
|
||||||
.when(self.has_any_dev_server_projects, |this| {
|
.when(self.has_any_non_local_projects, |this| {
|
||||||
this.child(if is_remote {
|
this.child(match location {
|
||||||
// if disabled, Color::Disabled
|
SerializedWorkspaceLocation::Local(_, _) => {
|
||||||
let indicator_color = match dev_server_status {
|
Icon::new(IconName::Screen)
|
||||||
Some(DevServerStatus::Online) => Color::Created,
|
.color(Color::Muted)
|
||||||
Some(DevServerStatus::Offline) => Color::Hidden,
|
.into_any_element()
|
||||||
_ => unreachable!(),
|
}
|
||||||
};
|
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Screen)
|
||||||
IconWithIndicator::new(
|
|
||||||
Icon::new(IconName::Server).color(Color::Muted),
|
|
||||||
Some(Indicator::dot()),
|
|
||||||
)
|
|
||||||
.indicator_color(indicator_color)
|
|
||||||
.indicator_border_color(if selected {
|
|
||||||
Some(cx.theme().colors().element_selected)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
} else {
|
|
||||||
Icon::new(IconName::Screen)
|
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
|
.into_any_element(),
|
||||||
|
SerializedWorkspaceLocation::DevServer(_) => {
|
||||||
|
let indicator_color = match dev_server_status {
|
||||||
|
Some(DevServerStatus::Online) => Color::Created,
|
||||||
|
Some(DevServerStatus::Offline) => Color::Hidden,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
IconWithIndicator::new(
|
||||||
|
Icon::new(IconName::Server).color(Color::Muted),
|
||||||
|
Some(Indicator::dot()),
|
||||||
|
)
|
||||||
|
.indicator_color(indicator_color)
|
||||||
|
.indicator_border_color(if selected {
|
||||||
|
Some(cx.theme().colors().element_selected)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.child({
|
.child({
|
||||||
|
|
|
@ -19,7 +19,6 @@ use ui::{
|
||||||
h_flex, v_flex, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement,
|
h_flex, v_flex, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement,
|
||||||
Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, WindowContext,
|
Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, WindowContext,
|
||||||
};
|
};
|
||||||
use util::paths::PathWithPosition;
|
|
||||||
use workspace::{AppState, ModalView, Workspace};
|
use workspace::{AppState, ModalView, Workspace};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
|
@ -358,24 +357,29 @@ pub fn connect_over_ssh(
|
||||||
|
|
||||||
pub async fn open_ssh_project(
|
pub async fn open_ssh_project(
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
paths: Vec<PathWithPosition>,
|
paths: Vec<PathBuf>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
_open_options: workspace::OpenOptions,
|
open_options: workspace::OpenOptions,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
||||||
let window = cx.open_window(options, |cx| {
|
|
||||||
let project = project::Project::local(
|
let window = if let Some(window) = open_options.replace_window {
|
||||||
app_state.client.clone(),
|
window
|
||||||
app_state.node_runtime.clone(),
|
} else {
|
||||||
app_state.user_store.clone(),
|
cx.open_window(options, |cx| {
|
||||||
app_state.languages.clone(),
|
let project = project::Project::local(
|
||||||
app_state.fs.clone(),
|
app_state.client.clone(),
|
||||||
None,
|
app_state.node_runtime.clone(),
|
||||||
cx,
|
app_state.user_store.clone(),
|
||||||
);
|
app_state.languages.clone(),
|
||||||
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
app_state.fs.clone(),
|
||||||
})?;
|
None,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
let result = window
|
let result = window
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
|
@ -387,40 +391,17 @@ pub async fn open_ssh_project(
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.prompt
|
.prompt
|
||||||
.clone();
|
.clone();
|
||||||
connect_over_ssh(connection_options, ui, cx)
|
connect_over_ssh(connection_options.clone(), ui, cx)
|
||||||
})?
|
})?
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if result.is_err() {
|
if result.is_err() {
|
||||||
window.update(cx, |_, cx| cx.remove_window()).ok();
|
window.update(cx, |_, cx| cx.remove_window()).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
let session = result?;
|
let session = result?;
|
||||||
|
|
||||||
let project = cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
project::Project::ssh(
|
workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx)
|
||||||
session,
|
})?
|
||||||
app_state.client.clone(),
|
.await
|
||||||
app_state.node_runtime.clone(),
|
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.languages.clone(),
|
|
||||||
app_state.fs.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for path in paths {
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.find_or_create_worktree(&path.path, true, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.update(cx, |_, cx| {
|
|
||||||
cx.replace_root_view(|cx| Workspace::new(None, project, app_state, cx))
|
|
||||||
})?;
|
|
||||||
window.update(cx, |_, cx| cx.activate_window())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,11 @@ use std::{
|
||||||
};
|
};
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
|
||||||
|
)]
|
||||||
|
pub struct SshProjectId(pub u64);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SshSocket {
|
pub struct SshSocket {
|
||||||
connection_options: SshConnectionOptions,
|
connection_options: SshConnectionOptions,
|
||||||
|
|
|
@ -196,6 +196,22 @@ impl Column for u32 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StaticColumnCount for u16 {}
|
||||||
|
impl Bind for u16 {
|
||||||
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
(*self as i64)
|
||||||
|
.bind(statement, start_index)
|
||||||
|
.with_context(|| format!("Failed to bind usize at index {start_index}"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column for u16 {
|
||||||
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
|
let result = statement.column_int64(start_index)?;
|
||||||
|
Ok((result as u16, start_index + 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl StaticColumnCount for usize {}
|
impl StaticColumnCount for usize {}
|
||||||
impl Bind for usize {
|
impl Bind for usize {
|
||||||
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
|
|
@ -74,7 +74,7 @@ impl Connection {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare a statement which takes a binding and selects a single row
|
/// Prepare a statement which takes a binding and selects a single row
|
||||||
/// from the database. WIll return none if no rows are returned and will
|
/// from the database. Will return none if no rows are returned and will
|
||||||
/// error if more than 1 row is returned.
|
/// error if more than 1 row is returned.
|
||||||
///
|
///
|
||||||
/// Note: If there are multiple statements that depend upon each other
|
/// Note: If there are multiple statements that depend upon each other
|
||||||
|
|
|
@ -51,6 +51,7 @@ postage.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
|
remote.workspace = true
|
||||||
schemars.workspace = true
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
@ -7,6 +7,7 @@ use client::DevServerProjectId;
|
||||||
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
|
||||||
use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};
|
use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId};
|
||||||
|
|
||||||
|
use remote::ssh_session::SshProjectId;
|
||||||
use sqlez::{
|
use sqlez::{
|
||||||
bindable::{Bind, Column, StaticColumnCount},
|
bindable::{Bind, Column, StaticColumnCount},
|
||||||
statement::Statement,
|
statement::Statement,
|
||||||
|
@ -20,7 +21,7 @@ use crate::WorkspaceId;
|
||||||
|
|
||||||
use model::{
|
use model::{
|
||||||
GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
GroupId, LocalPaths, PaneId, SerializedItem, SerializedPane, SerializedPaneGroup,
|
||||||
SerializedWorkspace,
|
SerializedSshProject, SerializedWorkspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
use self::model::{
|
use self::model::{
|
||||||
|
@ -354,7 +355,17 @@ define_connection! {
|
||||||
),
|
),
|
||||||
sql!(
|
sql!(
|
||||||
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
ALTER TABLE panes ADD COLUMN pinned_count INTEGER DEFAULT 0;
|
||||||
)
|
),
|
||||||
|
sql!(
|
||||||
|
CREATE TABLE ssh_projects (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
host TEXT NOT NULL,
|
||||||
|
port INTEGER,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
user TEXT
|
||||||
|
);
|
||||||
|
ALTER TABLE workspaces ADD COLUMN ssh_project_id INTEGER REFERENCES ssh_projects(id) ON DELETE CASCADE;
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -374,7 +385,6 @@ impl WorkspaceDb {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
local_paths,
|
local_paths,
|
||||||
local_paths_order,
|
local_paths_order,
|
||||||
dev_server_project_id,
|
|
||||||
window_bounds,
|
window_bounds,
|
||||||
display,
|
display,
|
||||||
centered_layout,
|
centered_layout,
|
||||||
|
@ -384,7 +394,6 @@ impl WorkspaceDb {
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
Option<LocalPaths>,
|
Option<LocalPaths>,
|
||||||
Option<LocalPathsOrder>,
|
Option<LocalPathsOrder>,
|
||||||
Option<u64>,
|
|
||||||
Option<SerializedWindowBounds>,
|
Option<SerializedWindowBounds>,
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
Option<bool>,
|
Option<bool>,
|
||||||
|
@ -396,7 +405,6 @@ impl WorkspaceDb {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
local_paths,
|
local_paths,
|
||||||
local_paths_order,
|
local_paths_order,
|
||||||
dev_server_project_id,
|
|
||||||
window_state,
|
window_state,
|
||||||
window_x,
|
window_x,
|
||||||
window_y,
|
window_y,
|
||||||
|
@ -422,28 +430,13 @@ impl WorkspaceDb {
|
||||||
.warn_on_err()
|
.warn_on_err()
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
let location = if let Some(dev_server_project_id) = dev_server_project_id {
|
let local_paths = local_paths?;
|
||||||
let dev_server_project: SerializedDevServerProject = self
|
let location = match local_paths_order {
|
||||||
.select_row_bound(sql! {
|
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
|
||||||
SELECT id, path, dev_server_name
|
None => {
|
||||||
FROM dev_server_projects
|
let order = LocalPathsOrder::default_for_paths(&local_paths);
|
||||||
WHERE id = ?
|
SerializedWorkspaceLocation::Local(local_paths, order)
|
||||||
})
|
|
||||||
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
|
|
||||||
.context("No remote project found")
|
|
||||||
.warn_on_err()
|
|
||||||
.flatten()?;
|
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project)
|
|
||||||
} else if let Some(local_paths) = local_paths {
|
|
||||||
match local_paths_order {
|
|
||||||
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
|
|
||||||
None => {
|
|
||||||
let order = LocalPathsOrder::default_for_paths(&local_paths);
|
|
||||||
SerializedWorkspaceLocation::Local(local_paths, order)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(SerializedWorkspace {
|
Some(SerializedWorkspace {
|
||||||
|
@ -470,8 +463,6 @@ impl WorkspaceDb {
|
||||||
// and we've grabbed the most recent workspace
|
// and we've grabbed the most recent workspace
|
||||||
let (
|
let (
|
||||||
workspace_id,
|
workspace_id,
|
||||||
local_paths,
|
|
||||||
local_paths_order,
|
|
||||||
dev_server_project_id,
|
dev_server_project_id,
|
||||||
window_bounds,
|
window_bounds,
|
||||||
display,
|
display,
|
||||||
|
@ -480,8 +471,6 @@ impl WorkspaceDb {
|
||||||
window_id,
|
window_id,
|
||||||
): (
|
): (
|
||||||
WorkspaceId,
|
WorkspaceId,
|
||||||
Option<LocalPaths>,
|
|
||||||
Option<LocalPathsOrder>,
|
|
||||||
Option<u64>,
|
Option<u64>,
|
||||||
Option<SerializedWindowBounds>,
|
Option<SerializedWindowBounds>,
|
||||||
Option<Uuid>,
|
Option<Uuid>,
|
||||||
|
@ -492,8 +481,6 @@ impl WorkspaceDb {
|
||||||
.select_row_bound(sql! {
|
.select_row_bound(sql! {
|
||||||
SELECT
|
SELECT
|
||||||
workspace_id,
|
workspace_id,
|
||||||
local_paths,
|
|
||||||
local_paths_order,
|
|
||||||
dev_server_project_id,
|
dev_server_project_id,
|
||||||
window_state,
|
window_state,
|
||||||
window_x,
|
window_x,
|
||||||
|
@ -520,29 +507,20 @@ impl WorkspaceDb {
|
||||||
.warn_on_err()
|
.warn_on_err()
|
||||||
.flatten()?;
|
.flatten()?;
|
||||||
|
|
||||||
let location = if let Some(dev_server_project_id) = dev_server_project_id {
|
let dev_server_project_id = dev_server_project_id?;
|
||||||
let dev_server_project: SerializedDevServerProject = self
|
|
||||||
.select_row_bound(sql! {
|
let dev_server_project: SerializedDevServerProject = self
|
||||||
SELECT id, path, dev_server_name
|
.select_row_bound(sql! {
|
||||||
FROM dev_server_projects
|
SELECT id, path, dev_server_name
|
||||||
WHERE id = ?
|
FROM dev_server_projects
|
||||||
})
|
WHERE id = ?
|
||||||
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
|
})
|
||||||
.context("No remote project found")
|
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
|
||||||
.warn_on_err()
|
.context("No remote project found")
|
||||||
.flatten()?;
|
.warn_on_err()
|
||||||
SerializedWorkspaceLocation::DevServer(dev_server_project)
|
.flatten()?;
|
||||||
} else if let Some(local_paths) = local_paths {
|
|
||||||
match local_paths_order {
|
let location = SerializedWorkspaceLocation::DevServer(dev_server_project);
|
||||||
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
|
|
||||||
None => {
|
|
||||||
let order = LocalPathsOrder::default_for_paths(&local_paths);
|
|
||||||
SerializedWorkspaceLocation::Local(local_paths, order)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
Some(SerializedWorkspace {
|
Some(SerializedWorkspace {
|
||||||
id: workspace_id,
|
id: workspace_id,
|
||||||
|
@ -560,6 +538,62 @@ impl WorkspaceDb {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn workspace_for_ssh_project(
|
||||||
|
&self,
|
||||||
|
ssh_project: &SerializedSshProject,
|
||||||
|
) -> Option<SerializedWorkspace> {
|
||||||
|
let (workspace_id, window_bounds, display, centered_layout, docks, window_id): (
|
||||||
|
WorkspaceId,
|
||||||
|
Option<SerializedWindowBounds>,
|
||||||
|
Option<Uuid>,
|
||||||
|
Option<bool>,
|
||||||
|
DockStructure,
|
||||||
|
Option<u64>,
|
||||||
|
) = self
|
||||||
|
.select_row_bound(sql! {
|
||||||
|
SELECT
|
||||||
|
workspace_id,
|
||||||
|
window_state,
|
||||||
|
window_x,
|
||||||
|
window_y,
|
||||||
|
window_width,
|
||||||
|
window_height,
|
||||||
|
display,
|
||||||
|
centered_layout,
|
||||||
|
left_dock_visible,
|
||||||
|
left_dock_active_panel,
|
||||||
|
left_dock_zoom,
|
||||||
|
right_dock_visible,
|
||||||
|
right_dock_active_panel,
|
||||||
|
right_dock_zoom,
|
||||||
|
bottom_dock_visible,
|
||||||
|
bottom_dock_active_panel,
|
||||||
|
bottom_dock_zoom,
|
||||||
|
window_id
|
||||||
|
FROM workspaces
|
||||||
|
WHERE ssh_project_id = ?
|
||||||
|
})
|
||||||
|
.and_then(|mut prepared_statement| (prepared_statement)(ssh_project.id.0))
|
||||||
|
.context("No workspaces found")
|
||||||
|
.warn_on_err()
|
||||||
|
.flatten()?;
|
||||||
|
|
||||||
|
Some(SerializedWorkspace {
|
||||||
|
id: workspace_id,
|
||||||
|
location: SerializedWorkspaceLocation::Ssh(ssh_project.clone()),
|
||||||
|
center_group: self
|
||||||
|
.get_center_pane_group(workspace_id)
|
||||||
|
.context("Getting center group")
|
||||||
|
.log_err()?,
|
||||||
|
window_bounds,
|
||||||
|
centered_layout: centered_layout.unwrap_or(false),
|
||||||
|
display,
|
||||||
|
docks,
|
||||||
|
session_id: None,
|
||||||
|
window_id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
|
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
|
||||||
/// that used this workspace previously
|
/// that used this workspace previously
|
||||||
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
|
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
|
||||||
|
@ -674,6 +708,49 @@ impl WorkspaceDb {
|
||||||
workspace.docks,
|
workspace.docks,
|
||||||
))
|
))
|
||||||
.context("Updating workspace")?;
|
.context("Updating workspace")?;
|
||||||
|
},
|
||||||
|
SerializedWorkspaceLocation::Ssh(ssh_project) => {
|
||||||
|
conn.exec_bound(sql!(
|
||||||
|
DELETE FROM workspaces WHERE ssh_project_id = ? AND workspace_id != ?
|
||||||
|
))?((ssh_project.id.0, workspace.id))
|
||||||
|
.context("clearing out old locations")?;
|
||||||
|
|
||||||
|
// Upsert
|
||||||
|
conn.exec_bound(sql!(
|
||||||
|
INSERT INTO workspaces(
|
||||||
|
workspace_id,
|
||||||
|
ssh_project_id,
|
||||||
|
left_dock_visible,
|
||||||
|
left_dock_active_panel,
|
||||||
|
left_dock_zoom,
|
||||||
|
right_dock_visible,
|
||||||
|
right_dock_active_panel,
|
||||||
|
right_dock_zoom,
|
||||||
|
bottom_dock_visible,
|
||||||
|
bottom_dock_active_panel,
|
||||||
|
bottom_dock_zoom,
|
||||||
|
timestamp
|
||||||
|
)
|
||||||
|
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT DO
|
||||||
|
UPDATE SET
|
||||||
|
ssh_project_id = ?2,
|
||||||
|
left_dock_visible = ?3,
|
||||||
|
left_dock_active_panel = ?4,
|
||||||
|
left_dock_zoom = ?5,
|
||||||
|
right_dock_visible = ?6,
|
||||||
|
right_dock_active_panel = ?7,
|
||||||
|
right_dock_zoom = ?8,
|
||||||
|
bottom_dock_visible = ?9,
|
||||||
|
bottom_dock_active_panel = ?10,
|
||||||
|
bottom_dock_zoom = ?11,
|
||||||
|
timestamp = CURRENT_TIMESTAMP
|
||||||
|
))?((
|
||||||
|
workspace.id,
|
||||||
|
ssh_project.id.0,
|
||||||
|
workspace.docks,
|
||||||
|
))
|
||||||
|
.context("Updating workspace")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -688,6 +765,46 @@ impl WorkspaceDb {
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_or_create_ssh_project(
|
||||||
|
&self,
|
||||||
|
host: String,
|
||||||
|
port: Option<u16>,
|
||||||
|
path: String,
|
||||||
|
user: Option<String>,
|
||||||
|
) -> Result<SerializedSshProject> {
|
||||||
|
if let Some(project) = self
|
||||||
|
.get_ssh_project(host.clone(), port, path.clone(), user.clone())
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Ok(project)
|
||||||
|
} else {
|
||||||
|
self.insert_ssh_project(host, port, path, user)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| anyhow!("failed to insert ssh project"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query! {
|
||||||
|
async fn get_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
||||||
|
SELECT id, host, port, path, user
|
||||||
|
FROM ssh_projects
|
||||||
|
WHERE host IS ? AND port IS ? AND path IS ? AND user IS ?
|
||||||
|
LIMIT 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
query! {
|
||||||
|
async fn insert_ssh_project(host: String, port: Option<u16>, path: String, user: Option<String>) -> Result<Option<SerializedSshProject>> {
|
||||||
|
INSERT INTO ssh_projects(
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
path,
|
||||||
|
user
|
||||||
|
) VALUES (?1, ?2, ?3, ?4)
|
||||||
|
RETURNING id, host, port, path, user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
pub async fn next_id() -> Result<WorkspaceId> {
|
pub async fn next_id() -> Result<WorkspaceId> {
|
||||||
INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
|
INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
|
||||||
|
@ -695,10 +812,12 @@ impl WorkspaceDb {
|
||||||
}
|
}
|
||||||
|
|
||||||
query! {
|
query! {
|
||||||
fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>)>> {
|
fn recent_workspaces() -> Result<Vec<(WorkspaceId, LocalPaths, LocalPathsOrder, Option<u64>, Option<u64>)>> {
|
||||||
SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id
|
SELECT workspace_id, local_paths, local_paths_order, dev_server_project_id, ssh_project_id
|
||||||
FROM workspaces
|
FROM workspaces
|
||||||
WHERE local_paths IS NOT NULL OR dev_server_project_id IS NOT NULL
|
WHERE local_paths IS NOT NULL
|
||||||
|
OR dev_server_project_id IS NOT NULL
|
||||||
|
OR ssh_project_id IS NOT NULL
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -719,6 +838,13 @@ impl WorkspaceDb {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
query! {
|
||||||
|
fn ssh_projects() -> Result<Vec<SerializedSshProject>> {
|
||||||
|
SELECT id, host, port, path, user
|
||||||
|
FROM ssh_projects
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn last_window(
|
pub(crate) fn last_window(
|
||||||
&self,
|
&self,
|
||||||
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
|
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
|
||||||
|
@ -768,8 +894,11 @@ impl WorkspaceDb {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut delete_tasks = Vec::new();
|
let mut delete_tasks = Vec::new();
|
||||||
let dev_server_projects = self.dev_server_projects()?;
|
let dev_server_projects = self.dev_server_projects()?;
|
||||||
|
let ssh_projects = self.ssh_projects()?;
|
||||||
|
|
||||||
for (id, location, order, dev_server_project_id) in self.recent_workspaces()? {
|
for (id, location, order, dev_server_project_id, ssh_project_id) in
|
||||||
|
self.recent_workspaces()?
|
||||||
|
{
|
||||||
if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
|
if let Some(dev_server_project_id) = dev_server_project_id.map(DevServerProjectId) {
|
||||||
if let Some(dev_server_project) = dev_server_projects
|
if let Some(dev_server_project) = dev_server_projects
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -782,6 +911,15 @@ impl WorkspaceDb {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ssh_project_id) = ssh_project_id.map(SshProjectId) {
|
||||||
|
if let Some(ssh_project) = ssh_projects.iter().find(|rp| rp.id == ssh_project_id) {
|
||||||
|
result.push((id, SerializedWorkspaceLocation::Ssh(ssh_project.clone())));
|
||||||
|
} else {
|
||||||
|
delete_tasks.push(self.delete_workspace_by_id(id));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if location.paths().iter().all(|path| path.exists())
|
if location.paths().iter().all(|path| path.exists())
|
||||||
&& location.paths().iter().any(|path| path.is_dir())
|
&& location.paths().iter().any(|path| path.is_dir())
|
||||||
{
|
{
|
||||||
|
@ -802,7 +940,9 @@ impl WorkspaceDb {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(_, location)| match location {
|
.filter_map(|(_, location)| match location {
|
||||||
SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
|
SerializedWorkspaceLocation::Local(local_paths, _) => Some(local_paths),
|
||||||
|
// Do not automatically reopen Dev Server and SSH workspaces
|
||||||
SerializedWorkspaceLocation::DevServer(_) => None,
|
SerializedWorkspaceLocation::DevServer(_) => None,
|
||||||
|
SerializedWorkspaceLocation::Ssh(_) => None,
|
||||||
})
|
})
|
||||||
.next())
|
.next())
|
||||||
}
|
}
|
||||||
|
@ -1512,6 +1652,122 @@ mod tests {
|
||||||
assert_eq!(have[3], LocalPaths::new([dir1.path().to_str().unwrap()]));
|
assert_eq!(have[3], LocalPaths::new([dir1.path().to_str().unwrap()]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
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) = (
|
||||||
|
"example.com".to_string(),
|
||||||
|
Some(22_u16),
|
||||||
|
"/home/user".to_string(),
|
||||||
|
Some("user".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let project = db
|
||||||
|
.get_or_create_ssh_project(host.clone(), port, path.clone(), user.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(project.host, host);
|
||||||
|
assert_eq!(project.path, path);
|
||||||
|
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())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(project.id, same_project.id);
|
||||||
|
|
||||||
|
// Test with different parameters
|
||||||
|
let (host2, path2, user2) = (
|
||||||
|
"otherexample.com".to_string(),
|
||||||
|
"/home/otheruser".to_string(),
|
||||||
|
Some("otheruser".to_string()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let different_project = db
|
||||||
|
.get_or_create_ssh_project(host2.clone(), None, path2.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.user, user2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
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) = (
|
||||||
|
"example.com".to_string(),
|
||||||
|
None,
|
||||||
|
"/home/user".to_string(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let project = db
|
||||||
|
.get_or_create_ssh_project(host.clone(), port, path.clone(), None)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(project.host, host);
|
||||||
|
assert_eq!(project.path, path);
|
||||||
|
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())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(project.id, same_project.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_get_ssh_projects() {
|
||||||
|
let db = WorkspaceDb(open_test_db("test_get_ssh_projects").await);
|
||||||
|
|
||||||
|
let projects = vec![
|
||||||
|
(
|
||||||
|
"example.com".to_string(),
|
||||||
|
None,
|
||||||
|
"/home/user".to_string(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"anotherexample.com".to_string(),
|
||||||
|
Some(123_u16),
|
||||||
|
"/home/user2".to_string(),
|
||||||
|
Some("user2".to_string()),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"yetanother.com".to_string(),
|
||||||
|
Some(345_u16),
|
||||||
|
"/home/user3".to_string(),
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (host, port, path, user) in projects.iter() {
|
||||||
|
let project = db
|
||||||
|
.get_or_create_ssh_project(host.clone(), *port, path.clone(), user.clone())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(&project.host, host);
|
||||||
|
assert_eq!(&project.port, port);
|
||||||
|
assert_eq!(&project.path, path);
|
||||||
|
assert_eq!(&project.user, user);
|
||||||
|
}
|
||||||
|
|
||||||
|
let stored_projects = db.ssh_projects().unwrap();
|
||||||
|
assert_eq!(stored_projects.len(), projects.len());
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_simple_split() {
|
async fn test_simple_split() {
|
||||||
env_logger::try_init().ok();
|
env_logger::try_init().ok();
|
||||||
|
|
|
@ -11,6 +11,7 @@ use db::sqlez::{
|
||||||
};
|
};
|
||||||
use gpui::{AsyncWindowContext, Model, View, WeakView};
|
use gpui::{AsyncWindowContext, Model, View, WeakView};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
|
use remote::ssh_session::SshProjectId;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -20,6 +21,69 @@ use ui::SharedString;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
|
pub struct SerializedSshProject {
|
||||||
|
pub id: SshProjectId,
|
||||||
|
pub host: String,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub path: String,
|
||||||
|
pub user: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticColumnCount for SerializedSshProject {
|
||||||
|
fn column_count() -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Bind for &SerializedSshProject {
|
||||||
|
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
|
||||||
|
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)?;
|
||||||
|
statement.bind(&self.user, next_index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Column for SerializedSshProject {
|
||||||
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
|
let id = statement.column_int64(start_index)?;
|
||||||
|
let host = statement.column_text(start_index + 1)?.to_string();
|
||||||
|
let (port, _) = Option::<u16>::column(statement, start_index + 2)?;
|
||||||
|
let path = statement.column_text(start_index + 3)?.to_string();
|
||||||
|
let (user, _) = Option::<String>::column(statement, start_index + 4)?;
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
Self {
|
||||||
|
id: SshProjectId(id as u64),
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
path,
|
||||||
|
user,
|
||||||
|
},
|
||||||
|
start_index + 5,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct SerializedDevServerProject {
|
pub struct SerializedDevServerProject {
|
||||||
pub id: DevServerProjectId,
|
pub id: DevServerProjectId,
|
||||||
|
@ -58,7 +122,6 @@ impl Column for LocalPaths {
|
||||||
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
|
||||||
let path_blob = statement.column_blob(start_index)?;
|
let path_blob = statement.column_blob(start_index)?;
|
||||||
let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
|
let paths: Arc<Vec<PathBuf>> = if path_blob.is_empty() {
|
||||||
println!("path blog is empty");
|
|
||||||
Default::default()
|
Default::default()
|
||||||
} else {
|
} else {
|
||||||
bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
|
bincode::deserialize(path_blob).context("Bincode deserialization of paths failed")?
|
||||||
|
@ -146,6 +209,7 @@ impl Column for SerializedDevServerProject {
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub enum SerializedWorkspaceLocation {
|
pub enum SerializedWorkspaceLocation {
|
||||||
Local(LocalPaths, LocalPathsOrder),
|
Local(LocalPaths, LocalPathsOrder),
|
||||||
|
Ssh(SerializedSshProject),
|
||||||
DevServer(SerializedDevServerProject),
|
DevServer(SerializedDevServerProject),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,15 +49,19 @@ use node_runtime::NodeRuntime;
|
||||||
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
|
use notifications::{simple_message_notification::MessageNotification, NotificationHandle};
|
||||||
pub use pane::*;
|
pub use pane::*;
|
||||||
pub use pane_group::*;
|
pub use pane_group::*;
|
||||||
use persistence::{model::SerializedWorkspace, SerializedWindowBounds, DB};
|
|
||||||
pub use persistence::{
|
pub use persistence::{
|
||||||
model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation},
|
model::{ItemId, LocalPaths, SerializedDevServerProject, SerializedWorkspaceLocation},
|
||||||
WorkspaceDb, DB as WORKSPACE_DB,
|
WorkspaceDb, DB as WORKSPACE_DB,
|
||||||
};
|
};
|
||||||
|
use persistence::{
|
||||||
|
model::{SerializedSshProject, SerializedWorkspace},
|
||||||
|
SerializedWindowBounds, DB,
|
||||||
|
};
|
||||||
use postage::stream::Stream;
|
use postage::stream::Stream;
|
||||||
use project::{
|
use project::{
|
||||||
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
|
||||||
};
|
};
|
||||||
|
use remote::{SshConnectionOptions, SshSession};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use session::AppSession;
|
use session::AppSession;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
|
@ -756,6 +760,7 @@ pub struct Workspace {
|
||||||
render_disconnected_overlay:
|
render_disconnected_overlay:
|
||||||
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
|
||||||
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
|
||||||
|
serialized_ssh_project: Option<SerializedSshProject>,
|
||||||
_items_serializer: Task<Result<()>>,
|
_items_serializer: Task<Result<()>>,
|
||||||
session_id: Option<String>,
|
session_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
@ -1054,6 +1059,7 @@ impl Workspace {
|
||||||
serializable_items_tx,
|
serializable_items_tx,
|
||||||
_items_serializer,
|
_items_serializer,
|
||||||
session_id: Some(session_id),
|
session_id: Some(session_id),
|
||||||
|
serialized_ssh_project: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1440,6 +1446,10 @@ impl Workspace {
|
||||||
self.on_prompt_for_open_path = Some(prompt)
|
self.on_prompt_for_open_path = Some(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
|
||||||
|
self.serialized_ssh_project = Some(serialized_ssh_project);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_render_disconnected_overlay(
|
pub fn set_render_disconnected_overlay(
|
||||||
&mut self,
|
&mut self,
|
||||||
render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
|
render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
|
||||||
|
@ -4097,7 +4107,9 @@ impl Workspace {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let location = if let Some(local_paths) = self.local_paths(cx) {
|
let location = if let Some(ssh_project) = &self.serialized_ssh_project {
|
||||||
|
Some(SerializedWorkspaceLocation::Ssh(ssh_project.clone()))
|
||||||
|
} else if let Some(local_paths) = self.local_paths(cx) {
|
||||||
if !local_paths.is_empty() {
|
if !local_paths.is_empty() {
|
||||||
Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
|
Some(SerializedWorkspaceLocation::from_local_paths(local_paths))
|
||||||
} else {
|
} else {
|
||||||
|
@ -5476,6 +5488,70 @@ pub fn join_hosted_project(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn open_ssh_project(
|
||||||
|
window: WindowHandle<Workspace>,
|
||||||
|
connection_options: SshConnectionOptions,
|
||||||
|
session: Arc<SshSession>,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
paths: Vec<PathBuf>,
|
||||||
|
cx: &mut AppContext,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
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(),
|
||||||
|
connection_options.username.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let project = cx.update(|cx| {
|
||||||
|
project::Project::ssh(
|
||||||
|
session,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree(&path, true, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let serialized_workspace =
|
||||||
|
persistence::DB.workspace_for_ssh_project(&serialized_ssh_project);
|
||||||
|
|
||||||
|
let workspace_id =
|
||||||
|
if let Some(workspace_id) = serialized_workspace.map(|workspace| workspace.id) {
|
||||||
|
workspace_id
|
||||||
|
} else {
|
||||||
|
persistence::DB.next_id().await?
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.update_window(window.into(), |_, cx| {
|
||||||
|
cx.replace_root_view(|cx| {
|
||||||
|
let mut workspace =
|
||||||
|
Workspace::new(Some(workspace_id), project, app_state.clone(), cx);
|
||||||
|
workspace.set_serialized_ssh_project(serialized_ssh_project);
|
||||||
|
workspace
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
window.update(&mut cx, |_, cx| cx.activate_window())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn join_dev_server_project(
|
pub fn join_dev_server_project(
|
||||||
dev_server_project_id: DevServerProjectId,
|
dev_server_project_id: DevServerProjectId,
|
||||||
project_id: ProjectId,
|
project_id: ProjectId,
|
||||||
|
|
|
@ -667,7 +667,11 @@ fn handle_open_request(
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
open_ssh_project(
|
open_ssh_project(
|
||||||
connection_info,
|
connection_info,
|
||||||
request.open_paths,
|
request
|
||||||
|
.open_paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| path.path)
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
app_state,
|
app_state,
|
||||||
workspace::OpenOptions::default(),
|
workspace::OpenOptions::default(),
|
||||||
&mut cx,
|
&mut cx,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue