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, 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 user_store: Entity<UserStore>,
224 _settings_subscription: Subscription,
225}
226
227impl Onboarding {
228 fn new(workspace: &Workspace, cx: &mut App) -> Entity<Self> {
229 cx.new(|cx| Self {
230 workspace: workspace.weak_handle(),
231 focus_handle: cx.focus_handle(),
232 selected_page: SelectedPage::Basics,
233 user_store: workspace.user_store().clone(),
234 _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
235 })
236 }
237
238 fn render_nav_button(
239 &mut self,
240 page: SelectedPage,
241 _: &mut Window,
242 cx: &mut Context<Self>,
243 ) -> impl IntoElement {
244 let text = match page {
245 SelectedPage::Basics => "Basics",
246 SelectedPage::Editing => "Editing",
247 SelectedPage::AiSetup => "AI Setup",
248 };
249
250 let binding = match page {
251 SelectedPage::Basics => {
252 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-1").unwrap()], cx)
253 .map(|kb| kb.size(rems_from_px(12.)))
254 }
255 SelectedPage::Editing => {
256 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-2").unwrap()], cx)
257 .map(|kb| kb.size(rems_from_px(12.)))
258 }
259 SelectedPage::AiSetup => {
260 KeyBinding::new(vec![gpui::Keystroke::parse("cmd-3").unwrap()], cx)
261 .map(|kb| kb.size(rems_from_px(12.)))
262 }
263 };
264
265 let selected = self.selected_page == page;
266
267 h_flex()
268 .id(text)
269 .relative()
270 .w_full()
271 .gap_2()
272 .px_2()
273 .py_0p5()
274 .justify_between()
275 .rounded_sm()
276 .when(selected, |this| {
277 this.child(
278 div()
279 .h_4()
280 .w_px()
281 .bg(cx.theme().colors().text_accent)
282 .absolute()
283 .left_0(),
284 )
285 })
286 .hover(|style| style.bg(cx.theme().colors().element_hover))
287 .child(Label::new(text).map(|this| {
288 if selected {
289 this.color(Color::Default)
290 } else {
291 this.color(Color::Muted)
292 }
293 }))
294 .child(binding)
295 .on_click(cx.listener(move |this, _, _, cx| {
296 this.selected_page = page;
297 cx.notify();
298 }))
299 }
300
301 fn render_nav(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
302 v_flex()
303 .h_full()
304 .w(rems_from_px(220.))
305 .flex_shrink_0()
306 .gap_4()
307 .justify_between()
308 .child(
309 v_flex()
310 .gap_6()
311 .child(
312 h_flex()
313 .px_2()
314 .gap_4()
315 .child(Vector::square(VectorName::ZedLogo, rems(2.5)))
316 .child(
317 v_flex()
318 .child(
319 Headline::new("Welcome to Zed").size(HeadlineSize::Small),
320 )
321 .child(
322 Label::new("The editor for what's next")
323 .color(Color::Muted)
324 .size(LabelSize::Small)
325 .italic(),
326 ),
327 ),
328 )
329 .child(
330 v_flex()
331 .gap_4()
332 .child(
333 v_flex()
334 .py_4()
335 .border_y_1()
336 .border_color(cx.theme().colors().border_variant.opacity(0.5))
337 .gap_1()
338 .children([
339 self.render_nav_button(SelectedPage::Basics, window, cx)
340 .into_element(),
341 self.render_nav_button(SelectedPage::Editing, window, cx)
342 .into_element(),
343 self.render_nav_button(SelectedPage::AiSetup, window, cx)
344 .into_element(),
345 ]),
346 )
347 .child(Button::new("skip_all", "Skip All")),
348 ),
349 )
350 .child(
351 if let Some(user) = self.user_store.read(cx).current_user() {
352 h_flex()
353 .gap_2()
354 .child(Avatar::new(user.avatar_uri.clone()))
355 .child(Label::new(user.github_login.clone()))
356 .into_any_element()
357 } else {
358 Button::new("sign_in", "Sign In")
359 .style(ButtonStyle::Outlined)
360 .full_width()
361 .on_click(|_, window, cx| {
362 let client = Client::global(cx);
363 window
364 .spawn(cx, async move |cx| {
365 client
366 .sign_in_with_optional_connect(true, &cx)
367 .await
368 .notify_async_err(cx);
369 })
370 .detach();
371 })
372 .into_any_element()
373 },
374 )
375 }
376
377 fn render_page(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
378 match self.selected_page {
379 SelectedPage::Basics => {
380 crate::basics_page::render_basics_page(window, cx).into_any_element()
381 }
382 SelectedPage::Editing => {
383 crate::editing_page::render_editing_page(window, cx).into_any_element()
384 }
385 SelectedPage::AiSetup => {
386 crate::ai_setup_page::render_ai_setup_page(&self, window, cx).into_any_element()
387 }
388 }
389 }
390}
391
392impl Render for Onboarding {
393 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
394 h_flex()
395 .image_cache(gpui::retain_all("onboarding-page"))
396 .key_context("onboarding-page")
397 .size_full()
398 .bg(cx.theme().colors().editor_background)
399 .child(
400 h_flex()
401 .max_w(rems_from_px(1100.))
402 .size_full()
403 .m_auto()
404 .py_20()
405 .px_12()
406 .items_start()
407 .gap_12()
408 .child(self.render_nav(window, cx))
409 .child(
410 v_flex()
411 .max_w_full()
412 .min_w_0()
413 .pl_12()
414 .border_l_1()
415 .border_color(cx.theme().colors().border_variant.opacity(0.5))
416 .size_full()
417 .child(self.render_page(window, cx)),
418 ),
419 )
420 }
421}
422
423impl EventEmitter<ItemEvent> for Onboarding {}
424
425impl Focusable for Onboarding {
426 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
427 self.focus_handle.clone()
428 }
429}
430
431impl Item for Onboarding {
432 type Event = ItemEvent;
433
434 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
435 "Onboarding".into()
436 }
437
438 fn telemetry_event_text(&self) -> Option<&'static str> {
439 Some("Onboarding Page Opened")
440 }
441
442 fn show_toolbar(&self) -> bool {
443 false
444 }
445
446 fn clone_on_split(
447 &self,
448 _workspace_id: Option<WorkspaceId>,
449 _: &mut Window,
450 cx: &mut Context<Self>,
451 ) -> Option<Entity<Self>> {
452 self.workspace
453 .update(cx, |workspace, cx| Onboarding::new(workspace, cx))
454 .ok()
455 }
456
457 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
458 f(*event)
459 }
460}
461
462pub async fn handle_import_vscode_settings(
463 source: VsCodeSettingsSource,
464 skip_prompt: bool,
465 fs: Arc<dyn Fs>,
466 cx: &mut AsyncWindowContext,
467) {
468 use util::truncate_and_remove_front;
469
470 let vscode_settings =
471 match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await {
472 Ok(vscode_settings) => vscode_settings,
473 Err(err) => {
474 zlog::error!("{err}");
475 let _ = cx.prompt(
476 gpui::PromptLevel::Info,
477 &format!("Could not find or load a {source} settings file"),
478 None,
479 &["Ok"],
480 );
481 return;
482 }
483 };
484
485 if !skip_prompt {
486 let prompt = cx.prompt(
487 gpui::PromptLevel::Warning,
488 &format!(
489 "Importing {} settings may overwrite your existing settings. \
490 Will import settings from {}",
491 vscode_settings.source,
492 truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128),
493 ),
494 None,
495 &["Ok", "Cancel"],
496 );
497 let result = cx.spawn(async move |_| prompt.await.ok()).await;
498 if result != Some(0) {
499 return;
500 }
501 };
502
503 cx.update(|_, cx| {
504 let source = vscode_settings.source;
505 let path = vscode_settings.path.clone();
506 cx.global::<SettingsStore>()
507 .import_vscode_settings(fs, vscode_settings);
508 zlog::info!("Imported {source} settings from {}", path.display());
509 })
510 .ok();
511}