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