1use crate::welcome::{ShowWelcome, WelcomePage};
2use client::{Client, UserStore};
3use command_palette_hooks::CommandPaletteFilter;
4use db::kvp::KEY_VALUE_STORE;
5use feature_flags::{FeatureFlag, FeatureFlagViewExt as _};
6use fs::Fs;
7use gpui::{
8 Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter,
9 FocusHandle, Focusable, IntoElement, KeyContext, Render, SharedString, Subscription, Task,
10 WeakEntity, Window, actions,
11};
12use schemars::JsonSchema;
13use serde::Deserialize;
14use settings::{SettingsStore, VsCodeSettingsSource};
15use std::sync::Arc;
16use ui::{
17 Avatar, ButtonLike, FluentBuilder, Headline, KeyBinding, ParentElement as _,
18 StatefulInteractiveElement, Vector, VectorName, prelude::*, rems_from_px,
19};
20use workspace::{
21 AppState, Workspace, WorkspaceId,
22 dock::DockPosition,
23 item::{Item, ItemEvent},
24 notifications::NotifyResultExt as _,
25 open_new, with_active_or_new_workspace,
26};
27
28mod ai_setup_page;
29mod basics_page;
30mod editing_page;
31mod theme_preview;
32mod welcome;
33
34pub struct OnBoardingFeatureFlag {}
35
36impl FeatureFlag for OnBoardingFeatureFlag {
37 const NAME: &'static str = "onboarding";
38}
39
40/// Imports settings from Visual Studio Code.
41#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
42#[action(namespace = zed)]
43#[serde(deny_unknown_fields)]
44pub struct ImportVsCodeSettings {
45 #[serde(default)]
46 pub skip_prompt: bool,
47}
48
49/// Imports settings from Cursor editor.
50#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
51#[action(namespace = zed)]
52#[serde(deny_unknown_fields)]
53pub struct ImportCursorSettings {
54 #[serde(default)]
55 pub skip_prompt: bool,
56}
57
58pub const FIRST_OPEN: &str = "first_open";
59
60actions!(
61 zed,
62 [
63 /// Opens the onboarding view.
64 OpenOnboarding
65 ]
66);
67
68actions!(
69 onboarding,
70 [
71 /// Activates the Basics page.
72 ActivateBasicsPage,
73 /// Activates the Editing page.
74 ActivateEditingPage,
75 /// Activates the AI Setup page.
76 ActivateAISetupPage,
77 ]
78);
79
80pub fn init(cx: &mut App) {
81 cx.on_action(|_: &OpenOnboarding, cx| {
82 with_active_or_new_workspace(cx, |workspace, window, cx| {
83 workspace
84 .with_local_workspace(window, cx, |workspace, window, cx| {
85 let existing = workspace
86 .active_pane()
87 .read(cx)
88 .items()
89 .find_map(|item| item.downcast::<Onboarding>());
90
91 if let Some(existing) = existing {
92 workspace.activate_item(&existing, true, true, window, cx);
93 } else {
94 let settings_page = Onboarding::new(workspace, cx);
95 workspace.add_item_to_active_pane(
96 Box::new(settings_page),
97 None,
98 true,
99 window,
100 cx,
101 )
102 }
103 })
104 .detach();
105 });
106 });
107
108 cx.on_action(|_: &ShowWelcome, cx| {
109 with_active_or_new_workspace(cx, |workspace, window, cx| {
110 workspace
111 .with_local_workspace(window, cx, |workspace, window, cx| {
112 let existing = workspace
113 .active_pane()
114 .read(cx)
115 .items()
116 .find_map(|item| item.downcast::<WelcomePage>());
117
118 if let Some(existing) = existing {
119 workspace.activate_item(&existing, true, true, window, cx);
120 } else {
121 let settings_page = WelcomePage::new(window, cx);
122 workspace.add_item_to_active_pane(
123 Box::new(settings_page),
124 None,
125 true,
126 window,
127 cx,
128 )
129 }
130 })
131 .detach();
132 });
133 });
134
135 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
136 workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
137 let fs = <dyn Fs>::global(cx);
138 let action = *action;
139
140 window
141 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
142 handle_import_vscode_settings(
143 VsCodeSettingsSource::VsCode,
144 action.skip_prompt,
145 fs,
146 cx,
147 )
148 .await
149 })
150 .detach();
151 });
152
153 workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
154 let fs = <dyn Fs>::global(cx);
155 let action = *action;
156
157 window
158 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
159 handle_import_vscode_settings(
160 VsCodeSettingsSource::Cursor,
161 action.skip_prompt,
162 fs,
163 cx,
164 )
165 .await
166 })
167 .detach();
168 });
169 })
170 .detach();
171
172 cx.observe_new::<Workspace>(|_, window, cx| {
173 let Some(window) = window else {
174 return;
175 };
176
177 let onboarding_actions = [
178 std::any::TypeId::of::<OpenOnboarding>(),
179 std::any::TypeId::of::<ShowWelcome>(),
180 ];
181
182 CommandPaletteFilter::update_global(cx, |filter, _cx| {
183 filter.hide_action_types(&onboarding_actions);
184 });
185
186 cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
187 if is_enabled {
188 CommandPaletteFilter::update_global(cx, |filter, _cx| {
189 filter.show_action_types(onboarding_actions.iter());
190 });
191 } else {
192 CommandPaletteFilter::update_global(cx, |filter, _cx| {
193 filter.hide_action_types(&onboarding_actions);
194 });
195 }
196 })
197 .detach();
198 })
199 .detach();
200}
201
202pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
203 open_new(
204 Default::default(),
205 app_state,
206 cx,
207 |workspace, window, cx| {
208 {
209 workspace.toggle_dock(DockPosition::Left, window, cx);
210 let onboarding_page = Onboarding::new(workspace, cx);
211 workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
212
213 window.focus(&onboarding_page.focus_handle(cx));
214
215 cx.notify();
216 };
217 db::write_and_log(cx, || {
218 KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
219 });
220 },
221 )
222}
223
224#[derive(Debug, Clone, Copy, PartialEq, Eq)]
225enum SelectedPage {
226 Basics,
227 Editing,
228 AiSetup,
229}
230
231struct Onboarding {
232 workspace: WeakEntity<Workspace>,
233 focus_handle: FocusHandle,
234 selected_page: SelectedPage,
235 user_store: Entity<UserStore>,
236 _settings_subscription: Subscription,
237}
238
239impl Onboarding {
240 fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
241 cx.new(|cx| Self {
242 workspace: workspace.weak_handle(),
243 focus_handle: cx.focus_handle(),
244 selected_page: SelectedPage::Basics,
245 user_store: workspace.user_store().clone(),
246 _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
247 })
248 }
249
250 fn render_nav_buttons(
251 &mut self,
252 window: &mut Window,
253 cx: &mut Context<Self>,
254 ) -> [impl IntoElement; 3] {
255 let pages = [
256 SelectedPage::Basics,
257 SelectedPage::Editing,
258 SelectedPage::AiSetup,
259 ];
260
261 let text = ["Basics", "Editing", "AI Setup"];
262
263 let actions: [&dyn Action; 3] = [
264 &ActivateBasicsPage,
265 &ActivateEditingPage,
266 &ActivateAISetupPage,
267 ];
268
269 let mut binding = actions.map(|action| {
270 KeyBinding::for_action_in(action, &self.focus_handle, window, cx)
271 .map(|kb| kb.size(rems_from_px(12.)))
272 });
273
274 pages.map(|page| {
275 let i = page as usize;
276 let selected = self.selected_page == page;
277 h_flex()
278 .id(text[i])
279 .relative()
280 .w_full()
281 .gap_2()
282 .px_2()
283 .py_0p5()
284 .justify_between()
285 .rounded_sm()
286 .when(selected, |this| {
287 this.child(
288 div()
289 .h_4()
290 .w_px()
291 .bg(cx.theme().colors().text_accent)
292 .absolute()
293 .left_0(),
294 )
295 })
296 .hover(|style| style.bg(cx.theme().colors().element_hover))
297 .child(Label::new(text[i]).map(|this| {
298 if selected {
299 this.color(Color::Default)
300 } else {
301 this.color(Color::Muted)
302 }
303 }))
304 .child(binding[i].take().map_or(
305 gpui::Empty.into_any_element(),
306 IntoElement::into_any_element,
307 ))
308 .on_click(cx.listener(move |this, _, _, cx| {
309 this.selected_page = page;
310 cx.notify();
311 }))
312 })
313 }
314
315 fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
316 v_flex()
317 .h_full()
318 .w(rems_from_px(220.))
319 .flex_shrink_0()
320 .gap_4()
321 .justify_between()
322 .child(
323 v_flex()
324 .gap_6()
325 .child(
326 h_flex()
327 .px_2()
328 .gap_4()
329 .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
330 .child(
331 v_flex()
332 .child(
333 Headline::new("Welcome to Zed").size(HeadlineSize::Small),
334 )
335 .child(
336 Label::new("The editor for what's next")
337 .color(Color::Muted)
338 .size(LabelSize::Small)
339 .italic(),
340 ),
341 ),
342 )
343 .child(
344 v_flex()
345 .gap_4()
346 .child(
347 v_flex()
348 .py_4()
349 .border_y_1()
350 .border_color(cx.theme().colors().border_variant.opacity(0.5))
351 .gap_1()
352 .children(self.render_nav_buttons(window, cx)),
353 )
354 .child(
355 ButtonLike::new("skip_all")
356 .child(Label::new("Skip All").ml_1())
357 .on_click(|_, _, cx| {
358 with_active_or_new_workspace(
359 cx,
360 |workspace, window, cx| {
361 let Some((onboarding_id, onboarding_idx)) =
362 workspace
363 .active_pane()
364 .read(cx)
365 .items()
366 .enumerate()
367 .find_map(|(idx, item)| {
368 let _ =
369 item.downcast::<Onboarding>()?;
370 Some((item.item_id(), idx))
371 })
372 else {
373 return;
374 };
375
376 workspace.active_pane().update(cx, |pane, cx| {
377 // Get the index here to get around the borrow checker
378 let idx = pane.items().enumerate().find_map(
379 |(idx, item)| {
380 let _ =
381 item.downcast::<WelcomePage>()?;
382 Some(idx)
383 },
384 );
385
386 if let Some(idx) = idx {
387 pane.activate_item(
388 idx, true, true, window, cx,
389 );
390 } else {
391 let item =
392 Box::new(WelcomePage::new(window, cx));
393 pane.add_item(
394 item,
395 true,
396 true,
397 Some(onboarding_idx),
398 window,
399 cx,
400 );
401 }
402
403 pane.remove_item(
404 onboarding_id,
405 false,
406 false,
407 window,
408 cx,
409 );
410 });
411 },
412 );
413 }),
414 ),
415 ),
416 )
417 .child(
418 if let Some(user) = self.user_store.read(cx).current_user() {
419 h_flex()
420 .pl_1p5()
421 .gap_2()
422 .child(Avatar::new(user.avatar_uri.clone()))
423 .child(Label::new(user.github_login.clone()))
424 .into_any_element()
425 } else {
426 Button::new("sign_in", "Sign In")
427 .style(ButtonStyle::Outlined)
428 .full_width()
429 .on_click(|_, window, cx| {
430 let client = Client::global(cx);
431 window
432 .spawn(cx, async move |cx| {
433 client
434 .sign_in_with_optional_connect(true, &cx)
435 .await
436 .notify_async_err(cx);
437 })
438 .detach();
439 })
440 .into_any_element()
441 },
442 )
443 }
444
445 fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
446 match self.selected_page {
447 SelectedPage::Basics => {
448 crate::basics_page::render_basics_page(window, cx).into_any_element()
449 }
450 SelectedPage::Editing => {
451 crate::editing_page::render_editing_page(window, cx).into_any_element()
452 }
453 SelectedPage::AiSetup => {
454 crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
455 }
456 }
457 }
458}
459
460impl Render for Onboarding {
461 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
462 h_flex()
463 .image_cache(gpui::retain_all("onboarding-page"))
464 .key_context({
465 let mut ctx = KeyContext::new_with_defaults();
466 ctx.add("Onboarding");
467 ctx
468 })
469 .track_focus(&self.focus_handle)
470 .size_full()
471 .bg(cx.theme().colors().editor_background)
472 .on_action(cx.listener(|this, _: &ActivateBasicsPage, _, cx| {
473 this.selected_page = SelectedPage::Basics;
474 cx.notify();
475 }))
476 .on_action(cx.listener(|this, _: &ActivateEditingPage, _, cx| {
477 this.selected_page = SelectedPage::Editing;
478 cx.notify();
479 }))
480 .on_action(cx.listener(|this, _: &ActivateAISetupPage, _, cx| {
481 this.selected_page = SelectedPage::AiSetup;
482 cx.notify();
483 }))
484 .child(
485 h_flex()
486 .max_w(rems_from_px(1100.))
487 .size_full()
488 .m_auto()
489 .py_20()
490 .px_12()
491 .items_start()
492 .gap_12()
493 .child(self.render_nav(window, cx))
494 .child(
495 v_flex()
496 .max_w_full()
497 .min_w_0()
498 .pl_12()
499 .border_l_1()
500 .border_color(cx.theme().colors().border_variant.opacity(0.5))
501 .size_full()
502 .child(self.render_page(window, cx)),
503 ),
504 )
505 }
506}
507
508impl EventEmitter<ItemEvent> for Onboarding {}
509
510impl Focusable for Onboarding {
511 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
512 self.focus_handle.clone()
513 }
514}
515
516impl Item for Onboarding {
517 type Event = ItemEvent;
518
519 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
520 "Onboarding".into()
521 }
522
523 fn telemetry_event_text(&self) -> Option<&'static str> {
524 Some("Onboarding Page Opened")
525 }
526
527 fn show_toolbar(&self) -> bool {
528 false
529 }
530
531 fn clone_on_split(
532 &self,
533 _workspace_id: Option<WorkspaceId>,
534 _: &mut Window,
535 cx: &mut Context<Self>,
536 ) -> Option<Entity<Self>> {
537 self.workspace
538 .update(cx, |workspace, cx| Onboarding::new(workspace, cx))
539 .ok()
540 }
541
542 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
543 f(*event)
544 }
545}
546
547pub async fn handle_import_vscode_settings(
548 source: VsCodeSettingsSource,
549 skip_prompt: bool,
550 fs: Arc<dyn Fs>,
551 cx: &mut AsyncWindowContext,
552) {
553 use util::truncate_and_remove_front;
554
555 let vscode_settings =
556 match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
557 Ok(vscode_settings) => vscode_settings,
558 Err(err) => {
559 zlog::error!("{err}");
560 let _ = cx.prompt(
561 gpui::PromptLevel::Info,
562 &format!("Could not find or load a {source} settings file"),
563 None,
564 &["Ok"],
565 );
566 return;
567 }
568 };
569
570 if !skip_prompt {
571 let prompt = cx.prompt(
572 gpui::PromptLevel::Warning,
573 &format!(
574 "Importing {} settings may overwrite your existing settings. \
575 Will import settings from {}",
576 vscode_settings.source,
577 truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
578 ),
579 None,
580 &["Ok", "Cancel"],
581 );
582 let result = cx.spawn(async move |_| prompt.await.ok()).await;
583 if result != Some(0) {
584 return;
585 }
586 };
587
588 cx.update(|_, cx| {
589 let source = vscode_settings.source;
590 let path = vscode_settings.path.clone();
591 cx.global::<SettingsStore>()
592 .import_vscode_settings(fs, vscode_settings);
593 zlog::info!("Imported {source} settings from {}", path.display());
594 })
595 .ok();
596}