@@ -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::<Editor>().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::<Editor>().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<MTime>,
+ cx: &mut Context<Buffer>,
+) {
+ // 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());
+ });
+ }
}
}
@@ -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<SerializedWorkspace> {
+ let (
+ paths,
+ paths_order,
+ window_bounds,
+ display,
+ centered_layout,
+ docks,
+ window_id,
+ remote_connection_id,
+ ): (
+ String,
+ String,
+ Option<SerializedWindowBounds>,
+ Option<Uuid>,
+ Option<bool>,
+ DockStructure,
+ Option<u64>,
+ Option<i32>,
+ ) = 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<Arc<Path>, Vec<SourceBreakpoint>> {
let breakpoints: Result<Vec<(PathBuf, Breakpoint)>> = 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<Vec<(PathList, Option<u64>, Option<RemoteConnectionId>)>> {
+ ) -> Result<
+ Vec<(
+ WorkspaceId,
+ PathList,
+ Option<u64>,
+ Option<RemoteConnectionId>,
+ )>,
+ > {
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<Vec<(String, String, Option<u64>, Option<u64>)>> {
- SELECT paths, paths_order, window_id, remote_connection_id
+ fn session_workspaces_query(session_id: String) -> Result<Vec<(i64, String, String, Option<u64>, Option<u64>)>> {
+ 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<Option<(SerializedWorkspaceLocation, PathList)>> {
- Ok(self
- .recent_workspaces_on_disk()
- .await?
- .into_iter()
- .next()
- .map(|(_, location, paths)| (location, paths)))
+ pub async fn last_workspace(
+ &self,
+ ) -> Result<Option<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
+ 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<Vec<WindowId>>,
- ) -> Result<Vec<(SerializedWorkspaceLocation, PathList)>> {
+ ) -> Result<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
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::<Vec<_>>())
}
@@ -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<P: AsRef<Path>>(
@@ -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);
@@ -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<Workspace> {
}
}
-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<Vec<WindowId>>,
-) -> Option<Vec<(SerializedWorkspaceLocation, PathList)>> {
+) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
.log_err()
}
@@ -8167,6 +8172,83 @@ pub struct OpenOptions {
pub env: Option<HashMap<String, String>>,
}
+/// 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<AppState>,
+ cx: &mut App,
+) -> Task<anyhow::Result<WindowHandle<Workspace>>> {
+ 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],
@@ -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<AppState>, cx: &mut AsyncApp
let mut results: Vec<Result<(), Error>> = 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<AppState>, cx: &mut AsyncApp
pub(crate) async fn restorable_workspace_locations(
cx: &mut AsyncApp,
app_state: &Arc<AppState>,
-) -> Option<Vec<(SerializedWorkspaceLocation, PathList)>> {
+) -> Option<Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>> {
let mut restore_behavior = cx.update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup);
let session_handle = app_state.session.clone();