workspace: Fix multiple remote projects not restoring on reconnect or restart and not visible in recent projects (#35398)

Closes #33787

We were not updating SSH paths after initial project was created. Now we
update paths when worktrees are added/removed and serialize these
updated paths. This is separate from workspace because unlike local
paths, SSH paths are not part of the workspace table, but the SSH table
instead. We don't need to update SSH paths every time we serialize the
workspace.

<img width="400"
src="https://github.com/user-attachments/assets/9e1a9893-e08e-4ecf-8dab-1e9befced58b"
/>

Release Notes:

- Fixed issue where multiple remote folders in a project were lost on
reconnect, not restored on restart, and not visible in recent projects.
This commit is contained in:
Smit Barmase 2025-07-31 16:32:31 +05:30 committed by GitHub
parent 4b9334b910
commit 89ed0b9601
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 130 additions and 13 deletions

View file

@ -939,6 +939,26 @@ impl WorkspaceDb {
}
}
query! {
pub async fn update_ssh_project_paths_query(ssh_project_id: u64, paths: String) -> Result<Option<SerializedSshProject>> {
UPDATE ssh_projects
SET paths = ?2
WHERE id = ?1
RETURNING id, host, port, paths, user
}
}
pub(crate) async fn update_ssh_project_paths(
&self,
ssh_project_id: SshProjectId,
new_paths: Vec<String>,
) -> Result<SerializedSshProject> {
let paths = serde_json::to_string(&new_paths)?;
self.update_ssh_project_paths_query(ssh_project_id.0, paths)
.await?
.context("failed to update ssh project paths")
}
query! {
pub async fn next_id() -> Result<WorkspaceId> {
INSERT INTO workspaces DEFAULT VALUES RETURNING workspace_id
@ -2624,4 +2644,56 @@ mod tests {
assert_eq!(workspace.center_group, new_workspace.center_group);
}
#[gpui::test]
async fn test_update_ssh_project_paths() {
zlog::init_test();
let db = WorkspaceDb::open_test_db("test_update_ssh_project_paths").await;
let (host, port, initial_paths, user) = (
"example.com".to_string(),
Some(22_u16),
vec!["/home/user".to_string(), "/etc/nginx".to_string()],
Some("user".to_string()),
);
let project = db
.get_or_create_ssh_project(host.clone(), port, initial_paths.clone(), user.clone())
.await
.unwrap();
assert_eq!(project.host, host);
assert_eq!(project.paths, initial_paths);
assert_eq!(project.user, user);
let new_paths = vec![
"/home/user".to_string(),
"/etc/nginx".to_string(),
"/var/log".to_string(),
"/opt/app".to_string(),
];
let updated_project = db
.update_ssh_project_paths(project.id, new_paths.clone())
.await
.unwrap();
assert_eq!(updated_project.id, project.id);
assert_eq!(updated_project.paths, new_paths);
let retrieved_project = db
.get_ssh_project(
host.clone(),
port,
serde_json::to_string(&new_paths).unwrap(),
user.clone(),
)
.await
.unwrap()
.unwrap();
assert_eq!(retrieved_project.id, project.id);
assert_eq!(retrieved_project.paths, new_paths);
}
}

View file

@ -1094,7 +1094,8 @@ pub struct Workspace {
_subscriptions: Vec<Subscription>,
_apply_leader_updates: Task<Result<()>>,
_observe_current_user: Task<Result<()>>,
_schedule_serialize: Option<Task<()>>,
_schedule_serialize_workspace: Option<Task<()>>,
_schedule_serialize_ssh_paths: Option<Task<()>>,
pane_history_timestamp: Arc<AtomicUsize>,
bounds: Bounds<Pixels>,
pub centered_layout: bool,
@ -1153,6 +1154,8 @@ impl Workspace {
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
this.update_window_title(window, cx);
this.update_ssh_paths(cx);
this.serialize_ssh_paths(window, cx);
this.serialize_workspace(window, cx);
// This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
this.update_history(cx);
@ -1420,7 +1423,8 @@ impl Workspace {
app_state,
_observe_current_user,
_apply_leader_updates,
_schedule_serialize: None,
_schedule_serialize_workspace: None,
_schedule_serialize_ssh_paths: None,
leader_updates_tx,
_subscriptions: subscriptions,
pane_history_timestamp,
@ -5077,6 +5081,46 @@ impl Workspace {
}
}
fn update_ssh_paths(&mut self, cx: &App) {
let project = self.project().read(cx);
if !project.is_local() {
let paths: Vec<String> = project
.visible_worktrees(cx)
.map(|worktree| worktree.read(cx).abs_path().to_string_lossy().to_string())
.collect();
if let Some(ssh_project) = &mut self.serialized_ssh_project {
ssh_project.paths = paths;
}
}
}
fn serialize_ssh_paths(&mut self, window: &mut Window, cx: &mut Context<Workspace>) {
if self._schedule_serialize_ssh_paths.is_none() {
self._schedule_serialize_ssh_paths =
Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(SERIALIZATION_THROTTLE_TIME)
.await;
this.update_in(cx, |this, window, cx| {
let task = if let Some(ssh_project) = &this.serialized_ssh_project {
let ssh_project_id = ssh_project.id;
let ssh_project_paths = ssh_project.paths.clone();
window.spawn(cx, async move |_| {
persistence::DB
.update_ssh_project_paths(ssh_project_id, ssh_project_paths)
.await
})
} else {
Task::ready(Err(anyhow::anyhow!("No SSH project to serialize")))
};
task.detach();
this._schedule_serialize_ssh_paths.take();
})
.log_err();
}));
}
}
fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context<Workspace>) {
match member {
Member::Axis(PaneAxis { members, .. }) => {
@ -5120,17 +5164,18 @@ impl Workspace {
}
fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self._schedule_serialize.is_none() {
self._schedule_serialize = Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update_in(cx, |this, window, cx| {
this.serialize_workspace_internal(window, cx).detach();
this._schedule_serialize.take();
})
.log_err();
}));
if self._schedule_serialize_workspace.is_none() {
self._schedule_serialize_workspace =
Some(cx.spawn_in(window, async move |this, cx| {
cx.background_executor()
.timer(SERIALIZATION_THROTTLE_TIME)
.await;
this.update_in(cx, |this, window, cx| {
this.serialize_workspace_internal(window, cx).detach();
this._schedule_serialize_workspace.take();
})
.log_err();
}));
}
}