Skip to content

Commit 92b1f1f

Browse files
workspace: Persist window values without project (#44937)
Persist and restore window values (size, position, etc.) to the KV Store when there are no projects open. Relates to Discussion #24228 (reply in thread) Release Notes: - Added persistence for window size when no projects are open --------- Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
1 parent 1c33dbc commit 92b1f1f

2 files changed

Lines changed: 150 additions & 25 deletions

File tree

crates/workspace/src/persistence.rs

Lines changed: 120 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use std::{
1111
use anyhow::{Context as _, Result, bail};
1212
use collections::{HashMap, HashSet, IndexSet};
1313
use db::{
14+
kvp::KEY_VALUE_STORE,
1415
query,
1516
sqlez::{connection::Connection, domain::Domain},
1617
sqlez_macros::sql,
@@ -27,6 +28,7 @@ use project::WorktreeId;
2728
use remote::{
2829
DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions,
2930
};
31+
use serde::{Deserialize, Serialize};
3032
use sqlez::{
3133
bindable::{Bind, Column, StaticColumnCount},
3234
statement::Statement,
@@ -163,6 +165,124 @@ impl Column for SerializedWindowBounds {
163165
}
164166
}
165167

168+
const DEFAULT_WINDOW_BOUNDS_KEY: &str = "default_window_bounds";
169+
170+
pub fn read_default_window_bounds() -> Option<(Uuid, WindowBounds)> {
171+
let json_str = KEY_VALUE_STORE
172+
.read_kvp(DEFAULT_WINDOW_BOUNDS_KEY)
173+
.log_err()
174+
.flatten()?;
175+
176+
let (display_uuid, persisted) =
177+
serde_json::from_str::<(Uuid, WindowBoundsJson)>(&json_str).ok()?;
178+
Some((display_uuid, persisted.into()))
179+
}
180+
181+
pub async fn write_default_window_bounds(
182+
bounds: WindowBounds,
183+
display_uuid: Uuid,
184+
) -> anyhow::Result<()> {
185+
let persisted = WindowBoundsJson::from(bounds);
186+
let json_str = serde_json::to_string(&(display_uuid, persisted))?;
187+
KEY_VALUE_STORE
188+
.write_kvp(DEFAULT_WINDOW_BOUNDS_KEY.to_string(), json_str)
189+
.await?;
190+
Ok(())
191+
}
192+
193+
#[derive(Serialize, Deserialize)]
194+
pub enum WindowBoundsJson {
195+
Windowed {
196+
x: i32,
197+
y: i32,
198+
width: i32,
199+
height: i32,
200+
},
201+
Maximized {
202+
x: i32,
203+
y: i32,
204+
width: i32,
205+
height: i32,
206+
},
207+
Fullscreen {
208+
x: i32,
209+
y: i32,
210+
width: i32,
211+
height: i32,
212+
},
213+
}
214+
215+
impl From<WindowBounds> for WindowBoundsJson {
216+
fn from(b: WindowBounds) -> Self {
217+
match b {
218+
WindowBounds::Windowed(bounds) => {
219+
let origin = bounds.origin;
220+
let size = bounds.size;
221+
WindowBoundsJson::Windowed {
222+
x: f32::from(origin.x).round() as i32,
223+
y: f32::from(origin.y).round() as i32,
224+
width: f32::from(size.width).round() as i32,
225+
height: f32::from(size.height).round() as i32,
226+
}
227+
}
228+
WindowBounds::Maximized(bounds) => {
229+
let origin = bounds.origin;
230+
let size = bounds.size;
231+
WindowBoundsJson::Maximized {
232+
x: f32::from(origin.x).round() as i32,
233+
y: f32::from(origin.y).round() as i32,
234+
width: f32::from(size.width).round() as i32,
235+
height: f32::from(size.height).round() as i32,
236+
}
237+
}
238+
WindowBounds::Fullscreen(bounds) => {
239+
let origin = bounds.origin;
240+
let size = bounds.size;
241+
WindowBoundsJson::Fullscreen {
242+
x: f32::from(origin.x).round() as i32,
243+
y: f32::from(origin.y).round() as i32,
244+
width: f32::from(size.width).round() as i32,
245+
height: f32::from(size.height).round() as i32,
246+
}
247+
}
248+
}
249+
}
250+
}
251+
252+
impl From<WindowBoundsJson> for WindowBounds {
253+
fn from(n: WindowBoundsJson) -> Self {
254+
match n {
255+
WindowBoundsJson::Windowed {
256+
x,
257+
y,
258+
width,
259+
height,
260+
} => WindowBounds::Windowed(Bounds {
261+
origin: point(px(x as f32), px(y as f32)),
262+
size: size(px(width as f32), px(height as f32)),
263+
}),
264+
WindowBoundsJson::Maximized {
265+
x,
266+
y,
267+
width,
268+
height,
269+
} => WindowBounds::Maximized(Bounds {
270+
origin: point(px(x as f32), px(y as f32)),
271+
size: size(px(width as f32), px(height as f32)),
272+
}),
273+
WindowBoundsJson::Fullscreen {
274+
x,
275+
y,
276+
width,
277+
height,
278+
} => WindowBounds::Fullscreen(Bounds {
279+
origin: point(px(x as f32), px(y as f32)),
280+
size: size(px(width as f32), px(height as f32)),
281+
}),
282+
}
283+
}
284+
}
285+
166286
#[derive(Debug)]
167287
pub struct Breakpoint {
168288
pub position: u32,
@@ -1381,24 +1501,6 @@ impl WorkspaceDb {
13811501
}
13821502
}
13831503

1384-
pub(crate) fn last_window(
1385-
&self,
1386-
) -> anyhow::Result<(Option<Uuid>, Option<SerializedWindowBounds>)> {
1387-
let mut prepared_query =
1388-
self.select::<(Option<Uuid>, Option<SerializedWindowBounds>)>(sql!(
1389-
SELECT
1390-
display,
1391-
window_state, window_x, window_y, window_width, window_height
1392-
FROM workspaces
1393-
WHERE paths
1394-
IS NOT NULL
1395-
ORDER BY timestamp DESC
1396-
LIMIT 1
1397-
))?;
1398-
let result = prepared_query()?;
1399-
Ok(result.into_iter().next().unwrap_or((None, None)))
1400-
}
1401-
14021504
query! {
14031505
pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> {
14041506
DELETE FROM workspaces

crates/workspace/src/workspace.rs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,15 @@ impl Workspace {
15081508
&& let Ok(display_uuid) = display.uuid()
15091509
{
15101510
let window_bounds = window.inner_window_bounds();
1511+
let has_paths = !this.root_paths(cx).is_empty();
1512+
if !has_paths {
1513+
cx.background_executor()
1514+
.spawn(persistence::write_default_window_bounds(
1515+
window_bounds,
1516+
display_uuid,
1517+
))
1518+
.detach_and_log_err(cx);
1519+
}
15111520
if let Some(database_id) = workspace_id {
15121521
cx.background_executor()
15131522
.spawn(DB.set_window_open_status(
@@ -1516,6 +1525,13 @@ impl Workspace {
15161525
display_uuid,
15171526
))
15181527
.detach_and_log_err(cx);
1528+
} else {
1529+
cx.background_executor()
1530+
.spawn(persistence::write_default_window_bounds(
1531+
window_bounds,
1532+
display_uuid,
1533+
))
1534+
.detach_and_log_err(cx);
15191535
}
15201536
}
15211537
this.bounds_save_task_queued.take();
@@ -1724,6 +1740,7 @@ impl Workspace {
17241740
window
17251741
} else {
17261742
let window_bounds_override = window_bounds_env_override();
1743+
let is_empty_workspace = project_paths.is_empty();
17271744

17281745
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
17291746
(Some(WindowBounds::Windowed(bounds)), None)
@@ -1736,6 +1753,13 @@ impl Workspace {
17361753
} else {
17371754
(None, None)
17381755
}
1756+
} else if is_empty_workspace {
1757+
// Empty workspace - try to restore the last known no-project window bounds
1758+
if let Some((display, bounds)) = persistence::read_default_window_bounds() {
1759+
(Some(bounds), Some(display))
1760+
} else {
1761+
(None, None)
1762+
}
17391763
} else {
17401764
// New window - let GPUI's default_bounds() handle cascading
17411765
(None, None)
@@ -8820,14 +8844,13 @@ pub fn remote_workspace_position_from_db(
88208844
} else {
88218845
let restorable_bounds = serialized_workspace
88228846
.as_ref()
8823-
.and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
8824-
.or_else(|| {
8825-
let (display, window_bounds) = DB.last_window().log_err()?;
8826-
Some((display?, window_bounds?))
8827-
});
8847+
.and_then(|workspace| {
8848+
Some((workspace.display?, workspace.window_bounds.map(|b| b.0)?))
8849+
})
8850+
.or_else(|| persistence::read_default_window_bounds());
88288851

8829-
if let Some((serialized_display, serialized_status)) = restorable_bounds {
8830-
(Some(serialized_status.0), Some(serialized_display))
8852+
if let Some((serialized_display, serialized_bounds)) = restorable_bounds {
8853+
(Some(serialized_bounds), Some(serialized_display))
88318854
} else {
88328855
(None, None)
88338856
}

0 commit comments

Comments
 (0)