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