editing_page.rs

  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}