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