editing_page.rs

  1use std::sync::Arc;
  2
  3use editor::{EditorSettings, ShowMinimap};
  4use fs::Fs;
  5use gpui::{Action, App, FontFeatures, IntoElement, Pixels, SharedString, Window};
  6use language::language_settings::{AllLanguageSettings, FormatOnSave};
  7use project::project_settings::ProjectSettings;
  8use settings::{Settings as _, update_settings_file};
  9use theme::{FontFamilyName, ThemeSettings};
 10use ui::{
 11    ButtonLike, PopoverMenu, SwitchField, ToggleButtonGroup, ToggleButtonGroupStyle,
 12    ToggleButtonSimple, ToggleState, Tooltip, prelude::*,
 13};
 14use ui_input::{NumericStepper, font_picker};
 15
 16use crate::{ImportCursorSettings, ImportVsCodeSettings, SettingsImportState};
 17
 18fn read_show_mini_map(cx: &App) -> ShowMinimap {
 19    editor::EditorSettings::get_global(cx).minimap.show
 20}
 21
 22fn write_show_mini_map(show: ShowMinimap, cx: &mut App) {
 23    let fs = <dyn Fs>::global(cx);
 24
 25    // This is used to speed up the UI
 26    // the UI reads the current values to get what toggle state to show on buttons
 27    // there's a slight delay if we just call update_settings_file so we manually set
 28    // the value here then call update_settings file to get around the delay
 29    let mut curr_settings = EditorSettings::get_global(cx).clone();
 30    curr_settings.minimap.show = show;
 31    EditorSettings::override_global(curr_settings, cx);
 32
 33    update_settings_file(fs, cx, move |settings, _| {
 34        telemetry::event!(
 35            "Welcome Minimap Clicked",
 36            from = settings.editor.minimap.clone().unwrap_or_default(),
 37            to = show
 38        );
 39        settings.editor.minimap.get_or_insert_default().show = Some(show);
 40    });
 41}
 42
 43fn read_inlay_hints(cx: &App) -> bool {
 44    AllLanguageSettings::get_global(cx)
 45        .defaults
 46        .inlay_hints
 47        .enabled
 48}
 49
 50fn write_inlay_hints(enabled: bool, cx: &mut App) {
 51    let fs = <dyn Fs>::global(cx);
 52
 53    let mut curr_settings = AllLanguageSettings::get_global(cx).clone();
 54    curr_settings.defaults.inlay_hints.enabled = enabled;
 55    AllLanguageSettings::override_global(curr_settings, cx);
 56
 57    update_settings_file(fs, cx, move |settings, _cx| {
 58        settings
 59            .project
 60            .all_languages
 61            .defaults
 62            .inlay_hints
 63            .get_or_insert_default()
 64            .enabled = Some(enabled);
 65    });
 66}
 67
 68fn read_git_blame(cx: &App) -> bool {
 69    ProjectSettings::get_global(cx).git.inline_blame.enabled
 70}
 71
 72fn write_git_blame(enabled: bool, cx: &mut App) {
 73    let fs = <dyn Fs>::global(cx);
 74
 75    let mut curr_settings = ProjectSettings::get_global(cx).clone();
 76    curr_settings.git.inline_blame.enabled = enabled;
 77    ProjectSettings::override_global(curr_settings, cx);
 78
 79    update_settings_file(fs, cx, move |settings, _| {
 80        settings
 81            .git
 82            .get_or_insert_default()
 83            .inline_blame
 84            .get_or_insert_default()
 85            .enabled = Some(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(fs, cx, move |settings, _| {
 93        telemetry::event!(
 94            "Welcome Font Changed",
 95            type = "ui font",
 96            old = settings.theme.ui_font_family,
 97            new = font
 98        );
 99        settings.theme.ui_font_family = Some(FontFamilyName(font.into()));
100    });
101}
102
103fn write_ui_font_size(size: Pixels, cx: &mut App) {
104    let fs = <dyn Fs>::global(cx);
105
106    update_settings_file(fs, cx, move |settings, _| {
107        settings.theme.ui_font_size = Some(size.into());
108    });
109}
110
111fn write_buffer_font_size(size: Pixels, cx: &mut App) {
112    let fs = <dyn Fs>::global(cx);
113
114    update_settings_file(fs, cx, move |settings, _| {
115        settings.theme.buffer_font_size = Some(size.into());
116    });
117}
118
119fn write_buffer_font_family(font_family: SharedString, cx: &mut App) {
120    let fs = <dyn Fs>::global(cx);
121
122    update_settings_file(fs, cx, move |settings, _| {
123        telemetry::event!(
124            "Welcome Font Changed",
125            type = "editor font",
126            old = settings.theme.buffer_font_family,
127            new = font_family
128        );
129
130        settings.theme.buffer_font_family = Some(FontFamilyName(font_family.into()));
131    });
132}
133
134fn read_font_ligatures(cx: &App) -> bool {
135    ThemeSettings::get_global(cx)
136        .buffer_font
137        .features
138        .is_calt_enabled()
139        .unwrap_or(true)
140}
141
142fn write_font_ligatures(enabled: bool, cx: &mut App) {
143    let fs = <dyn Fs>::global(cx);
144    let bit = if enabled { 1 } else { 0 };
145
146    update_settings_file(fs, cx, move |settings, _| {
147        let mut features = settings
148            .theme
149            .buffer_font_features
150            .as_mut()
151            .map(|features| features.tag_value_list().to_vec())
152            .unwrap_or_default();
153
154        if let Some(calt_index) = features.iter().position(|(tag, _)| tag == "calt") {
155            features[calt_index].1 = bit;
156        } else {
157            features.push(("calt".into(), bit));
158        }
159
160        settings.theme.buffer_font_features = Some(FontFeatures(Arc::new(features)));
161    });
162}
163
164fn read_format_on_save(cx: &App) -> bool {
165    match AllLanguageSettings::get_global(cx).defaults.format_on_save {
166        FormatOnSave::On => true,
167        FormatOnSave::Off => false,
168    }
169}
170
171fn write_format_on_save(format_on_save: bool, cx: &mut App) {
172    let fs = <dyn Fs>::global(cx);
173
174    update_settings_file(fs, cx, move |settings, _| {
175        settings.project.all_languages.defaults.format_on_save = Some(match format_on_save {
176            true => FormatOnSave::On,
177            false => FormatOnSave::Off,
178        });
179    });
180}
181
182fn render_setting_import_button(
183    tab_index: isize,
184    label: SharedString,
185    icon_name: IconName,
186    action: &dyn Action,
187    imported: bool,
188) -> impl IntoElement {
189    let action = action.boxed_clone();
190    h_flex().w_full().child(
191        ButtonLike::new(label.clone())
192            .full_width()
193            .style(ButtonStyle::Outlined)
194            .size(ButtonSize::Large)
195            .tab_index(tab_index)
196            .child(
197                h_flex()
198                    .w_full()
199                    .justify_between()
200                    .child(
201                        h_flex()
202                            .gap_1p5()
203                            .px_1()
204                            .child(
205                                Icon::new(icon_name)
206                                    .color(Color::Muted)
207                                    .size(IconSize::XSmall),
208                            )
209                            .child(Label::new(label.clone())),
210                    )
211                    .when(imported, |this| {
212                        this.child(
213                            h_flex()
214                                .gap_1p5()
215                                .child(
216                                    Icon::new(IconName::Check)
217                                        .color(Color::Success)
218                                        .size(IconSize::XSmall),
219                                )
220                                .child(Label::new("Imported").size(LabelSize::Small)),
221                        )
222                    }),
223            )
224            .on_click(move |_, window, cx| {
225                telemetry::event!("Welcome Import Settings", import_source = label,);
226                window.dispatch_action(action.boxed_clone(), cx);
227            }),
228    )
229}
230
231fn render_import_settings_section(tab_index: &mut isize, cx: &App) -> impl IntoElement {
232    let import_state = SettingsImportState::global(cx);
233    let imports: [(SharedString, IconName, &dyn Action, bool); 2] = [
234        (
235            "VS Code".into(),
236            IconName::EditorVsCode,
237            &ImportVsCodeSettings { skip_prompt: false },
238            import_state.vscode,
239        ),
240        (
241            "Cursor".into(),
242            IconName::EditorCursor,
243            &ImportCursorSettings { skip_prompt: false },
244            import_state.cursor,
245        ),
246    ];
247
248    let [vscode, cursor] = imports.map(|(label, icon_name, action, imported)| {
249        *tab_index += 1;
250        render_setting_import_button(*tab_index - 1, label, icon_name, action, imported)
251    });
252
253    v_flex()
254        .gap_4()
255        .child(
256            v_flex()
257                .child(Label::new("Import Settings").size(LabelSize::Large))
258                .child(
259                    Label::new("Automatically pull your settings from other editors.")
260                        .color(Color::Muted),
261                ),
262        )
263        .child(h_flex().w_full().gap_4().child(vscode).child(cursor))
264}
265
266fn render_font_customization_section(
267    tab_index: &mut isize,
268    window: &mut Window,
269    cx: &mut App,
270) -> impl IntoElement {
271    let theme_settings = ThemeSettings::get_global(cx);
272    let ui_font_size = theme_settings.ui_font_size(cx);
273    let ui_font_family = theme_settings.ui_font.family.clone();
274    let buffer_font_family = theme_settings.buffer_font.family.clone();
275    let buffer_font_size = theme_settings.buffer_font_size(cx);
276
277    let ui_font_picker =
278        cx.new(|cx| font_picker(ui_font_family.clone(), write_ui_font_family, window, cx));
279
280    let buffer_font_picker = cx.new(|cx| {
281        font_picker(
282            buffer_font_family.clone(),
283            write_buffer_font_family,
284            window,
285            cx,
286        )
287    });
288
289    let ui_font_handle = ui::PopoverMenuHandle::default();
290    let buffer_font_handle = ui::PopoverMenuHandle::default();
291
292    h_flex()
293        .w_full()
294        .gap_4()
295        .child(
296            v_flex()
297                .w_full()
298                .gap_1()
299                .child(Label::new("UI Font"))
300                .child(
301                    h_flex()
302                        .w_full()
303                        .justify_between()
304                        .gap_2()
305                        .child(
306                            PopoverMenu::new("ui-font-picker")
307                                .menu({
308                                    let ui_font_picker = ui_font_picker;
309                                    move |_window, _cx| Some(ui_font_picker.clone())
310                                })
311                                .trigger(
312                                    ButtonLike::new("ui-font-family-button")
313                                        .style(ButtonStyle::Outlined)
314                                        .size(ButtonSize::Medium)
315                                        .full_width()
316                                        .tab_index({
317                                            *tab_index += 1;
318                                            *tab_index - 1
319                                        })
320                                        .child(
321                                            h_flex()
322                                                .w_full()
323                                                .justify_between()
324                                                .child(Label::new(ui_font_family))
325                                                .child(
326                                                    Icon::new(IconName::ChevronUpDown)
327                                                        .color(Color::Muted)
328                                                        .size(IconSize::XSmall),
329                                                ),
330                                        ),
331                                )
332                                .full_width(true)
333                                .anchor(gpui::Corner::TopLeft)
334                                .offset(gpui::Point {
335                                    x: px(0.0),
336                                    y: px(4.0),
337                                })
338                                .with_handle(ui_font_handle),
339                        )
340                        .child(font_picker_stepper(
341                            "ui-font-size",
342                            &ui_font_size,
343                            tab_index,
344                            write_ui_font_size,
345                            window,
346                            cx,
347                        )),
348                ),
349        )
350        .child(
351            v_flex()
352                .w_full()
353                .gap_1()
354                .child(Label::new("Editor Font"))
355                .child(
356                    h_flex()
357                        .w_full()
358                        .justify_between()
359                        .gap_2()
360                        .child(
361                            PopoverMenu::new("buffer-font-picker")
362                                .menu({
363                                    let buffer_font_picker = buffer_font_picker;
364                                    move |_window, _cx| Some(buffer_font_picker.clone())
365                                })
366                                .trigger(
367                                    ButtonLike::new("buffer-font-family-button")
368                                        .style(ButtonStyle::Outlined)
369                                        .size(ButtonSize::Medium)
370                                        .full_width()
371                                        .tab_index({
372                                            *tab_index += 1;
373                                            *tab_index - 1
374                                        })
375                                        .child(
376                                            h_flex()
377                                                .w_full()
378                                                .justify_between()
379                                                .child(Label::new(buffer_font_family))
380                                                .child(
381                                                    Icon::new(IconName::ChevronUpDown)
382                                                        .color(Color::Muted)
383                                                        .size(IconSize::XSmall),
384                                                ),
385                                        ),
386                                )
387                                .full_width(true)
388                                .anchor(gpui::Corner::TopLeft)
389                                .offset(gpui::Point {
390                                    x: px(0.0),
391                                    y: px(4.0),
392                                })
393                                .with_handle(buffer_font_handle),
394                        )
395                        .child(font_picker_stepper(
396                            "buffer-font-size",
397                            &buffer_font_size,
398                            tab_index,
399                            write_buffer_font_size,
400                            window,
401                            cx,
402                        )),
403                ),
404        )
405}
406
407fn font_picker_stepper(
408    id: &'static str,
409    font_size: &Pixels,
410    tab_index: &mut isize,
411    write_font_size: fn(Pixels, &mut App),
412    window: &mut Window,
413    cx: &mut App,
414) -> NumericStepper<u32> {
415    window.with_id(id, |window| {
416        let optimistic_font_size: gpui::Entity<Option<u32>> = window.use_state(cx, |_, _| None);
417        optimistic_font_size.update(cx, |optimistic_font_size, _| {
418            if let Some(optimistic_font_size_val) = optimistic_font_size {
419                if *optimistic_font_size_val == u32::from(font_size) {
420                    *optimistic_font_size = None;
421                }
422            }
423        });
424
425        let stepper_font_size = optimistic_font_size
426            .read(cx)
427            .unwrap_or_else(|| font_size.into());
428
429        NumericStepper::new(
430            SharedString::new(format!("{}-stepper", id)),
431            stepper_font_size,
432            window,
433            cx,
434        )
435        .on_change(move |new_value, _, cx| {
436            optimistic_font_size.write(cx, Some(*new_value));
437            write_font_size(Pixels::from(*new_value), cx);
438        })
439        .format(|value| format!("{value}px"))
440        .style(ui_input::NumericStepperStyle::Outlined)
441        .tab_index({
442            *tab_index += 2;
443            *tab_index - 2
444        })
445        .min(6)
446        .max(32)
447    })
448}
449
450fn render_popular_settings_section(
451    tab_index: &mut isize,
452    window: &mut Window,
453    cx: &mut App,
454) -> impl IntoElement {
455    const LIGATURE_TOOLTIP: &str =
456        "Font ligatures combine two characters into one. For example, turning != into ≠.";
457
458    v_flex()
459        .pt_6()
460        .gap_4()
461        .border_t_1()
462        .border_color(cx.theme().colors().border_variant.opacity(0.5))
463        .child(Label::new("Popular Settings").size(LabelSize::Large))
464        .child(render_font_customization_section(tab_index, window, cx))
465        .child(
466            SwitchField::new(
467                "onboarding-font-ligatures",
468                "Font Ligatures",
469                Some("Combine text characters into their associated symbols.".into()),
470                if read_font_ligatures(cx) {
471                    ui::ToggleState::Selected
472                } else {
473                    ui::ToggleState::Unselected
474                },
475                |toggle_state, _, cx| {
476                    let enabled = toggle_state == &ToggleState::Selected;
477                    telemetry::event!(
478                        "Welcome Font Ligature",
479                        options = if enabled { "on" } else { "off" },
480                    );
481
482                    write_font_ligatures(enabled, cx);
483                },
484            )
485            .tab_index({
486                *tab_index += 1;
487                *tab_index - 1
488            })
489            .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
490        )
491        .child(
492            SwitchField::new(
493                "onboarding-format-on-save",
494                "Format on Save",
495                Some("Format code automatically when saving.".into()),
496                if read_format_on_save(cx) {
497                    ui::ToggleState::Selected
498                } else {
499                    ui::ToggleState::Unselected
500                },
501                |toggle_state, _, cx| {
502                    let enabled = toggle_state == &ToggleState::Selected;
503                    telemetry::event!(
504                        "Welcome Format On Save Changed",
505                        options = if enabled { "on" } else { "off" },
506                    );
507
508                    write_format_on_save(enabled, cx);
509                },
510            )
511            .tab_index({
512                *tab_index += 1;
513                *tab_index - 1
514            }),
515        )
516        .child(
517            SwitchField::new(
518                "onboarding-enable-inlay-hints",
519                "Inlay Hints",
520                Some("See parameter names for function and method calls inline.".into()),
521                if read_inlay_hints(cx) {
522                    ui::ToggleState::Selected
523                } else {
524                    ui::ToggleState::Unselected
525                },
526                |toggle_state, _, cx| {
527                    let enabled = toggle_state == &ToggleState::Selected;
528                    telemetry::event!(
529                        "Welcome Inlay Hints Changed",
530                        options = if enabled { "on" } else { "off" },
531                    );
532
533                    write_inlay_hints(enabled, cx);
534                },
535            )
536            .tab_index({
537                *tab_index += 1;
538                *tab_index - 1
539            }),
540        )
541        .child(
542            SwitchField::new(
543                "onboarding-git-blame-switch",
544                "Inline Git Blame",
545                Some("See who committed each line on a given file.".into()),
546                if read_git_blame(cx) {
547                    ui::ToggleState::Selected
548                } else {
549                    ui::ToggleState::Unselected
550                },
551                |toggle_state, _, cx| {
552                    let enabled = toggle_state == &ToggleState::Selected;
553                    telemetry::event!(
554                        "Welcome Git Blame Changed",
555                        options = if enabled { "on" } else { "off" },
556                    );
557
558                    write_git_blame(enabled, cx);
559                },
560            )
561            .tab_index({
562                *tab_index += 1;
563                *tab_index - 1
564            }),
565        )
566        .child(
567            h_flex()
568                .items_start()
569                .justify_between()
570                .child(
571                    v_flex().child(Label::new("Minimap")).child(
572                        Label::new("See a high-level overview of your source code.")
573                            .color(Color::Muted),
574                    ),
575                )
576                .child(
577                    ToggleButtonGroup::single_row(
578                        "onboarding-show-mini-map",
579                        [
580                            ToggleButtonSimple::new("Auto", |_, _, cx| {
581                                write_show_mini_map(ShowMinimap::Auto, cx);
582                            })
583                            .tooltip(Tooltip::text(
584                                "Show the minimap if the editor's scrollbar is visible.",
585                            )),
586                            ToggleButtonSimple::new("Always", |_, _, cx| {
587                                write_show_mini_map(ShowMinimap::Always, cx);
588                            }),
589                            ToggleButtonSimple::new("Never", |_, _, cx| {
590                                write_show_mini_map(ShowMinimap::Never, cx);
591                            }),
592                        ],
593                    )
594                    .selected_index(match read_show_mini_map(cx) {
595                        ShowMinimap::Auto => 0,
596                        ShowMinimap::Always => 1,
597                        ShowMinimap::Never => 2,
598                    })
599                    .tab_index(tab_index)
600                    .style(ToggleButtonGroupStyle::Outlined)
601                    .width(ui::rems_from_px(3. * 64.)),
602                ),
603        )
604}
605
606pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
607    let mut tab_index = 0;
608    v_flex()
609        .gap_6()
610        .child(render_import_settings_section(&mut tab_index, cx))
611        .child(render_popular_settings_section(&mut tab_index, window, cx))
612}