From e1c80f4706cefec5aaba9216471f4a80921aa937 Mon Sep 17 00:00:00 2001 From: Feng <24779889+shfc@users.noreply.github.com> Date: Sat, 24 Jan 2026 01:34:09 +1030 Subject: [PATCH] workspace: Support hot-exit for empty workspaces and single files (#46557) Enables restoration of empty workspaces (without folders) that contain unsaved items like drafts or single files. Empty workspaces are now identified by workspace_id rather than paths, allowing multiple empty workspaces to coexist and be properly restored on startup. This ensures users don't lose work when closing Zed with unsaved files in empty workspaces. Closes #15098 Release Notes: - Improved: Empty workspaces with unsaved files now restore on startup (hot-exit) --------- Co-authored-by: Kirill Bulatov --- crates/editor/src/items.rs | 174 ++++++++++++++------- crates/workspace/src/persistence.rs | 231 ++++++++++++++++++++++------ crates/workspace/src/workspace.rs | 88 ++++++++++- crates/zed/src/main.rs | 24 ++- crates/zed/src/zed/open_listener.rs | 33 ++-- 5 files changed, 430 insertions(+), 120 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index dc1cc5f0c7a33eb2913396d44c0b79c5d6442696..e8237e756bf57c83b2215ab0e264604fe9af8b4a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -10,6 +10,7 @@ use crate::{ use anyhow::{Context as _, Result, anyhow}; use collections::{HashMap, HashSet}; use file_icons::FileIcons; +use fs::MTime; use futures::future::try_join_all; use git::status::GitSummary; use gpui::{ @@ -1155,61 +1156,60 @@ impl SerializableItem for Editor { }); match opened_buffer { - Some(opened_buffer) => { - window.spawn(cx, async move |cx| { - let (_, buffer) = opened_buffer - .await - .context("Failed to open path in project")?; - - // This is a bit wasteful: we're loading the whole buffer from - // disk and then overwrite the content. - // But for now, it keeps the implementation of the content serialization - // simple, because we don't have to persist all of the metadata that we get - // by loading the file (git diff base, ...). - if let Some(buffer_text) = contents { - buffer.update(cx, |buffer, cx| { - // If we did restore an mtime, we want to store it on the buffer - // so that the next edit will mark the buffer as dirty/conflicted. - if mtime.is_some() { - buffer.did_reload( - buffer.version(), - buffer.line_ending(), - mtime, - cx, - ); - } - buffer.set_text(buffer_text, cx); - if let Some(entry) = buffer.peek_undo_stack() { - buffer.forget_transaction(entry.transaction_id()); - } - }); - } + Some(opened_buffer) => window.spawn(cx, async move |cx| { + let (_, buffer) = opened_buffer + .await + .context("Failed to open path in project")?; + + if let Some(contents) = contents { + buffer.update(cx, |buffer, cx| { + restore_serialized_buffer_contents(buffer, contents, mtime, cx); + }); + } - cx.update(|window, cx| { - cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer, Some(project), window, cx); + cx.update(|window, cx| { + cx.new(|cx| { + let mut editor = + Editor::for_buffer(buffer, Some(project), window, cx); - editor.read_metadata_from_db(item_id, workspace_id, window, cx); - editor - }) + editor.read_metadata_from_db(item_id, workspace_id, window, cx); + editor }) }) - } + }), None => { - let open_by_abs_path = workspace.update(cx, |workspace, cx| { - workspace.open_abs_path( - abs_path.clone(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - }); + // File is not in any worktree (e.g., opened as a standalone file) + // We need to open it via workspace and then restore dirty contents window.spawn(cx, async move |cx| { - let editor = open_by_abs_path?.await?.downcast::().with_context(|| format!("Failed to downcast to Editor after opening abs path {abs_path:?}"))?; + let open_by_abs_path = + workspace.update_in(cx, |workspace, window, cx| { + workspace.open_abs_path( + abs_path.clone(), + OpenOptions { + visible: Some(OpenVisible::None), + ..Default::default() + }, + window, + cx, + ) + })?; + let editor = + open_by_abs_path.await?.downcast::().with_context( + || format!("path {abs_path:?} cannot be opened as an Editor"), + )?; + + if let Some(contents) = contents { + editor.update_in(cx, |editor, _window, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + buffer.update(cx, |buffer, cx| { + restore_serialized_buffer_contents( + buffer, contents, mtime, cx, + ); + }); + } + })?; + } + editor.update_in(cx, |editor, window, cx| { editor.read_metadata_from_db(item_id, workspace_id, window, cx); })?; @@ -1252,9 +1252,9 @@ impl SerializableItem for Editor { let project = self.project.clone()?; let serialize_dirty_buffers = match buffer_serialization { - // If we don't have a worktree, we don't serialize, because - // projects without worktrees aren't deserialized. - BufferSerialization::All => project.read(cx).visible_worktrees(cx).next().is_some(), + // Always serialize dirty buffers, including for worktree-less windows. + // This enables hot-exit functionality for empty windows and single files. + BufferSerialization::All => true, BufferSerialization::NonDirtyBuffers => false, }; @@ -1933,6 +1933,27 @@ fn path_for_file<'a>( } } +/// Restores serialized buffer contents by overwriting the buffer with saved text. +/// This is somewhat wasteful since we load the whole buffer from disk then overwrite it, +/// but keeps implementation simple as we don't need to persist all metadata from loading +/// (git diff base, etc.). +fn restore_serialized_buffer_contents( + buffer: &mut Buffer, + contents: String, + mtime: Option, + cx: &mut Context, +) { + // If we did restore an mtime, store it on the buffer so that + // the next edit will mark the buffer as dirty/conflicted. + if mtime.is_some() { + buffer.did_reload(buffer.version(), buffer.line_ending(), mtime, cx); + } + buffer.set_text(contents, cx); + if let Some(entry) = buffer.peek_undo_stack() { + buffer.forget_transaction(entry.transaction_id()); + } +} + #[cfg(test)] mod tests { use crate::editor_tests::init_test; @@ -2161,5 +2182,54 @@ mod tests { assert!(buffer.file().is_none()); }); } + + // Test case 6: Deserialize with path and contents in an empty workspace (no worktree) + // This tests the hot-exit scenario where a file is opened in an empty workspace + // and has unsaved changes that should be restored. + { + let fs = FakeFs::new(cx.executor()); + fs.insert_file(path!("/standalone.rs"), "original content".into()) + .await; + + // Create an empty project with no worktrees + let project = Project::test(fs.clone(), [], cx).await; + let (workspace, cx) = + cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); + + let workspace_id = workspace::WORKSPACE_DB.next_id().await.unwrap(); + let item_id = 11000 as ItemId; + + let mtime = fs + .metadata(Path::new(path!("/standalone.rs"))) + .await + .unwrap() + .unwrap() + .mtime; + + // Simulate serialized state: file with unsaved changes + let serialized_editor = SerializedEditor { + abs_path: Some(PathBuf::from(path!("/standalone.rs"))), + contents: Some("modified content".to_string()), + language: Some("Rust".to_string()), + mtime: Some(mtime), + }; + + DB.save_serialized_editor(item_id, workspace_id, serialized_editor) + .await + .unwrap(); + + let deserialized = + deserialize_editor(item_id, workspace_id, workspace, project, cx).await; + + deserialized.update(cx, |editor, cx| { + // The editor should have the serialized contents, not the disk contents + assert_eq!(editor.text(cx), "modified content"); + assert!(editor.is_dirty(cx)); + assert!(!editor.has_conflict(cx)); + + let buffer = editor.buffer().read(cx).as_singleton().unwrap().read(cx); + assert!(buffer.file().is_some()); + }); + } } } diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index c79fd2f3d2d48e54ed9f64832a28d2a3563dc710..1c41c904edf94c0ad79e7beda2e116ed3148993f 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -943,6 +943,12 @@ impl WorkspaceDb { // doesn't affect the workspace selection for existing workspaces let root_paths = PathList::new(worktree_roots); + // Empty workspaces cannot be matched by paths (all empty workspaces have paths = ""). + // They should only be restored via workspace_for_id during session restoration. + if root_paths.is_empty() && remote_connection_id.is_none() { + return None; + } + // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace let ( @@ -1037,6 +1043,96 @@ impl WorkspaceDb { }) } + /// Returns the workspace with the given ID, loading all associated data. + pub(crate) fn workspace_for_id( + &self, + workspace_id: WorkspaceId, + ) -> Option { + let ( + paths, + paths_order, + window_bounds, + display, + centered_layout, + docks, + window_id, + remote_connection_id, + ): ( + String, + String, + Option, + Option, + Option, + DockStructure, + Option, + Option, + ) = self + .select_row_bound(sql! { + SELECT + paths, + paths_order, + window_state, + window_x, + window_y, + window_width, + window_height, + display, + centered_layout, + left_dock_visible, + left_dock_active_panel, + left_dock_zoom, + right_dock_visible, + right_dock_active_panel, + right_dock_zoom, + bottom_dock_visible, + bottom_dock_active_panel, + bottom_dock_zoom, + window_id, + remote_connection_id + FROM workspaces + WHERE workspace_id = ? + }) + .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)) + .context("No workspace found for id") + .warn_on_err() + .flatten()?; + + let paths = PathList::deserialize(&SerializedPathList { + paths, + order: paths_order, + }); + + let remote_connection_id = remote_connection_id.map(|id| RemoteConnectionId(id as u64)); + let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id { + self.remote_connection(remote_connection_id) + .context("Get remote connection") + .log_err() + } else { + None + }; + + Some(SerializedWorkspace { + id: workspace_id, + location: match remote_connection_options { + Some(options) => SerializedWorkspaceLocation::Remote(options), + None => SerializedWorkspaceLocation::Local, + }, + paths, + center_group: self + .get_center_pane_group(workspace_id) + .context("Getting center group") + .log_err()?, + window_bounds, + centered_layout: centered_layout.unwrap_or(false), + display, + docks, + session_id: None, + breakpoints: self.breakpoints(workspace_id), + window_id, + user_toolchains: self.user_toolchains(workspace_id, remote_connection_id), + }) + } + fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { let breakpoints: Result> = self .select_bound(sql! { @@ -1234,19 +1330,24 @@ impl WorkspaceDb { } } - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - remote_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - remote_connection_id, - )) - .context("clearing out old locations")?; + // Clear out old workspaces with the same paths. + // Skip this for empty workspaces - they are identified by workspace_id, not paths. + // Multiple empty workspaces with different content should coexist. + if !paths.paths.is_empty() { + conn.exec_bound(sql!( + DELETE + FROM workspaces + WHERE + workspace_id != ?1 AND + paths IS ?2 AND + remote_connection_id IS ?3 + ))?(( + workspace.id, + paths.paths.clone(), + remote_connection_id, + )) + .context("clearing out old locations")?; + } // Upsert let query = sql!( @@ -1465,23 +1566,33 @@ impl WorkspaceDb { fn session_workspaces( &self, session_id: String, - ) -> Result, Option)>> { + ) -> Result< + Vec<( + WorkspaceId, + PathList, + Option, + Option, + )>, + > { Ok(self .session_workspaces_query(session_id)? .into_iter() - .map(|(paths, order, window_id, remote_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - remote_connection_id.map(RemoteConnectionId), - ) - }) + .map( + |(workspace_id, paths, order, window_id, remote_connection_id)| { + ( + WorkspaceId(workspace_id), + PathList::deserialize(&SerializedPathList { paths, order }), + window_id, + remote_connection_id.map(RemoteConnectionId), + ) + }, + ) .collect()) } query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, remote_connection_id + fn session_workspaces_query(session_id: String) -> Result, Option)>> { + SELECT workspace_id, paths, paths_order, window_id, remote_connection_id FROM workspaces WHERE session_id = ?1 ORDER BY timestamp DESC @@ -1644,13 +1755,10 @@ impl WorkspaceDb { Ok(result) } - pub async fn last_workspace(&self) -> Result> { - Ok(self - .recent_workspaces_on_disk() - .await? - .into_iter() - .next() - .map(|(_, location, paths)| (location, paths))) + pub async fn last_workspace( + &self, + ) -> Result> { + Ok(self.recent_workspaces_on_disk().await?.into_iter().next()) } // Returns the locations of the workspaces that were still opened when the last @@ -1661,24 +1769,34 @@ impl WorkspaceDb { &self, last_session_id: &str, last_session_window_stack: Option>, - ) -> Result> { + ) -> Result> { let mut workspaces = Vec::new(); - for (paths, window_id, remote_connection_id) in + for (workspace_id, paths, window_id, remote_connection_id) in self.session_workspaces(last_session_id.to_owned())? { if let Some(remote_connection_id) = remote_connection_id { workspaces.push(( + workspace_id, SerializedWorkspaceLocation::Remote( self.remote_connection(remote_connection_id)?, ), paths, window_id.map(WindowId::from), )); + } else if paths.is_empty() { + // Empty workspace with items (drafts, files) - include for restoration + workspaces.push(( + workspace_id, + SerializedWorkspaceLocation::Local, + paths, + window_id.map(WindowId::from), + )); } else if paths.paths().iter().all(|path| path.exists()) && paths.paths().iter().any(|path| path.is_dir()) { workspaces.push(( + workspace_id, SerializedWorkspaceLocation::Local, paths, window_id.map(WindowId::from), @@ -1687,7 +1805,7 @@ impl WorkspaceDb { } if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { + workspaces.sort_by_key(|(_, _, _, window_id)| { window_id .and_then(|id| stack.iter().position(|&order_id| order_id == id)) .unwrap_or(usize::MAX) @@ -1696,7 +1814,7 @@ impl WorkspaceDb { Ok(workspaces .into_iter() - .map(|(location, paths, _)| (location, paths)) + .map(|(workspace_id, location, paths, _)| (workspace_id, location, paths)) .collect::>()) } @@ -2871,26 +2989,31 @@ mod tests { let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::new(&["/tmp2"])); - assert_eq!(locations[0].1, Some(20)); - assert_eq!(locations[1].0, PathList::new(&["/tmp1"])); - assert_eq!(locations[1].1, Some(10)); + assert_eq!(locations[0].0, WorkspaceId(2)); + assert_eq!(locations[0].1, PathList::new(&["/tmp2"])); + assert_eq!(locations[0].2, Some(20)); + assert_eq!(locations[1].0, WorkspaceId(1)); + assert_eq!(locations[1].1, PathList::new(&["/tmp1"])); + assert_eq!(locations[1].2, Some(10)); let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0].0, PathList::default()); - assert_eq!(locations[0].1, Some(50)); - assert_eq!(locations[0].2, Some(connection_id)); - assert_eq!(locations[1].0, PathList::new(&["/tmp3"])); - assert_eq!(locations[1].1, Some(30)); + assert_eq!(locations[0].0, WorkspaceId(5)); + assert_eq!(locations[0].1, PathList::default()); + assert_eq!(locations[0].2, Some(50)); + assert_eq!(locations[0].3, Some(connection_id)); + assert_eq!(locations[1].0, WorkspaceId(3)); + assert_eq!(locations[1].1, PathList::new(&["/tmp3"])); + assert_eq!(locations[1].2, Some(30)); let locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); assert_eq!(locations.len(), 1); + assert_eq!(locations[0].0, WorkspaceId(6)); assert_eq!( - locations[0].0, + locations[0].1, PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]), ); - assert_eq!(locations[0].1, Some(60)); + assert_eq!(locations[0].2, Some(60)); } fn default_workspace>( @@ -2968,26 +3091,32 @@ mod tests { locations, [ ( + WorkspaceId(4), SerializedWorkspaceLocation::Local, PathList::new(&[dir4.path()]) ), ( + WorkspaceId(3), SerializedWorkspaceLocation::Local, PathList::new(&[dir3.path()]) ), ( + WorkspaceId(2), SerializedWorkspaceLocation::Local, PathList::new(&[dir2.path()]) ), ( + WorkspaceId(1), SerializedWorkspaceLocation::Local, PathList::new(&[dir1.path()]) ), ( + WorkspaceId(5), SerializedWorkspaceLocation::Local, PathList::new(&[dir1.path(), dir2.path(), dir3.path()]) ), ( + WorkspaceId(6), SerializedWorkspaceLocation::Local, PathList::new(&[dir4.path(), dir3.path(), dir2.path()]) ), @@ -3064,6 +3193,7 @@ mod tests { assert_eq!( have[0], ( + WorkspaceId(4), SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), PathList::default() ) @@ -3071,6 +3201,7 @@ mod tests { assert_eq!( have[1], ( + WorkspaceId(3), SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), PathList::default() ) @@ -3078,6 +3209,7 @@ mod tests { assert_eq!( have[2], ( + WorkspaceId(2), SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), PathList::default() ) @@ -3085,6 +3217,7 @@ mod tests { assert_eq!( have[3], ( + WorkspaceId(1), SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), PathList::default() ) @@ -3405,8 +3538,12 @@ mod tests { .await .unwrap(); - // Retrieve it using empty paths - let retrieved = db.workspace_for_roots(empty_paths).unwrap(); + // Empty workspaces cannot be retrieved by paths (they'd all match). + // They must be retrieved by workspace_id. + assert!(db.workspace_for_roots(empty_paths).is_none()); + + // Retrieve using workspace_for_id instead + let retrieved = db.workspace_for_id(id).unwrap(); // Verify window bounds were persisted assert_eq!(retrieved.id, id); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 001cc349492b4dc272d52fa73eafa58bf73ae4ac..9c3e2cb44fcdfe9a9caa861a5e27fc2bf37ad168 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -5921,12 +5921,16 @@ impl Workspace { } } + fn has_any_items_open(&self, cx: &App) -> bool { + self.panes.iter().any(|pane| pane.read(cx).items_len() > 0) + } + fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { let paths = PathList::new(&self.root_paths(cx)); if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths) } else if self.project.read(cx).is_local() { - if !paths.is_empty() { + if !paths.is_empty() || self.has_any_items_open(cx) { WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) } else { WorkspaceLocation::DetachFromSession @@ -7827,14 +7831,15 @@ impl WorkspaceHandle for Entity { } } -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { +pub async fn last_opened_workspace_location() +-> Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)> { DB.last_workspace().await.log_err().flatten() } pub fn last_session_workspace_locations( last_session_id: &str, last_session_window_stack: Option>, -) -> Option> { +) -> Option> { DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } @@ -8167,6 +8172,83 @@ pub struct OpenOptions { pub env: Option>, } +/// Opens a workspace by its database ID, used for restoring empty workspaces with unsaved content. +pub fn open_workspace_by_id( + workspace_id: WorkspaceId, + app_state: Arc, + cx: &mut App, +) -> Task>> { + let project_handle = 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, + project::LocalProjectFlags { + init_worktree_trust: true, + ..project::LocalProjectFlags::default() + }, + cx, + ); + + cx.spawn(async move |cx| { + let serialized_workspace = persistence::DB + .workspace_for_id(workspace_id) + .with_context(|| format!("Workspace {workspace_id:?} not found"))?; + + let window_bounds_override = window_bounds_env_override(); + + let (window_bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(WindowBounds::Windowed(bounds)), None) + } else if let Some(display) = serialized_workspace.display + && let Some(bounds) = serialized_workspace.window_bounds.as_ref() + { + (Some(bounds.0), Some(display)) + } else if let Some((display, bounds)) = persistence::read_default_window_bounds() { + (Some(bounds), Some(display)) + } else { + (None, None) + }; + + let options = cx.update(|cx| { + let mut options = (app_state.build_window_options)(display, cx); + options.window_bounds = window_bounds; + options + }); + let centered_layout = serialized_workspace.centered_layout; + + let window = cx.open_window(options, { + let app_state = app_state.clone(); + let project_handle = project_handle.clone(); + move |window, cx| { + cx.new(|cx| { + let mut workspace = + Workspace::new(Some(workspace_id), project_handle, app_state, window, cx); + workspace.centered_layout = centered_layout; + workspace + }) + } + })?; + + notify_if_database_failed(window, cx); + + // Restore items from the serialized workspace + window + .update(cx, |_workspace, window, cx| { + open_items(Some(serialized_workspace), vec![], window, cx) + })? + .await?; + + window.update(cx, |workspace, window, cx| { + window.activate_window(); + workspace.serialize_workspace(window, cx); + })?; + + Ok(window) + }) +} + #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 545d030650e7452d71a8ca8d0238647844235e7a..f58cfd3413b1f000f1fe88e0bf27d31fe980d59b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -54,8 +54,8 @@ use theme::{ActiveTheme, GlobalTheme, ThemeRegistry}; use util::{ResultExt, TryFutureExt, maybe}; use uuid::Uuid; use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, + AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceId, + WorkspaceSettings, WorkspaceStore, notifications::NotificationId, }; use zed::{ OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, @@ -1246,8 +1246,24 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp let mut results: Vec> = Vec::new(); let mut tasks = Vec::new(); - for (index, (location, paths)) in locations.into_iter().enumerate() { + for (index, (workspace_id, location, paths)) in locations.into_iter().enumerate() { match location { + SerializedWorkspaceLocation::Local if paths.is_empty() => { + // Restore empty workspace by ID (has items like drafts but no folders) + let app_state = app_state.clone(); + let task = cx.spawn(async move |cx| { + let open_task = cx.update(|cx| { + workspace::open_workspace_by_id(workspace_id, app_state, cx) + }); + open_task.await.map(|_| ()) + }); + + if use_system_window_tabs && index == 0 { + results.push(task.await); + } else { + tasks.push(task); + } + } SerializedWorkspaceLocation::Local => { let app_state = app_state.clone(); let task = cx.spawn(async move |cx| { @@ -1368,7 +1384,7 @@ async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp pub(crate) async fn restorable_workspace_locations( cx: &mut AsyncApp, app_state: &Arc, -) -> Option> { +) -> Option> { let mut restore_behavior = cx.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup); let session_handle = app_state.session.clone(); diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs index 6e92e5042324428375a2a35cb829e74581e28a6e..987a755c586f038a33a5eca295aab27e6f99dcbd 100644 --- a/crates/zed/src/zed/open_listener.rs +++ b/crates/zed/src/zed/open_listener.rs @@ -457,21 +457,26 @@ async fn open_workspaces( env: Option>, cx: &mut AsyncApp, ) -> Result<()> { - let grouped_locations = if paths.is_empty() && diff_paths.is_empty() { - // If no paths are provided, restore from previous workspaces unless a new workspace is requested with -n - if open_new_workspace == Some(true) { - Vec::new() + let grouped_locations: Vec<(SerializedWorkspaceLocation, PathList)> = + if paths.is_empty() && diff_paths.is_empty() { + if open_new_workspace == Some(true) { + Vec::new() + } else { + // The workspace_id from the database is not used; + // open_paths will assign a new WorkspaceId when opening the workspace. + restorable_workspace_locations(cx, &app_state) + .await + .unwrap_or_default() + .into_iter() + .map(|(_workspace_id, location, paths)| (location, paths)) + .collect() + } } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() - } - } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), - )] - }; + vec![( + SerializedWorkspaceLocation::Local, + PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), + )] + }; if grouped_locations.is_empty() { // If we have no paths to open, show the welcome screen if this is the first launch