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