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 = "Ligatures are when a font creates a special character out of combining two characters into one. For example, with ligatures turned on, =/= would become ≠.";
588
589 v_flex()
590 .gap_5()
591 .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
592 .child(render_font_customization_section(tab_index, window, cx))
593 .child(
594 SwitchField::new(
595 "onboarding-font-ligatures",
596 "Font Ligatures",
597 Some("Combine text characters into their associated symbols.".into()),
598 if read_font_ligatures(cx) {
599 ui::ToggleState::Selected
600 } else {
601 ui::ToggleState::Unselected
602 },
603 |toggle_state, _, cx| {
604 write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
605 },
606 )
607 .tab_index({
608 *tab_index += 1;
609 *tab_index - 1
610 })
611 .tooltip(Tooltip::text(LIGATURE_TOOLTIP)),
612 )
613 .child(
614 SwitchField::new(
615 "onboarding-format-on-save",
616 "Format on Save",
617 Some("Format code automatically when saving.".into()),
618 if read_format_on_save(cx) {
619 ui::ToggleState::Selected
620 } else {
621 ui::ToggleState::Unselected
622 },
623 |toggle_state, _, cx| {
624 write_format_on_save(toggle_state == &ToggleState::Selected, cx);
625 },
626 )
627 .tab_index({
628 *tab_index += 1;
629 *tab_index - 1
630 }),
631 )
632 .child(
633 SwitchField::new(
634 "onboarding-enable-inlay-hints",
635 "Inlay Hints",
636 Some("See parameter names for function and method calls inline.".into()),
637 if read_inlay_hints(cx) {
638 ui::ToggleState::Selected
639 } else {
640 ui::ToggleState::Unselected
641 },
642 |toggle_state, _, cx| {
643 write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
644 },
645 )
646 .tab_index({
647 *tab_index += 1;
648 *tab_index - 1
649 }),
650 )
651 .child(
652 SwitchField::new(
653 "onboarding-git-blame-switch",
654 "Git Blame",
655 Some("See who committed each line on a given file.".into()),
656 if read_git_blame(cx) {
657 ui::ToggleState::Selected
658 } else {
659 ui::ToggleState::Unselected
660 },
661 |toggle_state, _, cx| {
662 set_git_blame(toggle_state == &ToggleState::Selected, cx);
663 },
664 )
665 .tab_index({
666 *tab_index += 1;
667 *tab_index - 1
668 }),
669 )
670 .child(
671 h_flex()
672 .items_start()
673 .justify_between()
674 .child(
675 v_flex().child(Label::new("Mini Map")).child(
676 Label::new("See a high-level overview of your source code.")
677 .color(Color::Muted),
678 ),
679 )
680 .child(
681 ToggleButtonGroup::single_row(
682 "onboarding-show-mini-map",
683 [
684 ToggleButtonSimple::new("Auto", |_, _, cx| {
685 write_show_mini_map(ShowMinimap::Auto, cx);
686 }),
687 ToggleButtonSimple::new("Always", |_, _, cx| {
688 write_show_mini_map(ShowMinimap::Always, cx);
689 }),
690 ToggleButtonSimple::new("Never", |_, _, cx| {
691 write_show_mini_map(ShowMinimap::Never, cx);
692 }),
693 ],
694 )
695 .selected_index(match read_show_mini_map(cx) {
696 ShowMinimap::Auto => 0,
697 ShowMinimap::Always => 1,
698 ShowMinimap::Never => 2,
699 })
700 .tab_index(tab_index)
701 .style(ToggleButtonGroupStyle::Outlined)
702 .button_width(ui::rems_from_px(64.)),
703 ),
704 )
705}
706
707pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
708 let mut tab_index = 0;
709 v_flex()
710 .gap_4()
711 .child(render_import_settings_section(&mut tab_index, cx))
712 .child(render_popular_settings_section(&mut tab_index, window, cx))
713}