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