diff --git a/Cargo.lock b/Cargo.lock index 10d230a167..7759a58650 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 88beedd35e..dea29b697f 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 627c76572a..edbe96e582 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 4183a94114..b3b35172b3 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 c7e50945d2..d0cfaa9fbb 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 507db1789e..1185c783be 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 7462819899..5e6ede0c92 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 e092d2679b..9d44e236ac 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 f8e7296545..b0f479c9bf 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 18a0de6441..ed513f6d99 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 3548911518..0df9f3936e 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 ca5b26adcc..7ab6282c5a 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 2366912d66..a26e6302ad 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 e7df28f6ef..4c33c68e36 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 c410846416..664d4f36d1 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 fa21f8102e..a80b062933 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 4795d76cfc..a697149861 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 bef4a7f1ff..1ca28269b4 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 5da4232626..48497087d7 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 }