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(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}