Skip to content

Commit e9f2e72

Browse files
mrnuggetbennetbo
andauthored
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>
1 parent 7d0a754 commit e9f2e72

File tree

12 files changed

+592
-141
lines changed

12 files changed

+592
-141
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/recent_projects/src/dev_servers.rs

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@ use ui::{
3939
RadioWithLabel, Tooltip,
4040
};
4141
use ui_input::{FieldLabelLayout, TextField};
42-
use util::paths::PathWithPosition;
4342
use util::ResultExt;
4443
use workspace::notifications::NotifyResultExt;
4544
use workspace::OpenOptions;
@@ -987,11 +986,7 @@ impl DevServerProjects {
987986
cx.spawn(|_, mut cx| async move {
988987
let result = open_ssh_project(
989988
server.into(),
990-
project
991-
.paths
992-
.into_iter()
993-
.map(|path| PathWithPosition::from_path(PathBuf::from(path)))
994-
.collect(),
989+
project.paths.into_iter().map(PathBuf::from).collect(),
995990
app_state,
996991
OpenOptions::default(),
997992
&mut cx,

crates/recent_projects/src/recent_projects.rs

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod dev_servers;
22
pub mod disconnected_overlay;
33
mod ssh_connections;
44
mod ssh_remotes;
5+
use remote::SshConnectionOptions;
56
pub use ssh_connections::open_ssh_project;
67

78
use client::{DevServerProjectId, ProjectId};
@@ -32,8 +33,8 @@ use ui::{
3233
};
3334
use util::{paths::PathExt, ResultExt};
3435
use workspace::{
35-
AppState, CloseIntent, ModalView, SerializedWorkspaceLocation, Workspace, WorkspaceId,
36-
WORKSPACE_DB,
36+
AppState, CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace,
37+
WorkspaceId, WORKSPACE_DB,
3738
};
3839

3940
#[derive(PartialEq, Clone, Deserialize, Default)]
@@ -172,7 +173,7 @@ pub struct RecentProjectsDelegate {
172173
create_new_window: bool,
173174
// Flag to reset index when there is a new query vs not reset index when user delete an item
174175
reset_selected_match_index: bool,
175-
has_any_dev_server_projects: bool,
176+
has_any_non_local_projects: bool,
176177
}
177178

178179
impl RecentProjectsDelegate {
@@ -185,16 +186,16 @@ impl RecentProjectsDelegate {
185186
create_new_window,
186187
render_paths,
187188
reset_selected_match_index: true,
188-
has_any_dev_server_projects: false,
189+
has_any_non_local_projects: false,
189190
}
190191
}
191192

192193
pub fn set_workspaces(&mut self, workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation)>) {
193194
self.workspaces = workspaces;
194-
self.has_any_dev_server_projects = self
195+
self.has_any_non_local_projects = !self
195196
.workspaces
196197
.iter()
197-
.any(|(_, location)| matches!(location, SerializedWorkspaceLocation::DevServer(_)));
198+
.all(|(_, location)| matches!(location, SerializedWorkspaceLocation::Local(_, _)));
198199
}
199200
}
200201
impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
@@ -258,6 +259,23 @@ impl PickerDelegate for RecentProjectsDelegate {
258259
dev_server_project.paths.join("")
259260
)
260261
}
262+
SerializedWorkspaceLocation::Ssh(ssh_project) => {
263+
format!(
264+
"{}{}{}{}",
265+
ssh_project.host,
266+
ssh_project
267+
.port
268+
.as_ref()
269+
.map(|port| port.to_string())
270+
.unwrap_or_default(),
271+
ssh_project.path,
272+
ssh_project
273+
.user
274+
.as_ref()
275+
.map(|user| user.to_string())
276+
.unwrap_or_default()
277+
)
278+
}
261279
};
262280

263281
StringMatchCandidate::new(id, combined_string)
@@ -364,6 +382,33 @@ impl PickerDelegate for RecentProjectsDelegate {
364382
};
365383
open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx)
366384
}
385+
SerializedWorkspaceLocation::Ssh(ssh_project) => {
386+
let app_state = workspace.app_state().clone();
387+
388+
let replace_window = if replace_current_window {
389+
cx.window_handle().downcast::<Workspace>()
390+
} else {
391+
None
392+
};
393+
394+
let open_options = OpenOptions {
395+
replace_window,
396+
..Default::default()
397+
};
398+
399+
let connection_options = SshConnectionOptions {
400+
host: ssh_project.host.clone(),
401+
username: ssh_project.user.clone(),
402+
port: ssh_project.port,
403+
password: None,
404+
};
405+
406+
let paths = vec![PathBuf::from(ssh_project.path.clone())];
407+
408+
cx.spawn(|_, mut cx| async move {
409+
open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
410+
})
411+
}
367412
}
368413
}
369414
})
@@ -392,7 +437,6 @@ impl PickerDelegate for RecentProjectsDelegate {
392437

393438
let (_, location) = self.workspaces.get(hit.candidate_id)?;
394439

395-
let is_remote = matches!(location, SerializedWorkspaceLocation::DevServer(_));
396440
let dev_server_status =
397441
if let SerializedWorkspaceLocation::DevServer(dev_server_project) = location {
398442
let store = dev_server_projects::Store::global(cx).read(cx);
@@ -416,6 +460,9 @@ impl PickerDelegate for RecentProjectsDelegate {
416460
.filter_map(|i| paths.paths().get(*i).cloned())
417461
.collect(),
418462
),
463+
SerializedWorkspaceLocation::Ssh(ssh_project) => {
464+
Arc::new(vec![PathBuf::from(ssh_project.ssh_url())])
465+
}
419466
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
420467
Arc::new(vec![PathBuf::from(format!(
421468
"{}:{}",
@@ -457,29 +504,34 @@ impl PickerDelegate for RecentProjectsDelegate {
457504
h_flex()
458505
.flex_grow()
459506
.gap_3()
460-
.when(self.has_any_dev_server_projects, |this| {
461-
this.child(if is_remote {
462-
// if disabled, Color::Disabled
463-
let indicator_color = match dev_server_status {
464-
Some(DevServerStatus::Online) => Color::Created,
465-
Some(DevServerStatus::Offline) => Color::Hidden,
466-
_ => unreachable!(),
467-
};
468-
IconWithIndicator::new(
469-
Icon::new(IconName::Server).color(Color::Muted),
470-
Some(Indicator::dot()),
471-
)
472-
.indicator_color(indicator_color)
473-
.indicator_border_color(if selected {
474-
Some(cx.theme().colors().element_selected)
475-
} else {
476-
None
477-
})
478-
.into_any_element()
479-
} else {
480-
Icon::new(IconName::Screen)
507+
.when(self.has_any_non_local_projects, |this| {
508+
this.child(match location {
509+
SerializedWorkspaceLocation::Local(_, _) => {
510+
Icon::new(IconName::Screen)
511+
.color(Color::Muted)
512+
.into_any_element()
513+
}
514+
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Screen)
481515
.color(Color::Muted)
516+
.into_any_element(),
517+
SerializedWorkspaceLocation::DevServer(_) => {
518+
let indicator_color = match dev_server_status {
519+
Some(DevServerStatus::Online) => Color::Created,
520+
Some(DevServerStatus::Offline) => Color::Hidden,
521+
_ => unreachable!(),
522+
};
523+
IconWithIndicator::new(
524+
Icon::new(IconName::Server).color(Color::Muted),
525+
Some(Indicator::dot()),
526+
)
527+
.indicator_color(indicator_color)
528+
.indicator_border_color(if selected {
529+
Some(cx.theme().colors().element_selected)
530+
} else {
531+
None
532+
})
482533
.into_any_element()
534+
}
483535
})
484536
})
485537
.child({

crates/recent_projects/src/ssh_connections.rs

Lines changed: 24 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use ui::{
1919
h_flex, v_flex, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement,
2020
Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, WindowContext,
2121
};
22-
use util::paths::PathWithPosition;
2322
use workspace::{AppState, ModalView, Workspace};
2423

2524
#[derive(Deserialize)]
@@ -358,24 +357,29 @@ pub fn connect_over_ssh(
358357

359358
pub async fn open_ssh_project(
360359
connection_options: SshConnectionOptions,
361-
paths: Vec<PathWithPosition>,
360+
paths: Vec<PathBuf>,
362361
app_state: Arc<AppState>,
363-
_open_options: workspace::OpenOptions,
362+
open_options: workspace::OpenOptions,
364363
cx: &mut AsyncAppContext,
365364
) -> Result<()> {
366365
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
367-
let window = cx.open_window(options, |cx| {
368-
let project = project::Project::local(
369-
app_state.client.clone(),
370-
app_state.node_runtime.clone(),
371-
app_state.user_store.clone(),
372-
app_state.languages.clone(),
373-
app_state.fs.clone(),
374-
None,
375-
cx,
376-
);
377-
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
378-
})?;
366+
367+
let window = if let Some(window) = open_options.replace_window {
368+
window
369+
} else {
370+
cx.open_window(options, |cx| {
371+
let project = project::Project::local(
372+
app_state.client.clone(),
373+
app_state.node_runtime.clone(),
374+
app_state.user_store.clone(),
375+
app_state.languages.clone(),
376+
app_state.fs.clone(),
377+
None,
378+
cx,
379+
);
380+
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
381+
})?
382+
};
379383

380384
let result = window
381385
.update(cx, |workspace, cx| {
@@ -387,40 +391,17 @@ pub async fn open_ssh_project(
387391
.read(cx)
388392
.prompt
389393
.clone();
390-
connect_over_ssh(connection_options, ui, cx)
394+
connect_over_ssh(connection_options.clone(), ui, cx)
391395
})?
392396
.await;
393397

394398
if result.is_err() {
395399
window.update(cx, |_, cx| cx.remove_window()).ok();
396400
}
397-
398401
let session = result?;
399402

400-
let project = cx.update(|cx| {
401-
project::Project::ssh(
402-
session,
403-
app_state.client.clone(),
404-
app_state.node_runtime.clone(),
405-
app_state.user_store.clone(),
406-
app_state.languages.clone(),
407-
app_state.fs.clone(),
408-
cx,
409-
)
410-
})?;
411-
412-
for path in paths {
413-
project
414-
.update(cx, |project, cx| {
415-
project.find_or_create_worktree(&path.path, true, cx)
416-
})?
417-
.await?;
418-
}
419-
420-
window.update(cx, |_, cx| {
421-
cx.replace_root_view(|cx| Workspace::new(None, project, app_state, cx))
422-
})?;
423-
window.update(cx, |_, cx| cx.activate_window())?;
424-
425-
Ok(())
403+
cx.update(|cx| {
404+
workspace::open_ssh_project(window, connection_options, session, app_state, paths, cx)
405+
})?
406+
.await
426407
}

crates/remote/src/ssh_session.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ use std::{
3333
};
3434
use tempfile::TempDir;
3535

36+
#[derive(
37+
Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize,
38+
)]
39+
pub struct SshProjectId(pub u64);
40+
3641
#[derive(Clone)]
3742
pub struct SshSocket {
3843
connection_options: SshConnectionOptions,

crates/sqlez/src/bindable.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,22 @@ impl Column for u32 {
196196
}
197197
}
198198

199+
impl StaticColumnCount for u16 {}
200+
impl Bind for u16 {
201+
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
202+
(*self as i64)
203+
.bind(statement, start_index)
204+
.with_context(|| format!("Failed to bind usize at index {start_index}"))
205+
}
206+
}
207+
208+
impl Column for u16 {
209+
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
210+
let result = statement.column_int64(start_index)?;
211+
Ok((result as u16, start_index + 1))
212+
}
213+
}
214+
199215
impl StaticColumnCount for usize {}
200216
impl Bind for usize {
201217
fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {

crates/sqlez/src/typed_statements.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ impl Connection {
7474
}
7575

7676
/// Prepare a statement which takes a binding and selects a single row
77-
/// from the database. WIll return none if no rows are returned and will
77+
/// from the database. Will return none if no rows are returned and will
7878
/// error if more than 1 row is returned.
7979
///
8080
/// Note: If there are multiple statements that depend upon each other

crates/workspace/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ postage.workspace = true
5151
project.workspace = true
5252
dev_server_projects.workspace = true
5353
task.workspace = true
54+
remote.workspace = true
5455
schemars.workspace = true
5556
serde.workspace = true
5657
serde_json.workspace = true

0 commit comments

Comments
 (0)