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