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
|
@ -39,7 +39,6 @@ use ui::{
|
|||
RadioWithLabel, Tooltip,
|
||||
};
|
||||
use ui_input::{FieldLabelLayout, TextField};
|
||||
use util::paths::PathWithPosition;
|
||||
use util::ResultExt;
|
||||
use workspace::notifications::NotifyResultExt;
|
||||
use workspace::OpenOptions;
|
||||
|
@ -987,11 +986,7 @@ impl DevServerProjects {
|
|||
cx.spawn(|_, mut cx| async move {
|
||||
let result = open_ssh_project(
|
||||
server.into(),
|
||||
project
|
||||
.paths
|
||||
.into_iter()
|
||||
.map(|path| PathWithPosition::from_path(PathBuf::from(path)))
|
||||
.collect(),
|
||||
project.paths.into_iter().map(PathBuf::from).collect(),
|
||||
app_state,
|
||||
OpenOptions::default(),
|
||||
&mut cx,
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -19,7 +19,6 @@ use ui::{
|
|||
h_flex, v_flex, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement,
|
||||
Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, WindowContext,
|
||||
};
|
||||
use util::paths::PathWithPosition;
|
||||
use workspace::{AppState, ModalView, Workspace};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -358,24 +357,29 @@ pub fn connect_over_ssh(
|
|||
|
||||
pub async fn open_ssh_project(
|
||||
connection_options: SshConnectionOptions,
|
||||
paths: Vec<PathWithPosition>,
|
||||
paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
_open_options: workspace::OpenOptions,
|
||||
open_options: workspace::OpenOptions,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
||||
let window = cx.open_window(options, |cx| {
|
||||
let project = project::Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||
})?;
|
||||
|
||||
let window = if let Some(window) = open_options.replace_window {
|
||||
window
|
||||
} else {
|
||||
cx.open_window(options, |cx| {
|
||||
let project = project::Project::local(
|
||||
app_state.client.clone(),
|
||||
app_state.node_runtime.clone(),
|
||||
app_state.user_store.clone(),
|
||||
app_state.languages.clone(),
|
||||
app_state.fs.clone(),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||
})?
|
||||
};
|
||||
|
||||
let result = window
|
||||
.update(cx, |workspace, cx| {
|
||||
|
@ -387,40 +391,17 @@ pub async fn open_ssh_project(
|
|||
.read(cx)
|
||||
.prompt
|
||||
.clone();
|
||||
connect_over_ssh(connection_options, ui, cx)
|
||||
connect_over_ssh(connection_options.clone(), ui, cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
window.update(cx, |_, cx| cx.remove_window()).ok();
|
||||
}
|
||||
|
||||
let session = result?;
|
||||
|
||||
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(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(())
|
||||
cx.update(|cx| {
|
||||
workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx)
|
||||
})?
|
||||
.await
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue