1use crate::welcome::{ShowWelcome, WelcomePage};
2use client::{Client, CloudUserStore, 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, Render, SharedString, Subscription, Task, WeakEntity,
10 Window, actions,
11};
12use schemars::JsonSchema;
13use serde::Deserialize;
14use settings::{SettingsStore, VsCodeSettingsSource};
15use std::sync::Arc;
16use ui::{
17 Avatar, FluentBuilder, Headline, KeyBinding, ParentElement as _, StatefulInteractiveElement,
18 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
68pub fn init(cx: &mut App) {
69 cx.on_action(|_: &OpenOnboarding, cx| {
70 with_active_or_new_workspace(cx, |workspace, window, cx| {
71 workspace
72 .with_local_workspace(window, cx, |workspace, window, cx| {
73 let existing = workspace
74 .active_pane()
75 .read(cx)
76 .items()
77 .find_map(|item| item.downcast::<Onboarding>());
78
79 if let Some(existing) = existing {
80 workspace.activate_item(&existing, true, true, window, cx);
81 } else {
82 let settings_page = Onboarding::new(workspace, cx);
83 workspace.add_item_to_active_pane(
84 Box::new(settings_page),
85 None,
86 true,
87 window,
88 cx,
89 )
90 }
91 })
92 .detach();
93 });
94 });
95
96 cx.on_action(|_: &ShowWelcome, cx| {
97 with_active_or_new_workspace(cx, |workspace, window, cx| {
98 workspace
99 .with_local_workspace(window, cx, |workspace, window, cx| {
100 let existing = workspace
101 .active_pane()
102 .read(cx)
103 .items()
104 .find_map(|item| item.downcast::<WelcomePage>());
105
106 if let Some(existing) = existing {
107 workspace.activate_item(&existing, true, true, window, cx);
108 } else {
109 let settings_page = WelcomePage::new(window, cx);
110 workspace.add_item_to_active_pane(
111 Box::new(settings_page),
112 None,
113 true,
114 window,
115 cx,
116 )
117 }
118 })
119 .detach();
120 });
121 });
122
123 cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
124 workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| {
125 let fs = <dyn Fs>::global(cx);
126 let action = *action;
127
128 window
129 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
130 handle_import_vscode_settings(
131 VsCodeSettingsSource::VsCode,
132 action.skip_prompt,
133 fs,
134 cx,
135 )
136 .await
137 })
138 .detach();
139 });
140
141 workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| {
142 let fs = <dyn Fs>::global(cx);
143 let action = *action;
144
145 window
146 .spawn(cx, async move |cx: &mut AsyncWindowContext| {
147 handle_import_vscode_settings(
148 VsCodeSettingsSource::Cursor,
149 action.skip_prompt,
150 fs,
151 cx,
152 )
153 .await
154 })
155 .detach();
156 });
157 })
158 .detach();
159
160 cx.observe_new::<Workspace>(|_, window, cx| {
161 let Some(window) = window else {
162 return;
163 };
164
165 let onboarding_actions = [
166 std::any::TypeId::of::<OpenOnboarding>(),
167 std::any::TypeId::of::<ShowWelcome>(),
168 ];
169
170 CommandPaletteFilter::update_global(cx, |filter, _cx| {
171 filter.hide_action_types(&onboarding_actions);
172 });
173
174 cx.observe_flag::<OnBoardingFeatureFlag, _>(window, move |is_enabled, _, _, cx| {
175 if is_enabled {
176 CommandPaletteFilter::update_global(cx, |filter, _cx| {
177 filter.show_action_types(onboarding_actions.iter());
178 });
179 } else {
180 CommandPaletteFilter::update_global(cx, |filter, _cx| {
181 filter.hide_action_types(&onboarding_actions);
182 });
183 }
184 })
185 .detach();
186 })
187 .detach();
188}
189
190pub fn show_onboarding_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
191 open_new(
192 Default::default(),
193 app_state,
194 cx,
195 |workspace, window, cx| {
196 {
197 workspace.toggle_dock(DockPosition::Left, window, cx);
198 let onboarding_page = Onboarding::new(workspace, cx);
199 workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx);
200
201 window.focus(&onboarding_page.focus_handle(cx));
202
203 cx.notify();
204 };
205 db::write_and_log(cx, || {
206 KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
207 });
208 },
209 )
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq)]
213enum SelectedPage {
214 Basics,
215 Editing,
216 AiSetup,
217}
218
219struct Onboarding {
220 workspace: WeakEntity<Workspace>,
221 focus_handle: FocusHandle,
222 selected_page: SelectedPage,
223 cloud_user_store: Entity<CloudUserStore>,
224 user_store: Entity<UserStore>,
225 _settings_subscription: Subscription,
226}
227
228impl Onboarding {
229 fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
230 cx.new(|cx| Self {
231 workspace: workspace.weak_handle(),
232 focus_handle: cx.focus_handle(),
233 selected_page: SelectedPage::Basics,
234 cloud_user_store: workspace.app_state().cloud_user_store.clone(),
235 user_store: workspace.user_store().clone(),
236 _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
237 })
238 }
239
240 fn render_nav_button(
241 &mut self,
242 page: SelectedPage,
243 _: &mut Window,
244 cx: &mut Context<Self>,
245 ) -> impl IntoElement {
246 let text = match page {
247 SelectedPage::Basics => "Basics",
248 SelectedPage::Editing => "Editing",
249 SelectedPage::AiSetup => "AI Setup",
250 };
251
252 let binding = match page {
253 SelectedPage::Basics => {
254 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
255 .map(|kb| kb.size(rems_from_px(12.)))
256 }
257 SelectedPage::Editing => {
258 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
259 .map(|kb| kb.size(rems_from_px(12.)))
260 }
261 SelectedPage::AiSetup => {
262 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
263 .map(|kb| kb.size(rems_from_px(12.)))
264 }
265 };
266
267 let selected = self.selected_page == page;
268
269 h_flex()
270 .id(text)
271 .relative()
272 .w_full()
273 .gap_2()
274 .px_2()
275 .py_0p5()
276 .justify_between()
277 .rounded_sm()
278 .when(selected, |this| {
279 this.child(
280 div()
281 .h_4()
282 .w_px()
283 .bg(cx.theme().colors().text_accent)
284 .absolute()
285 .left_0(),
286 )
287 })
288 .hover(|style| style.bg(cx.theme().colors().element_hover))
289 .child(Label::new(text).map(|this| {
290 if selected {
291 this.color(Color::Default)
292 } else {
293 this.color(Color::Muted)
294 }
295 }))
296 .child(binding)
297 .on_click(cx.listener(move |this, _, _, cx| {
298 this.selected_page = page;
299 cx.notify();
300 }))
301 }
302
303 fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
304 v_flex()
305 .h_full()
306 .w(rems_from_px(220.))
307 .flex_shrink_0()
308 .gap_4()
309 .justify_between()
310 .child(
311 v_flex()
312 .gap_6()
313 .child(
314 h_flex()
315 .px_2()
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").size(HeadlineSize::Small),
322 )
323 .child(
324 Label::new("The editor for what's next")
325 .color(Color::Muted)
326 .size(LabelSize::Small)
327 .italic(),
328 ),
329 ),
330 )
331 .child(
332 v_flex()
333 .gap_4()
334 .child(
335 v_flex()
336 .py_4()
337 .border_y_1()
338 .border_color(cx.theme().colors().border_variant.opacity(0.5))
339 .gap_1()
340 .children([
341 self.render_nav_button(SelectedPage::Basics, window, cx)
342 .into_element(),
343 self.render_nav_button(SelectedPage::Editing, window, cx)
344 .into_element(),
345 self.render_nav_button(SelectedPage::AiSetup, window, cx)
346 .into_element(),
347 ]),
348 )
349 .child(Button::new("skip_all", "Skip All")),
350 ),
351 )
352 .child(
353 if let Some(user) = self.user_store.read(cx).current_user() {
354 h_flex()
355 .gap_2()
356 .child(Avatar::new(user.avatar_uri.clone()))
357 .child(Label::new(user.github_login.clone()))
358 .into_any_element()
359 } else {
360 Button::new("sign_in", "Sign In")
361 .style(ButtonStyle::Outlined)
362 .full_width()
363 .on_click(|_, window, cx| {
364 let client = Client::global(cx);
365 window
366 .spawn(cx, async move |cx| {
367 client
368 .authenticate_and_connect(true, &cx)
369 .await
370 .into_response()
371 .notify_async_err(cx);
372 })
373 .detach();
374 })
375 .into_any_element()
376 },
377 )
378 }
379
380 fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
381 match self.selected_page {
382 SelectedPage::Basics => {
383 crate::basics_page::render_basics_page(window, cx).into_any_element()
384 }
385 SelectedPage::Editing => {
386 crate::editing_page::render_editing_page(window, cx).into_any_element()
387 }
388 SelectedPage::AiSetup => {
389 crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
390 }
391 }
392 }
393}
394
395impl Render for Onboarding {
396 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
397 h_flex()
398 .image_cache(gpui::retain_all("onboarding-page"))
399 .key_context("onboarding-page")
400 .size_full()
401 .bg(cx.theme().colors().editor_background)
402 .child(
403 h_flex()
404 .max_w(rems_from_px(1100.))
405 .size_full()
406 .m_auto()
407 .py_20()
408 .px_12()
409 .items_start()
410 .gap_12()
411 .child(self.render_nav(window, cx))
412 .child(
413 v_flex()
414 .max_w_full()
415 .min_w_0()
416 .pl_12()
417 .border_l_1()
418 .border_color(cx.theme().colors().border_variant.opacity(0.5))
419 .size_full()
420 .child(self.render_page(window, cx)),
421 ),
422 )
423 }
424}
425
426impl EventEmitter<ItemEvent> for Onboarding {}
427
428impl Focusable for Onboarding {
429 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
430 self.focus_handle.clone()
431 }
432}
433
434impl Item for Onboarding {
435 type Event = ItemEvent;
436
437 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
438 "Onboarding".into()
439 }
440
441 fn telemetry_event_text(&self) -> Option<&'static str> {
442 Some("Onboarding Page Opened")
443 }
444
445 fn show_toolbar(&self) -> bool {
446 false
447 }
448
449 fn clone_on_split(
450 &self,
451 _workspace_id: Option<WorkspaceId>,
452 _: &mut Window,
453 cx: &mut Context<Self>,
454 ) -> Option<Entity<Self>> {
455 self.workspace
456 .update(cx, |workspace, cx| Onboarding::new(workspace, cx))
457 .ok()
458 }
459
460 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
461 f(*event)
462 }
463}
464
465pub async fn handle_import_vscode_settings(
466 source: VsCodeSettingsSource,
467 skip_prompt: bool,
468 fs: Arc<dyn Fs>,
469 cx: &mut AsyncWindowContext,
470) {
471 use util::truncate_and_remove_front;
472
473 let vscode_settings =
474 match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
475 Ok(vscode_settings) => vscode_settings,
476 Err(err) => {
477 zlog::error!("{err}");
478 let _ = cx.prompt(
479 gpui::PromptLevel::Info,
480 &format!("Could not find or load a {source} settings file"),
481 None,
482 &["Ok"],
483 );
484 return;
485 }
486 };
487
488 if !skip_prompt {
489 let prompt = cx.prompt(
490 gpui::PromptLevel::Warning,
491 &format!(
492 "Importing {} settings may overwrite your existing settings. \
493 Will import settings from {}",
494 vscode_settings.source,
495 truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
496 ),
497 None,
498 &["Ok", "Cancel"],
499 );
500 let result = cx.spawn(async move |_| prompt.await.ok()).await;
501 if result != Some(0) {
502 return;
503 }
504 };
505
506 cx.update(|_, cx| {
507 let source = vscode_settings.source;
508 let path = vscode_settings.path.clone();
509 cx.global::<SettingsStore>()
510 .import_vscode_settings(fs, vscode_settings);
511 zlog::info!("Imported {source} settings from {}", path.display());
512 })
513 .ok();
514}