1pub use crate::welcome::ShowWelcome;
2use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage};
3use client::{Client, UserStore, zed_urls};
4use db::kvp::KEY_VALUE_STORE;
5use fs::Fs;
6use gpui::{
7 Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
8 FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, ScrollHandle, SharedString,
9 Subscription, Task, WeakEntity, Window, actions,
10};
11use notifications::status_toast::{StatusToast, ToastIcon};
12use schemars::JsonSchema;
13use serde::Deserialize;
14use settings::{SettingsStore, VsCodeSettingsSource};
15use std::sync::Arc;
16use ui::{
17 Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName,
18 WithScrollbar as _, prelude::*, rems_from_px,
19};
20pub use ui_input::font_picker;
21use workspace::{
22 AppState, Workspace, WorkspaceId,
23 dock::DockPosition,
24 item::{Item, ItemEvent},
25 notifications::NotifyResultExt as _,
26 open_new, register_serializable_item, with_active_or_new_workspace,
27};
28
29mod base_keymap_picker;
30mod basics_page;
31pub mod multibuffer_hint;
32mod theme_preview;
33mod welcome;
34
35/// Imports settings from Visual Studio Code.
36#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
37#[action(namespace = zed)]
38#[serde(deny_unknown_fields)]
39pub struct ImportVsCodeSettings {
40 #[serde(default)]
41 pub skip_prompt: bool,
42}
43
44/// Imports settings from Cursor editor.
45#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
46#[action(namespace = zed)]
47#[serde(deny_unknown_fields)]
48pub struct ImportCursorSettings {
49 #[serde(default)]
50 pub skip_prompt: bool,
51}
52
53pub const FIRST_OPEN: &str = "first_open";
54pub const DOCS_URL: &str = "https://zed.dev/docs/";
55
56actions!(
57 zed,
58 [
59 /// Opens the onboarding view.
60 OpenOnboarding
61 ]
62);
63
64actions!(
65 onboarding,
66 [
67 /// Finish the onboarding process.
68 Finish,
69 /// Sign in while in the onboarding flow.
70 SignIn,
71 /// Open the user account in zed.dev while in the onboarding flow.
72 OpenAccount,
73 /// Resets the welcome screen hints to their initial state.
74 ResetHints
75 ]
76);
77
78pub fn init(cx: &mut App) {
79 cx.observe_new(|workspace: &mut Workspace, _, _cx| {
80 workspace
81 .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx));
82 })
83 .detach();
84
85 cx.on_action(|_: &OpenOnboarding, cx| {
86 with_active_or_new_workspace(cx, |workspace, window, cx| {
87 workspace
88 .with_local_workspace(window, cx, |workspace, window, cx| {
89 let existing = workspace
90 .active_pane()
91 .read(cx)
92 .items()
93 .find_map(|item| item.downcast::<Onboarding>());
94
95 if let Some(existing) = existing {
96 workspace.activate_item(&existing, true, true, window, cx);
97 } else {
98 let settings_page = Onboarding::new(workspace, cx);
99 workspace.add_item_to_active_pane(
100 Box::new(settings_page),
101 None,
102 true,
103 window,
104 cx,
105 )
106 }
107 })
108 .detach();
109 });
110 });
111
112 cx.on_action(|_: &ShowWelcome, cx| {
113 with_active_or_new_workspace(cx, |workspace, window, cx| {
114 workspace
115 .with_local_workspace(window, cx, |workspace, window, cx| {
116 let existing = workspace
117 .active_pane()
118 .read(cx)
119 .items()
120 .find_map(|item| item.downcast::<WelcomePage>());
121
122 if let Some(existing) = existing {
123 workspace.activate_item(&existing, true, true, window, cx);
124 } else {
125 let settings_page = WelcomePage::new(window, cx);
126 workspace.add_item_to_active_pane(
127 Box::new(settings_page),
128 None,
129 true,
130 window,
131 cx,
132 )
133 }
134 })
135 .detach();
136 });
137 });
138
139 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
140 workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
141 let fs = <dyn Fs>::global(cx);
142 let action = *action;
143
144 let workspace = cx.weak_entity();
145
146 window
147 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
148 handle_import_vscode_settings(
149 workspace,
150 VsCodeSettingsSource::VsCode,
151 action.skip_prompt,
152 fs,
153 cx,
154 )
155 .await
156 })
157 .detach();
158 });
159
160 workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
161 let fs = <dyn Fs>::global(cx);
162 let action = *action;
163
164 let workspace = cx.weak_entity();
165
166 window
167 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
168 handle_import_vscode_settings(
169 workspace,
170 VsCodeSettingsSource::Cursor,
171 action.skip_prompt,
172 fs,
173 cx,
174 )
175 .await
176 })
177 .detach();
178 });
179 })
180 .detach();
181
182 base_keymap_picker::init(cx);
183
184 register_serializable_item::<Onboarding>(cx);
185 register_serializable_item::<WelcomePage>(cx);
186}
187
188pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
189 telemetry::event!("Onboarding Page Opened");
190 open_new(
191 Default::default(),
192 app_state,
193 cx,
194 |workspace, window, cx| {
195 {
196 workspace.toggle_dock(DockPosition::Left, window, cx);
197 let onboarding_page = Onboarding::new(workspace, cx);
198 workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
199
200 window.focus(&onboarding_page.focus_handle(cx));
201
202 cx.notify();
203 };
204 db::write_and_log(cx, || {
205 KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
206 });
207 },
208 )
209}
210
211struct Onboarding {
212 workspace: WeakEntity<Workspace>,
213 focus_handle: FocusHandle,
214 user_store: Entity<UserStore>,
215 scroll_handle: ScrollHandle,
216 _settings_subscription: Subscription,
217}
218
219impl Onboarding {
220 fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
221 let font_family_cache = theme::FontFamilyCache::global(cx);
222
223 cx.new(|cx| {
224 cx.spawn(async move |this, cx| {
225 font_family_cache.prefetch(cx).await;
226 this.update(cx, |_, cx| {
227 cx.notify();
228 })
229 })
230 .detach();
231
232 Self {
233 workspace: workspace.weak_handle(),
234 focus_handle: cx.focus_handle(),
235 scroll_handle: ScrollHandle::new(),
236 user_store: workspace.user_store().clone(),
237 _settings_subscription: cx
238 .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
239 }
240 })
241 }
242
243 fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) {
244 telemetry::event!("Finish Setup");
245 go_to_welcome_page(cx);
246 }
247
248 fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) {
249 let client = Client::global(cx);
250
251 window
252 .spawn(cx, async move |cx| {
253 client
254 .sign_in_with_optional_connect(true, cx)
255 .await
256 .notify_async_err(cx);
257 })
258 .detach();
259 }
260
261 fn handle_open_account(_: &OpenAccount, _: &mut Window, cx: &mut App) {
262 cx.open_url(&zed_urls::account_url(cx))
263 }
264
265 fn render_page(&mut self, cx: &mut Context<Self>) -> AnyElement {
266 crate::basics_page::render_basics_page(cx).into_any_element()
267 }
268}
269
270impl Render for Onboarding {
271 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
272 div()
273 .image_cache(gpui::retain_all("onboarding-page"))
274 .key_context({
275 let mut ctx = KeyContext::new_with_defaults();
276 ctx.add("Onboarding");
277 ctx.add("menu");
278 ctx
279 })
280 .track_focus(&self.focus_handle)
281 .size_full()
282 .bg(cx.theme().colors().editor_background)
283 .on_action(Self::on_finish)
284 .on_action(Self::handle_sign_in)
285 .on_action(Self::handle_open_account)
286 .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| {
287 window.focus_next();
288 cx.notify();
289 }))
290 .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| {
291 window.focus_prev();
292 cx.notify();
293 }))
294 .child(
295 div()
296 .max_w(Rems(48.0))
297 .size_full()
298 .mx_auto()
299 .child(
300 v_flex()
301 .id("page-content")
302 .m_auto()
303 .p_12()
304 .size_full()
305 .max_w_full()
306 .min_w_0()
307 .gap_6()
308 .overflow_y_scroll()
309 .child(
310 h_flex()
311 .w_full()
312 .gap_4()
313 .justify_between()
314 .child(
315 h_flex()
316 .gap_4()
317 .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
318 .child(
319 v_flex()
320 .child(
321 Headline::new("Welcome to Zed")
322 .size(HeadlineSize::Small),
323 )
324 .child(
325 Label::new("The editor for what's next")
326 .color(Color::Muted)
327 .size(LabelSize::Small)
328 .italic(),
329 ),
330 ),
331 )
332 .child({
333 Button::new("finish_setup", "Finish Setup")
334 .style(ButtonStyle::Filled)
335 .size(ButtonSize::Medium)
336 .width(Rems(12.0))
337 .key_binding(
338 KeyBinding::for_action_in(
339 &Finish,
340 &self.focus_handle,
341 window,
342 cx,
343 )
344 .map(|kb| kb.size(rems_from_px(12.))),
345 )
346 .on_click(|_, window, cx| {
347 window.dispatch_action(Finish.boxed_clone(), cx);
348 })
349 }),
350 )
351 .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
352 .child(self.render_page(cx))
353 .track_scroll(&self.scroll_handle),
354 )
355 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx),
356 )
357 }
358}
359
360impl EventEmitter<ItemEvent> for Onboarding {}
361
362impl Focusable for Onboarding {
363 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
364 self.focus_handle.clone()
365 }
366}
367
368impl Item for Onboarding {
369 type Event = ItemEvent;
370
371 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
372 "Onboarding".into()
373 }
374
375 fn telemetry_event_text(&self) -> Option<&'static str> {
376 Some("Onboarding Page Opened")
377 }
378
379 fn show_toolbar(&self) -> bool {
380 false
381 }
382
383 fn clone_on_split(
384 &self,
385 _workspace_id: Option<WorkspaceId>,
386 _: &mut Window,
387 cx: &mut Context<Self>,
388 ) -> Option<Entity<Self>> {
389 Some(cx.new(|cx| Onboarding {
390 workspace: self.workspace.clone(),
391 user_store: self.user_store.clone(),
392 scroll_handle: ScrollHandle::new(),
393 focus_handle: cx.focus_handle(),
394 _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
395 }))
396 }
397
398 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
399 f(*event)
400 }
401}
402
403fn go_to_welcome_page(cx: &mut App) {
404 with_active_or_new_workspace(cx, |workspace, window, cx| {
405 let Some((onboarding_id, onboarding_idx)) = workspace
406 .active_pane()
407 .read(cx)
408 .items()
409 .enumerate()
410 .find_map(|(idx, item)| {
411 let _ = item.downcast::<Onboarding>()?;
412 Some((item.item_id(), idx))
413 })
414 else {
415 return;
416 };
417
418 workspace.active_pane().update(cx, |pane, cx| {
419 // Get the index here to get around the borrow checker
420 let idx = pane.items().enumerate().find_map(|(idx, item)| {
421 let _ = item.downcast::<WelcomePage>()?;
422 Some(idx)
423 });
424
425 if let Some(idx) = idx {
426 pane.activate_item(idx, true, true, window, cx);
427 } else {
428 let item = Box::new(WelcomePage::new(window, cx));
429 pane.add_item(item, true, true, Some(onboarding_idx), window, cx);
430 }
431
432 pane.remove_item(onboarding_id, false, false, window, cx);
433 });
434 });
435}
436
437pub async fn handle_import_vscode_settings(
438 workspace: WeakEntity<Workspace>,
439 source: VsCodeSettingsSource,
440 skip_prompt: bool,
441 fs: Arc<dyn Fs>,
442 cx: &mut AsyncWindowContext,
443) {
444 use util::truncate_and_remove_front;
445
446 let vscode_settings =
447 match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
448 Ok(vscode_settings) => vscode_settings,
449 Err(err) => {
450 zlog::error!("{err}");
451 let _ = cx.prompt(
452 gpui::PromptLevel::Info,
453 &format!("Could not find or load a {source} settings file"),
454 None,
455 &["Ok"],
456 );
457 return;
458 }
459 };
460
461 if !skip_prompt {
462 let prompt = cx.prompt(
463 gpui::PromptLevel::Warning,
464 &format!(
465 "Importing {} settings may overwrite your existing settings. \
466 Will import settings from {}",
467 vscode_settings.source,
468 truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
469 ),
470 None,
471 &["Ok", "Cancel"],
472 );
473 let result = cx.spawn(async move |_| prompt.await.ok()).await;
474 if result != Some(0) {
475 return;
476 }
477 };
478
479 let Ok(result_channel) = cx.update(|_, cx| {
480 let source = vscode_settings.source;
481 let path = vscode_settings.path.clone();
482 let result_channel = cx
483 .global::<SettingsStore>()
484 .import_vscode_settings(fs, vscode_settings);
485 zlog::info!("Imported {source} settings from {}", path.display());
486 result_channel
487 }) else {
488 return;
489 };
490
491 let result = result_channel.await;
492 workspace
493 .update_in(cx, |workspace, _, cx| match result {
494 Ok(_) => {
495 let confirmation_toast = StatusToast::new(
496 format!("Your {} settings were successfully imported.", source),
497 cx,
498 |this, _| {
499 this.icon(ToastIcon::new(IconName::Check).color(Color::Success))
500 .dismiss_button(true)
501 },
502 );
503 SettingsImportState::update(cx, |state, _| match source {
504 VsCodeSettingsSource::VsCode => {
505 state.vscode = true;
506 }
507 VsCodeSettingsSource::Cursor => {
508 state.cursor = true;
509 }
510 });
511 workspace.toggle_status_toast(confirmation_toast, cx);
512 }
513 Err(_) => {
514 let error_toast = StatusToast::new(
515 "Failed to import settings. See log for details",
516 cx,
517 |this, _| {
518 this.icon(ToastIcon::new(IconName::Close).color(Color::Error))
519 .action("Open Log", |window, cx| {
520 window.dispatch_action(workspace::OpenLog.boxed_clone(), cx)
521 })
522 .dismiss_button(true)
523 },
524 );
525 workspace.toggle_status_toast(error_toast, cx);
526 }
527 })
528 .ok();
529}
530
531#[derive(Default, Copy, Clone)]
532pub struct SettingsImportState {
533 pub cursor: bool,
534 pub vscode: bool,
535}
536
537impl Global for SettingsImportState {}
538
539impl SettingsImportState {
540 pub fn global(cx: &App) -> Self {
541 cx.try_global().cloned().unwrap_or_default()
542 }
543 pub fn update<R>(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R {
544 cx.update_default_global(f)
545 }
546}
547
548impl workspace::SerializableItem for Onboarding {
549 fn serialized_item_kind() -> &'static str {
550 "OnboardingPage"
551 }
552
553 fn cleanup(
554 workspace_id: workspace::WorkspaceId,
555 alive_items: Vec<workspace::ItemId>,
556 _window: &mut Window,
557 cx: &mut App,
558 ) -> gpui::Task<gpui::Result<()>> {
559 workspace::delete_unloaded_items(
560 alive_items,
561 workspace_id,
562 "onboarding_pages",
563 &persistence::ONBOARDING_PAGES,
564 cx,
565 )
566 }
567
568 fn deserialize(
569 _project: Entity<project::Project>,
570 workspace: WeakEntity<Workspace>,
571 workspace_id: workspace::WorkspaceId,
572 item_id: workspace::ItemId,
573 window: &mut Window,
574 cx: &mut App,
575 ) -> gpui::Task<gpui::Result<Entity<Self>>> {
576 window.spawn(cx, async move |cx| {
577 if let Some(_) =
578 persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)?
579 {
580 workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx))
581 } else {
582 Err(anyhow::anyhow!("No onboarding page to deserialize"))
583 }
584 })
585 }
586
587 fn serialize(
588 &mut self,
589 workspace: &mut Workspace,
590 item_id: workspace::ItemId,
591 _closing: bool,
592 _window: &mut Window,
593 cx: &mut ui::Context<Self>,
594 ) -> Option<gpui::Task<gpui::Result<()>>> {
595 let workspace_id = workspace.database_id()?;
596
597 Some(cx.background_spawn(async move {
598 persistence::ONBOARDING_PAGES
599 .save_onboarding_page(item_id, workspace_id)
600 .await
601 }))
602 }
603
604 fn should_serialize(&self, event: &Self::Event) -> bool {
605 event == &ItemEvent::UpdateTab
606 }
607}
608
609mod persistence {
610 use db::{
611 query,
612 sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection},
613 sqlez_macros::sql,
614 };
615 use workspace::WorkspaceDb;
616
617 pub struct OnboardingPagesDb(ThreadSafeConnection);
618
619 impl Domain for OnboardingPagesDb {
620 const NAME: &str = stringify!(OnboardingPagesDb);
621
622 const MIGRATIONS: &[&str] = &[
623 sql!(
624 CREATE TABLE onboarding_pages (
625 workspace_id INTEGER,
626 item_id INTEGER UNIQUE,
627 page_number INTEGER,
628
629 PRIMARY KEY(workspace_id, item_id),
630 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
631 ON DELETE CASCADE
632 ) STRICT;
633 ),
634 sql!(
635 CREATE TABLE onboarding_pages_2 (
636 workspace_id INTEGER,
637 item_id INTEGER UNIQUE,
638
639 PRIMARY KEY(workspace_id, item_id),
640 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
641 ON DELETE CASCADE
642 ) STRICT;
643 INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages;
644 DROP TABLE onboarding_pages;
645 ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages;
646 ),
647 ];
648 }
649
650 db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]);
651
652 impl OnboardingPagesDb {
653 query! {
654 pub async fn save_onboarding_page(
655 item_id: workspace::ItemId,
656 workspace_id: workspace::WorkspaceId
657 ) -> Result<()> {
658 INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id)
659 VALUES (?, ?)
660 }
661 }
662
663 query! {
664 pub fn get_onboarding_page(
665 item_id: workspace::ItemId,
666 workspace_id: workspace::WorkspaceId
667 ) -> Result<Option<workspace::ItemId>> {
668 SELECT item_id
669 FROM onboarding_pages
670 WHERE item_id = ? AND workspace_id = ?
671 }
672 }
673 }
674}