1use std::sync::Arc;
2
3use editor::{EditorSettings, ShowMinimap};
4use fs::Fs;
5use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
6use language::language_settings::{AllLanguageSettings, FormatOnSave};
7use project::project_settings::ProjectSettings;
8use settings::{Settings as _, update_settings_file};
9use theme::{FontFamilyName, ThemeSettings};
10use ui::{
11 ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
12 ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
13};
14use ui_input::{NumericStepper, font_picker};
15
16use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
17
18fn read_show_mini_map(cx: &App) -> ShowMinimap {
19 editor::EditorSettings::get_global(cx).minimap.show
20}
21
22fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
23 let fs = <dyn Fs>::global(cx);
24
25 // This is used to speed up the UI
26 // the UI reads the current values to get what toggle state to show on buttons
27 // there's a slight delay if we just call update_settings_file so we manually set
28 // the value here then call update_settings file to get around the delay
29 let mut curr_settings = EditorSettings::get_global(cx).clone();
30 curr_settings.minimap.show = show;
31 EditorSettings::override_global(curr_settings, cx);
32
33 update_settings_file(fs, cx, move |settings, _| {
34 telemetry::event!(
35 "Welcome Minimap Clicked",
36 from = settings.editor.minimap.clone().unwrap_or_default(),
37 to = show
38 );
39 settings.editor.minimap.get_or_insert_default().show = Some(show);
40 });
41}
42
43fn read_inlay_hints(cx: &App) -> bool {
44 AllLanguageSettings::get_global(cx)
45 .defaults
46 .inlay_hints
47 .enabled
48}
49
50fn write_inlay_hints(enabled: bool, cx: &mut App) {
51 let fs = <dyn Fs>::global(cx);
52
53 let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
54 curr_settings.defaults.inlay_hints.enabled = enabled;
55 AllLanguageSettings::override_global(curr_settings, cx);
56
57 update_settings_file(fs, cx, move |settings, _cx| {
58 settings
59 .project
60 .all_languages
61 .defaults
62 .inlay_hints
63 .get_or_insert_default()
64 .enabled = Some(enabled);
65 });
66}
67
68fn read_git_blame(cx: &App) -> bool {
69 ProjectSettings::get_global(cx).git.inline_blame.enabled
70}
71
72fn write_git_blame(enabled: bool, cx: &mut App) {
73 let fs = <dyn Fs>::global(cx);
74
75 let mut curr_settings = ProjectSettings::get_global(cx).clone();
76 curr_settings.git.inline_blame.enabled = enabled;
77 ProjectSettings::override_global(curr_settings, cx);
78
79 update_settings_file(fs, cx, move |settings, _| {
80 settings
81 .git
82 .get_or_insert_default()
83 .inline_blame
84 .get_or_insert_default()
85 .enabled = Some(enabled);
86 });
87}
88
89fn write_ui_font_family(font: SharedString, cx: &mut App) {
90 let fs = <dyn Fs>::global(cx);
91
92 update_settings_file(fs, cx, move |settings, _| {
93 telemetry::event!(
94 "Welcome Font Changed",
95 type = "ui font",
96 old = settings.theme.ui_font_family,
97 new = font
98 );
99 settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
100 });
101}
102
103fn write_ui_font_size(size: Pixels, cx: &mut App) {
104 let fs = <dyn Fs>::global(cx);
105
106 update_settings_file(fs, cx, move |settings, _| {
107 settings.theme.ui_font_size = Some(size.into());
108 });
109}
110
111fn write_buffer_font_size(size: Pixels, cx: &mut App) {
112 let fs = <dyn Fs>::global(cx);
113
114 update_settings_file(fs, cx, move |settings, _| {
115 settings.theme.buffer_font_size = Some(size.into());
116 });
117}
118
119fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
120 let fs = <dyn Fs>::global(cx);
121
122 update_settings_file(fs, cx, move |settings, _| {
123 telemetry::event!(
124 "Welcome Font Changed",
125 type = "editor font",
126 old = settings.theme.buffer_font_family,
127 new = font_family
128 );
129
130 settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
131 });
132}
133
134fn read_font_ligatures(cx: &App) -> bool {
135 ThemeSettings::get_global(cx)
136 .buffer_font
137 .features
138 .is_calt_enabled()
139 .unwrap_or(true)
140}
141
142fn write_font_ligatures(enabled: bool, cx: &mut App) {
143 let fs = <dyn Fs>::global(cx);
144 let bit = if enabled { 1 } else { 0 };
145
146 update_settings_file(fs, cx, move |settings, _| {
147 let mut features = settings
148 .theme
149 .buffer_font_features
150 .as_mut()
151 .map(|features| features.tag_value_list().to_vec())
152 .unwrap_or_default();
153
154 if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
155 features[calt_index].1 = bit;
156 } else {
157 features.push(("calt".into(), bit));
158 }
159
160 settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
161 });
162}
163
164fn read_format_on_save(cx: &App) -> bool {
165 match AllLanguageSettings::get_global(cx).defaults.format_on_save {
166 FormatOnSave::On => true,
167 FormatOnSave::Off => false,
168 }
169}
170
171fn write_format_on_save(format_on_save: bool, cx: &mut App) {
172 let fs = <dyn Fs>::global(cx);
173
174 update_settings_file(fs, cx, move |settings, _| {
175 settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
176 true => FormatOnSave::On,
177 false => FormatOnSave::Off,
178 });
179 });
180}
181
182fn render_setting_import_button(
183 tab_index: isize,
184 label: SharedString,
185 icon_name: IconName,
186 action: &dyn Action,
187 imported: bool,
188) -> impl IntoElement {
189 let action = action.boxed_clone();
190 h_flex().w_full().child(
191 ButtonLike::new(label.clone())
192 .full_width()
193 .style(ButtonStyle::Outlined)
194 .size(ButtonSize::Large)
195 .tab_index(tab_index)
196 .child(
197 h_flex()
198 .w_full()
199 .justify_between()
200 .child(
201 h_flex()
202 .gap_1p5()
203 .px_1()
204 .child(
205 Icon::new(icon_name)
206 .color(Color::Muted)
207 .size(IconSize::XSmall),
208 )
209 .child(Label::new(label.clone())),
210 )
211 .when(imported, |this| {
212 this.child(
213 h_flex()
214 .gap_1p5()
215 .child(
216 Icon::new(IconName::Check)
217 .color(Color::Success)
218 .size(IconSize::XSmall),
219 )
220 .child(Label::new("Imported").size(LabelSize::Small)),
221 )
222 }),
223 )
224 .on_click(move |_, window, cx| {
225 telemetry::event!("Welcome Import Settings", import_source = label,);
226 window.dispatch_action(action.boxed_clone(), cx);
227 }),
228 )
229}
230
231fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
232 let import_state = SettingsImportState::global(cx);
233 let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
234 (
235 "VS Code".into(),
236 IconName::EditorVsCode,
237 &ImportVsCodeSettings { skip_prompt: false },
238 import_state.vscode,
239 ),
240 (
241 "Cursor".into(),
242 IconName::EditorCursor,
243 &ImportCursorSettings { skip_prompt: false },
244 import_state.cursor,
245 ),
246 ];
247
248 let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
249 *tab_index += 1;
250 render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
251 });
252
253 v_flex()
254 .gap_4()
255 .child(
256 v_flex()
257 .child(Label::new("Import Settings").size(LabelSize::Large))
258 .child(
259 Label::new("Automatically pull your settings from other editors.")
260 .color(Color::Muted),
261 ),
262 )
263 .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
264}
265
266fn render_font_customization_section(
267 tab_index: &mut isize,
268 window: &mut Window,
269 cx: &mut App,
270) -> impl IntoElement {
271 let theme_settings = ThemeSettings::get_global(cx);
272 let ui_font_size = theme_settings.ui_font_size(cx);
273 let ui_font_family = theme_settings.ui_font.family.clone();
274 let buffer_font_family = theme_settings.buffer_font.family.clone();
275 let buffer_font_size = theme_settings.buffer_font_size(cx);
276
277 let ui_font_picker =
278 cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
279
280 let buffer_font_picker = cx.new(|cx| {
281 font_picker(
282 buffer_font_family.clone(),
283 write_buffer_font_family,
284 window,
285 cx,
286 )
287 });
288
289 let ui_font_handle = ui::PopoverMenuHandle::default();
290 let buffer_font_handle = ui::PopoverMenuHandle::default();
291
292 h_flex()
293 .w_full()
294 .gap_4()
295 .child(
296 v_flex()
297 .w_full()
298 .gap_1()
299 .child(Label::new("UI Font"))
300 .child(
301 h_flex()
302 .w_full()
303 .justify_between()
304 .gap_2()
305 .child(
306 PopoverMenu::new("ui-font-picker")
307 .menu({
308 let ui_font_picker = ui_font_picker;
309 move |_window, _cx| Some(ui_font_picker.clone())
310 })
311 .trigger(
312 ButtonLike::new("ui-font-family-button")
313 .style(ButtonStyle::Outlined)
314 .size(ButtonSize::Medium)
315 .full_width()
316 .tab_index({
317 *tab_index += 1;
318 *tab_index - 1
319 })
320 .child(
321 h_flex()
322 .w_full()
323 .justify_between()
324 .child(Label::new(ui_font_family))
325 .child(
326 Icon::new(IconName::ChevronUpDown)
327 .color(Color::Muted)
328 .size(IconSize::XSmall),
329 ),
330 ),
331 )
332 .full_width(true)
333 .anchor(gpui::Corner::TopLeft)
334 .offset(gpui::Point {
335 x: px(0.0),
336 y: px(4.0),
337 })
338 .with_handle(ui_font_handle),
339 )
340 .child(font_picker_stepper(
341 "ui-font-size",
342 &ui_font_size,
343 tab_index,
344 write_ui_font_size,
345 window,
346 cx,
347 )),
348 ),
349 )
350 .child(
351 v_flex()
352 .w_full()
353 .gap_1()
354 .child(Label::new("Editor Font"))
355 .child(
356 h_flex()
357 .w_full()
358 .justify_between()
359 .gap_2()
360 .child(
361 PopoverMenu::new("buffer-font-picker")
362 .menu({
363 let buffer_font_picker = buffer_font_picker;
364 move |_window, _cx| Some(buffer_font_picker.clone())
365 })
366 .trigger(
367 ButtonLike::new("buffer-font-family-button")
368 .style(ButtonStyle::Outlined)
369 .size(ButtonSize::Medium)
370 .full_width()
371 .tab_index({
372 *tab_index += 1;
373 *tab_index - 1
374 })
375 .child(
376 h_flex()
377 .w_full()
378 .justify_between()
379 .child(Label::new(buffer_font_family))
380 .child(
381 Icon::new(IconName::ChevronUpDown)
382 .color(Color::Muted)
383 .size(IconSize::XSmall),
384 ),
385 ),
386 )
387 .full_width(true)
388 .anchor(gpui::Corner::TopLeft)
389 .offset(gpui::Point {
390 x: px(0.0),
391 y: px(4.0),
392 })
393 .with_handle(buffer_font_handle),
394 )
395 .child(font_picker_stepper(
396 "buffer-font-size",
397 &buffer_font_size,
398 tab_index,
399 write_buffer_font_size,
400 window,
401 cx,
402 )),
403 ),
404 )
405}
406
407fn font_picker_stepper(
408 id: &'static str,
409 font_size: &Pixels,
410 tab_index: &mut isize,
411 write_font_size: fn(Pixels, &mut App),
412 window: &mut Window,
413 cx: &mut App,
414) -> NumericStepper<u32> {
415 window.with_id(id, |window| {
416 let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
417 optimistic_font_size.update(cx, |optimistic_font_size, _| {
418 if let Some(optimistic_font_size_val) = optimistic_font_size {
419 if *optimistic_font_size_val == u32::from(font_size) {
420 *optimistic_font_size = None;
421 }
422 }
423 });
424
425 let stepper_font_size = optimistic_font_size
426 .read(cx)
427 .unwrap_or_else(|| font_size.into());
428
429 NumericStepper::new(
430 SharedString::new(format!("{}-stepper", id)),
431 stepper_font_size,
432 window,
433 cx,
434 )
435 .on_change(move |new_value, _, cx| {
436 optimistic_font_size.write(cx, Some(*new_value));
437 write_font_size(Pixels::from(*new_value), cx);
438 })
439 .format(|value| format!("{value}px"))
440 .style(ui_input::NumericStepperStyle::Outlined)
441 .tab_index({
442 *tab_index += 2;
443 *tab_index - 2
444 })
445 .min(6)
446 .max(32)
447 })
448}
449
450fn render_popular_settings_section(
451 tab_index: &mut isize,
452 window: &mut Window,
453 cx: &mut App,
454) -> impl IntoElement {
455 const LIGATURE_TOOLTIP: &str =
456 "Font ligatures combine two characters into one. For example, turning != into ≠.";
457
458 v_flex()
459 .pt_6()
460 .gap_4()
461 .border_t_1()
462 .border_color(cx.theme().colors().border_variant.opacity(0.5))
463 .child(Label::new("Popular Settings").size(LabelSize::Large))
464 .child(render_font_customization_section(tab_index, window, cx))
465 .child(
466 SwitchField::new(
467 "onboarding-font-ligatures",
468 "Font Ligatures",
469 Some("Combine text characters into their associated symbols.".into()),
470 if read_font_ligatures(cx) {
471 ui::ToggleState::Selected
472 } else {
473 ui::ToggleState::Unselected
474 },
475 |toggle_state, _, cx| {
476 let enabled = toggle_state == &ToggleState::Selected;
477 telemetry::event!(
478 "Welcome Font Ligature",
479 options = if enabled { "on" } else { "off" },
480 );
481
482 write_font_ligatures(enabled, cx);
483 },
484 )
485 .tab_index({
486 *tab_index += 1;
487 *tab_index - 1
488 })
489 .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
490 )
491 .child(
492 SwitchField::new(
493 "onboarding-format-on-save",
494 "Format on Save",
495 Some("Format code automatically when saving.".into()),
496 if read_format_on_save(cx) {
497 ui::ToggleState::Selected
498 } else {
499 ui::ToggleState::Unselected
500 },
501 |toggle_state, _, cx| {
502 let enabled = toggle_state == &ToggleState::Selected;
503 telemetry::event!(
504 "Welcome Format On Save Changed",
505 options = if enabled { "on" } else { "off" },
506 );
507
508 write_format_on_save(enabled, cx);
509 },
510 )
511 .tab_index({
512 *tab_index += 1;
513 *tab_index - 1
514 }),
515 )
516 .child(
517 SwitchField::new(
518 "onboarding-enable-inlay-hints",
519 "Inlay Hints",
520 Some("See parameter names for function and method calls inline.".into()),
521 if read_inlay_hints(cx) {
522 ui::ToggleState::Selected
523 } else {
524 ui::ToggleState::Unselected
525 },
526 |toggle_state, _, cx| {
527 let enabled = toggle_state == &ToggleState::Selected;
528 telemetry::event!(
529 "Welcome Inlay Hints Changed",
530 options = if enabled { "on" } else { "off" },
531 );
532
533 write_inlay_hints(enabled, cx);
534 },
535 )
536 .tab_index({
537 *tab_index += 1;
538 *tab_index - 1
539 }),
540 )
541 .child(
542 SwitchField::new(
543 "onboarding-git-blame-switch",
544 "Inline Git Blame",
545 Some("See who committed each line on a given file.".into()),
546 if read_git_blame(cx) {
547 ui::ToggleState::Selected
548 } else {
549 ui::ToggleState::Unselected
550 },
551 |toggle_state, _, cx| {
552 let enabled = toggle_state == &ToggleState::Selected;
553 telemetry::event!(
554 "Welcome Git Blame Changed",
555 options = if enabled { "on" } else { "off" },
556 );
557
558 write_git_blame(enabled, cx);
559 },
560 )
561 .tab_index({
562 *tab_index += 1;
563 *tab_index - 1
564 }),
565 )
566 .child(
567 h_flex()
568 .items_start()
569 .justify_between()
570 .child(
571 v_flex().child(Label::new("Minimap")).child(
572 Label::new("See a high-level overview of your source code.")
573 .color(Color::Muted),
574 ),
575 )
576 .child(
577 ToggleButtonGroup::single_row(
578 "onboarding-show-mini-map",
579 [
580 ToggleButtonSimple::new("Auto", |_, _, cx| {
581 write_show_mini_map(ShowMinimap::Auto, cx);
582 })
583 .tooltip(Tooltip::text(
584 "Show the minimap if the editor's scrollbar is visible.",
585 )),
586 ToggleButtonSimple::new("Always", |_, _, cx| {
587 write_show_mini_map(ShowMinimap::Always, cx);
588 }),
589 ToggleButtonSimple::new("Never", |_, _, cx| {
590 write_show_mini_map(ShowMinimap::Never, cx);
591 }),
592 ],
593 )
594 .selected_index(match read_show_mini_map(cx) {
595 ShowMinimap::Auto => 0,
596 ShowMinimap::Always => 1,
597 ShowMinimap::Never => 2,
598 })
599 .tab_index(tab_index)
600 .style(ToggleButtonGroupStyle::Outlined)
601 .width(ui::rems_from_px(3. * 64.)),
602 ),
603 )
604}
605
606pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
607 let mut tab_index = 0;
608 v_flex()
609 .gap_6()
610 .child(render_import_settings_section(&mut tab_index, cx))
611 .child(render_popular_settings_section(&mut tab_index, window, cx))
612}