onboarding: Serialize onboarding page (#35490)

Closes #ISSUE

Serializes the onboarding page to the database to ensure that if Zed is
closed during onboarding, re-opening Zed restores the onboarding state
and the most recently active page (Basics, Editing, etc) restored. Also
has the nice side effect of making dev a bit nicer as it removes the
need to re-open onboarding and navigate to the correct page on each
build.

Release Notes:

- N/A *or* Added/Fixed/Improved ...
This commit is contained in:
Ben Kunkle 2025-08-01 15:45:29 -05:00 committed by GitHub
parent ac75593198
commit 561ccf86aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -22,7 +22,7 @@ use workspace::{
dock::DockPosition,
item::{Item, ItemEvent},
notifications::NotifyResultExt as _,
open_new, with_active_or_new_workspace,
open_new, register_serializable_item, with_active_or_new_workspace,
};
mod ai_setup_page;
@ -197,6 +197,7 @@ pub fn init(cx: &mut App) {
.detach();
})
.detach();
register_serializable_item::<Onboarding>(cx);
}
pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
@ -247,6 +248,12 @@ impl Onboarding {
})
}
fn set_page(&mut self, page: SelectedPage, cx: &mut Context<Self>) {
self.selected_page = page;
cx.notify();
cx.emit(ItemEvent::UpdateTab);
}
fn render_nav_buttons(
&mut self,
window: &mut Window,
@ -306,8 +313,7 @@ impl Onboarding {
IntoElement::into_any_element,
))
.on_click(cx.listener(move |this, _, _, cx| {
this.selected_page = page;
cx.notify();
this.set_page(page, cx);
}))
})
}
@ -470,16 +476,13 @@ impl Render for Onboarding {
.size_full()
.bg(cx.theme().colors().editor_background)
.on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
this.selected_page = SelectedPage::Basics;
cx.notify();
this.set_page(SelectedPage::Basics, cx);
}))
.on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
this.selected_page = SelectedPage::Editing;
cx.notify();
this.set_page(SelectedPage::Editing, cx);
}))
.on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
this.selected_page = SelectedPage::AiSetup;
cx.notify();
this.set_page(SelectedPage::AiSetup, cx);
}))
.child(
h_flex()
@ -594,3 +597,125 @@ pub async fn handle_import_vscode_settings(
})
.ok();
}
impl workspace::SerializableItem for Onboarding {
fn serialized_item_kind() -> &'static str {
"OnboardingPage"
}
fn cleanup(
workspace_id: workspace::WorkspaceId,
alive_items: Vec<workspace::ItemId>,
_window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<()>> {
workspace::delete_unloaded_items(
alive_items,
workspace_id,
"onboarding_pages",
&persistence::ONBOARDING_PAGES,
cx,
)
}
fn deserialize(
_project: Entity<project::Project>,
workspace: WeakEntity<Workspace>,
workspace_id: workspace::WorkspaceId,
item_id: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<Entity<Self>>> {
window.spawn(cx, async move |cx| {
if let Some(page_number) =
persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
{
let page = match page_number {
0 => Some(SelectedPage::Basics),
1 => Some(SelectedPage::Editing),
2 => Some(SelectedPage::AiSetup),
_ => None,
};
workspace.update(cx, |workspace, cx| {
let onboarding_page = Onboarding::new(workspace, cx);
if let Some(page) = page {
zlog::info!("Onboarding page {page:?} loaded");
onboarding_page.update(cx, |onboarding_page, cx| {
onboarding_page.set_page(page, cx);
})
}
onboarding_page
})
} else {
Err(anyhow::anyhow!("No onboarding page to deserialize"))
}
})
}
fn serialize(
&mut self,
workspace: &mut Workspace,
item_id: workspace::ItemId,
_closing: bool,
_window: &mut Window,
cx: &mut ui::Context<Self>,
) -> Option<gpui::Task<gpui::Result<()>>> {
let workspace_id = workspace.database_id()?;
let page_number = self.selected_page as u16;
Some(cx.background_spawn(async move {
persistence::ONBOARDING_PAGES
.save_onboarding_page(item_id, workspace_id, page_number)
.await
}))
}
fn should_serialize(&self, event: &Self::Event) -> bool {
event == &ItemEvent::UpdateTab
}
}
mod persistence {
use db::{define_connection, query, sqlez_macros::sql};
use workspace::WorkspaceDb;
define_connection! {
pub static ref ONBOARDING_PAGES: OnboardingPagesDb<WorkspaceDb> =
&[
sql!(
CREATE TABLE onboarding_pages (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
page_number INTEGER,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
),
];
}
impl OnboardingPagesDb {
query! {
pub async fn save_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId,
page_number: u16
) -> Result<()> {
INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id, page_number)
VALUES (?, ?, ?)
}
}
query! {
pub fn get_onboarding_page(
item_id: workspace::ItemId,
workspace_id: workspace::WorkspaceId
) -> Result<Option<u16>> {
SELECT page_number
FROM onboarding_pages
WHERE item_id = ? AND workspace_id = ?
}
}
}
}