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