wip

Kay Simmons created

Change summary

crates/collab/src/tests.rs                  |   2 
crates/collab_ui/src/collab_ui.rs           |   2 
crates/editor/src/persistence.rs            |   2 
crates/gpui/src/app.rs                      | 109 ++++++++++
crates/gpui/src/platform.rs                 |   4 
crates/gpui/src/platform/mac/status_item.rs |   2 
crates/gpui/src/platform/mac/window.rs      |  23 ++
crates/gpui/src/platform/test.rs            |   8 
crates/gpui/src/presenter.rs                |   3 
crates/settings/src/settings.rs             |   3 
crates/sqlez/src/bindable.rs                | 236 +++++++++++++---------
crates/sqlez/src/statement.rs               |  11 
crates/workspace/src/dock.rs                |   2 
crates/workspace/src/persistence.rs         | 121 ++++++++++-
crates/workspace/src/persistence/model.rs   |  33 ++
crates/workspace/src/workspace.rs           |  49 +++-
crates/zed/src/zed.rs                       |  15 
17 files changed, 479 insertions(+), 146 deletions(-)

Detailed changes

crates/collab/src/tests.rs 🔗

@@ -196,7 +196,7 @@ impl TestServer {
             languages: Arc::new(LanguageRegistry::new(Task::ready(()))),
             themes: ThemeRegistry::new((), cx.font_cache()),
             fs: fs.clone(),
-            build_window_options: Default::default,
+            build_window_options: |_| Default::default(),
             initialize_workspace: |_, _, _| unimplemented!(),
             dock_default_item_factory: |_, _| unimplemented!(),
         });

crates/collab_ui/src/collab_ui.rs 🔗

@@ -54,7 +54,7 @@ pub fn init(app_state: Arc<AppState>, cx: &mut MutableAppContext) {
                     })
                     .await?;
 
-                let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
+                let (_, workspace) = cx.add_window((app_state.build_window_options)(None), |cx| {
                     let mut workspace = Workspace::new(
                         Default::default(),
                         0,

crates/editor/src/persistence.rs 🔗

@@ -6,7 +6,7 @@ use db::{define_connection, query};
 use workspace::{ItemId, WorkspaceDb, WorkspaceId};
 
 define_connection!(
-    // Current table shape using pseudo-rust syntax:
+    // Current schema shape using pseudo-rust syntax:
     // editors(
     //   item_id: usize,
     //   workspace_id: usize,

crates/gpui/src/app.rs 🔗

@@ -43,6 +43,7 @@ use crate::{
     util::post_inc,
     Appearance, AssetCache, AssetSource, ClipboardItem, FontCache, InputHandler, KeyUpEvent,
     ModifiersChangedEvent, MouseButton, MouseRegionId, PathPromptOptions, TextLayoutCache,
+    WindowBounds,
 };
 
 pub trait Entity: 'static {
@@ -594,6 +595,7 @@ type ReleaseObservationCallback = Box<dyn FnMut(&dyn Any, &mut MutableAppContext
 type ActionObservationCallback = Box<dyn FnMut(TypeId, &mut MutableAppContext)>;
 type WindowActivationCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
 type WindowFullscreenCallback = Box<dyn FnMut(bool, &mut MutableAppContext) -> bool>;
+type WindowBoundsCallback = Box<dyn FnMut(WindowBounds, &mut MutableAppContext) -> bool>;
 type KeystrokeCallback = Box<
     dyn FnMut(&Keystroke, &MatchResult, Option<&Box<dyn Action>>, &mut MutableAppContext) -> bool,
 >;
@@ -624,6 +626,7 @@ pub struct MutableAppContext {
     action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>,
     window_activation_observations: CallbackCollection<usize, WindowActivationCallback>,
     window_fullscreen_observations: CallbackCollection<usize, WindowFullscreenCallback>,
+    window_bounds_observations: CallbackCollection<usize, WindowBoundsCallback>,
     keystroke_observations: CallbackCollection<usize, KeystrokeCallback>,
 
     #[allow(clippy::type_complexity)]
@@ -681,6 +684,7 @@ impl MutableAppContext {
             global_observations: Default::default(),
             window_activation_observations: Default::default(),
             window_fullscreen_observations: Default::default(),
+            window_bounds_observations: Default::default(),
             keystroke_observations: Default::default(),
             action_dispatch_observations: Default::default(),
             presenters_and_platform_windows: Default::default(),
@@ -1240,6 +1244,23 @@ impl MutableAppContext {
         )
     }
 
+    fn observe_window_bounds<F>(&mut self, window_id: usize, callback: F) -> Subscription
+    where
+        F: 'static + FnMut(WindowBounds, &mut MutableAppContext) -> bool,
+    {
+        let subscription_id = post_inc(&mut self.next_subscription_id);
+        self.pending_effects
+            .push_back(Effect::WindowBoundsObservation {
+                window_id,
+                subscription_id,
+                callback: Box::new(callback),
+            });
+        Subscription::WindowBoundsObservation(
+            self.window_bounds_observations
+                .subscribe(window_id, subscription_id),
+        )
+    }
+
     pub fn observe_keystrokes<F>(&mut self, window_id: usize, callback: F) -> Subscription
     where
         F: 'static
@@ -1774,6 +1795,13 @@ impl MutableAppContext {
             }));
         }
 
+        {
+            let mut app = self.upgrade();
+            window.on_moved(Box::new(move || {
+                app.update(|cx| cx.window_was_moved(window_id))
+            }));
+        }
+
         {
             let mut app = self.upgrade();
             window.on_fullscreen(Box::new(move |is_fullscreen| {
@@ -2071,6 +2099,11 @@ impl MutableAppContext {
                                     .invalidation
                                     .get_or_insert(WindowInvalidation::default());
                             }
+                            self.handle_window_moved(window_id);
+                        }
+
+                        Effect::MoveWindow { window_id } => {
+                            self.handle_window_moved(window_id);
                         }
 
                         Effect::WindowActivationObservation {
@@ -2103,6 +2136,16 @@ impl MutableAppContext {
                             is_fullscreen,
                         } => self.handle_fullscreen_effect(window_id, is_fullscreen),
 
+                        Effect::WindowBoundsObservation {
+                            window_id,
+                            subscription_id,
+                            callback,
+                        } => self.window_bounds_observations.add_callback(
+                            window_id,
+                            subscription_id,
+                            callback,
+                        ),
+
                         Effect::RefreshWindows => {
                             refreshing = true;
                         }
@@ -2197,6 +2240,11 @@ impl MutableAppContext {
             .push_back(Effect::ResizeWindow { window_id });
     }
 
+    fn window_was_moved(&mut self, window_id: usize) {
+        self.pending_effects
+            .push_back(Effect::MoveWindow { window_id });
+    }
+
     fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) {
         self.pending_effects.push_back(Effect::FullscreenWindow {
             window_id,
@@ -2510,6 +2558,20 @@ impl MutableAppContext {
         }
     }
 
+    fn handle_window_moved(&mut self, window_id: usize) {
+        let bounds = if self.window_is_fullscreen(window_id) {
+            WindowBounds::Fullscreen
+        } else {
+            WindowBounds::Fixed(self.window_bounds(window_id))
+        };
+        self.window_bounds_observations
+            .clone()
+            .emit(window_id, self, move |callback, this| {
+                callback(bounds, this);
+                true
+            });
+    }
+
     pub fn focus(&mut self, window_id: usize, view_id: Option<usize>) {
         self.pending_effects
             .push_back(Effect::Focus { window_id, view_id });
@@ -2887,9 +2949,8 @@ pub enum Effect {
     ResizeWindow {
         window_id: usize,
     },
-    FullscreenWindow {
+    MoveWindow {
         window_id: usize,
-        is_fullscreen: bool,
     },
     ActivateWindow {
         window_id: usize,
@@ -2900,11 +2961,20 @@ pub enum Effect {
         subscription_id: usize,
         callback: WindowActivationCallback,
     },
+    FullscreenWindow {
+        window_id: usize,
+        is_fullscreen: bool,
+    },
     WindowFullscreenObservation {
         window_id: usize,
         subscription_id: usize,
         callback: WindowFullscreenCallback,
     },
+    WindowBoundsObservation {
+        window_id: usize,
+        subscription_id: usize,
+        callback: WindowBoundsCallback,
+    },
     Keystroke {
         window_id: usize,
         keystroke: Keystroke,
@@ -3015,6 +3085,10 @@ impl Debug for Effect {
                 .debug_struct("Effect::RefreshWindow")
                 .field("window_id", window_id)
                 .finish(),
+            Effect::MoveWindow { window_id } => f
+                .debug_struct("Effect::MoveWindow")
+                .field("window_id", window_id)
+                .finish(),
             Effect::WindowActivationObservation {
                 window_id,
                 subscription_id,
@@ -3049,6 +3123,16 @@ impl Debug for Effect {
                 .field("window_id", window_id)
                 .field("subscription_id", subscription_id)
                 .finish(),
+
+            Effect::WindowBoundsObservation {
+                window_id,
+                subscription_id,
+                callback: _,
+            } => f
+                .debug_struct("Effect::WindowBoundsObservation")
+                .field("window_id", window_id)
+                .field("subscription_id", subscription_id)
+                .finish(),
             Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(),
             Effect::WindowShouldCloseSubscription { window_id, .. } => f
                 .debug_struct("Effect::WindowShouldCloseSubscription")
@@ -3928,6 +4012,24 @@ impl<'a, T: View> ViewContext<'a, T> {
         )
     }
 
+    pub fn observe_window_bounds<F>(&mut self, mut callback: F) -> Subscription
+    where
+        F: 'static + FnMut(&mut T, WindowBounds, &mut ViewContext<T>),
+    {
+        let observer = self.weak_handle();
+        self.app
+            .observe_window_bounds(self.window_id(), move |bounds, cx| {
+                if let Some(observer) = observer.upgrade(cx) {
+                    observer.update(cx, |observer, cx| {
+                        callback(observer, bounds, cx);
+                    });
+                    true
+                } else {
+                    false
+                }
+            })
+    }
+
     pub fn emit(&mut self, payload: T::Event) {
         self.app.pending_effects.push_back(Effect::Event {
             entity_id: self.view_id,
@@ -5092,6 +5194,7 @@ pub enum Subscription {
     FocusObservation(callback_collection::Subscription<usize, FocusObservationCallback>),
     WindowActivationObservation(callback_collection::Subscription<usize, WindowActivationCallback>),
     WindowFullscreenObservation(callback_collection::Subscription<usize, WindowFullscreenCallback>),
+    WindowBoundsObservation(callback_collection::Subscription<usize, WindowBoundsCallback>),
     KeystrokeObservation(callback_collection::Subscription<usize, KeystrokeCallback>),
     ReleaseObservation(callback_collection::Subscription<usize, ReleaseObservationCallback>),
     ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>),
@@ -5107,6 +5210,7 @@ impl Subscription {
             Subscription::FocusObservation(subscription) => subscription.id(),
             Subscription::WindowActivationObservation(subscription) => subscription.id(),
             Subscription::WindowFullscreenObservation(subscription) => subscription.id(),
+            Subscription::WindowBoundsObservation(subscription) => subscription.id(),
             Subscription::KeystrokeObservation(subscription) => subscription.id(),
             Subscription::ReleaseObservation(subscription) => subscription.id(),
             Subscription::ActionObservation(subscription) => subscription.id(),
@@ -5123,6 +5227,7 @@ impl Subscription {
             Subscription::KeystrokeObservation(subscription) => subscription.detach(),
             Subscription::WindowActivationObservation(subscription) => subscription.detach(),
             Subscription::WindowFullscreenObservation(subscription) => subscription.detach(),
+            Subscription::WindowBoundsObservation(subscription) => subscription.detach(),
             Subscription::ReleaseObservation(subscription) => subscription.detach(),
             Subscription::ActionObservation(subscription) => subscription.detach(),
         }

crates/gpui/src/platform.rs 🔗

@@ -126,6 +126,7 @@ pub trait Window {
     fn on_active_status_change(&mut self, callback: Box<dyn FnMut(bool)>);
     fn on_resize(&mut self, callback: Box<dyn FnMut()>);
     fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>);
+    fn on_moved(&mut self, callback: Box<dyn FnMut()>);
     fn on_should_close(&mut self, callback: Box<dyn FnMut() -> bool>);
     fn on_close(&mut self, callback: Box<dyn FnOnce()>);
     fn set_input_handler(&mut self, input_handler: Box<dyn InputHandler>);
@@ -186,8 +187,9 @@ pub enum WindowKind {
     PopUp,
 }
 
-#[derive(Debug)]
+#[derive(Copy, Clone, Debug)]
 pub enum WindowBounds {
+    Fullscreen,
     Maximized,
     Fixed(RectF),
 }

crates/gpui/src/platform/mac/status_item.rs 🔗

@@ -183,6 +183,8 @@ impl platform::Window for StatusItem {
 
     fn on_resize(&mut self, _: Box<dyn FnMut()>) {}
 
+    fn on_moved(&mut self, _: Box<dyn FnMut()>) {}
+
     fn on_fullscreen(&mut self, _: Box<dyn FnMut(bool)>) {}
 
     fn on_should_close(&mut self, _: Box<dyn FnMut() -> bool>) {}

crates/gpui/src/platform/mac/window.rs 🔗

@@ -295,6 +295,10 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
         sel!(windowWillExitFullScreen:),
         window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
     );
+    decl.add_method(
+        sel!(windowDidMove:),
+        window_did_move as extern "C" fn(&Object, Sel, id),
+    );
     decl.add_method(
         sel!(windowDidBecomeKey:),
         window_did_change_key_status as extern "C" fn(&Object, Sel, id),
@@ -333,6 +337,7 @@ struct WindowState {
     activate_callback: Option<Box<dyn FnMut(bool)>>,
     resize_callback: Option<Box<dyn FnMut()>>,
     fullscreen_callback: Option<Box<dyn FnMut(bool)>>,
+    moved_callback: Option<Box<dyn FnMut()>>,
     should_close_callback: Option<Box<dyn FnMut() -> bool>>,
     close_callback: Option<Box<dyn FnOnce()>>,
     appearance_changed_callback: Option<Box<dyn FnMut()>>,
@@ -405,6 +410,9 @@ impl Window {
 
             let screen = native_window.screen();
             match options.bounds {
+                WindowBounds::Fullscreen => {
+                    native_window.toggleFullScreen_(nil);
+                }
                 WindowBounds::Maximized => {
                     native_window.setFrame_display_(screen.visibleFrame(), YES);
                 }
@@ -441,6 +449,7 @@ impl Window {
                 close_callback: None,
                 activate_callback: None,
                 fullscreen_callback: None,
+                moved_callback: None,
                 appearance_changed_callback: None,
                 input_handler: None,
                 pending_key_down: None,
@@ -588,6 +597,10 @@ impl platform::Window for Window {
         self.0.as_ref().borrow_mut().resize_callback = Some(callback);
     }
 
+    fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
+        self.0.as_ref().borrow_mut().moved_callback = Some(callback);
+    }
+
     fn on_fullscreen(&mut self, callback: Box<dyn FnMut(bool)>) {
         self.0.as_ref().borrow_mut().fullscreen_callback = Some(callback);
     }
@@ -1137,6 +1150,16 @@ fn window_fullscreen_changed(this: &Object, is_fullscreen: bool) {
     }
 }
 
+extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
+    let window_state = unsafe { get_window_state(this) };
+    let mut window_state_borrow = window_state.as_ref().borrow_mut();
+    if let Some(mut callback) = window_state_borrow.moved_callback.take() {
+        drop(window_state_borrow);
+        callback();
+        window_state.borrow_mut().moved_callback = Some(callback);
+    }
+}
+
 extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) {
     let window_state = unsafe { get_window_state(this) };
     let window_state_borrow = window_state.borrow();

crates/gpui/src/platform/test.rs 🔗

@@ -40,6 +40,7 @@ pub struct Window {
     current_scene: Option<crate::Scene>,
     event_handlers: Vec<Box<dyn FnMut(super::Event) -> bool>>,
     pub(crate) resize_handlers: Vec<Box<dyn FnMut()>>,
+    pub(crate) moved_handlers: Vec<Box<dyn FnMut()>>,
     close_handlers: Vec<Box<dyn FnOnce()>>,
     fullscreen_handlers: Vec<Box<dyn FnMut(bool)>>,
     pub(crate) active_status_change_handlers: Vec<Box<dyn FnMut(bool)>>,
@@ -143,7 +144,7 @@ impl super::Platform for Platform {
         _executor: Rc<super::executor::Foreground>,
     ) -> Box<dyn super::Window> {
         Box::new(Window::new(match options.bounds {
-            WindowBounds::Maximized => vec2f(1024., 768.),
+            WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.),
             WindowBounds::Fixed(rect) => rect.size(),
         }))
     }
@@ -225,6 +226,7 @@ impl Window {
             size,
             event_handlers: Default::default(),
             resize_handlers: Default::default(),
+            moved_handlers: Default::default(),
             close_handlers: Default::default(),
             should_close_handler: Default::default(),
             active_status_change_handlers: Default::default(),
@@ -273,6 +275,10 @@ impl super::Window for Window {
         self.resize_handlers.push(callback);
     }
 
+    fn on_moved(&mut self, callback: Box<dyn FnMut()>) {
+        self.moved_handlers.push(callback);
+    }
+
     fn on_close(&mut self, callback: Box<dyn FnOnce()>) {
         self.close_handlers.push(callback);
     }

crates/gpui/src/presenter.rs 🔗

@@ -23,7 +23,7 @@ use pathfinder_geometry::vector::{vec2f, Vector2F};
 use serde_json::json;
 use smallvec::SmallVec;
 use sqlez::{
-    bindable::{Bind, Column},
+    bindable::{Bind, Column, StaticRowComponent},
     statement::Statement,
 };
 use std::{
@@ -932,6 +932,7 @@ impl ToJson for Axis {
     }
 }
 
+impl StaticRowComponent for Axis {}
 impl Bind for Axis {
     fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
         match self {

crates/settings/src/settings.rs 🔗

@@ -15,7 +15,7 @@ use schemars::{
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_json::Value;
 use sqlez::{
-    bindable::{Bind, Column},
+    bindable::{Bind, Column, StaticRowComponent},
     statement::Statement,
 };
 use std::{collections::HashMap, fmt::Write as _, num::NonZeroU32, str, sync::Arc};
@@ -253,6 +253,7 @@ pub enum DockAnchor {
     Expanded,
 }
 
+impl StaticRowComponent for DockAnchor {}
 impl Bind for DockAnchor {
     fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result<i32> {
         match self {

crates/sqlez/src/bindable.rs 🔗

@@ -9,14 +9,37 @@ use anyhow::{Context, Result};
 
 use crate::statement::{SqlType, Statement};
 
-pub trait Bind {
+pub trait StaticRowComponent {
+    fn static_column_count() -> usize {
+        1
+    }
+}
+
+pub trait RowComponent {
+    fn column_count(&self) -> usize;
+}
+
+impl<T: StaticRowComponent> RowComponent for T {
+    fn column_count(&self) -> usize {
+        T::static_column_count()
+    }
+}
+
+impl<T: StaticRowComponent> StaticRowComponent for &T {
+    fn static_column_count() -> usize {
+        T::static_column_count()
+    }
+}
+
+pub trait Bind: RowComponent {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32>;
 }
 
-pub trait Column: Sized {
+pub trait Column: Sized + RowComponent {
     fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>;
 }
 
+impl StaticRowComponent for bool {}
 impl Bind for bool {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -33,6 +56,7 @@ impl Column for bool {
     }
 }
 
+impl StaticRowComponent for &[u8] {}
 impl Bind for &[u8] {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -42,6 +66,7 @@ impl Bind for &[u8] {
     }
 }
 
+impl<const C: usize> StaticRowComponent for &[u8; C] {}
 impl<const C: usize> Bind for &[u8; C] {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -51,6 +76,7 @@ impl<const C: usize> Bind for &[u8; C] {
     }
 }
 
+impl StaticRowComponent for Vec<u8> {}
 impl Bind for Vec<u8> {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -70,6 +96,7 @@ impl Column for Vec<u8> {
     }
 }
 
+impl StaticRowComponent for f64 {}
 impl Bind for f64 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -89,6 +116,7 @@ impl Column for f64 {
     }
 }
 
+impl StaticRowComponent for f32 {}
 impl Bind for f32 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -109,6 +137,7 @@ impl Column for f32 {
     }
 }
 
+impl StaticRowComponent for i32 {}
 impl Bind for i32 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -126,6 +155,7 @@ impl Column for i32 {
     }
 }
 
+impl StaticRowComponent for i64 {}
 impl Bind for i64 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement
@@ -142,6 +172,7 @@ impl Column for i64 {
     }
 }
 
+impl StaticRowComponent for u32 {}
 impl Bind for u32 {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         (*self as i64)
@@ -157,6 +188,7 @@ impl Column for u32 {
     }
 }
 
+impl StaticRowComponent for usize {}
 impl Bind for usize {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         (*self as i64)
@@ -172,6 +204,7 @@ impl Column for usize {
     }
 }
 
+impl StaticRowComponent for &str {}
 impl Bind for &str {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement.bind_text(start_index, self)?;
@@ -179,6 +212,7 @@ impl Bind for &str {
     }
 }
 
+impl StaticRowComponent for Arc<str> {}
 impl Bind for Arc<str> {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement.bind_text(start_index, self.as_ref())?;
@@ -186,6 +220,7 @@ impl Bind for Arc<str> {
     }
 }
 
+impl StaticRowComponent for String {}
 impl Bind for String {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         statement.bind_text(start_index, self)?;
@@ -207,28 +242,41 @@ impl Column for String {
     }
 }
 
-impl<T: Bind> Bind for Option<T> {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+impl<T: StaticRowComponent> StaticRowComponent for Option<T> {
+    fn static_column_count() -> usize {
+        T::static_column_count()
+    }
+}
+impl<T: Bind + StaticRowComponent> Bind for Option<T> {
+    fn bind(&self, statement: &Statement, mut start_index: i32) -> Result<i32> {
         if let Some(this) = self {
             this.bind(statement, start_index)
         } else {
-            statement.bind_null(start_index)?;
-            Ok(start_index + 1)
+            for _ in 0..T::static_column_count() {
+                statement.bind_null(start_index)?;
+                start_index += 1;
+            }
+            Ok(start_index)
         }
     }
 }
 
-impl<T: Column> Column for Option<T> {
+impl<T: Column + StaticRowComponent> Column for Option<T> {
     fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
         if let SqlType::Null = statement.column_type(start_index)? {
-            Ok((None, start_index + 1))
+            Ok((None, start_index + T::static_column_count() as i32))
         } else {
             T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index))
         }
     }
 }
 
-impl<T: Bind, const COUNT: usize> Bind for [T; COUNT] {
+impl<T: StaticRowComponent, const COUNT: usize> StaticRowComponent for [T; COUNT] {
+    fn static_column_count() -> usize {
+        T::static_column_count() * COUNT
+    }
+}
+impl<T: Bind + StaticRowComponent, const COUNT: usize> Bind for [T; COUNT] {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         let mut current_index = start_index;
         for binding in self {
@@ -239,7 +287,7 @@ impl<T: Bind, const COUNT: usize> Bind for [T; COUNT] {
     }
 }
 
-impl<T: Column + Default + Copy, const COUNT: usize> Column for [T; COUNT] {
+impl<T: Column + StaticRowComponent + Default + Copy, const COUNT: usize> Column for [T; COUNT] {
     fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
         let mut array = [Default::default(); COUNT];
         let mut current_index = start_index;
@@ -250,40 +298,21 @@ impl<T: Column + Default + Copy, const COUNT: usize> Column for [T; COUNT] {
     }
 }
 
-impl<T: Bind> Bind for Vec<T> {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let mut current_index = start_index;
-        for binding in self.iter() {
-            current_index = binding.bind(statement, current_index)?
-        }
-
-        Ok(current_index)
-    }
-}
-
-impl<T: Bind> Bind for &[T] {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let mut current_index = start_index;
-        for binding in *self {
-            current_index = binding.bind(statement, current_index)?
-        }
-
-        Ok(current_index)
-    }
-}
-
+impl StaticRowComponent for &Path {}
 impl Bind for &Path {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         self.as_os_str().as_bytes().bind(statement, start_index)
     }
 }
 
+impl StaticRowComponent for Arc<Path> {}
 impl Bind for Arc<Path> {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         self.as_ref().bind(statement, start_index)
     }
 }
 
+impl StaticRowComponent for PathBuf {}
 impl Bind for PathBuf {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         (self.as_ref() as &Path).bind(statement, start_index)
@@ -301,6 +330,11 @@ impl Column for PathBuf {
     }
 }
 
+impl StaticRowComponent for () {
+    fn static_column_count() -> usize {
+        0
+    }
+}
 /// Unit impls do nothing. This simplifies query macros
 impl Bind for () {
     fn bind(&self, _statement: &Statement, start_index: i32) -> Result<i32> {
@@ -314,74 +348,80 @@ impl Column for () {
     }
 }
 
-impl<T1: Bind, T2: Bind> Bind for (T1, T2) {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let next_index = self.0.bind(statement, start_index)?;
-        self.1.bind(statement, next_index)
-    }
-}
-
-impl<T1: Column, T2: Column> Column for (T1, T2) {
-    fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let (first, next_index) = T1::column(statement, start_index)?;
-        let (second, next_index) = T2::column(statement, next_index)?;
-        Ok(((first, second), next_index))
-    }
-}
-
-impl<T1: Bind, T2: Bind, T3: Bind> Bind for (T1, T2, T3) {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let next_index = self.0.bind(statement, start_index)?;
-        let next_index = self.1.bind(statement, next_index)?;
-        self.2.bind(statement, next_index)
-    }
-}
-
-impl<T1: Column, T2: Column, T3: Column> Column for (T1, T2, T3) {
-    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let (first, next_index) = T1::column(statement, start_index)?;
-        let (second, next_index) = T2::column(statement, next_index)?;
-        let (third, next_index) = T3::column(statement, next_index)?;
-        Ok(((first, second, third), next_index))
-    }
-}
-
-impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind> Bind for (T1, T2, T3, T4) {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let next_index = self.0.bind(statement, start_index)?;
-        let next_index = self.1.bind(statement, next_index)?;
-        let next_index = self.2.bind(statement, next_index)?;
-        self.3.bind(statement, next_index)
-    }
-}
+macro_rules! impl_tuple_row_traits {
+    ( $($local:ident: $type:ident),+ ) => {
+        impl<$($type: RowComponent),+> RowComponent for ($($type,)+) {
+            fn column_count(&self) -> usize {
+                let ($($local,)+) = self;
+                let mut count = 0;
+                $(count += $local.column_count();)+
+                count
+            }
+        }
 
-impl<T1: Column, T2: Column, T3: Column, T4: Column> Column for (T1, T2, T3, T4) {
-    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let (first, next_index) = T1::column(statement, start_index)?;
-        let (second, next_index) = T2::column(statement, next_index)?;
-        let (third, next_index) = T3::column(statement, next_index)?;
-        let (fourth, next_index) = T4::column(statement, next_index)?;
-        Ok(((first, second, third, fourth), next_index))
-    }
-}
+        impl<$($type: Bind),+> Bind for ($($type,)+) {
+            fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
+                let mut next_index = start_index;
+                let ($($local,)+) = self;
+                $(next_index = $local.bind(statement, next_index)?;)+
+                Ok(next_index)
+            }
+        }
 
-impl<T1: Bind, T2: Bind, T3: Bind, T4: Bind, T5: Bind> Bind for (T1, T2, T3, T4, T5) {
-    fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
-        let next_index = self.0.bind(statement, start_index)?;
-        let next_index = self.1.bind(statement, next_index)?;
-        let next_index = self.2.bind(statement, next_index)?;
-        let next_index = self.3.bind(statement, next_index)?;
-        self.4.bind(statement, next_index)
+        impl<$($type: Column),+> Column for ($($type,)+) {
+            fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
+                let mut next_index = start_index;
+                Ok((
+                    (
+                        $({
+                            let value;
+                            (value, next_index) = $type::column(statement, next_index)?;
+                            value
+                        },)+
+                    ),
+                    next_index,
+                ))
+            }
+        }
     }
 }
 
-impl<T1: Column, T2: Column, T3: Column, T4: Column, T5: Column> Column for (T1, T2, T3, T4, T5) {
-    fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
-        let (first, next_index) = T1::column(statement, start_index)?;
-        let (second, next_index) = T2::column(statement, next_index)?;
-        let (third, next_index) = T3::column(statement, next_index)?;
-        let (fourth, next_index) = T4::column(statement, next_index)?;
-        let (fifth, next_index) = T5::column(statement, next_index)?;
-        Ok(((first, second, third, fourth, fifth), next_index))
-    }
-}
+impl_tuple_row_traits!(t1: T1, t2: T2);
+impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3);
+impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4);
+impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5);
+impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6);
+impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7);
+impl_tuple_row_traits!(
+    t1: T1,
+    t2: T2,
+    t3: T3,
+    t4: T4,
+    t5: T5,
+    t6: T6,
+    t7: T7,
+    t8: T8
+);
+impl_tuple_row_traits!(
+    t1: T1,
+    t2: T2,
+    t3: T3,
+    t4: T4,
+    t5: T5,
+    t6: T6,
+    t7: T7,
+    t8: T8,
+    t9: T9
+);
+impl_tuple_row_traits!(
+    t1: T1,
+    t2: T2,
+    t3: T3,
+    t4: T4,
+    t5: T5,
+    t6: T6,
+    t7: T7,
+    t8: T8,
+    t9: T9,
+    t10: T10
+);

crates/sqlez/src/statement.rs 🔗

@@ -238,11 +238,14 @@ impl<'a> Statement<'a> {
 
     pub fn bind<T: Bind>(&self, value: T, index: i32) -> Result<i32> {
         debug_assert!(index > 0);
-        value.bind(self, index)
+        let after = value.bind(self, index)?;
+        debug_assert_eq!((after - index) as usize, value.column_count());
+        Ok(after)
     }
 
     pub fn column<T: Column>(&mut self) -> Result<T> {
-        let (result, _) = T::column(self, 0)?;
+        let (result, after) = T::column(self, 0)?;
+        debug_assert_eq!(T::column_count(&result), after as usize);
         Ok(result)
     }
 
@@ -260,7 +263,9 @@ impl<'a> Statement<'a> {
     }
 
     pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> {
-        self.bind(bindings, 1)?;
+        let column_count = bindings.column_count();
+        let after = self.bind(bindings, 1)?;
+        debug_assert_eq!((after - 1) as usize, column_count);
         Ok(self)
     }
 

crates/workspace/src/dock.rs 🔗

@@ -534,6 +534,8 @@ mod tests {
                 }],
             },
             left_sidebar_open: false,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         let fs = FakeFs::new(cx.background());

crates/workspace/src/persistence.rs 🔗

@@ -6,7 +6,7 @@ use std::path::Path;
 
 use anyhow::{anyhow, bail, Context, Result};
 use db::{define_connection, query, sqlez::connection::Connection, sqlez_macros::sql};
-use gpui::Axis;
+use gpui::{Axis, geometry::{rect::RectF, vector::Vector2F}};
 
 use util::{unzip_option, ResultExt};
 
@@ -19,6 +19,51 @@ use model::{
 };
 
 define_connection! {
+    // Current schema shape using pseudo-rust syntax:
+    //
+    // workspaces(
+    //   workspace_id: usize, // Primary key for workspaces
+    //   workspace_location: Bincode<Vec<PathBuf>>,
+    //   dock_visible: bool,
+    //   dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded'
+    //   dock_pane: Option<usize>, // PaneId
+    //   left_sidebar_open: boolean,
+    //   timestamp: String, // UTC YYYY-MM-DD HH:MM:SS
+    //   fullscreen bool, // Boolean
+    //   window_x: f32,
+    //   window_y: f32,
+    //   window_width: f32,
+    //   window_height: f32,
+    // )
+    //
+    // pane_groups(
+    //   group_id: usize, // Primary key for pane_groups
+    //   workspace_id: usize, // References workspaces table
+    //   parent_group_id: Option<usize>, // None indicates that this is the root node
+    //   position: Optiopn<usize>, // None indicates that this is the root node
+    //   axis: Option<Axis>, // 'Vertical', 'Horizontal'
+    // )
+    //
+    // panes(
+    //     pane_id: usize, // Primary key for panes
+    //     workspace_id: usize, // References workspaces table
+    //     active: bool,
+    // )
+    //
+    // center_panes(
+    //     pane_id: usize, // Primary key for center_panes
+    //     parent_group_id: Option<usize>, // References pane_groups. If none, this is the root
+    //     position: Option<usize>, // None indicates this is the root
+    // )
+    //
+    // CREATE TABLE items(
+    //     item_id: usize, // This is the item's view id, so this is not unique
+    //     workspace_id: usize, // References workspaces table
+    //     pane_id: usize, // References panes table
+    //     kind: String, // Indicates which view this connects to. This is the key in the item_deserializers global
+    //     position: usize, // Position of the item in the parent pane. This is equivalent to panes' position column
+    //     active: bool, // Indicates if this item is the active one in the pane
+    // )
     pub static ref DB: WorkspaceDb<()> =
         &[sql!(
             CREATE TABLE workspaces(
@@ -39,8 +84,8 @@ define_connection! {
                 position INTEGER, // NULL indicates that this is a root node
                 axis TEXT NOT NULL, // Enum: 'Vertical' / 'Horizontal'
                 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                ON DELETE CASCADE
-                ON UPDATE CASCADE,
+                    ON DELETE CASCADE
+                    ON UPDATE CASCADE,
                 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
             ) STRICT;
 
@@ -49,8 +94,8 @@ define_connection! {
                 workspace_id INTEGER NOT NULL,
                 active INTEGER NOT NULL, // Boolean
                 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                ON DELETE CASCADE
-                ON UPDATE CASCADE
+                    ON DELETE CASCADE
+                    ON UPDATE CASCADE
             ) STRICT;
 
             CREATE TABLE center_panes(
@@ -58,7 +103,7 @@ define_connection! {
                 parent_group_id INTEGER, // NULL means that this is a root pane
                 position INTEGER, // NULL means that this is a root pane
                 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
-                ON DELETE CASCADE,
+                    ON DELETE CASCADE,
                 FOREIGN KEY(parent_group_id) REFERENCES pane_groups(group_id) ON DELETE CASCADE
             ) STRICT;
 
@@ -70,12 +115,19 @@ define_connection! {
                 position INTEGER NOT NULL,
                 active INTEGER NOT NULL,
                 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
-                ON DELETE CASCADE
-                ON UPDATE CASCADE,
+                    ON DELETE CASCADE
+                    ON UPDATE CASCADE,
                 FOREIGN KEY(pane_id) REFERENCES panes(pane_id)
-                ON DELETE CASCADE,
+                    ON DELETE CASCADE,
                 PRIMARY KEY(item_id, workspace_id)
             ) STRICT;
+        ),
+        sql!(
+            ALTER TABLE workspaces ADD COLUMN fullscreen INTEGER; // Boolean
+            ALTER TABLE workspaces ADD COLUMN window_x REAL; // Null means set to whatever
+            ALTER TABLE workspaces ADD COLUMN window_y REAL; // Null means set to whatever
+            ALTER TABLE workspaces ADD COLUMN window_width REAL; // Null means set to whatever
+            ALTER TABLE workspaces ADD COLUMN window_height REAL; // Null means set to whatever
         )];
 }
 
@@ -91,14 +143,26 @@ impl WorkspaceDb {
 
         // Note that we re-assign the workspace_id here in case it's empty
         // and we've grabbed the most recent workspace
-        let (workspace_id, workspace_location, left_sidebar_open, dock_position): (
+        let (workspace_id, workspace_location, left_sidebar_open, dock_position, fullscreen, window_x, window_y, window_width, window_height): (
             WorkspaceId,
             WorkspaceLocation,
             bool,
             DockPosition,
-        ) =
-            self.select_row_bound(sql!{
-                SELECT workspace_id, workspace_location, left_sidebar_open, dock_visible, dock_anchor
+            bool,
+            f32, f32, f32, f32
+        ) = self
+            .select_row_bound(sql! {
+                SELECT
+                    workspace_id,
+                    workspace_location,
+                    left_sidebar_open,
+                    dock_visible,
+                    dock_anchor,
+                    fullscreen,
+                    window_x,
+                    window_y,
+                    window_width,
+                    window_height
                 FROM workspaces
                 WHERE workspace_location = ?
             })
@@ -120,6 +184,11 @@ impl WorkspaceDb {
                 .log_err()?,
             dock_position,
             left_sidebar_open,
+            fullscreen,
+            bounds: Some(RectF::new(
+                Vector2F::new(window_x, window_y),
+                Vector2F::new(window_width, window_height)
+            ))
         })
     }
 
@@ -410,6 +479,18 @@ impl WorkspaceDb {
             WHERE workspace_id = ?
         }
     }
+
+    query! {
+        pub async fn set_bounds(workspace_id: WorkspaceId, fullscreen: bool, x: f32, y: f32, width: f32, height: f32) -> Result<()> {
+            UPDATE workspaces
+            SET fullscreen = ?2,
+                window_x = ?3,
+                window_y = ?4,
+                window_width = ?5,
+                window_height = ?6
+            WHERE workspace_id = ?1
+        }
+    }
 }
 
 #[cfg(test)]
@@ -499,6 +580,8 @@ mod tests {
             center_group: Default::default(),
             dock_pane: Default::default(),
             left_sidebar_open: true,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         let mut workspace_2 = SerializedWorkspace {
@@ -508,6 +591,8 @@ mod tests {
             center_group: Default::default(),
             dock_pane: Default::default(),
             left_sidebar_open: false,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         db.save_workspace(workspace_1.clone()).await;
@@ -614,6 +699,8 @@ mod tests {
             center_group,
             dock_pane,
             left_sidebar_open: true,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         db.save_workspace(workspace.clone()).await;
@@ -642,6 +729,8 @@ mod tests {
             center_group: Default::default(),
             dock_pane: Default::default(),
             left_sidebar_open: true,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         let mut workspace_2 = SerializedWorkspace {
@@ -651,6 +740,8 @@ mod tests {
             center_group: Default::default(),
             dock_pane: Default::default(),
             left_sidebar_open: false,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         db.save_workspace(workspace_1.clone()).await;
@@ -687,6 +778,8 @@ mod tests {
             center_group: Default::default(),
             dock_pane: Default::default(),
             left_sidebar_open: false,
+            fullscreen: false,
+            bounds: Default::default(),
         };
 
         db.save_workspace(workspace_3.clone()).await;
@@ -722,6 +815,8 @@ mod tests {
             center_group: center_group.clone(),
             dock_pane,
             left_sidebar_open: true,
+            fullscreen: false,
+            bounds: Default::default(),
         }
     }
 

crates/workspace/src/persistence/model.rs 🔗

@@ -6,10 +6,12 @@ use std::{
 use anyhow::{Context, Result};
 
 use async_recursion::async_recursion;
-use gpui::{AsyncAppContext, Axis, ModelHandle, Task, ViewHandle};
+use gpui::{
+    geometry::rect::RectF, AsyncAppContext, Axis, ModelHandle, Task, ViewHandle, WindowBounds,
+};
 
 use db::sqlez::{
-    bindable::{Bind, Column},
+    bindable::{Bind, Column, StaticRowComponent},
     statement::Statement,
 };
 use project::Project;
@@ -40,6 +42,7 @@ impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {
     }
 }
 
+impl StaticRowComponent for WorkspaceLocation {}
 impl Bind for &WorkspaceLocation {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         bincode::serialize(&self.0)
@@ -58,7 +61,7 @@ impl Column for WorkspaceLocation {
     }
 }
 
-#[derive(Debug, PartialEq, Eq, Clone)]
+#[derive(Debug, PartialEq, Clone)]
 pub struct SerializedWorkspace {
     pub id: WorkspaceId,
     pub location: WorkspaceLocation,
@@ -66,6 +69,20 @@ pub struct SerializedWorkspace {
     pub center_group: SerializedPaneGroup,
     pub dock_pane: SerializedPane,
     pub left_sidebar_open: bool,
+    pub fullscreen: bool,
+    pub bounds: Option<RectF>,
+}
+
+impl SerializedWorkspace {
+    pub fn bounds(&self) -> WindowBounds {
+        if self.fullscreen {
+            WindowBounds::Fullscreen
+        } else if let Some(bounds) = self.bounds {
+            WindowBounds::Fixed(bounds)
+        } else {
+            WindowBounds::Maximized
+        }
+    }
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -237,6 +254,11 @@ impl Default for SerializedItem {
     }
 }
 
+impl StaticRowComponent for SerializedItem {
+    fn static_column_count() -> usize {
+        3
+    }
+}
 impl Bind for &SerializedItem {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         let next_index = statement.bind(self.kind.clone(), start_index)?;
@@ -261,6 +283,11 @@ impl Column for SerializedItem {
     }
 }
 
+impl StaticRowComponent for DockPosition {
+    fn static_column_count() -> usize {
+        2
+    }
+}
 impl Bind for DockPosition {
     fn bind(&self, statement: &Statement, start_index: i32) -> Result<i32> {
         let next_index = statement.bind(self.is_visible(), start_index)?;

crates/workspace/src/workspace.rs 🔗

@@ -38,7 +38,7 @@ use gpui::{
     platform::{CursorStyle, WindowOptions},
     AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
     MouseButton, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, SizeConstraint,
-    Task, View, ViewContext, ViewHandle, WeakViewHandle,
+    Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowBounds,
 };
 use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem};
 use language::LanguageRegistry;
@@ -339,7 +339,7 @@ pub struct AppState {
     pub client: Arc<client::Client>,
     pub user_store: ModelHandle<client::UserStore>,
     pub fs: Arc<dyn fs::Fs>,
-    pub build_window_options: fn() -> WindowOptions<'static>,
+    pub build_window_options: fn(Option<WindowBounds>) -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
     pub dock_default_item_factory: DockDefaultItemFactory,
 }
@@ -366,7 +366,7 @@ impl AppState {
             languages,
             user_store,
             initialize_workspace: |_, _, _| {},
-            build_window_options: Default::default,
+            build_window_options: |_| Default::default(),
             dock_default_item_factory: |_, _| unimplemented!(),
         })
     }
@@ -682,17 +682,36 @@ impl Workspace {
             };
 
             // Use the serialized workspace to construct the new window
-            let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| {
-                let mut workspace = Workspace::new(
-                    serialized_workspace,
-                    workspace_id,
-                    project_handle,
-                    app_state.dock_default_item_factory,
-                    cx,
-                );
-                (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
-                workspace
-            });
+            let (_, workspace) = cx.add_window(
+                (app_state.build_window_options)(
+                    serialized_workspace.as_ref().map(|sw| sw.bounds()),
+                ),
+                |cx| {
+                    let mut workspace = Workspace::new(
+                        serialized_workspace,
+                        workspace_id,
+                        project_handle,
+                        app_state.dock_default_item_factory,
+                        cx,
+                    );
+                    (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
+                    cx.observe_window_bounds(move |_, bounds, cx| {
+                        let fullscreen = cx.window_is_fullscreen(cx.window_id());
+                        cx.background()
+                            .spawn(DB.set_bounds(
+                                workspace_id,
+                                fullscreen,
+                                bounds.min_x(),
+                                bounds.min_y(),
+                                bounds.width(),
+                                bounds.height(),
+                            ))
+                            .detach_and_log_err(cx);
+                    })
+                    .detach();
+                    workspace
+                },
+            );
 
             notify_if_database_failed(&workspace, &mut cx);
 
@@ -2327,6 +2346,8 @@ impl Workspace {
                     dock_pane,
                     center_group,
                     left_sidebar_open: self.left_sidebar.read(cx).is_open(),
+                    fullscreen: false,
+                    bounds: Default::default(),
                 };
 
                 cx.background()

crates/zed/src/zed.rs 🔗

@@ -369,12 +369,15 @@ pub fn initialize_workspace(
     });
 }
 
-pub fn build_window_options() -> WindowOptions<'static> {
-    let bounds = if let Some((position, size)) = ZED_WINDOW_POSITION.zip(*ZED_WINDOW_SIZE) {
-        WindowBounds::Fixed(RectF::new(position, size))
-    } else {
-        WindowBounds::Maximized
-    };
+pub fn build_window_options(bounds: Option<WindowBounds>) -> WindowOptions<'static> {
+    let bounds = bounds
+        .or_else(|| {
+            ZED_WINDOW_POSITION
+                .zip(*ZED_WINDOW_SIZE)
+                .map(|(position, size)| WindowBounds::Fixed(RectF::new(position, size)))
+        })
+        .unwrap_or(WindowBounds::Maximized);
+
     WindowOptions {
         bounds,
         titlebar: Some(TitlebarOptions {