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
This commit is contained in:
Thorsten Ball 2024-07-29 17:05:56 +02:00 committed by GitHub
parent 6e1f7c6e1d
commit f58ef9b82b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 365 additions and 55 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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| {

View File

@ -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()

View File

@ -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,

View File

@ -63,6 +63,10 @@ impl LinuxClient for HeadlessClient {
None
}
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>> {
None
}
fn open_window(
&self,
_handle: AnyWindowHandle,

View File

@ -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,

View File

@ -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"
}

View File

@ -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:

View File

@ -56,6 +56,7 @@ x11rb::atom_manager! {
_GTK_SHOW_WINDOW_MENU,
_GTK_FRAME_EXTENTS,
_GTK_EDGE_CONSTRAINTS,
_NET_CLIENT_LIST_STACKING,
}
}

View File

@ -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,

View File

@ -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 {

View File

@ -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)]

View File

@ -19,5 +19,7 @@ test-support = [
[dependencies]
db.workspace = true
gpui.workspace = true
uuid.workspace = true
util.workspace = true
serde_json.workspace = true

View File

@ -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();
}
}

View File

@ -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

View File

@ -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();

View File

@ -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)]

View File

@ -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()
}

View File

@ -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
}