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}