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:
Thorsten Ball 2024-09-19 17:51:28 +02:00 committed by GitHub
parent 7d0a7541bf
commit e9f2e72ff0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 592 additions and 141 deletions

View file

@ -2,6 +2,7 @@ mod dev_servers;
pub mod disconnected_overlay;
mod ssh_connections;
mod ssh_remotes;
use remote::SshConnectionOptions;
pub use ssh_connections::open_ssh_project;
use client::{DevServerProjectId, ProjectId};
@ -32,8 +33,8 @@ use ui::{
};
use util::{paths::PathExt, ResultExt};
use workspace::{
AppState, CloseIntent, ModalView, SerializedWorkspaceLocation, Workspace, WorkspaceId,
WORKSPACE_DB,
AppState, CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace,
WorkspaceId, WORKSPACE_DB,
};
#[derive(PartialEq, Clone, Deserialize, Default)]
@ -172,7 +173,7 @@ pub struct RecentProjectsDelegate {
create_new_window: bool,
// Flag to reset index when there is a new query vs not reset index when user delete an item
reset_selected_match_index: bool,
has_any_dev_server_projects: bool,
has_any_non_local_projects: bool,
}
impl RecentProjectsDelegate {
@ -185,16 +186,16 @@ impl RecentProjectsDelegate {
create_new_window,
render_paths,
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)>) {
self.workspaces = workspaces;
self.has_any_dev_server_projects = self
self.has_any_non_local_projects = !self
.workspaces
.iter()
.any(|(_, location)| matches!(location, SerializedWorkspaceLocation::DevServer(_)));
.all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _)));
}
}
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
@ -258,6 +259,23 @@ impl PickerDelegate for RecentProjectsDelegate {
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)
@ -364,6 +382,33 @@ impl PickerDelegate for RecentProjectsDelegate {
};
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 is_remote = matches!(location, SerializedWorkspaceLocation::DevServer(_));
let dev_server_status =
if let SerializedWorkspaceLocation::DevServer(dev_server_project) = location {
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())
.collect(),
),
SerializedWorkspaceLocation::Ssh(ssh_project) => {
Arc::new(vec![PathBuf::from(ssh_project.ssh_url())])
}
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
Arc::new(vec![PathBuf::from(format!(
"{}:{}",
@ -457,29 +504,34 @@ impl PickerDelegate for RecentProjectsDelegate {
h_flex()
.flex_grow()
.gap_3()
.when(self.has_any_dev_server_projects, |this| {
this.child(if is_remote {
// if disabled, Color::Disabled
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()
} else {
Icon::new(IconName::Screen)
.when(self.has_any_non_local_projects, |this| {
this.child(match location {
SerializedWorkspaceLocation::Local(_, _) => {
Icon::new(IconName::Screen)
.color(Color::Muted)
.into_any_element()
}
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Screen)
.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()
}
})
})
.child({