wip onboarding

This commit is contained in:
Nate Butler
2025-05-08 12:36:19 -04:00
parent 7bbe256e92
commit 48a0262cfd
8 changed files with 533 additions and 64 deletions

View File

@@ -20,6 +20,7 @@ client.workspace = true
component.workspace = true
db.workspace = true
documented.workspace = true
editor.workspace = true
fs.workspace = true
fuzzy.workspace = true
gpui.workspace = true

View File

@@ -0,0 +1,465 @@
use editor::Editor;
use gpui::{
AnyElement, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ListAlignment,
ListState, SharedString, Task, UniformListScrollHandle, WeakEntity, div,
};
use language::LanguageRegistry;
use persistence::WALKTHROUGH_DB;
use picker::{Picker, PickerDelegate, PickerEditorPosition};
use std::sync::Arc;
use ui::{Button, ButtonStyle, Color, TabBar, TabPosition, Tooltip, prelude::*};
use util::ResultExt;
use workspace::{
AppState, Item, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items,
item::ItemEvent,
};
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
workspace::register_serializable_item::<OnboardingWalkthrough>(cx);
let app_state = app_state.clone();
cx.observe_new(move |workspace: &mut Workspace, _window, cx| {
let app_state = app_state.clone();
let weak_workspace = cx.entity().downgrade();
workspace.register_action(
move |workspace, _: &workspace::OnboardingWalkthrough, window, cx| {
let app_state = app_state.clone();
let language_registry = app_state.languages.clone();
let walkthrough = cx.new(|cx| {
OnboardingWalkthrough::new(
weak_workspace.clone(),
language_registry.clone(),
window,
cx,
)
// todo!("fail more graceefully")
.expect("Failed to create OnboardingWalkthrough")
});
workspace.add_item_to_active_pane(Box::new(walkthrough), None, true, window, cx)
},
);
})
.detach();
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub enum WalkthroughPage {
#[default]
Theme,
KeyBindings,
Extensions,
Settings,
}
pub struct OnboardingWalkthrough {
active_page: WalkthroughPage,
focus_handle: FocusHandle,
language_registry: Arc<LanguageRegistry>,
weak_handle: WeakEntity<Workspace>,
nav_picker: Entity<Picker<OnboardingNavDelegate>>,
nav_scroll_handle: UniformListScrollHandle,
}
impl OnboardingWalkthrough {
pub fn new(
weak_handle: WeakEntity<Workspace>,
language_registry: Arc<LanguageRegistry>,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> anyhow::Result<Self> {
let nav_delegate = OnboardingNavDelegate::new(cx.entity().downgrade(), 0);
let nav_picker = cx.new(|cx| {
let picker = Picker::uniform_list(nav_delegate, window, cx);
picker.focus(window, cx);
picker
});
let welcome = Self {
active_page: WalkthroughPage::default(),
focus_handle: cx.focus_handle(),
language_registry,
weak_handle,
nav_picker,
nav_scroll_handle: UniformListScrollHandle::new(),
};
Ok(welcome)
}
fn set_active_page(&mut self, page: WalkthroughPage, cx: &mut gpui::Context<Self>) {
if self.active_page != page {
self.active_page = page;
cx.emit(ItemEvent::UpdateTab);
cx.notify();
}
}
fn render_theme_page(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
div().child("Theme")
}
fn render_keybindings_page(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
div().child("Keybindings")
}
fn render_extensions_page(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
div().child("Extensions")
}
fn render_settings_page(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
div().child("Settings")
}
fn render_active_page(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
match self.active_page {
WalkthroughPage::Theme => self.render_theme_page(window, cx).into_any_element(),
WalkthroughPage::KeyBindings => {
self.render_keybindings_page(window, cx).into_any_element()
}
WalkthroughPage::Extensions => {
self.render_extensions_page(window, cx).into_any_element()
}
WalkthroughPage::Settings => self.render_settings_page(window, cx).into_any_element(),
}
}
fn render_navigation(
&mut self,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
div()
.w(rems(20.))
.h_full()
// .border_r_1()
// .border_color(ui::Color::Muted)
.child(self.nav_picker.clone())
}
}
impl Render for OnboardingWalkthrough {
fn render(
&mut self,
window: &mut gpui::Window,
cx: &mut gpui::Context<Self>,
) -> impl IntoElement {
h_flex().debug_below().h_full().w_full().child(
h_flex()
.w_full()
.max_w(px(800.))
.h_full()
.gap_6()
.child(self.render_navigation(window, cx))
.child(
div()
.flex_1()
.h_full()
.child(self.render_active_page(window, cx)),
),
)
}
}
struct OnboardingNavDelegate {
welcome: WeakEntity<OnboardingWalkthrough>,
selected_ix: usize,
nav_items: Vec<(SharedString, WalkthroughPage)>,
}
impl OnboardingNavDelegate {
fn new(welcome: WeakEntity<OnboardingWalkthrough>, selected_ix: usize) -> Self {
Self {
welcome,
selected_ix,
nav_items: vec![
("Theme".into(), WalkthroughPage::Theme),
("Key Bindings".into(), WalkthroughPage::KeyBindings),
("Extensions".into(), WalkthroughPage::Extensions),
("Settings".into(), WalkthroughPage::Settings),
],
}
}
}
impl PickerDelegate for OnboardingNavDelegate {
type ListItem = AnyElement;
fn match_count(&self) -> usize {
self.nav_items.len()
}
fn selected_index(&self) -> usize {
self.selected_ix
}
fn set_selected_index(
&mut self,
ix: usize,
window: &mut gpui::Window,
cx: &mut gpui::Context<Picker<Self>>,
) {
if ix < self.nav_items.len() {
self.selected_ix = ix;
// Update the active page in Welcome2
if let Some(welcome) = self.welcome.upgrade() {
welcome.update(cx, |welcome, cx| {
let page = self.nav_items[ix].1.clone();
welcome.set_active_page(page, cx);
});
}
}
}
fn placeholder_text(&self, _window: &mut gpui::Window, _cx: &mut gpui::App) -> Arc<str> {
"Navigation".into()
}
fn update_matches(
&mut self,
_query: String,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Picker<Self>>,
) -> Task<()> {
// We don't filter nav items, so just return a completed task
Task::ready(())
}
fn confirm(
&mut self,
_secondary: bool,
window: &mut gpui::Window,
cx: &mut gpui::Context<Picker<Self>>,
) {
// Just set the active page again to ensure it's set
if let Some(welcome) = self.welcome.upgrade() {
welcome.update(cx, |welcome, cx| {
let page = self.nav_items[self.selected_ix].1.clone();
welcome.set_active_page(page, cx);
});
}
}
fn dismissed(&mut self, _window: &mut gpui::Window, _cx: &mut gpui::Context<Picker<Self>>) {
// No-op
}
fn render_match(
&self,
ix: usize,
selected: bool,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Picker<Self>>,
) -> Option<Self::ListItem> {
if ix >= self.nav_items.len() {
return None;
}
let (name, _) = &self.nav_items[ix];
Some(
div()
.px_4()
.py_2()
// .bg(if selected {
// Color::Accent
// } else {
// Color::default_bg().with_alpha_factor(0.0)
// })
// .text_color(if selected {
// Color::White
// } else {
// Color::Default
// })
.rounded_md()
// .when(selected, |div| div.font_weight(FontWeight::BOLD))
.child(name.clone())
.into_any_element(),
)
}
fn editor_position(&self) -> PickerEditorPosition {
// Hide the editor since we're just using this for navigation
PickerEditorPosition::End
}
fn render_editor(
&self,
_editor: &gpui::Entity<Editor>,
_window: &mut gpui::Window,
_cx: &mut gpui::Context<Picker<Self>>,
) -> Div {
// Return an empty div to hide the editor
div()
}
}
impl EventEmitter<ItemEvent> for OnboardingWalkthrough {}
impl Focusable for OnboardingWalkthrough {
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl Item for OnboardingWalkthrough {
type Event = ItemEvent;
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
"Walkthrough".into()
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Walkthrough Page Opened")
}
fn show_toolbar(&self) -> bool {
false
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>> {
let weak_handle = self.weak_handle.clone();
let language_registry = self.language_registry.clone();
let onboarding_result =
OnboardingWalkthrough::new(weak_handle, language_registry, window, cx).log_err();
if let Some(onboarding) = onboarding_result {
Some(cx.new(|_| onboarding))
} else {
None
}
}
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
f(*event)
}
}
impl SerializableItem for OnboardingWalkthrough {
fn serialized_item_kind() -> &'static str {
"Walkthrough"
}
fn cleanup(
workspace_id: WorkspaceId,
alive_items: Vec<workspace::ItemId>,
_window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<()>> {
delete_unloaded_items(
alive_items,
workspace_id,
"walkthroughs",
&WALKTHROUGH_DB,
cx,
)
}
fn deserialize(
_project: Entity<project::Project>,
workspace: WeakEntity<Workspace>,
workspace_id: WorkspaceId,
item_id: workspace::ItemId,
window: &mut Window,
cx: &mut App,
) -> gpui::Task<gpui::Result<Entity<Self>>> {
// todo!("update")
// let has_walkthrough = WALKTHROUGH_DB.get_walkthrough(item_id, workspace_id);
// window.spawn(cx, async move |cx| {
// has_walkthrough?;
// workspace.update_in(cx, |workspace, window, cx| {
// let weak_handle = cx.entity().downgrade();
// let language_registry = self.language_registry.clone();
// OnboardingWalkthrough::new(weak_handle, language_registry, window, cx)
// })
// })
todo!("update")
}
fn serialize(
&mut self,
workspace: &mut Workspace,
item_id: workspace::ItemId,
_closing: bool,
_window: &mut Window,
cx: &mut Context<Self>,
) -> Option<gpui::Task<gpui::Result<()>>> {
let workspace_id = workspace.database_id()?;
Some(cx.background_spawn(async move {
WALKTHROUGH_DB.save_walkthrough(item_id, workspace_id).await
}))
}
fn should_serialize(&self, _event: &Self::Event) -> bool {
false
}
}
mod persistence {
use db::{define_connection, query, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb};
define_connection! {
pub static ref WALKTHROUGH_DB: WalkthroughDb<WorkspaceDb> =
&[sql!(
CREATE TABLE walkthroughs (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
impl WalkthroughDb {
query! {
pub async fn save_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<()> {
INSERT INTO walkthroughs(item_id, workspace_id)
VALUES (?1, ?2)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2
}
}
query! {
pub fn get_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<ItemId> {
SELECT item_id
FROM walkthroughs
WHERE item_id = ? AND workspace_id = ?
}
}
}
}

View File

@@ -0,0 +1,35 @@
use db::{define_connection, query, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb};
define_connection! {
pub static ref WALKTHROUGH_DB: WalkthroughDb<WorkspaceDb> =
&[sql!(
CREATE TABLE walkthroughs (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
impl WalkthroughDb {
query! {
pub async fn save_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<()> {
INSERT INTO walkthroughs(item_id, workspace_id)
VALUES (?1, ?2)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2
}
}
query! {
pub fn get_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<ItemId> {
SELECT item_id
FROM walkthroughs
WHERE item_id = ? AND workspace_id = ?
}
}
}

View File

@@ -1,5 +1,6 @@
use client::telemetry::Telemetry;
use crate::persistence::WALKTHROUGH_DB;
use client::TelemetrySettings;
use fs::Fs;
use gpui::{AnyView, ScrollHandle};
@@ -8,7 +9,6 @@ use gpui::{
ParentElement, Render, Styled, Subscription, WeakEntity, Window, list, svg,
};
use language_model::{LanguageModelProviderName, LanguageModelRegistry};
use persistence::WALKTHROUGH_DB;
use regex::Regex;
use settings::Settings;
use settings::SettingsStore;
@@ -582,17 +582,14 @@ impl Walkthrough {
// .size_full()
// .p_1()
// .child(
div()
.id("provider-configuration")
.size_full()
.track_scroll(&scroll_handle)
.overflow_y_scroll()
.child(
view.clone().into_any()
)
div()
.id("provider-configuration")
.size_full()
.track_scroll(&scroll_handle)
.overflow_y_scroll()
.child(view.clone().into_any())
.child(scrollbar(scrollbar_state.clone(), window))
// )
// )
}
});
}
@@ -850,41 +847,3 @@ impl SerializableItem for Walkthrough {
false
}
}
mod persistence {
use db::{define_connection, query, sqlez_macros::sql};
use workspace::{ItemId, WorkspaceDb};
define_connection! {
pub static ref WALKTHROUGH_DB: WalkthroughDb<WorkspaceDb> =
&[sql!(
CREATE TABLE walkthroughs (
workspace_id INTEGER,
item_id INTEGER UNIQUE,
PRIMARY KEY(workspace_id, item_id),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
ON DELETE CASCADE
) STRICT;
)];
}
impl WalkthroughDb {
query! {
pub async fn save_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<()> {
INSERT INTO walkthroughs(item_id, workspace_id)
VALUES (?1, ?2)
ON CONFLICT DO UPDATE SET
item_id = ?1,
workspace_id = ?2
}
}
query! {
pub fn get_walkthrough(item_id: ItemId, workspace_id: workspace::WorkspaceId) -> Result<ItemId> {
SELECT item_id
FROM walkthroughs
WHERE item_id = ? AND workspace_id = ?
}
}
}
}

View File

@@ -1,10 +1,26 @@
use client::{TelemetrySettings, telemetry::Telemetry};
mod base_keymap_picker;
mod base_keymap_setting;
mod multibuffer_hint;
mod onboarding;
mod persistence;
mod recent_projects;
mod walkthrough;
mod welcome_ui;
pub use base_keymap_setting::BaseKeymap;
use client::{Client, TelemetrySettings, telemetry::Telemetry};
use db::kvp::KEY_VALUE_STORE;
use gpui::{
Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
};
use language::language_settings::{EditPredictionProvider, all_language_settings};
use language::{
LanguageRegistry,
language_settings::{EditPredictionProvider, all_language_settings},
};
pub use multibuffer_hint::*;
use onboarding::OnboardingWalkthrough;
use project::Project;
use settings::{Settings, SettingsStore};
use std::sync::Arc;
use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
@@ -17,23 +33,13 @@ use workspace::{
open_new,
};
pub use base_keymap_setting::BaseKeymap;
pub use multibuffer_hint::*;
mod base_keymap_picker;
mod base_keymap_setting;
mod multibuffer_hint;
mod recent_projects;
mod walkthrough;
mod welcome_ui;
actions!(welcome, [ResetHints]);
pub const FIRST_OPEN: &str = "first_open";
pub const DOCS_URL: &str = "https://zed.dev/docs/";
const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
pub fn init(cx: &mut App) {
pub fn init(app_state: Arc<AppState>, cx: &mut App) {
BaseKeymap::register(cx);
cx.observe_new(|workspace: &mut Workspace, _, _cx| {
@@ -47,6 +53,7 @@ pub fn init(cx: &mut App) {
.detach();
walkthrough::init(cx);
onboarding::init(app_state.clone(), cx);
base_keymap_picker::init(cx);
}

View File

@@ -188,7 +188,9 @@ actions!(
ToggleZoom,
Unfollow,
Welcome,
// todo!("remove")
Walkthrough,
OnboardingWalkthrough,
RestoreBanner,
]
);

View File

@@ -557,7 +557,7 @@ fn main() {
git_ui::init(cx);
feedback::init(cx);
markdown_preview::init(cx);
welcome::init(cx);
welcome::init(app_state.clone(), cx);
settings_ui::init(cx);
extensions_ui::init(cx);
zeta::init(cx);

View File

@@ -3836,7 +3836,7 @@ mod tests {
client::init(&app_state.client, cx);
language::init(cx);
workspace::init(app_state.clone(), cx);
welcome::init(cx);
welcome::init(app_state.clone(), cx);
Project::init_settings(cx);
app_state
})