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