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}