Persist project and terminal panel sizes

Antonio Scandurra created

Change summary

Cargo.lock                                 |  1 
crates/project_panel/Cargo.toml            |  2 
crates/project_panel/src/project_panel.rs  | 77 ++++++++++++++++++++++-
crates/terminal/src/terminal.rs            |  1 
crates/terminal_view/src/terminal_panel.rs | 36 +++++++++-
crates/workspace/src/dock.rs               | 47 ++++++-------
crates/workspace/src/workspace.rs          | 20 +----
crates/zed/src/zed.rs                      |  9 +-
8 files changed, 139 insertions(+), 54 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -4888,6 +4888,7 @@ version = "0.1.0"
 dependencies = [
  "client",
  "context_menu",
+ "db",
  "drag_and_drop",
  "editor",
  "futures 0.3.28",

crates/project_panel/Cargo.toml πŸ”—

@@ -10,6 +10,7 @@ doctest = false
 
 [dependencies]
 context_menu = { path = "../context_menu" }
+db = { path = "../db" }
 drag_and_drop = { path = "../drag_and_drop" }
 editor = { path = "../editor" }
 gpui = { path = "../gpui" }
@@ -23,6 +24,7 @@ postage.workspace = true
 futures.workspace = true
 schemars.workspace = true
 serde.workspace = true
+serde_json.workspace = true
 unicase = "2.6"
 
 [dev-dependencies]

crates/project_panel/src/project_panel.rs πŸ”—

@@ -1,10 +1,11 @@
 use context_menu::{ContextMenu, ContextMenuItem};
+use db::kvp::KEY_VALUE_STORE;
 use drag_and_drop::{DragAndDrop, Draggable};
 use editor::{Cancel, Editor};
 use futures::stream::StreamExt;
 use gpui::{
     actions,
-    anyhow::{anyhow, Result},
+    anyhow::{self, anyhow, Result},
     elements::{
         AnchorCorner, ChildView, ComponentHost, ContainerStyle, Empty, Flex, MouseEventHandler,
         ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
@@ -12,8 +13,8 @@ use gpui::{
     geometry::vector::Vector2F,
     keymap_matcher::KeymapContext,
     platform::{CursorStyle, MouseButton, PromptLevel},
-    AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext,
-    ViewHandle, WeakViewHandle, WindowContext,
+    AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, Task,
+    View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
 use project::{
@@ -33,11 +34,13 @@ use std::{
 };
 use theme::{ui::FileName, ProjectPanelEntry};
 use unicase::UniCase;
+use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
     Workspace,
 };
 
+const PROJECT_PANEL_KEY: &'static str = "ProjectPanel";
 const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
 
 #[derive(Deserialize)]
@@ -67,6 +70,7 @@ pub struct ProjectPanelSettingsContent {
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
+#[serde(rename_all = "snake_case")]
 pub enum ProjectPanelDockPosition {
     Left,
     Right,
@@ -87,6 +91,8 @@ pub struct ProjectPanel {
     dragged_entry_destination: Option<Arc<Path>>,
     workspace: WeakViewHandle<Workspace>,
     has_focus: bool,
+    width: Option<f32>,
+    pending_serialization: Task<Option<()>>,
 }
 
 #[derive(Copy, Clone)]
@@ -183,8 +189,13 @@ pub enum Event {
     Focus,
 }
 
+#[derive(Serialize, Deserialize)]
+struct SerializedProjectPanel {
+    width: Option<f32>,
+}
+
 impl ProjectPanel {
-    pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+    fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
         let project = workspace.project().clone();
         let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
             cx.observe(&project, |this, _, cx| {
@@ -258,6 +269,8 @@ impl ProjectPanel {
                 dragged_entry_destination: None,
                 workspace: workspace.weak_handle(),
                 has_focus: false,
+                width: None,
+                pending_serialization: Task::ready(None),
             };
             this.update_visible_entries(None, cx);
 
@@ -311,6 +324,51 @@ impl ProjectPanel {
         project_panel
     }
 
+    pub fn load(
+        workspace: WeakViewHandle<Workspace>,
+        cx: AsyncAppContext,
+    ) -> Task<Result<ViewHandle<Self>>> {
+        cx.spawn(|mut cx| async move {
+            let serialized_panel = if let Some(panel) = cx
+                .background()
+                .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) })
+                .await
+                .log_err()
+                .flatten()
+            {
+                Some(serde_json::from_str::<SerializedProjectPanel>(&panel)?)
+            } else {
+                None
+            };
+            workspace.update(&mut cx, |workspace, cx| {
+                let panel = ProjectPanel::new(workspace, cx);
+                if let Some(serialized_panel) = serialized_panel {
+                    panel.update(cx, |panel, cx| {
+                        panel.width = serialized_panel.width;
+                        cx.notify();
+                    });
+                }
+                panel
+            })
+        })
+    }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let width = self.width;
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        PROJECT_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedProjectPanel { width })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
+
     fn deploy_context_menu(
         &mut self,
         position: Vector2F,
@@ -1435,8 +1493,15 @@ impl workspace::dock::Panel for ProjectPanel {
         );
     }
 
-    fn default_size(&self, cx: &WindowContext) -> f32 {
-        settings::get::<ProjectPanelSettings>(cx).default_width
+    fn size(&self, cx: &WindowContext) -> f32 {
+        self.width
+            .unwrap_or_else(|| settings::get::<ProjectPanelSettings>(cx).default_width)
+    }
+
+    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+        self.width = Some(size);
+        self.serialize(cx);
+        cx.notify();
     }
 
     fn should_zoom_in_on_event(_: &Self::Event) -> bool {

crates/terminal/src/terminal.rs πŸ”—

@@ -120,6 +120,7 @@ pub fn init(cx: &mut AppContext) {
 }
 
 #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
 pub enum TerminalDockPosition {
     Left,
     Bottom,

crates/terminal_view/src/terminal_panel.rs πŸ”—

@@ -37,12 +37,14 @@ pub struct TerminalPanel {
     pane: ViewHandle<Pane>,
     fs: Arc<dyn Fs>,
     workspace: WeakViewHandle<Workspace>,
+    width: Option<f32>,
+    height: Option<f32>,
     pending_serialization: Task<Option<()>>,
     _subscriptions: Vec<Subscription>,
 }
 
 impl TerminalPanel {
-    pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
+    fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
         let weak_self = cx.weak_handle();
         let pane = cx.add_view(|cx| {
             let window_id = cx.window_id();
@@ -90,6 +92,8 @@ impl TerminalPanel {
             fs: workspace.app_state().fs.clone(),
             workspace: workspace.weak_handle(),
             pending_serialization: Task::ready(None),
+            width: None,
+            height: None,
             _subscriptions: subscriptions,
         };
         let mut old_dock_position = this.position(cx);
@@ -112,7 +116,9 @@ impl TerminalPanel {
             let serialized_panel = if let Some(panel) = cx
                 .background()
                 .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) })
-                .await?
+                .await
+                .log_err()
+                .flatten()
             {
                 Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
             } else {
@@ -122,6 +128,9 @@ impl TerminalPanel {
                 let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
                 let items = if let Some(serialized_panel) = serialized_panel.as_ref() {
                     panel.update(cx, |panel, cx| {
+                        cx.notify();
+                        panel.height = serialized_panel.height;
+                        panel.width = serialized_panel.width;
                         panel.pane.update(cx, |_, cx| {
                             serialized_panel
                                 .items
@@ -226,6 +235,8 @@ impl TerminalPanel {
             .map(|item| item.id())
             .collect::<Vec<_>>();
         let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
+        let height = self.height;
+        let width = self.width;
         self.pending_serialization = cx.background().spawn(
             async move {
                 KEY_VALUE_STORE
@@ -234,6 +245,8 @@ impl TerminalPanel {
                         serde_json::to_string(&SerializedTerminalPanel {
                             items,
                             active_item_id,
+                            height,
+                            width,
                         })?,
                     )
                     .await?;
@@ -288,12 +301,23 @@ impl Panel for TerminalPanel {
         });
     }
 
-    fn default_size(&self, cx: &WindowContext) -> f32 {
+    fn size(&self, cx: &WindowContext) -> f32 {
         let settings = settings::get::<TerminalSettings>(cx);
         match self.position(cx) {
-            DockPosition::Left | DockPosition::Right => settings.default_width,
-            DockPosition::Bottom => settings.default_height,
+            DockPosition::Left | DockPosition::Right => {
+                self.width.unwrap_or_else(|| settings.default_width)
+            }
+            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
+        }
+    }
+
+    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
+        match self.position(cx) {
+            DockPosition::Left | DockPosition::Right => self.width = Some(size),
+            DockPosition::Bottom => self.height = Some(size),
         }
+        self.serialize(cx);
+        cx.notify();
     }
 
     fn should_zoom_in_on_event(event: &Event) -> bool {
@@ -360,4 +384,6 @@ impl Panel for TerminalPanel {
 struct SerializedTerminalPanel {
     items: Vec<usize>,
     active_item_id: Option<usize>,
+    width: Option<f32>,
+    height: Option<f32>,
 }

crates/workspace/src/dock.rs πŸ”—

@@ -13,7 +13,8 @@ pub trait Panel: View {
     fn position(&self, cx: &WindowContext) -> DockPosition;
     fn position_is_valid(&self, position: DockPosition) -> bool;
     fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>);
-    fn default_size(&self, cx: &WindowContext) -> f32;
+    fn size(&self, cx: &WindowContext) -> f32;
+    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>);
     fn icon_path(&self) -> &'static str;
     fn icon_tooltip(&self) -> String;
     fn icon_label(&self, _: &WindowContext) -> Option<String> {
@@ -39,7 +40,8 @@ pub trait PanelHandle {
     fn is_zoomed(&self, cx: &WindowContext) -> bool;
     fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext);
     fn set_active(&self, active: bool, cx: &mut WindowContext);
-    fn default_size(&self, cx: &WindowContext) -> f32;
+    fn size(&self, cx: &WindowContext) -> f32;
+    fn set_size(&self, size: f32, cx: &mut WindowContext);
     fn icon_path(&self, cx: &WindowContext) -> &'static str;
     fn icon_tooltip(&self, cx: &WindowContext) -> String;
     fn icon_label(&self, cx: &WindowContext) -> Option<String>;
@@ -67,8 +69,12 @@ where
         self.update(cx, |this, cx| this.set_position(position, cx))
     }
 
-    fn default_size(&self, cx: &WindowContext) -> f32 {
-        self.read(cx).default_size(cx)
+    fn size(&self, cx: &WindowContext) -> f32 {
+        self.read(cx).size(cx)
+    }
+
+    fn set_size(&self, size: f32, cx: &mut WindowContext) {
+        self.update(cx, |this, cx| this.set_size(size, cx))
     }
 
     fn is_zoomed(&self, cx: &WindowContext) -> bool {
@@ -151,7 +157,6 @@ impl DockPosition {
 
 struct PanelEntry {
     panel: Rc<dyn PanelHandle>,
-    size: f32,
     context_menu: ViewHandle<ContextMenu>,
     _subscriptions: [Subscription; 2],
 }
@@ -271,10 +276,8 @@ impl Dock {
         ];
 
         let dock_view_id = cx.view_id();
-        let size = panel.default_size(cx);
         self.panel_entries.push(PanelEntry {
             panel: Rc::new(panel),
-            size,
             context_menu: cx.add_view(|cx| {
                 let mut menu = ContextMenu::new(dock_view_id, cx);
                 menu.set_position_mode(OverlayPositionMode::Local);
@@ -343,28 +346,18 @@ impl Dock {
         }
     }
 
-    pub fn panel_size(&self, panel: &dyn PanelHandle) -> Option<f32> {
+    pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
         self.panel_entries
             .iter()
             .find(|entry| entry.panel.id() == panel.id())
-            .map(|entry| entry.size)
+            .map(|entry| entry.panel.size(cx))
     }
 
-    pub fn resize_panel(&mut self, panel: &dyn PanelHandle, size: f32) {
-        let entry = self
-            .panel_entries
-            .iter_mut()
-            .find(|entry| entry.panel.id() == panel.id());
-        if let Some(entry) = entry {
-            entry.size = size;
-        }
-    }
-
-    pub fn active_panel_size(&self) -> Option<f32> {
+    pub fn active_panel_size(&self, cx: &WindowContext) -> Option<f32> {
         if self.is_open {
             self.panel_entries
                 .get(self.active_panel_index)
-                .map(|entry| entry.size)
+                .map(|entry| entry.panel.size(cx))
         } else {
             None
         }
@@ -372,7 +365,7 @@ impl Dock {
 
     pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext<Self>) {
         if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) {
-            entry.size = size;
+            entry.panel.set_size(size, cx);
             cx.notify();
         }
     }
@@ -386,7 +379,7 @@ impl Dock {
                 .with_style(style.container)
                 .resizable(
                     self.position.to_resize_handle_side(),
-                    active_entry.size,
+                    active_entry.panel.size(cx),
                     |_, _, _| {},
                 )
                 .into_any()
@@ -413,7 +406,7 @@ impl View for Dock {
                 .with_style(style.container)
                 .resizable(
                     self.position.to_resize_handle_side(),
-                    active_entry.size,
+                    active_entry.panel.size(cx),
                     |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx),
                 )
                 .into_any()
@@ -630,13 +623,17 @@ pub(crate) mod test {
             unimplemented!()
         }
 
-        fn default_size(&self, _: &WindowContext) -> f32 {
+        fn size(&self, _: &WindowContext) -> f32 {
             match self.position.axis() {
                 Axis::Horizontal => 300.,
                 Axis::Vertical => 200.,
             }
         }
 
+        fn set_size(&mut self, _: f32, _: &mut ViewContext<Self>) {
+            unimplemented!()
+        }
+
         fn icon_path(&self) -> &'static str {
             "icons/test_panel.svg"
         }

crates/workspace/src/workspace.rs πŸ”—

@@ -853,11 +853,7 @@ impl Workspace {
                 if T::should_change_position_on_event(event) {
                     let new_position = panel.read(cx).position(cx);
                     let mut was_visible = false;
-                    let mut size = None;
                     dock.update(cx, |dock, cx| {
-                        if new_position.axis() == prev_position.axis() {
-                            size = dock.panel_size(&panel);
-                        }
                         prev_position = new_position;
 
                         was_visible = dock.is_open()
@@ -874,10 +870,6 @@ impl Workspace {
                     .clone();
                     dock.update(cx, |dock, cx| {
                         dock.add_panel(panel.clone(), cx);
-                        if let Some(size) = size {
-                            dock.resize_panel(&panel, size);
-                        }
-
                         if was_visible {
                             dock.set_open(true, cx);
                             dock.activate_panel(dock.panels_len() - 1, cx);
@@ -3961,8 +3953,8 @@ mod tests {
                 panel_1.id()
             );
             assert_eq!(
-                left_dock.read(cx).active_panel_size().unwrap(),
-                panel_1.default_size(cx)
+                left_dock.read(cx).active_panel_size(cx).unwrap(),
+                panel_1.size(cx)
             );
 
             left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx));
@@ -3989,7 +3981,7 @@ mod tests {
                 right_dock.read(cx).active_panel().unwrap().id(),
                 panel_1.id()
             );
-            assert_eq!(right_dock.read(cx).active_panel_size().unwrap(), 1337.);
+            assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
 
             // Now we move panel_2Β to the left
             panel_2.set_position(DockPosition::Left, cx);
@@ -4019,7 +4011,7 @@ mod tests {
                 left_dock.read(cx).active_panel().unwrap().id(),
                 panel_1.id()
             );
-            assert_eq!(left_dock.read(cx).active_panel_size().unwrap(), 1337.);
+            assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.);
             // And right the dock should be closed as it no longer has any panels.
             assert!(!workspace.right_dock().read(cx).is_open());
 
@@ -4034,8 +4026,8 @@ mod tests {
             // since the panel orientation changed from vertical to horizontal.
             let bottom_dock = workspace.bottom_dock();
             assert_eq!(
-                bottom_dock.read(cx).active_panel_size().unwrap(),
-                panel_1.default_size(cx),
+                bottom_dock.read(cx).active_panel_size(cx).unwrap(),
+                panel_1.size(cx),
             );
             // Close bottom dock and move panel_1 back to the left.
             bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx));

crates/zed/src/zed.rs πŸ”—

@@ -335,8 +335,12 @@ pub fn initialize_workspace(
                 }
                 false
             });
+        })?;
 
-            let project_panel = ProjectPanel::new(workspace, cx);
+        let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone());
+        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone());
+        let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?;
+        workspace_handle.update(&mut cx, |workspace, cx| {
             let project_panel_position = project_panel.position(cx);
             workspace.add_panel(project_panel, cx);
             if !was_deserialized
@@ -352,10 +356,7 @@ pub fn initialize_workspace(
             {
                 workspace.toggle_dock(project_panel_position, cx);
             }
-        })?;
 
-        let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()).await?;
-        workspace_handle.update(&mut cx, |workspace, cx| {
             workspace.add_panel(terminal_panel, cx)
         })?;
         Ok(())