From f58ef9b82b29738bbf998cc0c5d25f7fe344ccb3 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Mon, 29 Jul 2024 17:05:56 +0200 Subject: [PATCH] zed: Persist window stack order across restarts (#15419) This changes the workspace/session serialization to also persist the order of windows across restarts. Release Notes: - Improved restoring of windows across restarts: the order of the windows is now also restored. That means windows that were in the foreground when Zed was quit will be in the foreground after restart. (Right now only supported on Linux/X11, not on Linux/Wayland.) Demo: https://github.com/user-attachments/assets/0b8162f8-f06d-43df-88d3-c45d8460fb68 --- Cargo.lock | 3 + crates/collab/src/tests/test_server.rs | 8 +- crates/gpui/src/app.rs | 9 ++ crates/gpui/src/platform.rs | 3 + .../src/platform/linux/headless/client.rs | 4 + crates/gpui/src/platform/linux/platform.rs | 14 +- .../gpui/src/platform/linux/wayland/client.rs | 4 + crates/gpui/src/platform/linux/x11/client.rs | 42 +++++ crates/gpui/src/platform/linux/x11/window.rs | 1 + crates/gpui/src/platform/mac/platform.rs | 6 + crates/gpui/src/platform/mac/window.rs | 19 +++ crates/gpui/src/window.rs | 6 + crates/session/Cargo.toml | 2 + crates/session/src/session.rs | 90 ++++++++++- crates/workspace/Cargo.toml | 1 + crates/workspace/src/persistence.rs | 148 ++++++++++++++---- crates/workspace/src/persistence/model.rs | 1 + crates/workspace/src/workspace.rs | 21 ++- crates/zed/src/main.rs | 38 ++++- 19 files changed, 365 insertions(+), 55 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 10d230a167f959e90befde6aeb2b88e0e27d59e3..7759a5865094a359a21e1591f7dba5c978991a38 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9601,6 +9601,8 @@ name = "session" version = "0.1.0" dependencies = [ "db", + "gpui", + "serde_json", "util", "uuid", ] @@ -13353,6 +13355,7 @@ dependencies = [ "smallvec", "sqlez", "task", + "tempfile", "theme", "ui", "util", diff --git a/crates/collab/src/tests/test_server.rs b/crates/collab/src/tests/test_server.rs index 88beedd35eb2b4fc277038e3285eb1334c419e3c..dea29b697f8de36ca5c1b160952d8ca34d251859 100644 --- a/crates/collab/src/tests/test_server.rs +++ b/crates/collab/src/tests/test_server.rs @@ -32,7 +32,7 @@ use rpc::{ }; use semantic_version::SemanticVersion; use serde_json::json; -use session::Session; +use session::{AppSession, Session}; use settings::SettingsStore; use std::{ cell::{Ref, RefCell, RefMut}, @@ -270,6 +270,7 @@ impl TestServer { let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -278,7 +279,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - session: Session::test(), + session, }); let os_keymap = "keymaps/default-macos.json"; @@ -399,6 +400,7 @@ impl TestServer { let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); + let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); let app_state = Arc::new(workspace::AppState { client: client.clone(), user_store: user_store.clone(), @@ -407,7 +409,7 @@ impl TestServer { fs: fs.clone(), build_window_options: |_, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - session: Session::test(), + session, }); cx.update(|cx| { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 627c76572a6e54534f8c7092a8eff88464b5df4c..edbe96e582b743340ed3e5c4c4628750064b4c73 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -469,6 +469,15 @@ impl AppContext { .collect() } + /// Returns the window handles ordered by their appearance on screen, front to back. + /// + /// The first window in the returned list is the active/topmost window of the application. + /// + /// This method returns None if the platform doesn't implement the method yet. + pub fn window_stack(&self) -> Option> { + self.platform.window_stack() + } + /// Returns a handle to the window that is currently focused at the platform level, if one exists. pub fn active_window(&self) -> Option { self.platform.active_window() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 4183a941149a84f5b43c45e51af902252a6f39b0..b3b35172b313202646767031d6deead61c9101a4 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -121,6 +121,9 @@ pub(crate) trait Platform: 'static { fn displays(&self) -> Vec>; fn primary_display(&self) -> Option>; fn active_window(&self) -> Option; + fn window_stack(&self) -> Option> { + None + } fn open_window( &self, diff --git a/crates/gpui/src/platform/linux/headless/client.rs b/crates/gpui/src/platform/linux/headless/client.rs index c7e50945d283f8c74920b667f98fdca7d402fa18..d0cfaa9fbb37fd515d2607a198b5741a3f334224 100644 --- a/crates/gpui/src/platform/linux/headless/client.rs +++ b/crates/gpui/src/platform/linux/headless/client.rs @@ -63,6 +63,10 @@ impl LinuxClient for HeadlessClient { None } + fn window_stack(&self) -> Option> { + None + } + fn open_window( &self, _handle: AnyWindowHandle, diff --git a/crates/gpui/src/platform/linux/platform.rs b/crates/gpui/src/platform/linux/platform.rs index 507db1789eff2db534ae45bf5eb0897c556e21d6..1185c783be99c1796651b43d78e8fda0dd03ba44 100644 --- a/crates/gpui/src/platform/linux/platform.rs +++ b/crates/gpui/src/platform/linux/platform.rs @@ -77,6 +77,7 @@ pub trait LinuxClient { fn read_from_primary(&self) -> Option; fn read_from_clipboard(&self) -> Option; fn active_window(&self) -> Option; + fn window_stack(&self) -> Option>; fn run(&self); } @@ -144,11 +145,10 @@ impl Platform for P { LinuxClient::run(self); - self.with_common(|common| { - if let Some(mut fun) = common.callbacks.quit.take() { - fun(); - } - }); + let quit = self.with_common(|common| common.callbacks.quit.take()); + if let Some(mut fun) = quit { + fun(); + } } fn quit(&self) { @@ -240,6 +240,10 @@ impl Platform for P { self.active_window() } + fn window_stack(&self) -> Option> { + self.window_stack() + } + fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui/src/platform/linux/wayland/client.rs b/crates/gpui/src/platform/linux/wayland/client.rs index 74628198996e45b9b4f99558c8cd178fadaa3884..5e6ede0c92719b9a7b9a97d24dd1c0f81f82f1e3 100644 --- a/crates/gpui/src/platform/linux/wayland/client.rs +++ b/crates/gpui/src/platform/linux/wayland/client.rs @@ -750,6 +750,10 @@ impl LinuxClient for WaylandClient { .map(|window| window.handle()) } + fn window_stack(&self) -> Option> { + None + } + fn compositor_name(&self) -> &'static str { "Wayland" } diff --git a/crates/gpui/src/platform/linux/x11/client.rs b/crates/gpui/src/platform/linux/x11/client.rs index e092d2679b6f11d013431c935b8f1d67c3e40b37..9d44e236ac5dbd329e776e9782d42fdd2c1e1854 100644 --- a/crates/gpui/src/platform/linux/x11/client.rs +++ b/crates/gpui/src/platform/linux/x11/client.rs @@ -1345,6 +1345,48 @@ impl LinuxClient for X11Client { .map(|window| window.handle()) }) } + + fn window_stack(&self) -> Option> { + let state = self.0.borrow(); + let root = state.xcb_connection.setup().roots[state.x_root_index].root; + + let reply = state + .xcb_connection + .get_property( + false, + root, + state.atoms._NET_CLIENT_LIST_STACKING, + xproto::AtomEnum::WINDOW, + 0, + u32::MAX, + ) + .ok()? + .reply() + .ok()?; + + let window_ids = reply + .value + .chunks_exact(4) + .map(|chunk| u32::from_ne_bytes(chunk.try_into().unwrap())) + .collect::>(); + + let mut handles = Vec::new(); + + // We need to reverse, since _NET_CLIENT_LIST_STACKING has + // a back-to-front order. + // See: https://specifications.freedesktop.org/wm-spec/1.3/ar01s03.html + for window_ref in window_ids + .iter() + .rev() + .filter_map(|&win| state.windows.get(&win)) + { + if !window_ref.window.state.borrow().destroyed { + handles.push(window_ref.handle()); + } + } + + Some(handles) + } } // Adatpted from: diff --git a/crates/gpui/src/platform/linux/x11/window.rs b/crates/gpui/src/platform/linux/x11/window.rs index f8e729654553ef1542b2251eeeb9416266c3fa5f..b0f479c9bf96f9323f32ab028c687585e1cff94f 100644 --- a/crates/gpui/src/platform/linux/x11/window.rs +++ b/crates/gpui/src/platform/linux/x11/window.rs @@ -56,6 +56,7 @@ x11rb::atom_manager! { _GTK_SHOW_WINDOW_MENU, _GTK_FRAME_EXTENTS, _GTK_EDGE_CONSTRAINTS, + _NET_CLIENT_LIST_STACKING, } } diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 18a0de644189237edbe45b3191e25fe7c4d5e3b8..ed513f6d99cadf533b37885c2fc845cac3602744 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -522,6 +522,12 @@ impl Platform for MacPlatform { MacWindow::active_window() } + // Returns the windows ordered front-to-back, meaning that the active + // window is the first one in the returned vec. + fn window_stack(&self) -> Option> { + Some(MacWindow::ordered_windows()) + } + fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 35489115189b7d8f855befaad31112f3d3fd0bb6..0df9f3936e3a4fcd0ac6498315a2d4ecb41a0d97 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -738,6 +738,25 @@ impl MacWindow { } } } + + pub fn ordered_windows() -> Vec { + unsafe { + let app = NSApplication::sharedApplication(nil); + let windows: id = msg_send![app, orderedWindows]; + let count: NSUInteger = msg_send![windows, count]; + + let mut window_handles = Vec::new(); + for i in 0..count { + let window: id = msg_send![windows, objectAtIndex:i]; + if msg_send![window, isKindOfClass: WINDOW_CLASS] { + let handle = get_window_state(&*window).lock().handle; + window_handles.push(handle); + } + } + + window_handles + } + } } impl Drop for MacWindow { diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index ca5b26adccd41d60ca1a871e7a49f59c93ce1e0b..7ab6282c5abcbf5c46be06baedf40ec65e0213d3 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4576,6 +4576,12 @@ impl WindowId { } } +impl From for WindowId { + fn from(value: u64) -> Self { + WindowId(slotmap::KeyData::from_ffi(value)) + } +} + /// A handle to a window with a specific root view type. /// Note that this does not keep the window alive on its own. #[derive(Deref, DerefMut)] diff --git a/crates/session/Cargo.toml b/crates/session/Cargo.toml index 2366912d6614fa0df23d9a6d5fc0066abae7505b..a26e6302ad4ca209c5f5c731ef59b0920846748f 100644 --- a/crates/session/Cargo.toml +++ b/crates/session/Cargo.toml @@ -19,5 +19,7 @@ test-support = [ [dependencies] db.workspace = true +gpui.workspace = true uuid.workspace = true util.workspace = true +serde_json.workspace = true diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs index e7df28f6ef0c72a2b9cbd0b74592f6cdfc750b4f..4c33c68e36a9f75851434f11dbbec66d6ee9cdff 100644 --- a/crates/session/src/session.rs +++ b/crates/session/src/session.rs @@ -1,29 +1,45 @@ +use std::time::Duration; + use db::kvp::KEY_VALUE_STORE; +use gpui::{AnyWindowHandle, ModelContext, Subscription, Task, WindowId}; use util::ResultExt; use uuid::Uuid; -#[derive(Clone, Debug)] pub struct Session { session_id: String, old_session_id: Option, + old_window_ids: Option>, } +const SESSION_ID_KEY: &'static str = "session_id"; +const SESSION_WINDOW_STACK_KEY: &'static str = "session_window_stack"; + impl Session { pub async fn new() -> Self { - let key_name = "session_id".to_string(); - - let old_session_id = KEY_VALUE_STORE.read_kvp(&key_name).ok().flatten(); + let old_session_id = KEY_VALUE_STORE.read_kvp(&SESSION_ID_KEY).ok().flatten(); let session_id = Uuid::new_v4().to_string(); KEY_VALUE_STORE - .write_kvp(key_name, session_id.clone()) + .write_kvp(SESSION_ID_KEY.to_string(), session_id.clone()) .await .log_err(); + let old_window_ids = KEY_VALUE_STORE + .read_kvp(&SESSION_WINDOW_STACK_KEY) + .ok() + .flatten() + .and_then(|json| serde_json::from_str::>(&json).ok()) + .map(|vec| { + vec.into_iter() + .map(WindowId::from) + .collect::>() + }); + Self { session_id, old_session_id, + old_window_ids, } } @@ -32,13 +48,75 @@ impl Session { Self { session_id: Uuid::new_v4().to_string(), old_session_id: None, + old_window_ids: None, } } pub fn id(&self) -> &str { &self.session_id } +} + +pub struct AppSession { + session: Session, + _serialization_task: Option>, + _subscriptions: Vec, +} + +impl AppSession { + pub fn new(session: Session, cx: &mut ModelContext) -> Self { + let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)]; + + let _serialization_task = Some(cx.spawn(|_, cx| async move { + loop { + if let Some(windows) = cx.update(|cx| cx.window_stack()).ok().flatten() { + store_window_stack(windows).await; + } + + cx.background_executor() + .timer(Duration::from_millis(100)) + .await; + } + })); + + Self { + session, + _subscriptions, + _serialization_task, + } + } + + fn app_will_quit(&mut self, cx: &mut ModelContext) -> Task<()> { + if let Some(windows) = cx.window_stack() { + cx.background_executor().spawn(store_window_stack(windows)) + } else { + Task::ready(()) + } + } + + pub fn id(&self) -> &str { + self.session.id() + } + pub fn last_session_id(&self) -> Option<&str> { - self.old_session_id.as_deref() + self.session.old_session_id.as_deref() + } + + pub fn last_session_window_stack(&self) -> Option> { + self.session.old_window_ids.clone() + } +} + +async fn store_window_stack(windows: Vec) { + let window_ids = windows + .into_iter() + .map(|window| window.window_id().as_u64()) + .collect::>(); + + if let Ok(window_ids_json) = serde_json::to_string(&window_ids) { + KEY_VALUE_STORE + .write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json) + .await + .log_err(); } } diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index c410846416a4aab1811e943a467d982898893b8d..664d4f36d109aaefa4817402968b77a11ed2f335 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -74,3 +74,4 @@ project = { workspace = true, features = ["test-support"] } session = { workspace = true, features = ["test-support"] } settings = { workspace = true, features = ["test-support"] } http_client = { workspace = true, features = ["test-support"] } +tempfile.workspace = true diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index fa21f8102efb9f2908f9ab906587baa27b523e15..a80b062933484c4511f0ab738409dfab2f12c85a 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -5,7 +5,7 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; use client::DevServerProjectId; use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql}; -use gpui::{point, size, Axis, Bounds, WindowBounds}; +use gpui::{point, size, Axis, Bounds, WindowBounds, WindowId}; use sqlez::{ bindable::{Bind, Column, StaticColumnCount}, @@ -171,6 +171,7 @@ define_connection! { // fullscreen: Option, // Is the window fullscreen? // centered_layout: Option, // Is the Centered Layout mode activated? // session_id: Option, // Session id + // window_id: Option, // Window Id // ) // // pane_groups( @@ -348,6 +349,9 @@ define_connection! { sql!( ALTER TABLE workspaces ADD COLUMN session_id TEXT DEFAULT NULL; ), + sql!( + ALTER TABLE workspaces ADD COLUMN window_id INTEGER DEFAULT NULL; + ), ]; } @@ -372,6 +376,7 @@ impl WorkspaceDb { display, centered_layout, docks, + window_id, ): ( WorkspaceId, Option, @@ -381,6 +386,7 @@ impl WorkspaceDb { Option, Option, DockStructure, + Option, ) = self .select_row_bound(sql! { SELECT @@ -403,7 +409,8 @@ impl WorkspaceDb { right_dock_zoom, bottom_dock_visible, bottom_dock_active_panel, - bottom_dock_zoom + bottom_dock_zoom, + window_id FROM workspaces WHERE local_paths = ? }) @@ -448,6 +455,7 @@ impl WorkspaceDb { display, docks, session_id: None, + window_id, }) } @@ -466,6 +474,7 @@ impl WorkspaceDb { display, centered_layout, docks, + window_id, ): ( WorkspaceId, Option, @@ -475,6 +484,7 @@ impl WorkspaceDb { Option, Option, DockStructure, + Option, ) = self .select_row_bound(sql! { SELECT @@ -497,7 +507,8 @@ impl WorkspaceDb { right_dock_zoom, bottom_dock_visible, bottom_dock_active_panel, - bottom_dock_zoom + bottom_dock_zoom, + window_id FROM workspaces WHERE dev_server_project_id = ? }) @@ -542,6 +553,7 @@ impl WorkspaceDb { display, docks, session_id: None, + window_id, }) } @@ -564,7 +576,7 @@ impl WorkspaceDb { .context("clearing out old locations")?; // Upsert - conn.exec_bound(sql!( + let query = sql!( INSERT INTO workspaces( workspace_id, local_paths, @@ -579,9 +591,10 @@ impl WorkspaceDb { bottom_dock_active_panel, bottom_dock_zoom, session_id, + window_id, timestamp ) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, CURRENT_TIMESTAMP) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, CURRENT_TIMESTAMP) ON CONFLICT DO UPDATE SET local_paths = ?2, @@ -596,9 +609,13 @@ impl WorkspaceDb { bottom_dock_active_panel = ?11, bottom_dock_zoom = ?12, session_id = ?13, + window_id = ?14, timestamp = CURRENT_TIMESTAMP - ))?((workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id)) - .context("Updating workspace")?; + ); + let mut prepared_query = conn.exec_bound(query)?; + let args = (workspace.id, &local_paths, &local_paths_order, workspace.docks, workspace.session_id, workspace.window_id); + + prepared_query(args).context("Updating workspace")?; } SerializedWorkspaceLocation::DevServer(dev_server_project) => { conn.exec_bound(sql!( @@ -684,8 +701,8 @@ impl WorkspaceDb { } query! { - fn session_workspace_locations(session_id: String) -> Result> { - SELECT local_paths + fn session_workspaces(session_id: String) -> Result)>> { + SELECT local_paths, window_id FROM workspaces WHERE session_id = ?1 AND dev_server_project_id IS NULL ORDER BY timestamp DESC @@ -787,21 +804,37 @@ impl WorkspaceDb { .next()) } + // Returns the locations of the workspaces that were still opened when the last + // session was closed (i.e. when Zed was quit). + // If `last_session_window_order` is provided, the returned locations are ordered + // according to that. pub fn last_session_workspace_locations( &self, last_session_id: &str, + last_session_window_stack: Option>, ) -> Result> { - let mut result = Vec::new(); + let mut workspaces = Vec::new(); - for location in self.session_workspace_locations(last_session_id.to_owned())? { + for (location, window_id) in self.session_workspaces(last_session_id.to_owned())? { if location.paths().iter().all(|path| path.exists()) && location.paths().iter().any(|path| path.is_dir()) { - result.push(location); + workspaces.push((location, window_id.map(|id| WindowId::from(id)))); } } - Ok(result) + if let Some(stack) = last_session_window_stack { + workspaces.sort_by_key(|(_, window_id)| { + window_id + .and_then(|id| stack.iter().position(|&order_id| order_id == id)) + .unwrap_or(usize::MAX) + }); + } + + Ok(workspaces + .into_iter() + .map(|(paths, _)| paths) + .collect::>()) } fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { @@ -1017,10 +1050,11 @@ impl WorkspaceDb { #[cfg(test)] mod tests { - use super::*; + use crate::persistence::model::SerializedWorkspace; + use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; use db::open_test_db; - use gpui; + use gpui::{self}; #[gpui::test] async fn test_next_id_stability() { @@ -1101,6 +1135,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: None, }; let workspace_2 = SerializedWorkspace { @@ -1112,6 +1147,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: None, }; db.save_workspace(workspace_1.clone()).await; @@ -1215,6 +1251,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: Some(999), }; db.save_workspace(workspace.clone()).await; @@ -1248,6 +1285,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: Some(1), }; let mut workspace_2 = SerializedWorkspace { @@ -1259,6 +1297,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: Some(2), }; db.save_workspace(workspace_1.clone()).await; @@ -1300,6 +1339,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: Some(3), }; db.save_workspace(workspace_3.clone()).await; @@ -1321,7 +1361,7 @@ mod tests { } #[gpui::test] - async fn test_session_workspace_locations() { + async fn test_session_workspaces() { env_logger::try_init().ok(); let db = WorkspaceDb(open_test_db("test_serializing_workspaces_session_id").await); @@ -1335,6 +1375,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), + window_id: Some(10), }; let workspace_2 = SerializedWorkspace { @@ -1346,6 +1387,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-1".to_owned()), + window_id: Some(20), }; let workspace_3 = SerializedWorkspace { @@ -1357,6 +1399,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: Some("session-id-2".to_owned()), + window_id: Some(30), }; let workspace_4 = SerializedWorkspace { @@ -1368,6 +1411,7 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: None, }; db.save_workspace(workspace_1.clone()).await; @@ -1375,23 +1419,19 @@ mod tests { db.save_workspace(workspace_3.clone()).await; db.save_workspace(workspace_4.clone()).await; - let locations = db - .session_workspace_locations("session-id-1".to_owned()) - .unwrap(); + let locations = db.session_workspaces("session-id-1".to_owned()).unwrap(); assert_eq!(locations.len(), 2); - assert_eq!(locations[0], LocalPaths::new(["/tmp1"])); - assert_eq!(locations[1], LocalPaths::new(["/tmp2"])); + assert_eq!(locations[0].0, LocalPaths::new(["/tmp1"])); + assert_eq!(locations[0].1, Some(10)); + assert_eq!(locations[1].0, LocalPaths::new(["/tmp2"])); + assert_eq!(locations[1].1, Some(20)); - let locations = db - .session_workspace_locations("session-id-2".to_owned()) - .unwrap(); + let locations = db.session_workspaces("session-id-2".to_owned()).unwrap(); assert_eq!(locations.len(), 1); - assert_eq!(locations[0], LocalPaths::new(["/tmp3"])); + assert_eq!(locations[0].0, LocalPaths::new(["/tmp3"])); + assert_eq!(locations[0].1, Some(30)); } - use crate::persistence::model::SerializedWorkspace; - use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; - fn default_workspace>( workspace_id: &[P], center_group: &SerializedPaneGroup, @@ -1405,9 +1445,61 @@ mod tests { docks: Default::default(), centered_layout: false, session_id: None, + window_id: None, } } + #[gpui::test] + async fn test_last_session_workspace_locations() { + let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap(); + let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap(); + let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap(); + let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap(); + + let db = + WorkspaceDb(open_test_db("test_serializing_workspaces_last_session_workspaces").await); + + let workspaces = [ + (1, dir1.path().to_str().unwrap(), 9), + (2, dir2.path().to_str().unwrap(), 5), + (3, dir3.path().to_str().unwrap(), 8), + (4, dir4.path().to_str().unwrap(), 2), + ] + .into_iter() + .map(|(id, location, window_id)| SerializedWorkspace { + id: WorkspaceId(id), + location: SerializedWorkspaceLocation::from_local_paths([location]), + center_group: Default::default(), + window_bounds: Default::default(), + display: Default::default(), + docks: Default::default(), + centered_layout: false, + session_id: Some("one-session".to_owned()), + window_id: Some(window_id), + }) + .collect::>(); + + for workspace in workspaces.iter() { + db.save_workspace(workspace.clone()).await; + } + + let stack = Some(Vec::from([ + WindowId::from(2), // Top + WindowId::from(8), + WindowId::from(5), + WindowId::from(9), // Bottom + ])); + + let have = db + .last_session_workspace_locations("one-session", stack) + .unwrap(); + assert_eq!(have.len(), 4); + assert_eq!(have[0], LocalPaths::new([dir4.path().to_str().unwrap()])); + assert_eq!(have[1], LocalPaths::new([dir3.path().to_str().unwrap()])); + assert_eq!(have[2], LocalPaths::new([dir2.path().to_str().unwrap()])); + assert_eq!(have[3], LocalPaths::new([dir1.path().to_str().unwrap()])); + } + #[gpui::test] async fn test_simple_split() { env_logger::try_init().ok(); diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index 4795d76cfcefa4f2d4e625cc95b3ab3803c1c566..a697149861963c8d8a0d142007c7447f58bcad1e 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -216,6 +216,7 @@ pub(crate) struct SerializedWorkspace { pub(crate) display: Option, pub(crate) docks: DockStructure, pub(crate) session_id: Option, + pub(crate) window_id: Option, } #[derive(Debug, PartialEq, Clone, Default)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index bef4a7f1ff446d14a3f9a22c160423e74cd80511..1ca28269b4993696b5ba66bc0bf70e9ae801ea69 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -36,7 +36,7 @@ use gpui::{ EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, View, WeakView, WindowBounds, - WindowHandle, WindowOptions, + WindowHandle, WindowId, WindowOptions, }; use item::{ FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, @@ -58,7 +58,7 @@ pub use persistence::{ use postage::stream::Stream; use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; -use session::Session; +use session::AppSession; use settings::Settings; use shared_screen::SharedScreen; use sqlez::{ @@ -539,7 +539,7 @@ pub struct AppState { pub fs: Arc, pub build_window_options: fn(Option, &mut AppContext) -> WindowOptions, pub node_runtime: Arc, - pub session: Session, + pub session: Model, } struct GlobalAppState(Weak); @@ -587,7 +587,7 @@ impl AppState { let clock = Arc::new(clock::FakeSystemClock::default()); let http_client = http_client::FakeHttpClient::with_404_response(); let client = Client::new(clock, http_client.clone(), cx); - let session = Session::test(); + let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); let user_store = cx.new_model(|cx| UserStore::new(client.clone(), cx)); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); @@ -917,7 +917,7 @@ impl Workspace { let modal_layer = cx.new_view(|_| ModalLayer::new()); - let session_id = app_state.session.id().to_owned(); + let session_id = app_state.session.read(cx).id().to_owned(); let mut active_call = None; if let Some(call) = ActiveCall::try_global(cx) { @@ -4032,6 +4032,7 @@ impl Workspace { docks, centered_layout: self.centered_layout, session_id: self.session_id.clone(), + window_id: Some(cx.window_handle().window_id().as_u64()), }; return cx.spawn(|_| persistence::DB.save_workspace(serialized_workspace)); } @@ -4291,6 +4292,7 @@ impl Workspace { let user_store = project.read(cx).user_store(); let workspace_store = cx.new_model(|cx| WorkspaceStore::new(client.clone(), cx)); + let session = cx.new_model(|cx| AppSession::new(Session::test(), cx)); cx.activate_window(); let app_state = Arc::new(AppState { languages: project.read(cx).languages().clone(), @@ -4300,7 +4302,7 @@ impl Workspace { fs: project.read(cx).fs().clone(), build_window_options: |_, _| Default::default(), node_runtime: FakeNodeRuntime::new(), - session: Session::test(), + session, }); let workspace = Self::new(Default::default(), project, app_state, cx); workspace.active_pane.update(cx, |pane, cx| pane.focus(cx)); @@ -4902,8 +4904,11 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } -pub fn last_session_workspace_locations(last_session_id: &str) -> Option> { - DB.last_session_workspace_locations(last_session_id) +pub fn last_session_workspace_locations( + last_session_id: &str, + last_session_window_stack: Option>, +) -> Option> { + DB.last_session_workspace_locations(last_session_id, last_session_window_stack) .log_err() } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 5da423262665429beb593d7846bd419c79ecab31..48497087d77218ee35ea0faa4ff24ae01f444a45 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,7 +29,7 @@ use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use recent_projects::open_ssh_project; use release_channel::{AppCommitSha, AppVersion}; -use session::Session; +use session::{AppSession, Session}; use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore}; use simplelog::ConfigBuilder; use smol::process::Command; @@ -444,6 +444,8 @@ fn main() { } .to_string(), ); + let app_session = cx.new_model(|cx| AppSession::new(session, cx)); + let app_state = Arc::new(AppState { languages: languages.clone(), client: client.clone(), @@ -452,7 +454,7 @@ fn main() { build_window_options, workspace_store, node_runtime: node_runtime.clone(), - session, + session: app_session, }); AppState::set_global(Arc::downgrade(&app_state), cx); @@ -706,7 +708,18 @@ pub(crate) async fn restorable_workspace_locations( .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) .ok()?; - let last_session_id = app_state.session.last_session_id(); + let session_handle = app_state.session.clone(); + let (last_session_id, last_session_window_stack) = cx + .update(|cx| { + let session = session_handle.read(cx); + + ( + session.last_session_id().map(|id| id.to_string()), + session.last_session_window_stack(), + ) + }) + .ok()?; + if last_session_id.is_none() && matches!( restore_behavior, @@ -724,8 +737,23 @@ pub(crate) async fn restorable_workspace_locations( } workspace::RestoreOnStartupBehavior::LastSession => { if let Some(last_session_id) = last_session_id { - workspace::last_session_workspace_locations(last_session_id) - .filter(|locations| !locations.is_empty()) + let ordered = last_session_window_stack.is_some(); + + let mut locations = workspace::last_session_workspace_locations( + &last_session_id, + last_session_window_stack, + ) + .filter(|locations| !locations.is_empty()); + + // Since last_session_window_order returns the windows ordered front-to-back + // we need to open the window that was frontmost last. + if ordered { + if let Some(locations) = locations.as_mut() { + locations.reverse(); + } + } + + locations } else { None }