diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index d1cf4280b5..5a352b8ffe 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -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 diff --git a/crates/welcome/src/onboarding.rs b/crates/welcome/src/onboarding.rs new file mode 100644 index 0000000000..1d3e26821e --- /dev/null +++ b/crates/welcome/src/onboarding.rs @@ -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, cx: &mut App) { + workspace::register_serializable_item::(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, + weak_handle: WeakEntity, + nav_picker: Entity>, + nav_scroll_handle: UniformListScrollHandle, +} + +impl OnboardingWalkthrough { + pub fn new( + weak_handle: WeakEntity, + language_registry: Arc, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> anyhow::Result { + 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) { + 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, + ) -> impl IntoElement { + div().child("Theme") + } + + fn render_keybindings_page( + &mut self, + _window: &mut gpui::Window, + _cx: &mut gpui::Context, + ) -> impl IntoElement { + div().child("Keybindings") + } + + fn render_extensions_page( + &mut self, + _window: &mut gpui::Window, + _cx: &mut gpui::Context, + ) -> impl IntoElement { + div().child("Extensions") + } + + fn render_settings_page( + &mut self, + _window: &mut gpui::Window, + _cx: &mut gpui::Context, + ) -> impl IntoElement { + div().child("Settings") + } + + fn render_active_page( + &mut self, + window: &mut gpui::Window, + cx: &mut gpui::Context, + ) -> 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, + ) -> 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, + ) -> 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, + selected_ix: usize, + nav_items: Vec<(SharedString, WalkthroughPage)>, +} + +impl OnboardingNavDelegate { + fn new(welcome: WeakEntity, 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>, + ) { + 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 { + "Navigation".into() + } + + fn update_matches( + &mut self, + _query: String, + _window: &mut gpui::Window, + _cx: &mut gpui::Context>, + ) -> 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>, + ) { + // 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>) { + // No-op + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _window: &mut gpui::Window, + _cx: &mut gpui::Context>, + ) -> Option { + 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, + _window: &mut gpui::Window, + _cx: &mut gpui::Context>, + ) -> Div { + // Return an empty div to hide the editor + div() + } +} + +impl EventEmitter 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, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + 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, + _window: &mut Window, + cx: &mut App, + ) -> gpui::Task> { + delete_unloaded_items( + alive_items, + workspace_id, + "walkthroughs", + &WALKTHROUGH_DB, + cx, + ) + } + + fn deserialize( + _project: Entity, + workspace: WeakEntity, + workspace_id: WorkspaceId, + item_id: workspace::ItemId, + window: &mut Window, + cx: &mut App, + ) -> gpui::Task>> { + // 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, + ) -> Option>> { + 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 = + &[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 { + SELECT item_id + FROM walkthroughs + WHERE item_id = ? AND workspace_id = ? + } + } + } +} diff --git a/crates/welcome/src/persistence.rs b/crates/welcome/src/persistence.rs new file mode 100644 index 0000000000..5640988c0c --- /dev/null +++ b/crates/welcome/src/persistence.rs @@ -0,0 +1,35 @@ +use db::{define_connection, query, sqlez_macros::sql}; +use workspace::{ItemId, WorkspaceDb}; + +define_connection! { + pub static ref WALKTHROUGH_DB: WalkthroughDb = + &[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 { + SELECT item_id + FROM walkthroughs + WHERE item_id = ? AND workspace_id = ? + } + } +} diff --git a/crates/welcome/src/walkthrough.rs b/crates/welcome/src/walkthrough.rs index 2594ccd9c3..1c8da0f7e7 100644 --- a/crates/welcome/src/walkthrough.rs +++ b/crates/welcome/src/walkthrough.rs @@ -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 = - &[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 { - SELECT item_id - FROM walkthroughs - WHERE item_id = ? AND workspace_id = ? - } - } - } -} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 83805dd489..900538d353 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -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, 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); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 74b13086f1..c95bf569b7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -188,7 +188,9 @@ actions!( ToggleZoom, Unfollow, Welcome, + // todo!("remove") Walkthrough, + OnboardingWalkthrough, RestoreBanner, ] ); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 29a5f04dd8..9f80eba78e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -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); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 410b8ff204..1fc3f53b70 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -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 })