Closes https://github.com/zed-industries/zed/issues/48468 ## Solution This change addresses a macOS shutdown ordering issue where closing the last window with the native close button could clear session state before persistence tasks complete, causing next launch to open no project/untitled. - [crates/workspace/src/workspace.rs](/Users/caleul/Documents/Work/Projects/zed/crates/workspace/src/workspace.rs) - Make `save_last_workspace` dynamic on macOS based on whether closing the last window will quit the app (`on_last_window_closed == quit_app`). - Keeps `save_last_workspace` true only when the window close path is equivalent to quit, so last-session data remains valid only in the intended scenario. - [crates/workspace/src/multi_workspace.rs](/Users/caleul/Documents/Work/Projects/zed/crates/workspace/src/multi_workspace.rs) - In `close_window`, before removing the window, explicitly flush: - workspace serialization tasks - pending workspace removal tasks - multi-workspace serialization task - This prevents losing in-memory state on fast close/quit paths. - [crates/session/src/session.rs](/Users/caleul/Documents/Work/Projects/zed/crates/session/src/session.rs) - Guard `app_will_quit` to only persist window stack when non-empty, preventing empty-stack state from being written during close path that destroys the window stack first. ## Testing - Did you test these changes? - Manual repro/fix confirmed in local testing flow by the previous author. - Please confirm after merge with: 1) open 2–3 projects in workspace sidebar 2) set `on_last_window_closed: "quit_app"` and `restore_on_startup: "last_session"` 3) close with red X 4) relaunch app and verify sessions restore all active workspaces - Are there any parts that need more testing? - Multi-window mixed close scenarios and one-window with dirty non-serializable items. - Regression for non-quit close mode (`on_last_window_closed: "close") to ensure behavior remains unchanged. - How can others test this? - Follow the 4-step repro path above on macOS. - Check logs for `timed out waiting on app_will_quit` in `~/Library/Logs/Zed/Zed.log` and confirm no restore regression. - Platforms tested: - Verified for macOS (target path of bug). - Not validated on Linux/Windows in this patch path. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards (no UI/UX diff here) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes - Closing last macOS window via the red close button now correctly preserves and restores last-session workspace state when configured to quit on last-window close. --------- Co-authored-by: Kirill Bulatov <kirill@zed.dev>
151 lines
4.4 KiB
Rust
151 lines
4.4 KiB
Rust
use db::kvp::KeyValueStore;
|
|
use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId};
|
|
use util::ResultExt;
|
|
|
|
pub struct Session {
|
|
session_id: String,
|
|
old_session_id: Option<String>,
|
|
old_window_ids: Option<Vec<WindowId>>,
|
|
}
|
|
|
|
const SESSION_ID_KEY: &str = "session_id";
|
|
const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack";
|
|
|
|
impl Session {
|
|
pub async fn new(session_id: String, db: KeyValueStore) -> Self {
|
|
let old_session_id = db.read_kvp(SESSION_ID_KEY).ok().flatten();
|
|
|
|
db.write_kvp(SESSION_ID_KEY.to_string(), session_id.clone())
|
|
.await
|
|
.log_err();
|
|
|
|
let old_window_ids = db
|
|
.read_kvp(SESSION_WINDOW_STACK_KEY)
|
|
.ok()
|
|
.flatten()
|
|
.and_then(|json| serde_json::from_str::<Vec<u64>>(&json).ok())
|
|
.map(|vec: Vec<u64>| {
|
|
vec.into_iter()
|
|
.map(WindowId::from)
|
|
.collect::<Vec<WindowId>>()
|
|
});
|
|
|
|
Self {
|
|
session_id,
|
|
old_session_id,
|
|
old_window_ids,
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn test() -> Self {
|
|
Self {
|
|
session_id: uuid::Uuid::new_v4().to_string(),
|
|
old_session_id: None,
|
|
old_window_ids: None,
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn test_with_old_session(old_session_id: String) -> Self {
|
|
Self {
|
|
session_id: uuid::Uuid::new_v4().to_string(),
|
|
old_session_id: Some(old_session_id),
|
|
old_window_ids: None,
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> &str {
|
|
&self.session_id
|
|
}
|
|
}
|
|
|
|
pub struct AppSession {
|
|
session: Session,
|
|
_serialization_task: Task<()>,
|
|
_subscriptions: Vec<Subscription>,
|
|
}
|
|
|
|
impl AppSession {
|
|
pub fn new(session: Session, cx: &Context<Self>) -> Self {
|
|
let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)];
|
|
|
|
let _serialization_task = if cfg!(not(any(test, feature = "test-support"))) {
|
|
let db = KeyValueStore::global(cx);
|
|
cx.spawn(async move |_, cx| {
|
|
// Disabled in tests: the infinite loop bypasses "parking forbidden" checks,
|
|
// causing tests to hang instead of panicking.
|
|
{
|
|
let mut current_window_stack = Vec::new();
|
|
loop {
|
|
if let Some(windows) = cx.update(|cx| window_stack(cx))
|
|
&& !windows.is_empty()
|
|
&& windows != current_window_stack
|
|
{
|
|
store_window_stack(db.clone(), &windows).await;
|
|
current_window_stack = windows;
|
|
}
|
|
|
|
cx.background_executor()
|
|
.timer(std::time::Duration::from_millis(500))
|
|
.await;
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
Task::ready(())
|
|
};
|
|
|
|
Self {
|
|
session,
|
|
_subscriptions,
|
|
_serialization_task,
|
|
}
|
|
}
|
|
|
|
fn app_will_quit(&mut self, cx: &mut Context<Self>) -> Task<()> {
|
|
if let Some(window_stack) = window_stack(cx)
|
|
&& !window_stack.is_empty()
|
|
{
|
|
let db = KeyValueStore::global(cx);
|
|
cx.background_spawn(async move { store_window_stack(db, &window_stack).await })
|
|
} else {
|
|
Task::ready(())
|
|
}
|
|
}
|
|
|
|
pub fn id(&self) -> &str {
|
|
self.session.id()
|
|
}
|
|
|
|
pub fn last_session_id(&self) -> Option<&str> {
|
|
self.session.old_session_id.as_deref()
|
|
}
|
|
|
|
#[cfg(any(test, feature = "test-support"))]
|
|
pub fn replace_session_for_test(&mut self, session: Session) {
|
|
self.session = session;
|
|
}
|
|
|
|
pub fn last_session_window_stack(&self) -> Option<Vec<WindowId>> {
|
|
self.session.old_window_ids.clone()
|
|
}
|
|
}
|
|
|
|
fn window_stack(cx: &App) -> Option<Vec<u64>> {
|
|
Some(
|
|
cx.window_stack()?
|
|
.into_iter()
|
|
.map(|window| window.window_id().as_u64())
|
|
.collect(),
|
|
)
|
|
}
|
|
|
|
async fn store_window_stack(db: KeyValueStore, windows: &[u64]) {
|
|
if let Ok(window_ids_json) = serde_json::to_string(windows) {
|
|
db.write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json)
|
|
.await
|
|
.log_err();
|
|
}
|
|
}
|