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