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:
parent
4b9334b910
commit
89ed0b9601
2 changed files with 130 additions and 13 deletions
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue