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