Detailed changes
@@ -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",
@@ -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| {
@@ -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<Vec<AnyWindowHandle>> {
+ 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<AnyWindowHandle> {
self.platform.active_window()
@@ -121,6 +121,9 @@ pub(crate) trait Platform: 'static {
fn displays(&self) -> Vec<Rc<dyn PlatformDisplay>>;
fn primary_display(&self) -> Option<Rc<dyn PlatformDisplay>>;
fn active_window(&self) -> Option<AnyWindowHandle>;
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ None
+ }
fn open_window(
&self,
@@ -63,6 +63,10 @@ impl LinuxClient for HeadlessClient {
None
}
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ None
+ }
+
fn open_window(
&self,
_handle: AnyWindowHandle,
@@ -77,6 +77,7 @@ pub trait LinuxClient {
fn read_from_primary(&self) -> Option<ClipboardItem>;
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
fn active_window(&self) -> Option<AnyWindowHandle>;
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
fn run(&self);
}
@@ -144,11 +145,10 @@ impl<P: LinuxClient + 'static> 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<P: LinuxClient + 'static> Platform for P {
self.active_window()
}
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ self.window_stack()
+ }
+
fn open_window(
&self,
handle: AnyWindowHandle,
@@ -750,6 +750,10 @@ impl LinuxClient for WaylandClient {
.map(|window| window.handle())
}
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ None
+ }
+
fn compositor_name(&self) -> &'static str {
"Wayland"
}
@@ -1345,6 +1345,48 @@ impl LinuxClient for X11Client {
.map(|window| window.handle())
})
}
+
+ fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
+ 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::<Vec<xproto::Window>>();
+
+ 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:
@@ -56,6 +56,7 @@ x11rb::atom_manager! {
_GTK_SHOW_WINDOW_MENU,
_GTK_FRAME_EXTENTS,
_GTK_EDGE_CONSTRAINTS,
+ _NET_CLIENT_LIST_STACKING,
}
}
@@ -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<Vec<AnyWindowHandle>> {
+ Some(MacWindow::ordered_windows())
+ }
+
fn open_window(
&self,
handle: AnyWindowHandle,
@@ -738,6 +738,25 @@ impl MacWindow {
}
}
}
+
+ pub fn ordered_windows() -> Vec<AnyWindowHandle> {
+ 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 {
@@ -4576,6 +4576,12 @@ impl WindowId {
}
}
+impl From<u64> 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)]
@@ -19,5 +19,7 @@ test-support = [
[dependencies]
db.workspace = true
+gpui.workspace = true
uuid.workspace = true
util.workspace = true
+serde_json.workspace = true
@@ -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<String>,
+ old_window_ids: Option<Vec<WindowId>>,
}
+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::<Vec<u64>>(&json).ok())
+ .map(|vec| {
+ vec.into_iter()
+ .map(WindowId::from)
+ .collect::<Vec<WindowId>>()
+ });
+
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<Task<()>>,
+ _subscriptions: Vec<Subscription>,
+}
+
+impl AppSession {
+ pub fn new(session: Session, cx: &mut ModelContext<Self>) -> 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<Self>) -> 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<Vec<WindowId>> {
+ self.session.old_window_ids.clone()
+ }
+}
+
+async fn store_window_stack(windows: Vec<AnyWindowHandle>) {
+ let window_ids = windows
+ .into_iter()
+ .map(|window| window.window_id().as_u64())
+ .collect::<Vec<_>>();
+
+ 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();
}
}
@@ -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
@@ -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<bool>, // Is the window fullscreen?
// centered_layout: Option<bool>, // Is the Centered Layout mode activated?
// session_id: Option<String>, // Session id
+ // window_id: Option<u64>, // 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<LocalPaths>,
@@ -381,6 +386,7 @@ impl WorkspaceDb {
Option<Uuid>,
Option<bool>,
DockStructure,
+ Option<u64>,
) = 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<LocalPaths>,
@@ -475,6 +484,7 @@ impl WorkspaceDb {
Option<Uuid>,
Option<bool>,
DockStructure,
+ Option<u64>,
) = 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<Vec<LocalPaths>> {
- SELECT local_paths
+ fn session_workspaces(session_id: String) -> Result<Vec<(LocalPaths, Option<u64>)>> {
+ 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<Vec<WindowId>>,
) -> Result<Vec<LocalPaths>> {
- 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::<Vec<_>>())
}
fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result<SerializedPaneGroup> {
@@ -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<P: AsRef<Path>>(
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::<Vec<_>>();
+
+ 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();
@@ -216,6 +216,7 @@ pub(crate) struct SerializedWorkspace {
pub(crate) display: Option<Uuid>,
pub(crate) docks: DockStructure,
pub(crate) session_id: Option<String>,
+ pub(crate) window_id: Option<u64>,
}
#[derive(Debug, PartialEq, Clone, Default)]
@@ -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<dyn fs::Fs>,
pub build_window_options: fn(Option<Uuid>, &mut AppContext) -> WindowOptions,
pub node_runtime: Arc<dyn NodeRuntime>,
- pub session: Session,
+ pub session: Model<AppSession>,
}
struct GlobalAppState(Weak<AppState>);
@@ -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<LocalPaths> {
DB.last_workspace().await.log_err().flatten()
}
-pub fn last_session_workspace_locations(last_session_id: &str) -> Option<Vec<LocalPaths>> {
- DB.last_session_workspace_locations(last_session_id)
+pub fn last_session_workspace_locations(
+ last_session_id: &str,
+ last_session_window_stack: Option<Vec<WindowId>>,
+) -> Option<Vec<LocalPaths>> {
+ DB.last_session_workspace_locations(last_session_id, last_session_window_stack)
.log_err()
}
@@ -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
}