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, prelude::*,
13};
14
15use crate::{ImportCursorSettings, ImportVsCodeSettings};
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_import_settings_section() -> impl IntoElement {
169 v_flex()
170 .gap_4()
171 .child(
172 v_flex()
173 .child(Label::new("Import Settings").size(LabelSize::Large))
174 .child(
175 Label::new("Automatically pull your settings from other editors.")
176 .color(Color::Muted),
177 ),
178 )
179 .child(
180 h_flex()
181 .w_full()
182 .gap_4()
183 .child(
184 h_flex().w_full().child(
185 ButtonLike::new("import_vs_code")
186 .full_width()
187 .style(ButtonStyle::Outlined)
188 .size(ButtonSize::Large)
189 .child(
190 h_flex()
191 .w_full()
192 .gap_1p5()
193 .px_1()
194 .child(
195 Icon::new(IconName::EditorVsCode)
196 .color(Color::Muted)
197 .size(IconSize::XSmall),
198 )
199 .child(Label::new("VS Code")),
200 )
201 .on_click(|_, window, cx| {
202 window.dispatch_action(
203 ImportVsCodeSettings::default().boxed_clone(),
204 cx,
205 )
206 }),
207 ),
208 )
209 .child(
210 h_flex().w_full().child(
211 ButtonLike::new("import_cursor")
212 .full_width()
213 .style(ButtonStyle::Outlined)
214 .size(ButtonSize::Large)
215 .child(
216 h_flex()
217 .w_full()
218 .gap_1p5()
219 .px_1()
220 .child(
221 Icon::new(IconName::EditorCursor)
222 .color(Color::Muted)
223 .size(IconSize::XSmall),
224 )
225 .child(Label::new("Cursor")),
226 )
227 .on_click(|_, window, cx| {
228 window.dispatch_action(
229 ImportCursorSettings::default().boxed_clone(),
230 cx,
231 )
232 }),
233 ),
234 ),
235 )
236}
237
238fn render_font_customization_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
239 let theme_settings = ThemeSettings::get_global(cx);
240 let ui_font_size = theme_settings.ui_font_size(cx);
241 let font_family = theme_settings.buffer_font.family.clone();
242 let buffer_font_size = theme_settings.buffer_font_size(cx);
243
244 h_flex()
245 .w_full()
246 .gap_4()
247 .child(
248 v_flex()
249 .w_full()
250 .gap_1()
251 .child(Label::new("UI Font"))
252 .child(
253 h_flex()
254 .w_full()
255 .justify_between()
256 .gap_2()
257 .child(
258 DropdownMenu::new(
259 "ui-font-family",
260 theme_settings.ui_font.family.clone(),
261 ContextMenu::build(window, cx, |mut menu, _, cx| {
262 let font_family_cache = FontFamilyCache::global(cx);
263
264 for font_name in font_family_cache.list_font_families(cx) {
265 menu = menu.custom_entry(
266 {
267 let font_name = font_name.clone();
268 move |_window, _cx| {
269 Label::new(font_name.clone()).into_any_element()
270 }
271 },
272 {
273 let font_name = font_name.clone();
274 move |_window, cx| {
275 write_ui_font_family(font_name.clone(), cx);
276 }
277 },
278 )
279 }
280
281 menu
282 }),
283 )
284 .style(ui::DropdownStyle::Outlined)
285 .full_width(true),
286 )
287 .child(
288 NumericStepper::new(
289 "ui-font-size",
290 ui_font_size.to_string(),
291 move |_, _, cx| {
292 write_ui_font_size(ui_font_size - px(1.), cx);
293 },
294 move |_, _, cx| {
295 write_ui_font_size(ui_font_size + px(1.), cx);
296 },
297 )
298 .style(ui::NumericStepperStyle::Outlined),
299 ),
300 ),
301 )
302 .child(
303 v_flex()
304 .w_full()
305 .gap_1()
306 .child(Label::new("Editor Font"))
307 .child(
308 h_flex()
309 .w_full()
310 .justify_between()
311 .gap_2()
312 .child(
313 DropdownMenu::new(
314 "buffer-font-family",
315 font_family,
316 ContextMenu::build(window, cx, |mut menu, _, cx| {
317 let font_family_cache = FontFamilyCache::global(cx);
318
319 for font_name in font_family_cache.list_font_families(cx) {
320 menu = menu.custom_entry(
321 {
322 let font_name = font_name.clone();
323 move |_window, _cx| {
324 Label::new(font_name.clone()).into_any_element()
325 }
326 },
327 {
328 let font_name = font_name.clone();
329 move |_window, cx| {
330 write_buffer_font_family(font_name.clone(), cx);
331 }
332 },
333 )
334 }
335
336 menu
337 }),
338 )
339 .style(ui::DropdownStyle::Outlined)
340 .full_width(true),
341 )
342 .child(
343 NumericStepper::new(
344 "buffer-font-size",
345 buffer_font_size.to_string(),
346 move |_, _, cx| {
347 write_buffer_font_size(buffer_font_size - px(1.), cx);
348 },
349 move |_, _, cx| {
350 write_buffer_font_size(buffer_font_size + px(1.), cx);
351 },
352 )
353 .style(ui::NumericStepperStyle::Outlined),
354 ),
355 ),
356 )
357}
358
359fn render_popular_settings_section(window: &mut Window, cx: &mut App) -> impl IntoElement {
360 v_flex()
361 .gap_5()
362 .child(Label::new("Popular Settings").size(LabelSize::Large).mt_8())
363 .child(render_font_customization_section(window, cx))
364 .child(SwitchField::new(
365 "onboarding-font-ligatures",
366 "Font Ligatures",
367 Some("Combine text characters into their associated symbols.".into()),
368 if read_font_ligatures(cx) {
369 ui::ToggleState::Selected
370 } else {
371 ui::ToggleState::Unselected
372 },
373 |toggle_state, _, cx| {
374 write_font_ligatures(toggle_state == &ToggleState::Selected, cx);
375 },
376 ))
377 .child(SwitchField::new(
378 "onboarding-format-on-save",
379 "Format on Save",
380 Some("Format code automatically when saving.".into()),
381 if read_format_on_save(cx) {
382 ui::ToggleState::Selected
383 } else {
384 ui::ToggleState::Unselected
385 },
386 |toggle_state, _, cx| {
387 write_format_on_save(toggle_state == &ToggleState::Selected, cx);
388 },
389 ))
390 .child(
391 h_flex()
392 .items_start()
393 .justify_between()
394 .child(
395 v_flex().child(Label::new("Mini Map")).child(
396 Label::new("See a high-level overview of your source code.")
397 .color(Color::Muted),
398 ),
399 )
400 .child(
401 ToggleButtonGroup::single_row(
402 "onboarding-show-mini-map",
403 [
404 ToggleButtonSimple::new("Auto", |_, _, cx| {
405 write_show_mini_map(ShowMinimap::Auto, cx);
406 }),
407 ToggleButtonSimple::new("Always", |_, _, cx| {
408 write_show_mini_map(ShowMinimap::Always, cx);
409 }),
410 ToggleButtonSimple::new("Never", |_, _, cx| {
411 write_show_mini_map(ShowMinimap::Never, cx);
412 }),
413 ],
414 )
415 .selected_index(match read_show_mini_map(cx) {
416 ShowMinimap::Auto => 0,
417 ShowMinimap::Always => 1,
418 ShowMinimap::Never => 2,
419 })
420 .style(ToggleButtonGroupStyle::Outlined)
421 .button_width(ui::rems_from_px(64.)),
422 ),
423 )
424 .child(SwitchField::new(
425 "onboarding-enable-inlay-hints",
426 "Inlay Hints",
427 Some("See parameter names for function and method calls inline.".into()),
428 if read_inlay_hints(cx) {
429 ui::ToggleState::Selected
430 } else {
431 ui::ToggleState::Unselected
432 },
433 |toggle_state, _, cx| {
434 write_inlay_hints(toggle_state == &ToggleState::Selected, cx);
435 },
436 ))
437 .child(SwitchField::new(
438 "onboarding-git-blame-switch",
439 "Git Blame",
440 Some("See who committed each line on a given file.".into()),
441 if read_git_blame(cx) {
442 ui::ToggleState::Selected
443 } else {
444 ui::ToggleState::Unselected
445 },
446 |toggle_state, _, cx| {
447 set_git_blame(toggle_state == &ToggleState::Selected, cx);
448 },
449 ))
450}
451
452pub(crate) fn render_editing_page(window: &mut Window, cx: &mut App) -> impl IntoElement {
453 v_flex()
454 .gap_4()
455 .child(render_import_settings_section())
456 .child(render_popular_settings_section(window, cx))
457}