1use client::{TelemetrySettings, telemetry::Telemetry};
2use db::kvp::KEY_VALUE_STORE;
3use gpui::{
4 Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement,
5 ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, actions, svg,
6};
7use language::language_settings::{EditPredictionProvider, all_language_settings};
8use settings::{Settings, SettingsStore};
9use std::sync::Arc;
10use ui::{CheckboxWithLabel, ElevationIndex, Tooltip, prelude::*};
11use util::ResultExt;
12use vim_mode_setting::VimModeSetting;
13use workspace::{
14 AppState, Welcome, Workspace, WorkspaceId,
15 dock::DockPosition,
16 item::{Item, ItemEvent},
17 open_new,
18};
19
20pub use base_keymap_setting::BaseKeymap;
21pub use multibuffer_hint::*;
22
23mod base_keymap_picker;
24mod base_keymap_setting;
25mod multibuffer_hint;
26mod welcome_ui;
27
28actions!(welcome, [ResetHints]);
29
30pub const FIRST_OPEN: &str = "first_open";
31pub const DOCS_URL: &str = "https://zed.dev/docs/";
32const BOOK_ONBOARDING: &str = "https://dub.sh/zed-c-onboarding";
33
34pub fn init(cx: &mut App) {
35 BaseKeymap::register(cx);
36
37 cx.observe_new(|workspace: &mut Workspace, _, _cx| {
38 workspace.register_action(|workspace, _: &Welcome, window, cx| {
39 let welcome_page = WelcomePage::new(workspace, cx);
40 workspace.add_item_to_active_pane(Box::new(welcome_page), None, true, window, cx)
41 });
42 workspace
43 .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx));
44 })
45 .detach();
46
47 base_keymap_picker::init(cx);
48}
49
50pub fn show_welcome_view(app_state: Arc<AppState>, cx: &mut App) -> Task<anyhow::Result<()>> {
51 open_new(
52 Default::default(),
53 app_state,
54 cx,
55 |workspace, window, cx| {
56 workspace.toggle_dock(DockPosition::Left, window, cx);
57 let welcome_page = WelcomePage::new(workspace, cx);
58 workspace.add_item_to_center(Box::new(welcome_page.clone()), window, cx);
59
60 window.focus(&welcome_page.focus_handle(cx));
61
62 cx.notify();
63
64 db::write_and_log(cx, || {
65 KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
66 });
67 },
68 )
69}
70
71pub struct WelcomePage {
72 workspace: WeakEntity<Workspace>,
73 focus_handle: FocusHandle,
74 telemetry: Arc<Telemetry>,
75 _settings_subscription: Subscription,
76}
77
78impl Render for WelcomePage {
79 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
80 let edit_prediction_provider_is_zed =
81 all_language_settings(None, cx).edit_predictions.provider
82 == EditPredictionProvider::Zed;
83
84 let edit_prediction_label = if edit_prediction_provider_is_zed {
85 "Edit Prediction Enabled"
86 } else {
87 "Try Edit Prediction"
88 };
89
90 h_flex()
91 .size_full()
92 .bg(cx.theme().colors().editor_background)
93 .key_context("Welcome")
94 .track_focus(&self.focus_handle(cx))
95 .child(
96 v_flex()
97 .gap_8()
98 .mx_auto()
99 .child(
100 v_flex()
101 .w_full()
102 .child(
103 svg()
104 .path("icons/logo_96.svg")
105 .text_color(cx.theme().colors().icon_disabled)
106 .w(px(40.))
107 .h(px(40.))
108 .mx_auto()
109 .mb_4(),
110 )
111 .child(
112 h_flex()
113 .w_full()
114 .justify_center()
115 .child(Headline::new("Welcome to Zed")),
116 )
117 .child(
118 h_flex().w_full().justify_center().child(
119 Label::new("The editor for what's next")
120 .color(Color::Muted)
121 .italic(),
122 ),
123 ),
124 )
125 .child(
126 h_flex()
127 .items_start()
128 .gap_8()
129 .child(
130 v_flex()
131 .gap_2()
132 .pr_8()
133 .border_r_1()
134 .border_color(cx.theme().colors().border_variant)
135 .child(
136 self.section_label( cx).child(
137 Label::new("Get Started")
138 .size(LabelSize::XSmall)
139 .color(Color::Muted),
140 ),
141 )
142 .child(
143 Button::new("choose-theme", "Choose a Theme")
144 .icon(IconName::SwatchBook)
145 .icon_size(IconSize::XSmall)
146 .icon_color(Color::Muted)
147 .icon_position(IconPosition::Start)
148 .on_click(cx.listener(|this, _, window, cx| {
149 telemetry::event!("Welcome Theme Changed");
150 this.workspace
151 .update(cx, |_workspace, cx| {
152 window.dispatch_action(zed_actions::theme_selector::Toggle::default().boxed_clone(), cx);
153 })
154 .ok();
155 })),
156 )
157 .child(
158 Button::new("choose-keymap", "Choose a Keymap")
159 .icon(IconName::Keyboard)
160 .icon_size(IconSize::XSmall)
161 .icon_color(Color::Muted)
162 .icon_position(IconPosition::Start)
163 .on_click(cx.listener(|this, _, window, cx| {
164 telemetry::event!("Welcome Keymap Changed");
165 this.workspace
166 .update(cx, |workspace, cx| {
167 base_keymap_picker::toggle(
168 workspace,
169 &Default::default(),
170 window, cx,
171 )
172 })
173 .ok();
174 })),
175 )
176 .child(
177 Button::new(
178 "try-zed-edit-prediction",
179 edit_prediction_label,
180 )
181 .disabled(edit_prediction_provider_is_zed)
182 .icon(IconName::ZedPredict)
183 .icon_size(IconSize::XSmall)
184 .icon_color(Color::Muted)
185 .icon_position(IconPosition::Start)
186 .on_click(
187 cx.listener(|_, _, window, cx| {
188 telemetry::event!("Welcome Screen Try Edit Prediction clicked");
189 window.dispatch_action(zed_actions::OpenZedPredictOnboarding.boxed_clone(), cx);
190 }),
191 ),
192 )
193 .child(
194 Button::new("edit settings", "Edit Settings")
195 .icon(IconName::Settings)
196 .icon_size(IconSize::XSmall)
197 .icon_color(Color::Muted)
198 .icon_position(IconPosition::Start)
199 .on_click(cx.listener(|_, _, window, cx| {
200 telemetry::event!("Welcome Settings Edited");
201 window.dispatch_action(Box::new(
202 zed_actions::OpenSettings,
203 ), cx);
204 })),
205 )
206
207 )
208 .child(
209 v_flex()
210 .gap_2()
211 .child(
212 self.section_label(cx).child(
213 Label::new("Resources")
214 .size(LabelSize::XSmall)
215 .color(Color::Muted),
216 ),
217 )
218 .when(cfg!(target_os = "macos"), |el| {
219 el.child(
220 Button::new("install-cli", "Install the CLI")
221 .icon(IconName::Terminal)
222 .icon_size(IconSize::XSmall)
223 .icon_color(Color::Muted)
224 .icon_position(IconPosition::Start)
225 .on_click(cx.listener(|this, _, window, cx| {
226 telemetry::event!("Welcome CLI Installed");
227 this.workspace.update(cx, |_, cx|{
228 install_cli::install_cli(window, cx);
229 }).log_err();
230 })),
231 )
232 })
233 .child(
234 Button::new("view-docs", "View Documentation")
235 .icon(IconName::FileCode)
236 .icon_size(IconSize::XSmall)
237 .icon_color(Color::Muted)
238 .icon_position(IconPosition::Start)
239 .on_click(cx.listener(|_, _, _, cx| {
240 telemetry::event!("Welcome Documentation Viewed");
241 cx.open_url(DOCS_URL);
242 })),
243 )
244 .child(
245 Button::new("explore-extensions", "Explore Extensions")
246 .icon(IconName::Blocks)
247 .icon_size(IconSize::XSmall)
248 .icon_color(Color::Muted)
249 .icon_position(IconPosition::Start)
250 .on_click(cx.listener(|_, _, window, cx| {
251 telemetry::event!("Welcome Extensions Page Opened");
252 window.dispatch_action(Box::new(
253 zed_actions::Extensions::default(),
254 ), cx);
255 })),
256 )
257 .child(
258 Button::new("book-onboarding", "Book Onboarding")
259 .icon(IconName::PhoneIncoming)
260 .icon_size(IconSize::XSmall)
261 .icon_color(Color::Muted)
262 .icon_position(IconPosition::Start)
263 .on_click(cx.listener(|_, _, _, cx| {
264 cx.open_url(BOOK_ONBOARDING);
265 })),
266 ),
267 ),
268 )
269 .child(
270 v_container()
271 .px_2()
272 .gap_2()
273 .child(
274 h_flex()
275 .justify_between()
276 .child(
277 CheckboxWithLabel::new(
278 "enable-vim",
279 Label::new("Enable Vim Mode"),
280 if VimModeSetting::get_global(cx).0 {
281 ui::ToggleState::Selected
282 } else {
283 ui::ToggleState::Unselected
284 },
285 cx.listener(move |this, selection, _window, cx| {
286 telemetry::event!("Welcome Vim Mode Toggled");
287 this.update_settings::<VimModeSetting>(
288 selection,
289 cx,
290 |setting, value| *setting = Some(value),
291 );
292 }),
293 )
294 .fill()
295 .elevation(ElevationIndex::ElevatedSurface),
296 )
297 .child(
298 IconButton::new("vim-mode", IconName::Info)
299 .icon_size(IconSize::XSmall)
300 .icon_color(Color::Muted)
301 .tooltip(
302 Tooltip::text(
303 "You can also toggle Vim Mode via the command palette or Editor Controls menu.")
304 ),
305 ),
306 )
307 .child(
308 CheckboxWithLabel::new(
309 "enable-crash",
310 Label::new("Send Crash Reports"),
311 if TelemetrySettings::get_global(cx).diagnostics {
312 ui::ToggleState::Selected
313 } else {
314 ui::ToggleState::Unselected
315 },
316 cx.listener(move |this, selection, _window, cx| {
317 telemetry::event!("Welcome Diagnostic Telemetry Toggled");
318 this.update_settings::<TelemetrySettings>(selection, cx, {
319 move |settings, value| {
320 settings.diagnostics = Some(value);
321 telemetry::event!(
322 "Settings Changed",
323 setting = "diagnostic telemetry",
324 value
325 );
326 }
327 });
328 }),
329 )
330 .fill()
331 .elevation(ElevationIndex::ElevatedSurface),
332 )
333 .child(
334 CheckboxWithLabel::new(
335 "enable-telemetry",
336 Label::new("Send Telemetry"),
337 if TelemetrySettings::get_global(cx).metrics {
338 ui::ToggleState::Selected
339 } else {
340 ui::ToggleState::Unselected
341 },
342 cx.listener(move |this, selection, _window, cx| {
343 telemetry::event!("Welcome Metric Telemetry Toggled");
344 this.update_settings::<TelemetrySettings>(selection, cx, {
345 move |settings, value| {
346 settings.metrics = Some(value);
347 telemetry::event!(
348 "Settings Changed",
349 setting = "metric telemetry",
350 value
351 );
352 }
353 });
354 }),
355 )
356 .fill()
357 .elevation(ElevationIndex::ElevatedSurface),
358 ),
359 ),
360 )
361 }
362}
363
364impl WelcomePage {
365 pub fn new(workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
366 let this = cx.new(|cx| {
367 cx.on_release(|_: &mut Self, _| {
368 telemetry::event!("Welcome Page Closed");
369 })
370 .detach();
371
372 WelcomePage {
373 focus_handle: cx.focus_handle(),
374 workspace: workspace.weak_handle(),
375 telemetry: workspace.client().telemetry().clone(),
376 _settings_subscription: cx
377 .observe_global::<SettingsStore>(move |_, cx| cx.notify()),
378 }
379 });
380
381 this
382 }
383
384 fn section_label(&self, cx: &mut App) -> Div {
385 div()
386 .pl_1()
387 .font_buffer(cx)
388 .text_color(Color::Muted.color(cx))
389 }
390
391 fn update_settings<T: Settings>(
392 &mut self,
393 selection: &ToggleState,
394 cx: &mut Context<Self>,
395 callback: impl 'static + Send + Fn(&mut T::FileContent, bool),
396 ) {
397 if let Some(workspace) = self.workspace.upgrade() {
398 let fs = workspace.read(cx).app_state().fs.clone();
399 let selection = *selection;
400 settings::update_settings_file::<T>(fs, cx, move |settings, _| {
401 let value = match selection {
402 ToggleState::Unselected => false,
403 ToggleState::Selected => true,
404 _ => return,
405 };
406
407 callback(settings, value)
408 });
409 }
410 }
411}
412
413impl EventEmitter<ItemEvent> for WelcomePage {}
414
415impl Focusable for WelcomePage {
416 fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
417 self.focus_handle.clone()
418 }
419}
420
421impl Item for WelcomePage {
422 type Event = ItemEvent;
423
424 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
425 "Welcome".into()
426 }
427
428 fn telemetry_event_text(&self) -> Option<&'static str> {
429 Some("Welcome Page Opened")
430 }
431
432 fn show_toolbar(&self) -> bool {
433 false
434 }
435
436 fn clone_on_split(
437 &self,
438 _workspace_id: Option<WorkspaceId>,
439 _: &mut Window,
440 cx: &mut Context<Self>,
441 ) -> Option<Entity<Self>> {
442 Some(cx.new(|cx| WelcomePage {
443 focus_handle: cx.focus_handle(),
444 workspace: self.workspace.clone(),
445 telemetry: self.telemetry.clone(),
446 _settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
447 }))
448 }
449
450 fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
451 f(*event)
452 }
453}