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