Serialize and deserialize `TerminalPanel`

Antonio Scandurra created

Change summary

crates/terminal_view/src/terminal_panel.rs | 115 +++++++++++++++
crates/workspace/src/workspace.rs          |  69 +++++---
crates/zed/src/zed.rs                      | 175 ++++++++++++-----------
3 files changed, 246 insertions(+), 113 deletions(-)

Detailed changes

crates/terminal_view/src/terminal_panel.rs 🔗

@@ -1,15 +1,20 @@
 use crate::TerminalView;
+use db::kvp::KEY_VALUE_STORE;
 use gpui::{
-    actions, anyhow, elements::*, AppContext, Entity, Subscription, View, ViewContext, ViewHandle,
-    WeakViewHandle, WindowContext,
+    actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity,
+    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
 };
+use serde::{Deserialize, Serialize};
 use settings::{settings_file::SettingsFile, Settings, TerminalDockPosition, WorkingDirectory};
-use util::ResultExt;
+use util::{ResultExt, TryFutureExt};
 use workspace::{
     dock::{DockPosition, Panel},
+    item::Item,
     pane, DraggedItem, Pane, Workspace,
 };
 
+const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel";
+
 actions!(terminal_panel, [ToggleFocus]);
 
 pub fn init(cx: &mut AppContext) {
@@ -27,6 +32,7 @@ pub enum Event {
 pub struct TerminalPanel {
     pane: ViewHandle<Pane>,
     workspace: WeakViewHandle<Workspace>,
+    pending_serialization: Task<Option<()>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -86,10 +92,79 @@ impl TerminalPanel {
         Self {
             pane,
             workspace: workspace.weak_handle(),
+            pending_serialization: Task::ready(None),
             _subscriptions: subscriptions,
         }
     }
 
+    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(TERMINAL_PANEL_KEY) })
+                .await?
+            {
+                Some(serde_json::from_str::<SerializedTerminalPanel>(&panel)?)
+            } else {
+                None
+            };
+            let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| {
+                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| {
+                        panel.pane.update(cx, |_, cx| {
+                            serialized_panel
+                                .items
+                                .iter()
+                                .map(|item_id| {
+                                    TerminalView::deserialize(
+                                        workspace.project().clone(),
+                                        workspace.weak_handle(),
+                                        workspace.database_id(),
+                                        *item_id,
+                                        cx,
+                                    )
+                                })
+                                .collect::<Vec<_>>()
+                        })
+                    })
+                } else {
+                    Default::default()
+                };
+                let pane = panel.read(cx).pane.clone();
+                (panel, pane, items)
+            })?;
+
+            let items = futures::future::join_all(items).await;
+            workspace.update(&mut cx, |workspace, cx| {
+                let active_item_id = serialized_panel
+                    .as_ref()
+                    .and_then(|panel| panel.active_item_id);
+                let mut active_ix = None;
+                for item in items {
+                    if let Some(item) = item.log_err() {
+                        let item_id = item.id();
+                        Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx);
+                        if Some(item_id) == active_item_id {
+                            active_ix = Some(pane.read(cx).items_len() - 1);
+                        }
+                    }
+                }
+
+                if let Some(active_ix) = active_ix {
+                    pane.update(cx, |pane, cx| {
+                        pane.activate_item(active_ix, false, false, cx)
+                    });
+                }
+            })?;
+
+            Ok(panel)
+        })
+    }
+
     fn handle_pane_event(
         &mut self,
         _pane: ViewHandle<Pane>,
@@ -97,6 +172,8 @@ impl TerminalPanel {
         cx: &mut ViewContext<Self>,
     ) {
         match event {
+            pane::Event::ActivateItem { .. } => self.serialize(cx),
+            pane::Event::RemoveItem { .. } => self.serialize(cx),
             pane::Event::Remove => cx.emit(Event::Close),
             pane::Event::ZoomIn => cx.emit(Event::ZoomIn),
             pane::Event::ZoomOut => cx.emit(Event::ZoomOut),
@@ -131,10 +208,36 @@ impl TerminalPanel {
                     Pane::add_item(workspace, &pane, terminal, true, true, None, cx);
                 }
             })?;
+            this.update(&mut cx, |this, cx| this.serialize(cx))?;
             anyhow::Ok(())
         })
         .detach_and_log_err(cx);
     }
+
+    fn serialize(&mut self, cx: &mut ViewContext<Self>) {
+        let items = self
+            .pane
+            .read(cx)
+            .items()
+            .map(|item| item.id())
+            .collect::<Vec<_>>();
+        let active_item_id = self.pane.read(cx).active_item().map(|item| item.id());
+        self.pending_serialization = cx.background().spawn(
+            async move {
+                KEY_VALUE_STORE
+                    .write_kvp(
+                        TERMINAL_PANEL_KEY.into(),
+                        serde_json::to_string(&SerializedTerminalPanel {
+                            items,
+                            active_item_id,
+                        })?,
+                    )
+                    .await?;
+                anyhow::Ok(())
+            }
+            .log_err(),
+        );
+    }
 }
 
 impl Entity for TerminalPanel {
@@ -255,3 +358,9 @@ impl Panel for TerminalPanel {
         matches!(event, Event::Focus)
     }
 }
+
+#[derive(Serialize, Deserialize)]
+struct SerializedTerminalPanel {
+    items: Vec<usize>,
+    active_item_id: Option<usize>,
+}

crates/workspace/src/workspace.rs 🔗

@@ -361,7 +361,8 @@ pub struct AppState {
     pub fs: Arc<dyn fs::Fs>,
     pub build_window_options:
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
-    pub initialize_workspace: fn(&mut Workspace, bool, &Arc<AppState>, &mut ViewContext<Workspace>),
+    pub initialize_workspace:
+        fn(WeakViewHandle<Workspace>, bool, Arc<AppState>, AsyncAppContext) -> Task<Result<()>>,
     pub background_actions: BackgroundActions,
 }
 
@@ -383,7 +384,7 @@ impl AppState {
             fs,
             languages,
             user_store,
-            initialize_workspace: |_, _, _, _| {},
+            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             build_window_options: |_, _, _| Default::default(),
             background_actions: || &[],
         })
@@ -730,31 +731,19 @@ impl Workspace {
                         ))
                     });
 
-            let build_workspace =
-                |cx: &mut ViewContext<Workspace>,
-                 serialized_workspace: Option<SerializedWorkspace>| {
-                    let was_deserialized = serialized_workspace.is_some();
-                    let mut workspace = Workspace::new(
-                        serialized_workspace,
-                        workspace_id,
-                        project_handle.clone(),
-                        app_state.clone(),
-                        cx,
-                    );
-                    (app_state.initialize_workspace)(
-                        &mut workspace,
-                        was_deserialized,
-                        &app_state,
-                        cx,
-                    );
-                    workspace
-                };
+            let was_deserialized = serialized_workspace.is_some();
 
             let workspace = requesting_window_id
                 .and_then(|window_id| {
                     cx.update(|cx| {
                         cx.replace_root_view(window_id, |cx| {
-                            build_workspace(cx, serialized_workspace.take())
+                            Workspace::new(
+                                serialized_workspace.take(),
+                                workspace_id,
+                                project_handle.clone(),
+                                app_state.clone(),
+                                cx,
+                            )
                         })
                     })
                 })
@@ -794,11 +783,28 @@ impl Workspace {
                     // Use the serialized workspace to construct the new window
                     cx.add_window(
                         (app_state.build_window_options)(bounds, display, cx.platform().as_ref()),
-                        |cx| build_workspace(cx, serialized_workspace),
+                        |cx| {
+                            Workspace::new(
+                                serialized_workspace,
+                                workspace_id,
+                                project_handle.clone(),
+                                app_state.clone(),
+                                cx,
+                            )
+                        },
                     )
                     .1
                 });
 
+            (app_state.initialize_workspace)(
+                workspace.downgrade(),
+                was_deserialized,
+                app_state.clone(),
+                cx.clone(),
+            )
+            .await
+            .log_err();
+
             let workspace = workspace.downgrade();
             notify_if_database_failed(&workspace, &mut cx);
 
@@ -2740,7 +2746,7 @@ impl Workspace {
             user_store: project.read(cx).user_store(),
             fs: project.read(cx).fs().clone(),
             build_window_options: |_, _, _| Default::default(),
-            initialize_workspace: |_, _, _, _| {},
+            initialize_workspace: |_, _, _, _| Task::ready(Ok(())),
             background_actions: || &[],
         });
         Self::new(None, 0, project, app_state, cx)
@@ -3097,12 +3103,17 @@ pub fn join_remote_project(
 
             let (_, workspace) = cx.add_window(
                 (app_state.build_window_options)(None, None, cx.platform().as_ref()),
-                |cx| {
-                    let mut workspace = Workspace::new(None, 0, project, app_state.clone(), cx);
-                    (app_state.initialize_workspace)(&mut workspace, false, &app_state, cx);
-                    workspace
-                },
+                |cx| Workspace::new(None, 0, project, app_state.clone(), cx),
             );
+            (app_state.initialize_workspace)(
+                workspace.downgrade(),
+                false,
+                app_state.clone(),
+                cx.clone(),
+            )
+            .await
+            .log_err();
+
             workspace.downgrade()
         };
 

crates/zed/src/zed.rs 🔗

@@ -18,10 +18,11 @@ use feedback::{
 use futures::StreamExt;
 use gpui::{
     actions,
+    anyhow::{self, Result},
     geometry::vector::vec2f,
     impl_actions,
     platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions},
-    AppContext, ViewContext,
+    AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle,
 };
 pub use lsp;
 pub use project;
@@ -281,93 +282,105 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut gpui::AppContext) {
 }
 
 pub fn initialize_workspace(
-    workspace: &mut Workspace,
+    workspace_handle: WeakViewHandle<Workspace>,
     was_deserialized: bool,
-    app_state: &Arc<AppState>,
-    cx: &mut ViewContext<Workspace>,
-) {
-    let workspace_handle = cx.handle();
-    cx.subscribe(&workspace_handle, {
-        move |workspace, _, event, cx| {
-            if let workspace::Event::PaneAdded(pane) = event {
-                pane.update(cx, |pane, cx| {
-                    pane.toolbar().update(cx, |toolbar, cx| {
-                        let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
-                        toolbar.add_item(breadcrumbs, cx);
-                        let buffer_search_bar = cx.add_view(BufferSearchBar::new);
-                        toolbar.add_item(buffer_search_bar, cx);
-                        let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
-                        toolbar.add_item(project_search_bar, cx);
-                        let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new());
-                        toolbar.add_item(submit_feedback_button, cx);
-                        let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
-                        toolbar.add_item(feedback_info_text, cx);
-                        let lsp_log_item = cx.add_view(|_| {
-                            lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
+    app_state: Arc<AppState>,
+    cx: AsyncAppContext,
+) -> Task<Result<()>> {
+    cx.spawn(|mut cx| async move {
+        workspace_handle.update(&mut cx, |workspace, cx| {
+            let workspace_handle = cx.handle();
+            cx.subscribe(&workspace_handle, {
+                move |workspace, _, event, cx| {
+                    if let workspace::Event::PaneAdded(pane) = event {
+                        pane.update(cx, |pane, cx| {
+                            pane.toolbar().update(cx, |toolbar, cx| {
+                                let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace));
+                                toolbar.add_item(breadcrumbs, cx);
+                                let buffer_search_bar = cx.add_view(BufferSearchBar::new);
+                                toolbar.add_item(buffer_search_bar, cx);
+                                let project_search_bar = cx.add_view(|_| ProjectSearchBar::new());
+                                toolbar.add_item(project_search_bar, cx);
+                                let submit_feedback_button =
+                                    cx.add_view(|_| SubmitFeedbackButton::new());
+                                toolbar.add_item(submit_feedback_button, cx);
+                                let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new());
+                                toolbar.add_item(feedback_info_text, cx);
+                                let lsp_log_item = cx.add_view(|_| {
+                                    lsp_log::LspLogToolbarItemView::new(workspace.project().clone())
+                                });
+                                toolbar.add_item(lsp_log_item, cx);
+                            })
                         });
-                        toolbar.add_item(lsp_log_item, cx);
-                    })
-                });
-            }
-        }
-    })
-    .detach();
-
-    cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
-
-    let collab_titlebar_item =
-        cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
-    workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
-
-    let project_panel = ProjectPanel::new(workspace, cx);
-    let project_panel_position = project_panel.position(cx);
-    workspace.add_panel(project_panel, cx);
-    if !was_deserialized
-        && workspace
-            .project()
-            .read(cx)
-            .visible_worktrees(cx)
-            .any(|tree| {
-                tree.read(cx)
-                    .root_entry()
-                    .map_or(false, |entry| entry.is_dir())
+                    }
+                }
             })
-    {
-        workspace.toggle_dock(project_panel_position, cx);
-    }
+            .detach();
 
-    let terminal_panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx));
-    workspace.add_panel(terminal_panel, cx);
-
-    let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
-    let diagnostic_summary =
-        cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
-    let activity_indicator =
-        activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
-    let active_buffer_language =
-        cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
-    let feedback_button =
-        cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace));
-    let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
-    workspace.status_bar().update(cx, |status_bar, cx| {
-        status_bar.add_left_item(diagnostic_summary, cx);
-        status_bar.add_left_item(activity_indicator, cx);
-        status_bar.add_right_item(feedback_button, cx);
-        status_bar.add_right_item(copilot, cx);
-        status_bar.add_right_item(active_buffer_language, cx);
-        status_bar.add_right_item(cursor_position, cx);
-    });
+            cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone()));
+
+            let collab_titlebar_item =
+                cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
+            workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
+
+            let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(cx));
+            let diagnostic_summary =
+                cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
+            let activity_indicator = activity_indicator::ActivityIndicator::new(
+                workspace,
+                app_state.languages.clone(),
+                cx,
+            );
+            let active_buffer_language =
+                cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+            let feedback_button = cx.add_view(|_| {
+                feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
+            });
+            let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new());
+            workspace.status_bar().update(cx, |status_bar, cx| {
+                status_bar.add_left_item(diagnostic_summary, cx);
+                status_bar.add_left_item(activity_indicator, cx);
+                status_bar.add_right_item(feedback_button, cx);
+                status_bar.add_right_item(copilot, cx);
+                status_bar.add_right_item(active_buffer_language, cx);
+                status_bar.add_right_item(cursor_position, cx);
+            });
 
-    auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
+            auto_update::notify_of_any_new_update(cx.weak_handle(), cx);
 
-    vim::observe_keystrokes(cx);
+            vim::observe_keystrokes(cx);
 
-    cx.on_window_should_close(|workspace, cx| {
-        if let Some(task) = workspace.close(&Default::default(), cx) {
-            task.detach_and_log_err(cx);
-        }
-        false
-    });
+            cx.on_window_should_close(|workspace, cx| {
+                if let Some(task) = workspace.close(&Default::default(), cx) {
+                    task.detach_and_log_err(cx);
+                }
+                false
+            });
+
+            let project_panel = ProjectPanel::new(workspace, cx);
+            let project_panel_position = project_panel.position(cx);
+            workspace.add_panel(project_panel, cx);
+            if !was_deserialized
+                && workspace
+                    .project()
+                    .read(cx)
+                    .visible_worktrees(cx)
+                    .any(|tree| {
+                        tree.read(cx)
+                            .root_entry()
+                            .map_or(false, |entry| entry.is_dir())
+                    })
+            {
+                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(())
+    })
 }
 
 pub fn build_window_options(