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