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    label: SharedString,
175    icon_name: IconName,
176    action: &dyn Action,
177    imported: bool,
178) -> impl IntoElement {
179    let action = action.boxed_clone();
180    h_flex().w_full().child(
181        ButtonLike::new(label.clone())
182            .full_width()
183            .style(ButtonStyle::Outlined)
184            .size(ButtonSize::Large)
185            .child(
186                h_flex()
187                    .w_full()
188                    .justify_between()
189                    .child(
190                        h_flex()
191                            .gap_1p5()
192                            .px_1()
193                            .child(
194                                Icon::new(icon_name)
195                                    .color(Color::Muted)
196                                    .size(IconSize::XSmall),
197                            )
198                            .child(Label::new(label)),
199                    )
200                    .when(imported, |this| {
201                        this.child(
202                            h_flex()
203                                .gap_1p5()
204                                .child(
205                                    Icon::new(IconName::Check)
206                                        .color(Color::Success)
207                                        .size(IconSize::XSmall),
208                                )
209                                .child(Label::new("Imported").size(LabelSize::Small)),
210                        )
211                    }),
212            )
213            .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
214    )
215}
216
217fn render_import_settings_section(cx: &App) -> impl IntoElement {
218    let import_state = SettingsImportState::global(cx);
219    let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
220        (
221            "VS Code".into(),
222            IconName::EditorVsCode,
223            &ImportVsCodeSettings { skip_prompt: false },
224            import_state.vscode,
225        ),
226        (
227            "Cursor".into(),
228            IconName::EditorCursor,
229            &ImportCursorSettings { skip_prompt: false },
230            import_state.cursor,
231        ),
232    ];
233
234    let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
235        render_setting_import_button(label, icon_name, action, imported)
236    });
237
238    v_flex()
239        .gap_4()
240        .child(
241            v_flex()
242                .child(Label::new("Import Settings").size(LabelSize::Large))
243                .child(
244                    Label::new("Automatically pull your settings from other editors.")
245                        .color(Color::Muted),
246                ),
247        )
248        .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
249}
250
251fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
252    let theme_settings = ThemeSettings::get_global(cx);
253    let ui_font_size = theme_settings.ui_font_size(cx);
254    let ui_font_family = theme_settings.ui_font.family.clone();
255    let buffer_font_family = theme_settings.buffer_font.family.clone();
256    let buffer_font_size = theme_settings.buffer_font_size(cx);
257
258    let ui_font_picker =
259        cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
260
261    let buffer_font_picker = cx.new(|cx| {
262        font_picker(
263            buffer_font_family.clone(),
264            write_buffer_font_family,
265            window,
266            cx,
267        )
268    });
269
270    let ui_font_handle = ui::PopoverMenuHandle::default();
271    let buffer_font_handle = ui::PopoverMenuHandle::default();
272
273    h_flex()
274        .w_full()
275        .gap_4()
276        .child(
277            v_flex()
278                .w_full()
279                .gap_1()
280                .child(Label::new("UI Font"))
281                .child(
282                    h_flex()
283                        .w_full()
284                        .justify_between()
285                        .gap_2()
286                        .child(
287                            PopoverMenu::new("ui-font-picker")
288                                .menu({
289                                    let ui_font_picker = ui_font_picker.clone();
290                                    move |_window, _cx| Some(ui_font_picker.clone())
291                                })
292                                .trigger(
293                                    ButtonLike::new("ui-font-family-button")
294                                        .style(ButtonStyle::Outlined)
295                                        .size(ButtonSize::Medium)
296                                        .full_width()
297                                        .child(
298                                            h_flex()
299                                                .w_full()
300                                                .justify_between()
301                                                .child(Label::new(ui_font_family))
302                                                .child(
303                                                    Icon::new(IconName::ChevronUpDown)
304                                                        .color(Color::Muted)
305                                                        .size(IconSize::XSmall),
306                                                ),
307                                        ),
308                                )
309                                .full_width(true)
310                                .anchor(gpui::Corner::TopLeft)
311                                .offset(gpui::Point {
312                                    x: px(0.0),
313                                    y: px(4.0),
314                                })
315                                .with_handle(ui_font_handle),
316                        )
317                        .child(
318                            NumericStepper::new(
319                                "ui-font-size",
320                                ui_font_size.to_string(),
321                                move |_, _, cx| {
322                                    write_ui_font_size(ui_font_size - px(1.), cx);
323                                },
324                                move |_, _, cx| {
325                                    write_ui_font_size(ui_font_size + px(1.), cx);
326                                },
327                            )
328                            .style(ui::NumericStepperStyle::Outlined),
329                        ),
330                ),
331        )
332        .child(
333            v_flex()
334                .w_full()
335                .gap_1()
336                .child(Label::new("Editor Font"))
337                .child(
338                    h_flex()
339                        .w_full()
340                        .justify_between()
341                        .gap_2()
342                        .child(
343                            PopoverMenu::new("buffer-font-picker")
344                                .menu({
345                                    let buffer_font_picker = buffer_font_picker.clone();
346                                    move |_window, _cx| Some(buffer_font_picker.clone())
347                                })
348                                .trigger(
349                                    ButtonLike::new("buffer-font-family-button")
350                                        .style(ButtonStyle::Outlined)
351                                        .size(ButtonSize::Medium)
352                                        .full_width()
353                                        .child(
354                                            h_flex()
355                                                .w_full()
356                                                .justify_between()
357                                                .child(Label::new(buffer_font_family))
358                                                .child(
359                                                    Icon::new(IconName::ChevronUpDown)
360                                                        .color(Color::Muted)
361                                                        .size(IconSize::XSmall),
362                                                ),
363                                        ),
364                                )
365                                .full_width(true)
366                                .anchor(gpui::Corner::TopLeft)
367                                .offset(gpui::Point {
368                                    x: px(0.0),
369                                    y: px(4.0),
370                                })
371                                .with_handle(buffer_font_handle),
372                        )
373                        .child(
374                            NumericStepper::new(
375                                "buffer-font-size",
376                                buffer_font_size.to_string(),
377                                move |_, _, cx| {
378                                    write_buffer_font_size(buffer_font_size - px(1.), cx);
379                                },
380                                move |_, _, cx| {
381                                    write_buffer_font_size(buffer_font_size + px(1.), cx);
382                                },
383                            )
384                            .style(ui::NumericStepperStyle::Outlined),
385                        ),
386                ),
387        )
388}
389
390type FontPicker = Picker<FontPickerDelegate>;
391
392pub struct FontPickerDelegate {
393    fonts: Vec<SharedString>,
394    filtered_fonts: Vec<StringMatch>,
395    selected_index: usize,
396    current_font: SharedString,
397    on_font_changed: Arc<dyn Fn(SharedString, &mut App) + 'static>,
398}
399
400impl FontPickerDelegate {
401    fn new(
402        current_font: SharedString,
403        on_font_changed: impl Fn(SharedString, &mut App) + 'static,
404        cx: &mut Context<FontPicker>,
405    ) -> Self {
406        let font_family_cache = FontFamilyCache::global(cx);
407
408        let fonts: Vec<SharedString> = font_family_cache
409            .list_font_families(cx)
410            .into_iter()
411            .collect();
412
413        let selected_index = fonts
414            .iter()
415            .position(|font| *font == current_font)
416            .unwrap_or(0);
417
418        Self {
419            fonts: fonts.clone(),
420            filtered_fonts: fonts
421                .iter()
422                .enumerate()
423                .map(|(index, font)| StringMatch {
424                    candidate_id: index,
425                    string: font.to_string(),
426                    positions: Vec::new(),
427                    score: 0.0,
428                })
429                .collect(),
430            selected_index,
431            current_font,
432            on_font_changed: Arc::new(on_font_changed),
433        }
434    }
435}
436
437impl PickerDelegate for FontPickerDelegate {
438    type ListItem = AnyElement;
439
440    fn match_count(&self) -> usize {
441        self.filtered_fonts.len()
442    }
443
444    fn selected_index(&self) -> usize {
445        self.selected_index
446    }
447
448    fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<FontPicker>) {
449        self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1));
450        cx.notify();
451    }
452
453    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
454        "Search fonts…".into()
455    }
456
457    fn update_matches(
458        &mut self,
459        query: String,
460        _window: &mut Window,
461        cx: &mut Context<FontPicker>,
462    ) -> Task<()> {
463        let fonts = self.fonts.clone();
464        let current_font = self.current_font.clone();
465
466        let matches: Vec<StringMatch> = if query.is_empty() {
467            fonts
468                .iter()
469                .enumerate()
470                .map(|(index, font)| StringMatch {
471                    candidate_id: index,
472                    string: font.to_string(),
473                    positions: Vec::new(),
474                    score: 0.0,
475                })
476                .collect()
477        } else {
478            let _candidates: Vec<StringMatchCandidate> = fonts
479                .iter()
480                .enumerate()
481                .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref()))
482                .collect();
483
484            fonts
485                .iter()
486                .enumerate()
487                .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase()))
488                .map(|(index, font)| StringMatch {
489                    candidate_id: index,
490                    string: font.to_string(),
491                    positions: Vec::new(),
492                    score: 0.0,
493                })
494                .collect()
495        };
496
497        let selected_index = if query.is_empty() {
498            fonts
499                .iter()
500                .position(|font| *font == current_font)
501                .unwrap_or(0)
502        } else {
503            matches
504                .iter()
505                .position(|m| fonts[m.candidate_id] == current_font)
506                .unwrap_or(0)
507        };
508
509        self.filtered_fonts = matches;
510        self.selected_index = selected_index;
511        cx.notify();
512
513        Task::ready(())
514    }
515
516    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<FontPicker>) {
517        if let Some(font_match) = self.filtered_fonts.get(self.selected_index) {
518            let font = font_match.string.clone();
519            (self.on_font_changed)(font.into(), cx);
520        }
521    }
522
523    fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<FontPicker>) {}
524
525    fn render_match(
526        &self,
527        ix: usize,
528        selected: bool,
529        _window: &mut Window,
530        _cx: &mut Context<FontPicker>,
531    ) -> Option<Self::ListItem> {
532        let font_match = self.filtered_fonts.get(ix)?;
533
534        Some(
535            ListItem::new(ix)
536                .inset(true)
537                .spacing(ListItemSpacing::Sparse)
538                .toggle_state(selected)
539                .child(Label::new(font_match.string.clone()))
540                .into_any_element(),
541        )
542    }
543}
544
545fn font_picker(
546    current_font: SharedString,
547    on_font_changed: impl Fn(SharedString, &mut App) + 'static,
548    window: &mut Window,
549    cx: &mut Context<FontPicker>,
550) -> FontPicker {
551    let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx);
552
553    Picker::list(delegate, window, cx)
554        .show_scrollbar(true)
555        .width(rems_from_px(210.))
556        .max_height(Some(rems(20.).into()))
557}
558
559fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
560    const LIGATURE_TOOLTIP: &'static str = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";
561
562    v_flex()
563        .gap_5()
564        .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
565        .child(render_font_customization_section(window, cx))
566        .child(
567            SwitchField::new(
568                "onboarding-font-ligatures",
569                "Font Ligatures",
570                Some("Combine text characters into their associated symbols.".into()),
571                if read_font_ligatures(cx) {
572                    ui::ToggleState::Selected
573                } else {
574                    ui::ToggleState::Unselected
575                },
576                |toggle_state, _, cx| {
577                    write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
578                },
579            )
580            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
581        )
582        .child(SwitchField::new(
583            "onboarding-format-on-save",
584            "Format on Save",
585            Some("Format code automatically when saving.".into()),
586            if read_format_on_save(cx) {
587                ui::ToggleState::Selected
588            } else {
589                ui::ToggleState::Unselected
590            },
591            |toggle_state, _, cx| {
592                write_format_on_save(toggle_state == &ToggleState::Selected, cx);
593            },
594        ))
595        .child(SwitchField::new(
596            "onboarding-enable-inlay-hints",
597            "Inlay Hints",
598            Some("See parameter names for function and method calls inline.".into()),
599            if read_inlay_hints(cx) {
600                ui::ToggleState::Selected
601            } else {
602                ui::ToggleState::Unselected
603            },
604            |toggle_state, _, cx| {
605                write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
606            },
607        ))
608        .child(SwitchField::new(
609            "onboarding-git-blame-switch",
610            "Git Blame",
611            Some("See who committed each line on a given file.".into()),
612            if read_git_blame(cx) {
613                ui::ToggleState::Selected
614            } else {
615                ui::ToggleState::Unselected
616            },
617            |toggle_state, _, cx| {
618                set_git_blame(toggle_state == &ToggleState::Selected, cx);
619            },
620        ))
621        .child(
622            h_flex()
623                .items_start()
624                .justify_between()
625                .child(
626                    v_flex().child(Label::new("Mini Map")).child(
627                        Label::new("See a high-level overview of your source code.")
628                            .color(Color::Muted),
629                    ),
630                )
631                .child(
632                    ToggleButtonGroup::single_row(
633                        "onboarding-show-mini-map",
634                        [
635                            ToggleButtonSimple::new("Auto", |_, _, cx| {
636                                write_show_mini_map(ShowMinimap::Auto, cx);
637                            }),
638                            ToggleButtonSimple::new("Always", |_, _, cx| {
639                                write_show_mini_map(ShowMinimap::Always, cx);
640                            }),
641                            ToggleButtonSimple::new("Never", |_, _, cx| {
642                                write_show_mini_map(ShowMinimap::Never, cx);
643                            }),
644                        ],
645                    )
646                    .selected_index(match read_show_mini_map(cx) {
647                        ShowMinimap::Auto => 0,
648                        ShowMinimap::Always => 1,
649                        ShowMinimap::Never => 2,
650                    })
651                    .style(ToggleButtonGroupStyle::Outlined)
652                    .button_width(ui::rems_from_px(64.)),
653                ),
654        )
655}
656
657pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
658    v_flex()
659        .gap_4()
660        .child(render_import_settings_section(cx))
661        .child(render_popular_settings_section(window, cx))
662}