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(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(
345 NumericStepper::new(
346 "ui-font-size",
347 ui_font_size.to_string(),
348 move |_, _, cx| {
349 write_ui_font_size(ui_font_size - px(1.), cx);
350 },
351 move |_, _, cx| {
352 write_ui_font_size(ui_font_size + px(1.), cx);
353 },
354 )
355 .style(ui::NumericStepperStyle::Outlined)
356 .tab_index({
357 *tab_index += 2;
358 *tab_index - 2
359 }),
360 ),
361 ),
362 )
363 .child(
364 v_flex()
365 .w_full()
366 .gap_1()
367 .child(Label::new("Editor Font"))
368 .child(
369 h_flex()
370 .w_full()
371 .justify_between()
372 .gap_2()
373 .child(
374 PopoverMenu::new("buffer-font-picker")
375 .menu({
376 let buffer_font_picker = buffer_font_picker;
377 move |_window, _cx| Some(buffer_font_picker.clone())
378 })
379 .trigger(
380 ButtonLike::new("buffer-font-family-button")
381 .style(ButtonStyle::Outlined)
382 .size(ButtonSize::Medium)
383 .full_width()
384 .tab_index({
385 *tab_index += 1;
386 *tab_index - 1
387 })
388 .child(
389 h_flex()
390 .w_full()
391 .justify_between()
392 .child(Label::new(buffer_font_family))
393 .child(
394 Icon::new(IconName::ChevronUpDown)
395 .color(Color::Muted)
396 .size(IconSize::XSmall),
397 ),
398 ),
399 )
400 .full_width(true)
401 .anchor(gpui::Corner::TopLeft)
402 .offset(gpui::Point {
403 x: px(0.0),
404 y: px(4.0),
405 })
406 .with_handle(buffer_font_handle),
407 )
408 .child(
409 NumericStepper::new(
410 "buffer-font-size",
411 buffer_font_size.to_string(),
412 move |_, _, cx| {
413 write_buffer_font_size(buffer_font_size - px(1.), cx);
414 },
415 move |_, _, cx| {
416 write_buffer_font_size(buffer_font_size + px(1.), cx);
417 },
418 )
419 .style(ui::NumericStepperStyle::Outlined)
420 .tab_index({
421 *tab_index += 2;
422 *tab_index - 2
423 }),
424 ),
425 ),
426 )
427}
428
429type FontPicker = Picker<FontPickerDelegate>;
430
431pub struct FontPickerDelegate {
432 fonts: Vec<SharedString>,
433 filtered_fonts: Vec<StringMatch>,
434 selected_index: usize,
435 current_font: SharedString,
436 on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
437}
438
439impl FontPickerDelegate {
440 fn new(
441 current_font: SharedString,
442 on_font_changed: impl Fn(SharedString, &mut App) + 'static,
443 cx: &mut Context<FontPicker>,
444 ) -> Self {
445 let font_family_cache = FontFamilyCache::global(cx);
446
447 let fonts = font_family_cache
448 .try_list_font_families()
449 .unwrap_or_else(|| vec![current_font.clone()]);
450 let selected_index = fonts
451 .iter()
452 .position(|font| *font == current_font)
453 .unwrap_or(0);
454
455 let filtered_fonts = fonts
456 .iter()
457 .enumerate()
458 .map(|(index, font)| StringMatch {
459 candidate_id: index,
460 string: font.to_string(),
461 positions: Vec::new(),
462 score: 0.0,
463 })
464 .collect();
465
466 Self {
467 fonts,
468 filtered_fonts,
469 selected_index,
470 current_font,
471 on_font_changed: Arc::new(on_font_changed),
472 }
473 }
474}
475
476impl PickerDelegate for FontPickerDelegate {
477 type ListItem = AnyElement;
478
479 fn match_count(&self) -> usize {
480 self.filtered_fonts.len()
481 }
482
483 fn selected_index(&self) -> usize {
484 self.selected_index
485 }
486
487 fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<FontPicker>) {
488 self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1));
489 cx.notify();
490 }
491
492 fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
493 "Search fonts…".into()
494 }
495
496 fn update_matches(
497 &mut self,
498 query: String,
499 _window: &mut Window,
500 cx: &mut Context<FontPicker>,
501 ) -> Task<()> {
502 let fonts = self.fonts.clone();
503 let current_font = self.current_font.clone();
504
505 let matches: Vec<StringMatch> = if query.is_empty() {
506 fonts
507 .iter()
508 .enumerate()
509 .map(|(index, font)| StringMatch {
510 candidate_id: index,
511 string: font.to_string(),
512 positions: Vec::new(),
513 score: 0.0,
514 })
515 .collect()
516 } else {
517 let _candidates: Vec<StringMatchCandidate> = fonts
518 .iter()
519 .enumerate()
520 .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref()))
521 .collect();
522
523 fonts
524 .iter()
525 .enumerate()
526 .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase()))
527 .map(|(index, font)| StringMatch {
528 candidate_id: index,
529 string: font.to_string(),
530 positions: Vec::new(),
531 score: 0.0,
532 })
533 .collect()
534 };
535
536 let selected_index = if query.is_empty() {
537 fonts
538 .iter()
539 .position(|font| *font == current_font)
540 .unwrap_or(0)
541 } else {
542 matches
543 .iter()
544 .position(|m| fonts[m.candidate_id] == current_font)
545 .unwrap_or(0)
546 };
547
548 self.filtered_fonts = matches;
549 self.selected_index = selected_index;
550 cx.notify();
551
552 Task::ready(())
553 }
554
555 fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
556 if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
557 let font = font_match.string.clone();
558 (self.on_font_changed)(font.into(), cx);
559 }
560 }
561
562 fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<FontPicker>) {}
563
564 fn render_match(
565 &self,
566 ix: usize,
567 selected: bool,
568 _window: &mut Window,
569 _cx: &mut Context<FontPicker>,
570 ) -> Option<Self::ListItem> {
571 let font_match = self.filtered_fonts.get(ix)?;
572
573 Some(
574 ListItem::new(ix)
575 .inset(true)
576 .spacing(ListItemSpacing::Sparse)
577 .toggle_state(selected)
578 .child(Label::new(font_match.string.clone()))
579 .into_any_element(),
580 )
581 }
582}
583
584fn font_picker(
585 current_font: SharedString,
586 on_font_changed: impl Fn(SharedString, &mut App) + 'static,
587 window: &mut Window,
588 cx: &mut Context<FontPicker>,
589) -> FontPicker {
590 let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
591
592 Picker::uniform_list(delegate, window, cx)
593 .show_scrollbar(true)
594 .width(rems_from_px(210.))
595 .max_height(Some(rems(20.).into()))
596}
597
598fn render_popular_settings_section(
599 tab_index: &mut isize,
600 window: &mut Window,
601 cx: &mut App,
602) -> impl IntoElement {
603 const LIGATURE_TOOLTIP: &str =
604 "Font ligatures combine two characters into one. For example, turning != into ≠.";
605
606 v_flex()
607 .pt_6()
608 .gap_4()
609 .border_t_1()
610 .border_color(cx.theme().colors().border_variant.opacity(0.5))
611 .child(Label::new("Popular Settings").size(LabelSize::Large))
612 .child(render_font_customization_section(tab_index, window, cx))
613 .child(
614 SwitchField::new(
615 "onboarding-font-ligatures",
616 "Font Ligatures",
617 Some("Combine text characters into their associated symbols.".into()),
618 if read_font_ligatures(cx) {
619 ui::ToggleState::Selected
620 } else {
621 ui::ToggleState::Unselected
622 },
623 |toggle_state, _, cx| {
624 let enabled = toggle_state == &ToggleState::Selected;
625 telemetry::event!(
626 "Welcome Font Ligature",
627 options = if enabled { "on" } else { "off" },
628 );
629
630 write_font_ligatures(enabled, cx);
631 },
632 )
633 .tab_index({
634 *tab_index += 1;
635 *tab_index - 1
636 })
637 .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
638 )
639 .child(
640 SwitchField::new(
641 "onboarding-format-on-save",
642 "Format on Save",
643 Some("Format code automatically when saving.".into()),
644 if read_format_on_save(cx) {
645 ui::ToggleState::Selected
646 } else {
647 ui::ToggleState::Unselected
648 },
649 |toggle_state, _, cx| {
650 let enabled = toggle_state == &ToggleState::Selected;
651 telemetry::event!(
652 "Welcome Format On Save Changed",
653 options = if enabled { "on" } else { "off" },
654 );
655
656 write_format_on_save(enabled, cx);
657 },
658 )
659 .tab_index({
660 *tab_index += 1;
661 *tab_index - 1
662 }),
663 )
664 .child(
665 SwitchField::new(
666 "onboarding-enable-inlay-hints",
667 "Inlay Hints",
668 Some("See parameter names for function and method calls inline.".into()),
669 if read_inlay_hints(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 Inlay Hints Changed",
678 options = if enabled { "on" } else { "off" },
679 );
680
681 write_inlay_hints(enabled, cx);
682 },
683 )
684 .tab_index({
685 *tab_index += 1;
686 *tab_index - 1
687 }),
688 )
689 .child(
690 SwitchField::new(
691 "onboarding-git-blame-switch",
692 "Inline Git Blame",
693 Some("See who committed each line on a given file.".into()),
694 if read_git_blame(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 Git Blame Changed",
703 options = if enabled { "on" } else { "off" },
704 );
705
706 write_git_blame(enabled, cx);
707 },
708 )
709 .tab_index({
710 *tab_index += 1;
711 *tab_index - 1
712 }),
713 )
714 .child(
715 h_flex()
716 .items_start()
717 .justify_between()
718 .child(
719 v_flex().child(Label::new("Minimap")).child(
720 Label::new("See a high-level overview of your source code.")
721 .color(Color::Muted),
722 ),
723 )
724 .child(
725 ToggleButtonGroup::single_row(
726 "onboarding-show-mini-map",
727 [
728 ToggleButtonSimple::new("Auto", |_, _, cx| {
729 write_show_mini_map(ShowMinimap::Auto, cx);
730 })
731 .tooltip(Tooltip::text(
732 "Show the minimap if the editor's scrollbar is visible.",
733 )),
734 ToggleButtonSimple::new("Always", |_, _, cx| {
735 write_show_mini_map(ShowMinimap::Always, cx);
736 }),
737 ToggleButtonSimple::new("Never", |_, _, cx| {
738 write_show_mini_map(ShowMinimap::Never, cx);
739 }),
740 ],
741 )
742 .selected_index(match read_show_mini_map(cx) {
743 ShowMinimap::Auto => 0,
744 ShowMinimap::Always => 1,
745 ShowMinimap::Never => 2,
746 })
747 .tab_index(tab_index)
748 .style(ToggleButtonGroupStyle::Outlined)
749 .width(ui::rems_from_px(3. * 64.)),
750 ),
751 )
752}
753
754pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
755 let mut tab_index = 0;
756 v_flex()
757 .gap_6()
758 .child(render_import_settings_section(&mut tab_index, cx))
759 .child(render_popular_settings_section(&mut tab_index, window, cx))
760}