editing_page.rs

  1use std::sync::Arc;
  2
  3use editor::{EditorSettings, ShowMinimap};
  4use fs::Fs;
  5use gpui::{Action, App, FontFeatures, IntoElement, Pixels, Window};
  6use language::language_settings::{AllLanguageSettings, FormatOnSave};
  7use project::project_settings::ProjectSettings;
  8use settings::{Settings as _, update_settings_file};
  9use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
 10use ui::{
 11    ButtonLike, ContextMenu, DropdownMenu, NumericStepper, SwitchField, ToggleButtonGroup,
 12    ToggleButtonGroupStyle, ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
 13};
 14
 15use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
 16
 17fn read_show_mini_map(cx: &App) -> ShowMinimap {
 18    editor::EditorSettings::get_global(cx).minimap.show
 19}
 20
 21fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
 22    let fs = <dyn Fs>::global(cx);
 23
 24    // This is used to speed up the UI
 25    // the UI reads the current values to get what toggle state to show on buttons
 26    // there's a slight delay if we just call update_settings_file so we manually set
 27    // the value here then call update_settings file to get around the delay
 28    let mut curr_settings = EditorSettings::get_global(cx).clone();
 29    curr_settings.minimap.show = show;
 30    EditorSettings::override_global(curr_settings, cx);
 31
 32    update_settings_file::<EditorSettings>(fs, cx, move |editor_settings, _| {
 33        editor_settings.minimap.get_or_insert_default().show = Some(show);
 34    });
 35}
 36
 37fn read_inlay_hints(cx: &App) -> bool {
 38    AllLanguageSettings::get_global(cx)
 39        .defaults
 40        .inlay_hints
 41        .enabled
 42}
 43
 44fn write_inlay_hints(enabled: bool, cx: &mut App) {
 45    let fs = <dyn Fs>::global(cx);
 46
 47    let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
 48    curr_settings.defaults.inlay_hints.enabled = enabled;
 49    AllLanguageSettings::override_global(curr_settings, cx);
 50
 51    update_settings_file::<AllLanguageSettings>(fs, cx, move |all_language_settings, cx| {
 52        all_language_settings
 53            .defaults
 54            .inlay_hints
 55            .get_or_insert_with(|| {
 56                AllLanguageSettings::get_global(cx)
 57                    .clone()
 58                    .defaults
 59                    .inlay_hints
 60            })
 61            .enabled = enabled;
 62    });
 63}
 64
 65fn read_git_blame(cx: &App) -> bool {
 66    ProjectSettings::get_global(cx).git.inline_blame_enabled()
 67}
 68
 69fn set_git_blame(enabled: bool, cx: &mut App) {
 70    let fs = <dyn Fs>::global(cx);
 71
 72    let mut curr_settings = ProjectSettings::get_global(cx).clone();
 73    curr_settings
 74        .git
 75        .inline_blame
 76        .get_or_insert_default()
 77        .enabled = enabled;
 78    ProjectSettings::override_global(curr_settings, cx);
 79
 80    update_settings_file::<ProjectSettings>(fs, cx, move |project_settings, _| {
 81        project_settings
 82            .git
 83            .inline_blame
 84            .get_or_insert_default()
 85            .enabled = enabled;
 86    });
 87}
 88
 89fn write_ui_font_family(font: SharedString, cx: &mut App) {
 90    let fs = <dyn Fs>::global(cx);
 91
 92    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
 93        theme_settings.ui_font_family = Some(FontFamilyName(font.into()));
 94    });
 95}
 96
 97fn write_ui_font_size(size: Pixels, cx: &mut App) {
 98    let fs = <dyn Fs>::global(cx);
 99
100    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
101        theme_settings.ui_font_size = Some(size.into());
102    });
103}
104
105fn write_buffer_font_size(size: Pixels, cx: &mut App) {
106    let fs = <dyn Fs>::global(cx);
107
108    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
109        theme_settings.buffer_font_size = Some(size.into());
110    });
111}
112
113fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
114    let fs = <dyn Fs>::global(cx);
115
116    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
117        theme_settings.buffer_font_family = Some(FontFamilyName(font_family.into()));
118    });
119}
120
121fn read_font_ligatures(cx: &App) -> bool {
122    ThemeSettings::get_global(cx)
123        .buffer_font
124        .features
125        .is_calt_enabled()
126        .unwrap_or(true)
127}
128
129fn write_font_ligatures(enabled: bool, cx: &mut App) {
130    let fs = <dyn Fs>::global(cx);
131    let bit = if enabled { 1 } else { 0 };
132
133    update_settings_file::<ThemeSettings>(fs, cx, move |theme_settings, _| {
134        let mut features = theme_settings
135            .buffer_font_features
136            .as_mut()
137            .map(|features| features.tag_value_list().to_vec())
138            .unwrap_or_default();
139
140        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
141            features[calt_index].1 = bit;
142        } else {
143            features.push(("calt".into(), bit));
144        }
145
146        theme_settings.buffer_font_features = Some(FontFeatures(Arc::new(features)));
147    });
148}
149
150fn read_format_on_save(cx: &App) -> bool {
151    match AllLanguageSettings::get_global(cx).defaults.format_on_save {
152        FormatOnSave::On | FormatOnSave::List(_) => true,
153        FormatOnSave::Off => false,
154    }
155}
156
157fn write_format_on_save(format_on_save: bool, cx: &mut App) {
158    let fs = <dyn Fs>::global(cx);
159
160    update_settings_file::<AllLanguageSettings>(fs, cx, move |language_settings, _| {
161        language_settings.defaults.format_on_save = Some(match format_on_save {
162            true => FormatOnSave::On,
163            false => FormatOnSave::Off,
164        });
165    });
166}
167
168fn render_setting_import_button(
169    label: SharedString,
170    icon_name: IconName,
171    action: &dyn Action,
172    imported: bool,
173) -> impl IntoElement {
174    let action = action.boxed_clone();
175    h_flex().w_full().child(
176        ButtonLike::new(label.clone())
177            .full_width()
178            .style(ButtonStyle::Outlined)
179            .size(ButtonSize::Large)
180            .child(
181                h_flex()
182                    .w_full()
183                    .justify_between()
184                    .child(
185                        h_flex()
186                            .gap_1p5()
187                            .px_1()
188                            .child(
189                                Icon::new(icon_name)
190                                    .color(Color::Muted)
191                                    .size(IconSize::XSmall),
192                            )
193                            .child(Label::new(label)),
194                    )
195                    .when(imported, |this| {
196                        this.child(
197                            h_flex()
198                                .gap_1p5()
199                                .child(
200                                    Icon::new(IconName::Check)
201                                        .color(Color::Success)
202                                        .size(IconSize::XSmall),
203                                )
204                                .child(Label::new("Imported").size(LabelSize::Small)),
205                        )
206                    }),
207            )
208            .on_click(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)),
209    )
210}
211
212fn render_import_settings_section(cx: &App) -> impl IntoElement {
213    let import_state = SettingsImportState::global(cx);
214    let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
215        (
216            "VS Code".into(),
217            IconName::EditorVsCode,
218            &ImportVsCodeSettings { skip_prompt: false },
219            import_state.vscode,
220        ),
221        (
222            "Cursor".into(),
223            IconName::EditorCursor,
224            &ImportCursorSettings { skip_prompt: false },
225            import_state.cursor,
226        ),
227    ];
228
229    let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
230        render_setting_import_button(label, icon_name, action, imported)
231    });
232
233    v_flex()
234        .gap_4()
235        .child(
236            v_flex()
237                .child(Label::new("Import Settings").size(LabelSize::Large))
238                .child(
239                    Label::new("Automatically pull your settings from other editors.")
240                        .color(Color::Muted),
241                ),
242        )
243        .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
244}
245
246fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
247    let theme_settings = ThemeSettings::get_global(cx);
248    let ui_font_size = theme_settings.ui_font_size(cx);
249    let font_family = theme_settings.buffer_font.family.clone();
250    let buffer_font_size = theme_settings.buffer_font_size(cx);
251
252    h_flex()
253        .w_full()
254        .gap_4()
255        .child(
256            v_flex()
257                .w_full()
258                .gap_1()
259                .child(Label::new("UI Font"))
260                .child(
261                    h_flex()
262                        .w_full()
263                        .justify_between()
264                        .gap_2()
265                        .child(
266                            DropdownMenu::new(
267                                "ui-font-family",
268                                theme_settings.ui_font.family.clone(),
269                                ContextMenu::build(window, cx, |mut menu, _, cx| {
270                                    let font_family_cache = FontFamilyCache::global(cx);
271
272                                    for font_name in font_family_cache.list_font_families(cx) {
273                                        menu = menu.custom_entry(
274                                            {
275                                                let font_name = font_name.clone();
276                                                move |_window, _cx| {
277                                                    Label::new(font_name.clone()).into_any_element()
278                                                }
279                                            },
280                                            {
281                                                let font_name = font_name.clone();
282                                                move |_window, cx| {
283                                                    write_ui_font_family(font_name.clone(), cx);
284                                                }
285                                            },
286                                        )
287                                    }
288
289                                    menu
290                                }),
291                            )
292                            .style(ui::DropdownStyle::Outlined)
293                            .full_width(true),
294                        )
295                        .child(
296                            NumericStepper::new(
297                                "ui-font-size",
298                                ui_font_size.to_string(),
299                                move |_, _, cx| {
300                                    write_ui_font_size(ui_font_size - px(1.), cx);
301                                },
302                                move |_, _, cx| {
303                                    write_ui_font_size(ui_font_size + px(1.), cx);
304                                },
305                            )
306                            .style(ui::NumericStepperStyle::Outlined),
307                        ),
308                ),
309        )
310        .child(
311            v_flex()
312                .w_full()
313                .gap_1()
314                .child(Label::new("Editor Font"))
315                .child(
316                    h_flex()
317                        .w_full()
318                        .justify_between()
319                        .gap_2()
320                        .child(
321                            DropdownMenu::new(
322                                "buffer-font-family",
323                                font_family,
324                                ContextMenu::build(window, cx, |mut menu, _, cx| {
325                                    let font_family_cache = FontFamilyCache::global(cx);
326
327                                    for font_name in font_family_cache.list_font_families(cx) {
328                                        menu = menu.custom_entry(
329                                            {
330                                                let font_name = font_name.clone();
331                                                move |_window, _cx| {
332                                                    Label::new(font_name.clone()).into_any_element()
333                                                }
334                                            },
335                                            {
336                                                let font_name = font_name.clone();
337                                                move |_window, cx| {
338                                                    write_buffer_font_family(font_name.clone(), cx);
339                                                }
340                                            },
341                                        )
342                                    }
343
344                                    menu
345                                }),
346                            )
347                            .style(ui::DropdownStyle::Outlined)
348                            .full_width(true),
349                        )
350                        .child(
351                            NumericStepper::new(
352                                "buffer-font-size",
353                                buffer_font_size.to_string(),
354                                move |_, _, cx| {
355                                    write_buffer_font_size(buffer_font_size - px(1.), cx);
356                                },
357                                move |_, _, cx| {
358                                    write_buffer_font_size(buffer_font_size + px(1.), cx);
359                                },
360                            )
361                            .style(ui::NumericStepperStyle::Outlined),
362                        ),
363                ),
364        )
365}
366
367fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
368    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 ≠.";
369
370    v_flex()
371        .gap_5()
372        .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
373        .child(render_font_customization_section(window, cx))
374        .child(
375            SwitchField::new(
376                "onboarding-font-ligatures",
377                "Font Ligatures",
378                Some("Combine text characters into their associated symbols.".into()),
379                if read_font_ligatures(cx) {
380                    ui::ToggleState::Selected
381                } else {
382                    ui::ToggleState::Unselected
383                },
384                |toggle_state, _, cx| {
385                    write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
386                },
387            )
388            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
389        )
390        .child(SwitchField::new(
391            "onboarding-format-on-save",
392            "Format on Save",
393            Some("Format code automatically when saving.".into()),
394            if read_format_on_save(cx) {
395                ui::ToggleState::Selected
396            } else {
397                ui::ToggleState::Unselected
398            },
399            |toggle_state, _, cx| {
400                write_format_on_save(toggle_state == &ToggleState::Selected, cx);
401            },
402        ))
403        .child(SwitchField::new(
404            "onboarding-enable-inlay-hints",
405            "Inlay Hints",
406            Some("See parameter names for function and method calls inline.".into()),
407            if read_inlay_hints(cx) {
408                ui::ToggleState::Selected
409            } else {
410                ui::ToggleState::Unselected
411            },
412            |toggle_state, _, cx| {
413                write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
414            },
415        ))
416        .child(SwitchField::new(
417            "onboarding-git-blame-switch",
418            "Git Blame",
419            Some("See who committed each line on a given file.".into()),
420            if read_git_blame(cx) {
421                ui::ToggleState::Selected
422            } else {
423                ui::ToggleState::Unselected
424            },
425            |toggle_state, _, cx| {
426                set_git_blame(toggle_state == &ToggleState::Selected, cx);
427            },
428        ))
429        .child(
430            h_flex()
431                .items_start()
432                .justify_between()
433                .child(
434                    v_flex().child(Label::new("Mini Map")).child(
435                        Label::new("See a high-level overview of your source code.")
436                            .color(Color::Muted),
437                    ),
438                )
439                .child(
440                    ToggleButtonGroup::single_row(
441                        "onboarding-show-mini-map",
442                        [
443                            ToggleButtonSimple::new("Auto", |_, _, cx| {
444                                write_show_mini_map(ShowMinimap::Auto, cx);
445                            }),
446                            ToggleButtonSimple::new("Always", |_, _, cx| {
447                                write_show_mini_map(ShowMinimap::Always, cx);
448                            }),
449                            ToggleButtonSimple::new("Never", |_, _, cx| {
450                                write_show_mini_map(ShowMinimap::Never, cx);
451                            }),
452                        ],
453                    )
454                    .selected_index(match read_show_mini_map(cx) {
455                        ShowMinimap::Auto => 0,
456                        ShowMinimap::Always => 1,
457                        ShowMinimap::Never => 2,
458                    })
459                    .style(ToggleButtonGroupStyle::Outlined)
460                    .button_width(ui::rems_from_px(64.)),
461                ),
462        )
463}
464
465pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
466    v_flex()
467        .gap_4()
468        .child(render_import_settings_section(cx))
469        .child(render_popular_settings_section(window, cx))
470}