1//! # settings_ui
2mod components;
3use anyhow::Result;
4use editor::{Editor, EditorEvent};
5use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
6use fuzzy::StringMatchCandidate;
7use gpui::{
8 App, Div, Entity, Focusable, FontWeight, Global, ReadGlobal as _, ScrollHandle, Task,
9 TitlebarOptions, UniformListScrollHandle, Window, WindowHandle, WindowOptions, div, point,
10 prelude::*, px, size, uniform_list,
11};
12use project::WorktreeId;
13use settings::{
14 BottomDockLayout, CloseWindowWhenNoItems, CodeFade, CursorShape, OnLastWindowClosed,
15 RestoreOnStartupBehavior, SaturatingBool, SettingsContent, SettingsStore,
16};
17use std::{
18 any::{Any, TypeId, type_name},
19 cell::RefCell,
20 collections::HashMap,
21 num::NonZeroU32,
22 ops::Range,
23 rc::Rc,
24 sync::{Arc, atomic::AtomicBool},
25};
26use ui::{
27 ButtonLike, ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, PopoverMenu,
28 Switch, SwitchColor, TreeViewItem, WithScrollbar, prelude::*,
29};
30use ui_input::{NumericStepper, NumericStepperStyle, NumericStepperType};
31use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
32use zed_actions::OpenSettingsEditor;
33
34use crate::components::SettingsEditor;
35
36#[derive(Clone, Copy)]
37struct SettingField<T: 'static> {
38 pick: fn(&SettingsContent) -> &Option<T>,
39 pick_mut: fn(&mut SettingsContent) -> &mut Option<T>,
40}
41
42trait AnySettingField {
43 fn as_any(&self) -> &dyn Any;
44 fn type_name(&self) -> &'static str;
45 fn type_id(&self) -> TypeId;
46 fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile;
47}
48
49impl<T> AnySettingField for SettingField<T> {
50 fn as_any(&self) -> &dyn Any {
51 self
52 }
53
54 fn type_name(&self) -> &'static str {
55 type_name::<T>()
56 }
57
58 fn type_id(&self) -> TypeId {
59 TypeId::of::<T>()
60 }
61
62 fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> settings::SettingsFile {
63 let (file, _) = cx
64 .global::<SettingsStore>()
65 .get_value_from_file(file.to_settings(), self.pick);
66 return file;
67 }
68}
69
70#[derive(Default, Clone)]
71struct SettingFieldRenderer {
72 renderers: Rc<
73 RefCell<
74 HashMap<
75 TypeId,
76 Box<
77 dyn Fn(
78 &dyn AnySettingField,
79 SettingsUiFile,
80 Option<&SettingsFieldMetadata>,
81 &mut Window,
82 &mut App,
83 ) -> AnyElement,
84 >,
85 >,
86 >,
87 >,
88}
89
90impl Global for SettingFieldRenderer {}
91
92impl SettingFieldRenderer {
93 fn add_renderer<T: 'static>(
94 &mut self,
95 renderer: impl Fn(
96 &SettingField<T>,
97 SettingsUiFile,
98 Option<&SettingsFieldMetadata>,
99 &mut Window,
100 &mut App,
101 ) -> AnyElement
102 + 'static,
103 ) -> &mut Self {
104 let key = TypeId::of::<T>();
105 let renderer = Box::new(
106 move |any_setting_field: &dyn AnySettingField,
107 settings_file: SettingsUiFile,
108 metadata: Option<&SettingsFieldMetadata>,
109 window: &mut Window,
110 cx: &mut App| {
111 let field = any_setting_field
112 .as_any()
113 .downcast_ref::<SettingField<T>>()
114 .unwrap();
115 renderer(field, settings_file, metadata, window, cx)
116 },
117 );
118 self.renderers.borrow_mut().insert(key, renderer);
119 self
120 }
121
122 fn render(
123 &self,
124 any_setting_field: &dyn AnySettingField,
125 settings_file: SettingsUiFile,
126 metadata: Option<&SettingsFieldMetadata>,
127 window: &mut Window,
128 cx: &mut App,
129 ) -> AnyElement {
130 let key = any_setting_field.type_id();
131 if let Some(renderer) = self.renderers.borrow().get(&key) {
132 renderer(any_setting_field, settings_file, metadata, window, cx)
133 } else {
134 panic!(
135 "No renderer found for type: {}",
136 any_setting_field.type_name()
137 )
138 }
139 }
140}
141
142struct SettingsFieldMetadata {
143 placeholder: Option<&'static str>,
144}
145
146fn user_settings_data() -> Vec<SettingsPage> {
147 vec![
148 SettingsPage {
149 title: "General Page",
150 items: vec![
151 SettingsPageItem::SectionHeader("General"),
152 SettingsPageItem::SettingItem(SettingItem {
153 title: "Confirm Quit",
154 description: "Whether to confirm before quitting Zed",
155 field: Box::new(SettingField {
156 pick: |settings_content| &settings_content.workspace.confirm_quit,
157 pick_mut: |settings_content| &mut settings_content.workspace.confirm_quit,
158 }),
159 metadata: None,
160 }),
161 SettingsPageItem::SettingItem(SettingItem {
162 title: "Restore On Startup",
163 description: "Whether to restore previous session when opening Zed",
164 field: Box::new(SettingField {
165 pick: |settings_content| &settings_content.workspace.restore_on_startup,
166 pick_mut: |settings_content| {
167 &mut settings_content.workspace.restore_on_startup
168 },
169 }),
170 metadata: None,
171 }),
172 SettingsPageItem::SettingItem(SettingItem {
173 title: "Restore File State",
174 description: "Whether to restore previous file state when reopening",
175 field: Box::new(SettingField {
176 pick: |settings_content| &settings_content.workspace.restore_on_file_reopen,
177 pick_mut: |settings_content| {
178 &mut settings_content.workspace.restore_on_file_reopen
179 },
180 }),
181 metadata: None,
182 }),
183 SettingsPageItem::SettingItem(SettingItem {
184 title: "Close on File Delete",
185 description: "Whether to automatically close files that have been deleted",
186 field: Box::new(SettingField {
187 pick: |settings_content| &settings_content.workspace.close_on_file_delete,
188 pick_mut: |settings_content| {
189 &mut settings_content.workspace.close_on_file_delete
190 },
191 }),
192 metadata: None,
193 }),
194 SettingsPageItem::SettingItem(SettingItem {
195 title: "When Closing With No Tabs",
196 description: "What to do when using 'close active item' with no tabs",
197 field: Box::new(SettingField {
198 pick: |settings_content| {
199 &settings_content.workspace.when_closing_with_no_tabs
200 },
201 pick_mut: |settings_content| {
202 &mut settings_content.workspace.when_closing_with_no_tabs
203 },
204 }),
205 metadata: None,
206 }),
207 SettingsPageItem::SettingItem(SettingItem {
208 title: "On Last Window Closed",
209 description: "What to do when the last window is closed",
210 field: Box::new(SettingField {
211 pick: |settings_content| &settings_content.workspace.on_last_window_closed,
212 pick_mut: |settings_content| {
213 &mut settings_content.workspace.on_last_window_closed
214 },
215 }),
216 metadata: None,
217 }),
218 SettingsPageItem::SettingItem(SettingItem {
219 title: "Use System Path Prompts",
220 description: "Whether to use system dialogs for Open and Save As",
221 field: Box::new(SettingField {
222 pick: |settings_content| {
223 &settings_content.workspace.use_system_path_prompts
224 },
225 pick_mut: |settings_content| {
226 &mut settings_content.workspace.use_system_path_prompts
227 },
228 }),
229 metadata: None,
230 }),
231 SettingsPageItem::SettingItem(SettingItem {
232 title: "Use System Prompts",
233 description: "Whether to use system prompts for confirmations",
234 field: Box::new(SettingField {
235 pick: |settings_content| &settings_content.workspace.use_system_prompts,
236 pick_mut: |settings_content| {
237 &mut settings_content.workspace.use_system_prompts
238 },
239 }),
240 metadata: None,
241 }),
242 SettingsPageItem::SectionHeader("Scoped Settings"),
243 // todo(settings_ui): Implement another setting item type that just shows an edit in settings.json
244 // SettingsPageItem::SettingItem(SettingItem {
245 // title: "Preview Channel",
246 // description: "Which settings should be activated only in Preview build of Zed",
247 // field: Box::new(SettingField {
248 // pick: |settings_content| &settings_content.workspace.use_system_prompts,
249 // pick_mut: |settings_content| {
250 // &mut settings_content.workspace.use_system_prompts
251 // },
252 // }),
253 // metadata: None,
254 // }),
255 // SettingsPageItem::SettingItem(SettingItem {
256 // title: "Settings Profiles",
257 // description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.",
258 // field: Box::new(SettingField {
259 // pick: |settings_content| &settings_content.workspace.use_system_prompts,
260 // pick_mut: |settings_content| {
261 // &mut settings_content.workspace.use_system_prompts
262 // },
263 // }),
264 // metadata: None,
265 // }),
266 SettingsPageItem::SectionHeader("Privacy"),
267 SettingsPageItem::SettingItem(SettingItem {
268 title: "Telemetry Diagnostics",
269 description: "Send debug info like crash reports.",
270 field: Box::new(SettingField {
271 pick: |settings_content| {
272 if let Some(telemetry) = &settings_content.telemetry {
273 &telemetry.diagnostics
274 } else {
275 &None
276 }
277 },
278 pick_mut: |settings_content| {
279 &mut settings_content
280 .telemetry
281 .get_or_insert_default()
282 .diagnostics
283 },
284 }),
285 metadata: None,
286 }),
287 SettingsPageItem::SettingItem(SettingItem {
288 title: "Telemetry Metrics",
289 description: "Send anonymized usage data like what languages you're using Zed with.",
290 field: Box::new(SettingField {
291 pick: |settings_content| {
292 if let Some(telemetry) = &settings_content.telemetry {
293 &telemetry.metrics
294 } else {
295 &None
296 }
297 },
298 pick_mut: |settings_content| {
299 &mut settings_content.telemetry.get_or_insert_default().metrics
300 },
301 }),
302 metadata: None,
303 }),
304 ],
305 },
306 SettingsPage {
307 title: "Appearance & Behavior",
308 items: vec![
309 SettingsPageItem::SectionHeader("Theme"),
310 // todo(settings_ui): Figure out how we want to add these
311 // SettingsPageItem::SettingItem(SettingItem {
312 // title: "Theme Mode",
313 // description: "How to select the theme",
314 // field: Box::new(SettingField {
315 // pick: |settings_content| &settings_content.theme.theme,
316 // pick_mut: |settings_content| &mut settings_content.theme.theme,
317 // }),
318 // metadata: None,
319 // }),
320 // SettingsPageItem::SettingItem(SettingItem {
321 // title: "Icon Theme",
322 // // todo(settings_ui)
323 // // This description is misleading because the icon theme is used in more places than the file explorer)
324 // description: "Choose the icon theme for file explorer",
325 // field: Box::new(SettingField {
326 // pick: |settings_content| &settings_content.theme.icon_theme,
327 // pick_mut: |settings_content| &mut settings_content.theme.icon_theme,
328 // }),
329 // metadata: None,
330 // }),
331 SettingsPageItem::SectionHeader("Layout"),
332 SettingsPageItem::SettingItem(SettingItem {
333 title: "Bottom Dock Layout",
334 description: "Layout mode for the bottom dock",
335 field: Box::new(SettingField {
336 pick: |settings_content| &settings_content.workspace.bottom_dock_layout,
337 pick_mut: |settings_content| {
338 &mut settings_content.workspace.bottom_dock_layout
339 },
340 }),
341 metadata: None,
342 }),
343 SettingsPageItem::SettingItem(SettingItem {
344 title: "Zoomed Padding",
345 description: "Whether to show padding for zoomed panels",
346 field: Box::new(SettingField {
347 pick: |settings_content| &settings_content.workspace.zoomed_padding,
348 pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
349 }),
350 metadata: None,
351 }),
352 SettingsPageItem::SettingItem(SettingItem {
353 title: "Use System Window Tabs",
354 description: "Whether to allow windows to tab together based on the user's tabbing preference (macOS only)",
355 field: Box::new(SettingField {
356 pick: |settings_content| &settings_content.workspace.use_system_window_tabs,
357 pick_mut: |settings_content| {
358 &mut settings_content.workspace.use_system_window_tabs
359 },
360 }),
361 metadata: None,
362 }),
363 SettingsPageItem::SectionHeader("Fonts"),
364 SettingsPageItem::SettingItem(SettingItem {
365 title: "Buffer Font Family",
366 description: "Font family for editor text",
367 field: Box::new(SettingField {
368 pick: |settings_content| &settings_content.theme.buffer_font_family,
369 pick_mut: |settings_content| &mut settings_content.theme.buffer_font_family,
370 }),
371 metadata: None,
372 }),
373 SettingsPageItem::SettingItem(SettingItem {
374 title: "Buffer Font Size",
375 description: "Font size for editor text",
376 field: Box::new(SettingField {
377 pick: |settings_content| &settings_content.theme.buffer_font_size,
378 pick_mut: |settings_content| &mut settings_content.theme.buffer_font_size,
379 }),
380 metadata: None,
381 }),
382 SettingsPageItem::SettingItem(SettingItem {
383 title: "Buffer Font Weight",
384 description: "Font weight for editor text (100-900)",
385 field: Box::new(SettingField {
386 pick: |settings_content| &settings_content.theme.buffer_font_weight,
387 pick_mut: |settings_content| &mut settings_content.theme.buffer_font_weight,
388 }),
389 metadata: None,
390 }),
391 SettingsPageItem::SettingItem(SettingItem {
392 title: "Buffer Line Height",
393 description: "Line height for editor text",
394 field: Box::new(SettingField {
395 pick: |settings_content| &settings_content.theme.buffer_line_height,
396 pick_mut: |settings_content| &mut settings_content.theme.buffer_line_height,
397 }),
398 metadata: None,
399 }),
400 SettingsPageItem::SettingItem(SettingItem {
401 title: "UI Font Family",
402 description: "Font family for UI elements",
403 field: Box::new(SettingField {
404 pick: |settings_content| &settings_content.theme.ui_font_family,
405 pick_mut: |settings_content| &mut settings_content.theme.ui_font_family,
406 }),
407 metadata: None,
408 }),
409 SettingsPageItem::SettingItem(SettingItem {
410 title: "UI Font Size",
411 description: "Font size for UI elements",
412 field: Box::new(SettingField {
413 pick: |settings_content| &settings_content.theme.ui_font_size,
414 pick_mut: |settings_content| &mut settings_content.theme.ui_font_size,
415 }),
416 metadata: None,
417 }),
418 SettingsPageItem::SettingItem(SettingItem {
419 title: "UI Font Weight",
420 description: "Font weight for UI elements (100-900)",
421 field: Box::new(SettingField {
422 pick: |settings_content| &settings_content.theme.ui_font_weight,
423 pick_mut: |settings_content| &mut settings_content.theme.ui_font_weight,
424 }),
425 metadata: None,
426 }),
427 SettingsPageItem::SectionHeader("Keymap"),
428 SettingsPageItem::SettingItem(SettingItem {
429 title: "Base Keymap",
430 description: "The name of a base set of key bindings to use",
431 field: Box::new(SettingField {
432 pick: |settings_content| &settings_content.base_keymap,
433 pick_mut: |settings_content| &mut settings_content.base_keymap,
434 }),
435 metadata: None,
436 }),
437 // todo(settings_ui): Vim/Helix Mode should be apart of one type because it's undefined
438 // behavior to have them both enabled at the same time
439 SettingsPageItem::SettingItem(SettingItem {
440 title: "Vim Mode",
441 description: "Whether to enable vim modes and key bindings",
442 field: Box::new(SettingField {
443 pick: |settings_content| &settings_content.vim_mode,
444 pick_mut: |settings_content| &mut settings_content.vim_mode,
445 }),
446 metadata: None,
447 }),
448 SettingsPageItem::SettingItem(SettingItem {
449 title: "Helix Mode",
450 description: "Whether to enable helix modes and key bindings",
451 field: Box::new(SettingField {
452 pick: |settings_content| &settings_content.helix_mode,
453 pick_mut: |settings_content| &mut settings_content.helix_mode,
454 }),
455 metadata: None,
456 }),
457 SettingsPageItem::SettingItem(SettingItem {
458 title: "Multi Cursor Modifier",
459 description: "Modifier key for adding multiple cursors",
460 field: Box::new(SettingField {
461 pick: |settings_content| &settings_content.editor.multi_cursor_modifier,
462 pick_mut: |settings_content| {
463 &mut settings_content.editor.multi_cursor_modifier
464 },
465 }),
466 metadata: None,
467 }),
468 SettingsPageItem::SectionHeader("Cursor"),
469 SettingsPageItem::SettingItem(SettingItem {
470 title: "Cursor Blink",
471 description: "Whether the cursor blinks in the editor",
472 field: Box::new(SettingField {
473 pick: |settings_content| &settings_content.editor.cursor_blink,
474 pick_mut: |settings_content| &mut settings_content.editor.cursor_blink,
475 }),
476 metadata: None,
477 }),
478 SettingsPageItem::SettingItem(SettingItem {
479 title: "Cursor Shape",
480 description: "Cursor shape for the editor",
481 field: Box::new(SettingField {
482 pick: |settings_content| &settings_content.editor.cursor_shape,
483 pick_mut: |settings_content| &mut settings_content.editor.cursor_shape,
484 }),
485 metadata: None,
486 }),
487 SettingsPageItem::SettingItem(SettingItem {
488 title: "Hide Mouse",
489 description: "When to hide the mouse cursor",
490 field: Box::new(SettingField {
491 pick: |settings_content| &settings_content.editor.hide_mouse,
492 pick_mut: |settings_content| &mut settings_content.editor.hide_mouse,
493 }),
494 metadata: None,
495 }),
496 SettingsPageItem::SectionHeader("Highlighting"),
497 SettingsPageItem::SettingItem(SettingItem {
498 title: "Unnecessary Code Fade",
499 description: "How much to fade out unused code (0.0 - 0.9)",
500 field: Box::new(SettingField {
501 pick: |settings_content| &settings_content.theme.unnecessary_code_fade,
502 pick_mut: |settings_content| {
503 &mut settings_content.theme.unnecessary_code_fade
504 },
505 }),
506 metadata: None,
507 }),
508 SettingsPageItem::SettingItem(SettingItem {
509 title: "Current Line Highlight",
510 description: "How to highlight the current line",
511 field: Box::new(SettingField {
512 pick: |settings_content| &settings_content.editor.current_line_highlight,
513 pick_mut: |settings_content| {
514 &mut settings_content.editor.current_line_highlight
515 },
516 }),
517 metadata: None,
518 }),
519 SettingsPageItem::SettingItem(SettingItem {
520 title: "Selection Highlight",
521 description: "Whether to highlight all occurrences of selected text",
522 field: Box::new(SettingField {
523 pick: |settings_content| &settings_content.editor.selection_highlight,
524 pick_mut: |settings_content| {
525 &mut settings_content.editor.selection_highlight
526 },
527 }),
528 metadata: None,
529 }),
530 SettingsPageItem::SettingItem(SettingItem {
531 title: "Rounded Selection",
532 description: "Whether the text selection should have rounded corners",
533 field: Box::new(SettingField {
534 pick: |settings_content| &settings_content.editor.rounded_selection,
535 pick_mut: |settings_content| &mut settings_content.editor.rounded_selection,
536 }),
537 metadata: None,
538 }),
539 SettingsPageItem::SectionHeader("Guides"),
540 SettingsPageItem::SettingItem(SettingItem {
541 title: "Show Wrap Guides",
542 description: "Whether to show wrap guides (vertical rulers)",
543 field: Box::new(SettingField {
544 pick: |settings_content| {
545 &settings_content
546 .project
547 .all_languages
548 .defaults
549 .show_wrap_guides
550 },
551 pick_mut: |settings_content| {
552 &mut settings_content
553 .project
554 .all_languages
555 .defaults
556 .show_wrap_guides
557 },
558 }),
559 metadata: None,
560 }),
561 // todo(settings_ui): This needs a custom component
562 // SettingsPageItem::SettingItem(SettingItem {
563 // title: "Wrap Guides",
564 // description: "Character counts at which to show wrap guides",
565 // field: Box::new(SettingField {
566 // pick: |settings_content| {
567 // &settings_content
568 // .project
569 // .all_languages
570 // .defaults
571 // .wrap_guides
572 // },
573 // pick_mut: |settings_content| {
574 // &mut settings_content
575 // .project
576 // .all_languages
577 // .defaults
578 // .wrap_guides
579 // },
580 // }),
581 // metadata: None,
582 // }),
583 SettingsPageItem::SectionHeader("Whitespace"),
584 SettingsPageItem::SettingItem(SettingItem {
585 title: "Show Whitespace",
586 description: "Whether to show tabs and spaces",
587 field: Box::new(SettingField {
588 pick: |settings_content| {
589 &settings_content
590 .project
591 .all_languages
592 .defaults
593 .show_whitespaces
594 },
595 pick_mut: |settings_content| {
596 &mut settings_content
597 .project
598 .all_languages
599 .defaults
600 .show_whitespaces
601 },
602 }),
603 metadata: None,
604 }),
605 SettingsPageItem::SectionHeader("Window"),
606 // todo(settings_ui): Should we filter by platform?
607 SettingsPageItem::SettingItem(SettingItem {
608 title: "Use System Window Tabs",
609 description: "Whether to allow windows to tab together (macOS only)",
610 field: Box::new(SettingField {
611 pick: |settings_content| &settings_content.workspace.use_system_window_tabs,
612 pick_mut: |settings_content| {
613 &mut settings_content.workspace.use_system_window_tabs
614 },
615 }),
616 metadata: None,
617 }),
618 SettingsPageItem::SectionHeader("Layout"),
619 SettingsPageItem::SettingItem(SettingItem {
620 title: "Zoomed Padding",
621 description: "Whether to show padding for zoomed panels",
622 field: Box::new(SettingField {
623 pick: |settings_content| &settings_content.workspace.zoomed_padding,
624 pick_mut: |settings_content| &mut settings_content.workspace.zoomed_padding,
625 }),
626 metadata: None,
627 }),
628 // todo(settings_ui): Needs numeric stepper + option within an option
629 // SettingsPageItem::SettingItem(SettingItem {
630 // title: "Centered Layout Left Padding",
631 // description: "Left padding for centered layout",
632 // field: Box::new(SettingField {
633 // pick: |settings_content| {
634 // &settings_content.workspace.centered_layout.left_padding
635 // },
636 // pick_mut: |settings_content| {
637 // &mut settings_content.workspace.centered_layout.left_padding
638 // },
639 // }),
640 // metadata: None,
641 // }),
642 // SettingsPageItem::SettingItem(SettingItem {
643 // title: "Centered Layout Right Padding",
644 // description: "Right padding for centered layout",
645 // field: Box::new(SettingField {
646 // pick: |settings_content| {
647 // if let Some(centered_layout) =
648 // &settings_content.workspace.centered_layout
649 // {
650 // ¢ered_layout.right_padding
651 // } else {
652 // &None
653 // }
654 // },
655 // pick_mut: |settings_content| {
656 // if let Some(mut centered_layout) =
657 // settings_content.workspace.centered_layout
658 // {
659 // &mut centered_layout.right_padding
660 // } else {
661 // &mut None
662 // }
663 // },
664 // }),
665 // metadata: None,
666 // }),
667 SettingsPageItem::SettingItem(SettingItem {
668 title: "Bottom Dock Layout",
669 description: "Layout mode of the bottom dock",
670 field: Box::new(SettingField {
671 pick: |settings_content| &settings_content.workspace.bottom_dock_layout,
672 pick_mut: |settings_content| {
673 &mut settings_content.workspace.bottom_dock_layout
674 },
675 }),
676 metadata: None,
677 }),
678 ],
679 },
680 SettingsPage {
681 title: "Editor",
682 items: vec![
683 SettingsPageItem::SectionHeader("Indentation"),
684 // todo(settings_ui): Needs numeric stepper
685 SettingsPageItem::SettingItem(SettingItem {
686 title: "Tab Size",
687 description: "How many columns a tab should occupy",
688 field: Box::new(SettingField {
689 pick: |settings_content| {
690 &settings_content.project.all_languages.defaults.tab_size
691 },
692 pick_mut: |settings_content| {
693 &mut settings_content.project.all_languages.defaults.tab_size
694 },
695 }),
696 metadata: None,
697 }),
698 SettingsPageItem::SettingItem(SettingItem {
699 title: "Hard Tabs",
700 description: "Whether to indent lines using tab characters, as opposed to multiple spaces",
701 field: Box::new(SettingField {
702 pick: |settings_content| {
703 &settings_content.project.all_languages.defaults.hard_tabs
704 },
705 pick_mut: |settings_content| {
706 &mut settings_content.project.all_languages.defaults.hard_tabs
707 },
708 }),
709 metadata: None,
710 }),
711 SettingsPageItem::SettingItem(SettingItem {
712 title: "Auto Indent",
713 description: "Whether indentation should be adjusted based on the context whilst typing",
714 field: Box::new(SettingField {
715 pick: |settings_content| {
716 &settings_content.project.all_languages.defaults.auto_indent
717 },
718 pick_mut: |settings_content| {
719 &mut settings_content.project.all_languages.defaults.auto_indent
720 },
721 }),
722 metadata: None,
723 }),
724 SettingsPageItem::SettingItem(SettingItem {
725 title: "Auto Indent On Paste",
726 description: "Whether indentation of pasted content should be adjusted based on the context",
727 field: Box::new(SettingField {
728 pick: |settings_content| {
729 &settings_content
730 .project
731 .all_languages
732 .defaults
733 .auto_indent_on_paste
734 },
735 pick_mut: |settings_content| {
736 &mut settings_content
737 .project
738 .all_languages
739 .defaults
740 .auto_indent_on_paste
741 },
742 }),
743 metadata: None,
744 }),
745 SettingsPageItem::SectionHeader("Wrapping"),
746 // todo(settings_ui): Needs numeric stepper
747 // SettingsPageItem::SettingItem(SettingItem {
748 // title: "Preferred Line Length",
749 // description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled",
750 // field: Box::new(SettingField {
751 // pick: |settings_content| &settings_content.project.all_languages.defaults.preferred_line_length,
752 // pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.preferred_line_length,
753 // }),
754 // metadata: None,
755 // }),
756 SettingsPageItem::SettingItem(SettingItem {
757 title: "Soft Wrap",
758 description: "How to soft-wrap long lines of text",
759 field: Box::new(SettingField {
760 pick: |settings_content| {
761 &settings_content.project.all_languages.defaults.soft_wrap
762 },
763 pick_mut: |settings_content| {
764 &mut settings_content.project.all_languages.defaults.soft_wrap
765 },
766 }),
767 metadata: None,
768 }),
769 SettingsPageItem::SectionHeader("Search"),
770 SettingsPageItem::SettingItem(SettingItem {
771 title: "Search Wrap",
772 description: "Whether the editor search results will loop",
773 field: Box::new(SettingField {
774 pick: |settings_content| &settings_content.editor.search_wrap,
775 pick_mut: |settings_content| &mut settings_content.editor.search_wrap,
776 }),
777 metadata: None,
778 }),
779 SettingsPageItem::SettingItem(SettingItem {
780 title: "Seed Search Query From Cursor",
781 description: "When to populate a new search's query based on the text under the cursor",
782 field: Box::new(SettingField {
783 pick: |settings_content| {
784 &settings_content.editor.seed_search_query_from_cursor
785 },
786 pick_mut: |settings_content| {
787 &mut settings_content.editor.seed_search_query_from_cursor
788 },
789 }),
790 metadata: None,
791 }),
792 SettingsPageItem::SettingItem(SettingItem {
793 title: "Use Smartcase Search",
794 description: "Whether to use smartcase search",
795 field: Box::new(SettingField {
796 pick: |settings_content| &settings_content.editor.use_smartcase_search,
797 pick_mut: |settings_content| {
798 &mut settings_content.editor.use_smartcase_search
799 },
800 }),
801 metadata: None,
802 }),
803 SettingsPageItem::SectionHeader("Editor Behavior"),
804 SettingsPageItem::SettingItem(SettingItem {
805 title: "Redact Private Values",
806 description: "Hide the values of variables in private files",
807 field: Box::new(SettingField {
808 pick: |settings_content| &settings_content.editor.redact_private_values,
809 pick_mut: |settings_content| {
810 &mut settings_content.editor.redact_private_values
811 },
812 }),
813 metadata: None,
814 }),
815 SettingsPageItem::SettingItem(SettingItem {
816 title: "Middle Click Paste",
817 description: "Whether to enable middle-click paste on Linux",
818 field: Box::new(SettingField {
819 pick: |settings_content| &settings_content.editor.middle_click_paste,
820 pick_mut: |settings_content| {
821 &mut settings_content.editor.middle_click_paste
822 },
823 }),
824 metadata: None,
825 }),
826 SettingsPageItem::SettingItem(SettingItem {
827 title: "Double Click In Multibuffer",
828 description: "What to do when multibuffer is double clicked in some of its excerpts",
829 field: Box::new(SettingField {
830 pick: |settings_content| {
831 &settings_content.editor.double_click_in_multibuffer
832 },
833 pick_mut: |settings_content| {
834 &mut settings_content.editor.double_click_in_multibuffer
835 },
836 }),
837 metadata: None,
838 }),
839 SettingsPageItem::SettingItem(SettingItem {
840 title: "Go To Definition Fallback",
841 description: "Whether to follow-up empty go to definition responses from the language server",
842 field: Box::new(SettingField {
843 pick: |settings_content| &settings_content.editor.go_to_definition_fallback,
844 pick_mut: |settings_content| {
845 &mut settings_content.editor.go_to_definition_fallback
846 },
847 }),
848 metadata: None,
849 }),
850 SettingsPageItem::SectionHeader("Scrolling"),
851 SettingsPageItem::SettingItem(SettingItem {
852 title: "Scroll Beyond Last Line",
853 description: "Whether the editor will scroll beyond the last line",
854 field: Box::new(SettingField {
855 pick: |settings_content| &settings_content.editor.scroll_beyond_last_line,
856 pick_mut: |settings_content| {
857 &mut settings_content.editor.scroll_beyond_last_line
858 },
859 }),
860 metadata: None,
861 }),
862 SettingsPageItem::SettingItem(SettingItem {
863 title: "Vertical Scroll Margin",
864 description: "The number of lines to keep above/below the cursor when auto-scrolling",
865 field: Box::new(SettingField {
866 pick: |settings_content| &settings_content.editor.vertical_scroll_margin,
867 pick_mut: |settings_content| {
868 &mut settings_content.editor.vertical_scroll_margin
869 },
870 }),
871 metadata: None,
872 }),
873 SettingsPageItem::SettingItem(SettingItem {
874 title: "Horizontal Scroll Margin",
875 description: "The number of characters to keep on either side when scrolling with the mouse",
876 field: Box::new(SettingField {
877 pick: |settings_content| &settings_content.editor.horizontal_scroll_margin,
878 pick_mut: |settings_content| {
879 &mut settings_content.editor.horizontal_scroll_margin
880 },
881 }),
882 metadata: None,
883 }),
884 SettingsPageItem::SettingItem(SettingItem {
885 title: "Scroll Sensitivity",
886 description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling",
887 field: Box::new(SettingField {
888 pick: |settings_content| &settings_content.editor.scroll_sensitivity,
889 pick_mut: |settings_content| {
890 &mut settings_content.editor.scroll_sensitivity
891 },
892 }),
893 metadata: None,
894 }),
895 SettingsPageItem::SettingItem(SettingItem {
896 title: "Fast Scroll Sensitivity",
897 description: "Fast Scroll sensitivity multiplier for both horizontal and vertical scrolling",
898 field: Box::new(SettingField {
899 pick: |settings_content| &settings_content.editor.fast_scroll_sensitivity,
900 pick_mut: |settings_content| {
901 &mut settings_content.editor.fast_scroll_sensitivity
902 },
903 }),
904 metadata: None,
905 }),
906 SettingsPageItem::SettingItem(SettingItem {
907 title: "Autoscroll On Clicks",
908 description: "Whether to scroll when clicking near the edge of the visible text area",
909 field: Box::new(SettingField {
910 pick: |settings_content| &settings_content.editor.autoscroll_on_clicks,
911 pick_mut: |settings_content| {
912 &mut settings_content.editor.autoscroll_on_clicks
913 },
914 }),
915 metadata: None,
916 }),
917 SettingsPageItem::SectionHeader("Auto Actions"),
918 SettingsPageItem::SettingItem(SettingItem {
919 title: "Use Autoclose",
920 description: "Whether to automatically type closing characters for you",
921 field: Box::new(SettingField {
922 pick: |settings_content| {
923 &settings_content
924 .project
925 .all_languages
926 .defaults
927 .use_autoclose
928 },
929 pick_mut: |settings_content| {
930 &mut settings_content
931 .project
932 .all_languages
933 .defaults
934 .use_autoclose
935 },
936 }),
937 metadata: None,
938 }),
939 SettingsPageItem::SettingItem(SettingItem {
940 title: "Use Auto Surround",
941 description: "Whether to automatically surround text with characters for you",
942 field: Box::new(SettingField {
943 pick: |settings_content| {
944 &settings_content
945 .project
946 .all_languages
947 .defaults
948 .use_auto_surround
949 },
950 pick_mut: |settings_content| {
951 &mut settings_content
952 .project
953 .all_languages
954 .defaults
955 .use_auto_surround
956 },
957 }),
958 metadata: None,
959 }),
960 SettingsPageItem::SettingItem(SettingItem {
961 title: "Use On Type Format",
962 description: "Whether to use additional LSP queries to format the code after every trigger symbol input",
963 field: Box::new(SettingField {
964 pick: |settings_content| {
965 &settings_content
966 .project
967 .all_languages
968 .defaults
969 .use_on_type_format
970 },
971 pick_mut: |settings_content| {
972 &mut settings_content
973 .project
974 .all_languages
975 .defaults
976 .use_on_type_format
977 },
978 }),
979 metadata: None,
980 }),
981 SettingsPageItem::SettingItem(SettingItem {
982 title: "Always Treat Brackets As Autoclosed",
983 description: "Controls how the editor handles the autoclosed characters",
984 field: Box::new(SettingField {
985 pick: |settings_content| {
986 &settings_content
987 .project
988 .all_languages
989 .defaults
990 .always_treat_brackets_as_autoclosed
991 },
992 pick_mut: |settings_content| {
993 &mut settings_content
994 .project
995 .all_languages
996 .defaults
997 .always_treat_brackets_as_autoclosed
998 },
999 }),
1000 metadata: None,
1001 }),
1002 SettingsPageItem::SectionHeader("Formatting"),
1003 SettingsPageItem::SettingItem(SettingItem {
1004 title: "Remove Trailing Whitespace On Save",
1005 description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it",
1006 field: Box::new(SettingField {
1007 pick: |settings_content| {
1008 &settings_content
1009 .project
1010 .all_languages
1011 .defaults
1012 .remove_trailing_whitespace_on_save
1013 },
1014 pick_mut: |settings_content| {
1015 &mut settings_content
1016 .project
1017 .all_languages
1018 .defaults
1019 .remove_trailing_whitespace_on_save
1020 },
1021 }),
1022 metadata: None,
1023 }),
1024 SettingsPageItem::SettingItem(SettingItem {
1025 title: "Ensure Final Newline On Save",
1026 description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it",
1027 field: Box::new(SettingField {
1028 pick: |settings_content| {
1029 &settings_content
1030 .project
1031 .all_languages
1032 .defaults
1033 .ensure_final_newline_on_save
1034 },
1035 pick_mut: |settings_content| {
1036 &mut settings_content
1037 .project
1038 .all_languages
1039 .defaults
1040 .ensure_final_newline_on_save
1041 },
1042 }),
1043 metadata: None,
1044 }),
1045 SettingsPageItem::SettingItem(SettingItem {
1046 title: "Extend Comment On Newline",
1047 description: "Whether to start a new line with a comment when a previous line is a comment as well",
1048 field: Box::new(SettingField {
1049 pick: |settings_content| {
1050 &settings_content
1051 .project
1052 .all_languages
1053 .defaults
1054 .extend_comment_on_newline
1055 },
1056 pick_mut: |settings_content| {
1057 &mut settings_content
1058 .project
1059 .all_languages
1060 .defaults
1061 .extend_comment_on_newline
1062 },
1063 }),
1064 metadata: None,
1065 }),
1066 SettingsPageItem::SectionHeader("Completions"),
1067 SettingsPageItem::SettingItem(SettingItem {
1068 title: "Show Completions On Input",
1069 description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it",
1070 field: Box::new(SettingField {
1071 pick: |settings_content| {
1072 &settings_content
1073 .project
1074 .all_languages
1075 .defaults
1076 .show_completions_on_input
1077 },
1078 pick_mut: |settings_content| {
1079 &mut settings_content
1080 .project
1081 .all_languages
1082 .defaults
1083 .show_completions_on_input
1084 },
1085 }),
1086 metadata: None,
1087 }),
1088 SettingsPageItem::SettingItem(SettingItem {
1089 title: "Show Completion Documentation",
1090 description: "Whether to display inline and alongside documentation for items in the completions menu",
1091 field: Box::new(SettingField {
1092 pick: |settings_content| {
1093 &settings_content
1094 .project
1095 .all_languages
1096 .defaults
1097 .show_completion_documentation
1098 },
1099 pick_mut: |settings_content| {
1100 &mut settings_content
1101 .project
1102 .all_languages
1103 .defaults
1104 .show_completion_documentation
1105 },
1106 }),
1107 metadata: None,
1108 }),
1109 SettingsPageItem::SettingItem(SettingItem {
1110 title: "Auto Signature Help",
1111 description: "Whether to automatically show a signature help pop-up or not",
1112 field: Box::new(SettingField {
1113 pick: |settings_content| &settings_content.editor.auto_signature_help,
1114 pick_mut: |settings_content| {
1115 &mut settings_content.editor.auto_signature_help
1116 },
1117 }),
1118 metadata: None,
1119 }),
1120 SettingsPageItem::SettingItem(SettingItem {
1121 title: "Show Signature Help After Edits",
1122 description: "Whether to show the signature help pop-up after completions or bracket pairs inserted",
1123 field: Box::new(SettingField {
1124 pick: |settings_content| {
1125 &settings_content.editor.show_signature_help_after_edits
1126 },
1127 pick_mut: |settings_content| {
1128 &mut settings_content.editor.show_signature_help_after_edits
1129 },
1130 }),
1131 metadata: None,
1132 }),
1133 SettingsPageItem::SettingItem(SettingItem {
1134 title: "Snippet Sort Order",
1135 description: "Determines how snippets are sorted relative to other completion items",
1136 field: Box::new(SettingField {
1137 pick: |settings_content| &settings_content.editor.snippet_sort_order,
1138 pick_mut: |settings_content| {
1139 &mut settings_content.editor.snippet_sort_order
1140 },
1141 }),
1142 metadata: None,
1143 }),
1144 SettingsPageItem::SectionHeader("Hover"),
1145 SettingsPageItem::SettingItem(SettingItem {
1146 title: "Hover Popover Enabled",
1147 description: "Whether to show the informational hover box when moving the mouse over symbols in the editor",
1148 field: Box::new(SettingField {
1149 pick: |settings_content| &settings_content.editor.hover_popover_enabled,
1150 pick_mut: |settings_content| {
1151 &mut settings_content.editor.hover_popover_enabled
1152 },
1153 }),
1154 metadata: None,
1155 }),
1156 // todo(settings ui): add units to this numeric stepper
1157 SettingsPageItem::SettingItem(SettingItem {
1158 title: "Hover Popover Delay",
1159 description: "Time to wait in milliseconds before showing the informational hover box",
1160 field: Box::new(SettingField {
1161 pick: |settings_content| &settings_content.editor.hover_popover_delay,
1162 pick_mut: |settings_content| {
1163 &mut settings_content.editor.hover_popover_delay
1164 },
1165 }),
1166 metadata: None,
1167 }),
1168 SettingsPageItem::SectionHeader("Code Actions"),
1169 SettingsPageItem::SettingItem(SettingItem {
1170 title: "Inline Code Actions",
1171 description: "Whether to show code action button at start of buffer line",
1172 field: Box::new(SettingField {
1173 pick: |settings_content| &settings_content.editor.inline_code_actions,
1174 pick_mut: |settings_content| {
1175 &mut settings_content.editor.inline_code_actions
1176 },
1177 }),
1178 metadata: None,
1179 }),
1180 SettingsPageItem::SectionHeader("Selection"),
1181 SettingsPageItem::SettingItem(SettingItem {
1182 title: "Drag And Drop Selection",
1183 description: "Whether to enable drag and drop selection",
1184 field: Box::new(SettingField {
1185 pick: |settings_content| {
1186 if let Some(drag_and_drop) =
1187 &settings_content.editor.drag_and_drop_selection
1188 {
1189 &drag_and_drop.enabled
1190 } else {
1191 &None
1192 }
1193 },
1194 pick_mut: |settings_content| {
1195 &mut settings_content
1196 .editor
1197 .drag_and_drop_selection
1198 .get_or_insert_default()
1199 .enabled
1200 },
1201 }),
1202 metadata: None,
1203 }),
1204 // todo(settings_ui): Needs numeric stepper
1205 // SettingsPageItem::SettingItem(SettingItem {
1206 // title: "Drag And Drop Selection Delay",
1207 // description: "Delay in milliseconds before drag and drop selection starts",
1208 // field: Box::new(SettingField {
1209 // pick: |settings_content| {
1210 // if let Some(drag_and_drop) = &settings_content.editor.drag_and_drop_selection {
1211 // &drag_and_drop.delay
1212 // } else {
1213 // &None
1214 // }
1215 // },
1216 // pick_mut: |settings_content| {
1217 // &mut settings_content.editor.drag_and_drop_selection.get_or_insert_default().delay
1218 // },
1219 // }),
1220 // metadata: None,
1221 // }),
1222 SettingsPageItem::SectionHeader("Line Numbers"),
1223 SettingsPageItem::SettingItem(SettingItem {
1224 title: "Relative Line Numbers",
1225 description: "Whether the line numbers on editors gutter are relative or not",
1226 field: Box::new(SettingField {
1227 pick: |settings_content| &settings_content.editor.relative_line_numbers,
1228 pick_mut: |settings_content| {
1229 &mut settings_content.editor.relative_line_numbers
1230 },
1231 }),
1232 metadata: None,
1233 }),
1234 SettingsPageItem::SectionHeader("Gutter"),
1235 SettingsPageItem::SettingItem(SettingItem {
1236 title: "Show Line Numbers",
1237 description: "Whether to show line numbers in the gutter",
1238 field: Box::new(SettingField {
1239 pick: |settings_content| {
1240 if let Some(gutter) = &settings_content.editor.gutter {
1241 &gutter.line_numbers
1242 } else {
1243 &None
1244 }
1245 },
1246 pick_mut: |settings_content| {
1247 &mut settings_content
1248 .editor
1249 .gutter
1250 .get_or_insert_default()
1251 .line_numbers
1252 },
1253 }),
1254 metadata: None,
1255 }),
1256 SettingsPageItem::SettingItem(SettingItem {
1257 title: "Show Runnables",
1258 description: "Whether to show runnable buttons in the gutter",
1259 field: Box::new(SettingField {
1260 pick: |settings_content| {
1261 if let Some(gutter) = &settings_content.editor.gutter {
1262 &gutter.runnables
1263 } else {
1264 &None
1265 }
1266 },
1267 pick_mut: |settings_content| {
1268 &mut settings_content
1269 .editor
1270 .gutter
1271 .get_or_insert_default()
1272 .runnables
1273 },
1274 }),
1275 metadata: None,
1276 }),
1277 SettingsPageItem::SettingItem(SettingItem {
1278 title: "Show Breakpoints",
1279 description: "Whether to show breakpoints in the gutter",
1280 field: Box::new(SettingField {
1281 pick: |settings_content| {
1282 if let Some(gutter) = &settings_content.editor.gutter {
1283 &gutter.breakpoints
1284 } else {
1285 &None
1286 }
1287 },
1288 pick_mut: |settings_content| {
1289 &mut settings_content
1290 .editor
1291 .gutter
1292 .get_or_insert_default()
1293 .breakpoints
1294 },
1295 }),
1296 metadata: None,
1297 }),
1298 SettingsPageItem::SettingItem(SettingItem {
1299 title: "Show Folds",
1300 description: "Whether to show code folding controls in the gutter",
1301 field: Box::new(SettingField {
1302 pick: |settings_content| {
1303 if let Some(gutter) = &settings_content.editor.gutter {
1304 &gutter.folds
1305 } else {
1306 &None
1307 }
1308 },
1309 pick_mut: |settings_content| {
1310 &mut settings_content.editor.gutter.get_or_insert_default().folds
1311 },
1312 }),
1313 metadata: None,
1314 }),
1315 SettingsPageItem::SectionHeader("Tabs"),
1316 SettingsPageItem::SettingItem(SettingItem {
1317 title: "Show Tab Bar",
1318 description: "Whether or not to show the tab bar in the editor",
1319 field: Box::new(SettingField {
1320 pick: |settings_content| {
1321 if let Some(tab_bar) = &settings_content.tab_bar {
1322 &tab_bar.show
1323 } else {
1324 &None
1325 }
1326 },
1327 pick_mut: |settings_content| {
1328 &mut settings_content.tab_bar.get_or_insert_default().show
1329 },
1330 }),
1331 metadata: None,
1332 }),
1333 SettingsPageItem::SettingItem(SettingItem {
1334 title: "Show Git Status In Tabs",
1335 description: "Whether to show the Git file status on a tab item",
1336 field: Box::new(SettingField {
1337 pick: |settings_content| {
1338 if let Some(tabs) = &settings_content.tabs {
1339 &tabs.git_status
1340 } else {
1341 &None
1342 }
1343 },
1344 pick_mut: |settings_content| {
1345 &mut settings_content.tabs.get_or_insert_default().git_status
1346 },
1347 }),
1348 metadata: None,
1349 }),
1350 SettingsPageItem::SettingItem(SettingItem {
1351 title: "Show File Icons In Tabs",
1352 description: "Whether to show the file icon for a tab",
1353 field: Box::new(SettingField {
1354 pick: |settings_content| {
1355 if let Some(tabs) = &settings_content.tabs {
1356 &tabs.file_icons
1357 } else {
1358 &None
1359 }
1360 },
1361 pick_mut: |settings_content| {
1362 &mut settings_content.tabs.get_or_insert_default().file_icons
1363 },
1364 }),
1365 metadata: None,
1366 }),
1367 SettingsPageItem::SettingItem(SettingItem {
1368 title: "Tab Close Position",
1369 description: "Position of the close button in a tab",
1370 field: Box::new(SettingField {
1371 pick: |settings_content| {
1372 if let Some(tabs) = &settings_content.tabs {
1373 &tabs.close_position
1374 } else {
1375 &None
1376 }
1377 },
1378 pick_mut: |settings_content| {
1379 &mut settings_content.tabs.get_or_insert_default().close_position
1380 },
1381 }),
1382 metadata: None,
1383 }),
1384 // todo(settings_ui): Needs numeric stepper
1385 // SettingsPageItem::SettingItem(SettingItem {
1386 // title: "Maximum Tabs",
1387 // description: "Maximum open tabs in a pane. Will not close an unsaved tab",
1388 // field: Box::new(SettingField {
1389 // pick: |settings_content| &settings_content.workspace.max_tabs,
1390 // pick_mut: |settings_content| &mut settings_content.workspace.max_tabs,
1391 // }),
1392 // metadata: None,
1393 // }),
1394 ],
1395 },
1396 SettingsPage {
1397 title: "Languages & Frameworks",
1398 items: vec![
1399 SettingsPageItem::SectionHeader("General"),
1400 SettingsPageItem::SettingItem(SettingItem {
1401 title: "Enable Language Server",
1402 description: "Whether to use language servers to provide code intelligence",
1403 field: Box::new(SettingField {
1404 pick: |settings_content| {
1405 &settings_content
1406 .project
1407 .all_languages
1408 .defaults
1409 .enable_language_server
1410 },
1411 pick_mut: |settings_content| {
1412 &mut settings_content
1413 .project
1414 .all_languages
1415 .defaults
1416 .enable_language_server
1417 },
1418 }),
1419 metadata: None,
1420 }),
1421 SettingsPageItem::SectionHeader("Languages"),
1422 SettingsPageItem::SubPageLink(SubPageLink {
1423 title: "JSON",
1424 render: Rc::new(|_, _, _| "A settings page!".into_any_element()),
1425 }),
1426 ],
1427 },
1428 SettingsPage {
1429 title: "Workbench & Window",
1430 items: vec![
1431 SettingsPageItem::SectionHeader("Workbench"),
1432 SettingsPageItem::SettingItem(SettingItem {
1433 title: "Editor Tabs",
1434 description: "Whether or not to show the tab bar in the editor",
1435 field: Box::new(SettingField {
1436 pick: |settings_content| {
1437 if let Some(tab_bar) = &settings_content.tab_bar {
1438 &tab_bar.show
1439 } else {
1440 &None
1441 }
1442 },
1443 pick_mut: |settings_content| {
1444 &mut settings_content.tab_bar.get_or_insert_default().show
1445 },
1446 }),
1447 metadata: None,
1448 }),
1449 SettingsPageItem::SettingItem(SettingItem {
1450 title: "Active language Button",
1451 description: "Whether to show the active language button in the status bar",
1452 field: Box::new(SettingField {
1453 pick: |settings_content| {
1454 if let Some(status_bar) = &settings_content.status_bar {
1455 &status_bar.active_language_button
1456 } else {
1457 &None
1458 }
1459 },
1460 pick_mut: |settings_content| {
1461 &mut settings_content
1462 .status_bar
1463 .get_or_insert_default()
1464 .active_language_button
1465 },
1466 }),
1467 metadata: None,
1468 }),
1469 SettingsPageItem::SettingItem(SettingItem {
1470 title: "Cursor Position Button",
1471 description: "Whether to show the cursor position button in the status bar",
1472 field: Box::new(SettingField {
1473 pick: |settings_content| {
1474 if let Some(status_bar) = &settings_content.status_bar {
1475 &status_bar.cursor_position_button
1476 } else {
1477 &None
1478 }
1479 },
1480 pick_mut: |settings_content| {
1481 &mut settings_content
1482 .status_bar
1483 .get_or_insert_default()
1484 .cursor_position_button
1485 },
1486 }),
1487 metadata: None,
1488 }),
1489 SettingsPageItem::SectionHeader("Terminal"),
1490 SettingsPageItem::SettingItem(SettingItem {
1491 title: "Terminal Button",
1492 description: "Whether to show the terminal button in the status bar",
1493 field: Box::new(SettingField {
1494 pick: |settings_content| {
1495 if let Some(terminal) = &settings_content.terminal {
1496 &terminal.button
1497 } else {
1498 &None
1499 }
1500 },
1501 pick_mut: |settings_content| {
1502 &mut settings_content.terminal.get_or_insert_default().button
1503 },
1504 }),
1505 metadata: None,
1506 }),
1507 SettingsPageItem::SettingItem(SettingItem {
1508 title: "Show Navigation History Buttons",
1509 description: "Whether or not to show the navigation history buttons in the tab bar",
1510 field: Box::new(SettingField {
1511 pick: |settings_content| {
1512 if let Some(tab_bar) = &settings_content.tab_bar {
1513 &tab_bar.show_nav_history_buttons
1514 } else {
1515 &None
1516 }
1517 },
1518 pick_mut: |settings_content| {
1519 &mut settings_content
1520 .tab_bar
1521 .get_or_insert_default()
1522 .show_nav_history_buttons
1523 },
1524 }),
1525 metadata: None,
1526 }),
1527 ],
1528 },
1529 SettingsPage {
1530 title: "Panels & Tools",
1531 items: vec![
1532 SettingsPageItem::SectionHeader("Project Panel"),
1533 SettingsPageItem::SettingItem(SettingItem {
1534 title: "Project Panel Button",
1535 description: "Whether to show the project panel button in the status bar",
1536 field: Box::new(SettingField {
1537 pick: |settings_content| {
1538 if let Some(project_panel) = &settings_content.project_panel {
1539 &project_panel.button
1540 } else {
1541 &None
1542 }
1543 },
1544 pick_mut: |settings_content| {
1545 &mut settings_content
1546 .project_panel
1547 .get_or_insert_default()
1548 .button
1549 },
1550 }),
1551 metadata: None,
1552 }),
1553 SettingsPageItem::SettingItem(SettingItem {
1554 title: "Project Panel Dock",
1555 description: "Where to dock the project panel",
1556 field: Box::new(SettingField {
1557 pick: |settings_content| {
1558 if let Some(project_panel) = &settings_content.project_panel {
1559 &project_panel.dock
1560 } else {
1561 &None
1562 }
1563 },
1564 pick_mut: |settings_content| {
1565 &mut settings_content.project_panel.get_or_insert_default().dock
1566 },
1567 }),
1568 metadata: None,
1569 }),
1570 // todo(settings_ui): Needs numeric stepper
1571 // SettingsPageItem::SettingItem(SettingItem {
1572 // title: "Project Panel Default Width",
1573 // description: "Default width of the project panel in pixels",
1574 // field: Box::new(SettingField {
1575 // pick: |settings_content| {
1576 // if let Some(project_panel) = &settings_content.project_panel {
1577 // &project_panel.default_width
1578 // } else {
1579 // &None
1580 // }
1581 // },
1582 // pick_mut: |settings_content| {
1583 // &mut settings_content
1584 // .project_panel
1585 // .get_or_insert_default()
1586 // .default_width
1587 // },
1588 // }),
1589 // metadata: None,
1590 // }),
1591 SettingsPageItem::SectionHeader("Terminal"),
1592 SettingsPageItem::SettingItem(SettingItem {
1593 title: "Terminal Dock",
1594 description: "Where to dock the terminal panel",
1595 field: Box::new(SettingField {
1596 pick: |settings_content| {
1597 if let Some(terminal) = &settings_content.terminal {
1598 &terminal.dock
1599 } else {
1600 &None
1601 }
1602 },
1603 pick_mut: |settings_content| {
1604 &mut settings_content.terminal.get_or_insert_default().dock
1605 },
1606 }),
1607 metadata: None,
1608 }),
1609 SettingsPageItem::SectionHeader("Tab Settings"),
1610 SettingsPageItem::SettingItem(SettingItem {
1611 title: "Activate On Close",
1612 description: "What to do after closing the current tab",
1613 field: Box::new(SettingField {
1614 pick: |settings_content| {
1615 if let Some(tabs) = &settings_content.tabs {
1616 &tabs.activate_on_close
1617 } else {
1618 &None
1619 }
1620 },
1621 pick_mut: |settings_content| {
1622 &mut settings_content
1623 .tabs
1624 .get_or_insert_default()
1625 .activate_on_close
1626 },
1627 }),
1628 metadata: None,
1629 }),
1630 SettingsPageItem::SettingItem(SettingItem {
1631 title: "Tab Show Diagnostics",
1632 description: "Which files containing diagnostic errors/warnings to mark in the tabs",
1633 field: Box::new(SettingField {
1634 pick: |settings_content| {
1635 if let Some(tabs) = &settings_content.tabs {
1636 &tabs.show_diagnostics
1637 } else {
1638 &None
1639 }
1640 },
1641 pick_mut: |settings_content| {
1642 &mut settings_content
1643 .tabs
1644 .get_or_insert_default()
1645 .show_diagnostics
1646 },
1647 }),
1648 metadata: None,
1649 }),
1650 SettingsPageItem::SettingItem(SettingItem {
1651 title: "Show Close Button",
1652 description: "Controls the appearance behavior of the tab's close button",
1653 field: Box::new(SettingField {
1654 pick: |settings_content| {
1655 if let Some(tabs) = &settings_content.tabs {
1656 &tabs.show_close_button
1657 } else {
1658 &None
1659 }
1660 },
1661 pick_mut: |settings_content| {
1662 &mut settings_content
1663 .tabs
1664 .get_or_insert_default()
1665 .show_close_button
1666 },
1667 }),
1668 metadata: None,
1669 }),
1670 SettingsPageItem::SectionHeader("Preview Tabs"),
1671 SettingsPageItem::SettingItem(SettingItem {
1672 title: "Preview Tabs Enabled",
1673 description: "Whether to show opened editors as preview tabs",
1674 field: Box::new(SettingField {
1675 pick: |settings_content| {
1676 if let Some(preview_tabs) = &settings_content.preview_tabs {
1677 &preview_tabs.enabled
1678 } else {
1679 &None
1680 }
1681 },
1682 pick_mut: |settings_content| {
1683 &mut settings_content
1684 .preview_tabs
1685 .get_or_insert_default()
1686 .enabled
1687 },
1688 }),
1689 metadata: None,
1690 }),
1691 SettingsPageItem::SettingItem(SettingItem {
1692 title: "Enable Preview From File Finder",
1693 description: "Whether to open tabs in preview mode when selected from the file finder",
1694 field: Box::new(SettingField {
1695 pick: |settings_content| {
1696 if let Some(preview_tabs) = &settings_content.preview_tabs {
1697 &preview_tabs.enable_preview_from_file_finder
1698 } else {
1699 &None
1700 }
1701 },
1702 pick_mut: |settings_content| {
1703 &mut settings_content
1704 .preview_tabs
1705 .get_or_insert_default()
1706 .enable_preview_from_file_finder
1707 },
1708 }),
1709 metadata: None,
1710 }),
1711 SettingsPageItem::SettingItem(SettingItem {
1712 title: "Enable Preview From Code Navigation",
1713 description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab",
1714 field: Box::new(SettingField {
1715 pick: |settings_content| {
1716 if let Some(preview_tabs) = &settings_content.preview_tabs {
1717 &preview_tabs.enable_preview_from_code_navigation
1718 } else {
1719 &None
1720 }
1721 },
1722 pick_mut: |settings_content| {
1723 &mut settings_content
1724 .preview_tabs
1725 .get_or_insert_default()
1726 .enable_preview_from_code_navigation
1727 },
1728 }),
1729 metadata: None,
1730 }),
1731 ],
1732 },
1733 SettingsPage {
1734 title: "Version Control",
1735 items: vec![
1736 SettingsPageItem::SectionHeader("Git"),
1737 SettingsPageItem::SettingItem(SettingItem {
1738 title: "Git Gutter",
1739 description: "Control whether the git gutter is shown",
1740 field: Box::new(SettingField {
1741 pick: |settings_content| {
1742 if let Some(git) = &settings_content.git {
1743 &git.git_gutter
1744 } else {
1745 &None
1746 }
1747 },
1748 pick_mut: |settings_content| {
1749 &mut settings_content.git.get_or_insert_default().git_gutter
1750 },
1751 }),
1752 metadata: None,
1753 }),
1754 // todo(settings_ui): Figure out the right default for this value in default.json
1755 // SettingsPageItem::SettingItem(SettingItem {
1756 // title: "Gutter Debounce",
1757 // description: "Debounce threshold in milliseconds after which changes are reflected in the git gutter",
1758 // field: Box::new(SettingField {
1759 // pick: |settings_content| {
1760 // if let Some(git) = &settings_content.git {
1761 // &git.gutter_debounce
1762 // } else {
1763 // &None
1764 // }
1765 // },
1766 // pick_mut: |settings_content| {
1767 // &mut settings_content.git.get_or_insert_default().gutter_debounce
1768 // },
1769 // }),
1770 // metadata: None,
1771 // }),
1772 SettingsPageItem::SettingItem(SettingItem {
1773 title: "Inline Blame Enabled",
1774 description: "Whether or not to show git blame data inline in the currently focused line",
1775 field: Box::new(SettingField {
1776 pick: |settings_content| {
1777 if let Some(git) = &settings_content.git {
1778 if let Some(inline_blame) = &git.inline_blame {
1779 &inline_blame.enabled
1780 } else {
1781 &None
1782 }
1783 } else {
1784 &None
1785 }
1786 },
1787 pick_mut: |settings_content| {
1788 &mut settings_content
1789 .git
1790 .get_or_insert_default()
1791 .inline_blame
1792 .get_or_insert_default()
1793 .enabled
1794 },
1795 }),
1796 metadata: None,
1797 }),
1798 SettingsPageItem::SettingItem(SettingItem {
1799 title: "Inline Blame Delay",
1800 description: "The delay after which the inline blame information is shown",
1801 field: Box::new(SettingField {
1802 pick: |settings_content| {
1803 if let Some(git) = &settings_content.git {
1804 if let Some(inline_blame) = &git.inline_blame {
1805 &inline_blame.delay_ms
1806 } else {
1807 &None
1808 }
1809 } else {
1810 &None
1811 }
1812 },
1813 pick_mut: |settings_content| {
1814 &mut settings_content
1815 .git
1816 .get_or_insert_default()
1817 .inline_blame
1818 .get_or_insert_default()
1819 .delay_ms
1820 },
1821 }),
1822 metadata: None,
1823 }),
1824 SettingsPageItem::SettingItem(SettingItem {
1825 title: "Inline Blame Padding",
1826 description: "Padding between the end of the source line and the start of the inline blame in columns",
1827 field: Box::new(SettingField {
1828 pick: |settings_content| {
1829 if let Some(git) = &settings_content.git {
1830 if let Some(inline_blame) = &git.inline_blame {
1831 &inline_blame.padding
1832 } else {
1833 &None
1834 }
1835 } else {
1836 &None
1837 }
1838 },
1839 pick_mut: |settings_content| {
1840 &mut settings_content
1841 .git
1842 .get_or_insert_default()
1843 .inline_blame
1844 .get_or_insert_default()
1845 .padding
1846 },
1847 }),
1848 metadata: None,
1849 }),
1850 SettingsPageItem::SettingItem(SettingItem {
1851 title: "Inline Blame Min Column",
1852 description: "The minimum column number to show the inline blame information at",
1853 field: Box::new(SettingField {
1854 pick: |settings_content| {
1855 if let Some(git) = &settings_content.git {
1856 if let Some(inline_blame) = &git.inline_blame {
1857 &inline_blame.min_column
1858 } else {
1859 &None
1860 }
1861 } else {
1862 &None
1863 }
1864 },
1865 pick_mut: |settings_content| {
1866 &mut settings_content
1867 .git
1868 .get_or_insert_default()
1869 .inline_blame
1870 .get_or_insert_default()
1871 .min_column
1872 },
1873 }),
1874 metadata: None,
1875 }),
1876 SettingsPageItem::SettingItem(SettingItem {
1877 title: "Show Commit Summary",
1878 description: "Whether to show commit summary as part of the inline blame",
1879 field: Box::new(SettingField {
1880 pick: |settings_content| {
1881 if let Some(git) = &settings_content.git {
1882 if let Some(inline_blame) = &git.inline_blame {
1883 &inline_blame.show_commit_summary
1884 } else {
1885 &None
1886 }
1887 } else {
1888 &None
1889 }
1890 },
1891 pick_mut: |settings_content| {
1892 &mut settings_content
1893 .git
1894 .get_or_insert_default()
1895 .inline_blame
1896 .get_or_insert_default()
1897 .show_commit_summary
1898 },
1899 }),
1900 metadata: None,
1901 }),
1902 SettingsPageItem::SettingItem(SettingItem {
1903 title: "Show Avatar",
1904 description: "Whether to show the avatar of the author of the commit",
1905 field: Box::new(SettingField {
1906 pick: |settings_content| {
1907 if let Some(git) = &settings_content.git {
1908 if let Some(blame) = &git.blame {
1909 &blame.show_avatar
1910 } else {
1911 &None
1912 }
1913 } else {
1914 &None
1915 }
1916 },
1917 pick_mut: |settings_content| {
1918 &mut settings_content
1919 .git
1920 .get_or_insert_default()
1921 .blame
1922 .get_or_insert_default()
1923 .show_avatar
1924 },
1925 }),
1926 metadata: None,
1927 }),
1928 SettingsPageItem::SettingItem(SettingItem {
1929 title: "Show Author Name In Branch Picker",
1930 description: "Whether to show author name as part of the commit information in branch picker",
1931 field: Box::new(SettingField {
1932 pick: |settings_content| {
1933 if let Some(git) = &settings_content.git {
1934 if let Some(branch_picker) = &git.branch_picker {
1935 &branch_picker.show_author_name
1936 } else {
1937 &None
1938 }
1939 } else {
1940 &None
1941 }
1942 },
1943 pick_mut: |settings_content| {
1944 &mut settings_content
1945 .git
1946 .get_or_insert_default()
1947 .branch_picker
1948 .get_or_insert_default()
1949 .show_author_name
1950 },
1951 }),
1952 metadata: None,
1953 }),
1954 SettingsPageItem::SettingItem(SettingItem {
1955 title: "Hunk Style",
1956 description: "How git hunks are displayed visually in the editor",
1957 field: Box::new(SettingField {
1958 pick: |settings_content| {
1959 if let Some(git) = &settings_content.git {
1960 &git.hunk_style
1961 } else {
1962 &None
1963 }
1964 },
1965 pick_mut: |settings_content| {
1966 &mut settings_content.git.get_or_insert_default().hunk_style
1967 },
1968 }),
1969 metadata: None,
1970 }),
1971 ],
1972 },
1973 SettingsPage {
1974 title: "System & Network",
1975 items: vec![
1976 SettingsPageItem::SectionHeader("Network"),
1977 // todo(settings_ui): Proxy needs a default
1978 // SettingsPageItem::SettingItem(SettingItem {
1979 // title: "Proxy",
1980 // description: "The proxy to use for network requests",
1981 // field: Box::new(SettingField {
1982 // pick: |settings_content| &settings_content.proxy,
1983 // pick_mut: |settings_content| &mut settings_content.proxy,
1984 // }),
1985 // metadata: Some(Box::new(SettingsFieldMetadata {
1986 // placeholder: Some("socks5h://localhost:10808"),
1987 // })),
1988 // }),
1989 SettingsPageItem::SettingItem(SettingItem {
1990 title: "Server URL",
1991 description: "The URL of the Zed server to connect to",
1992 field: Box::new(SettingField {
1993 pick: |settings_content| &settings_content.server_url,
1994 pick_mut: |settings_content| &mut settings_content.server_url,
1995 }),
1996 metadata: Some(Box::new(SettingsFieldMetadata {
1997 placeholder: Some("https://zed.dev"),
1998 })),
1999 }),
2000 SettingsPageItem::SectionHeader("System"),
2001 SettingsPageItem::SettingItem(SettingItem {
2002 title: "Auto Update",
2003 description: "Whether or not to automatically check for updates",
2004 field: Box::new(SettingField {
2005 pick: |settings_content| &settings_content.auto_update,
2006 pick_mut: |settings_content| &mut settings_content.auto_update,
2007 }),
2008 metadata: None,
2009 }),
2010 ],
2011 },
2012 SettingsPage {
2013 title: "Diagnostics & Errors",
2014 items: vec![
2015 SettingsPageItem::SectionHeader("Display"),
2016 SettingsPageItem::SettingItem(SettingItem {
2017 title: "Diagnostics Button",
2018 description: "Whether to show the project diagnostics button in the status bar",
2019 field: Box::new(SettingField {
2020 pick: |settings_content| {
2021 if let Some(diagnostics) = &settings_content.diagnostics {
2022 &diagnostics.button
2023 } else {
2024 &None
2025 }
2026 },
2027 pick_mut: |settings_content| {
2028 &mut settings_content.diagnostics.get_or_insert_default().button
2029 },
2030 }),
2031 metadata: None,
2032 }),
2033 SettingsPageItem::SectionHeader("Filtering"),
2034 SettingsPageItem::SettingItem(SettingItem {
2035 title: "Max Severity",
2036 description: "Which level to use to filter out diagnostics displayed in the editor",
2037 field: Box::new(SettingField {
2038 pick: |settings_content| &settings_content.editor.diagnostics_max_severity,
2039 pick_mut: |settings_content| {
2040 &mut settings_content.editor.diagnostics_max_severity
2041 },
2042 }),
2043 metadata: None,
2044 }),
2045 SettingsPageItem::SettingItem(SettingItem {
2046 title: "Include Warnings",
2047 description: "Whether to show warnings or not by default",
2048 field: Box::new(SettingField {
2049 pick: |settings_content| {
2050 if let Some(diagnostics) = &settings_content.diagnostics {
2051 &diagnostics.include_warnings
2052 } else {
2053 &None
2054 }
2055 },
2056 pick_mut: |settings_content| {
2057 &mut settings_content
2058 .diagnostics
2059 .get_or_insert_default()
2060 .include_warnings
2061 },
2062 }),
2063 metadata: None,
2064 }),
2065 SettingsPageItem::SectionHeader("Inline"),
2066 SettingsPageItem::SettingItem(SettingItem {
2067 title: "Inline Diagnostics Enabled",
2068 description: "Whether to show diagnostics inline or not",
2069 field: Box::new(SettingField {
2070 pick: |settings_content| {
2071 if let Some(diagnostics) = &settings_content.diagnostics {
2072 if let Some(inline) = &diagnostics.inline {
2073 &inline.enabled
2074 } else {
2075 &None
2076 }
2077 } else {
2078 &None
2079 }
2080 },
2081 pick_mut: |settings_content| {
2082 &mut settings_content
2083 .diagnostics
2084 .get_or_insert_default()
2085 .inline
2086 .get_or_insert_default()
2087 .enabled
2088 },
2089 }),
2090 metadata: None,
2091 }),
2092 // todo(settings_ui): Needs numeric stepper
2093 // SettingsPageItem::SettingItem(SettingItem {
2094 // title: "Inline Update Debounce",
2095 // description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update",
2096 // field: Box::new(SettingField {
2097 // pick: |settings_content| {
2098 // if let Some(diagnostics) = &settings_content.diagnostics {
2099 // if let Some(inline) = &diagnostics.inline {
2100 // &inline.update_debounce_ms
2101 // } else {
2102 // &None
2103 // }
2104 // } else {
2105 // &None
2106 // }
2107 // },
2108 // pick_mut: |settings_content| {
2109 // &mut settings_content
2110 // .diagnostics
2111 // .get_or_insert_default()
2112 // .inline
2113 // .get_or_insert_default()
2114 // .update_debounce_ms
2115 // },
2116 // }),
2117 // metadata: None,
2118 // }),
2119 // todo(settings_ui): Needs numeric stepper
2120 // SettingsPageItem::SettingItem(SettingItem {
2121 // title: "Inline Padding",
2122 // description: "The amount of padding between the end of the source line and the start of the inline diagnostic",
2123 // field: Box::new(SettingField {
2124 // pick: |settings_content| {
2125 // if let Some(diagnostics) = &settings_content.diagnostics {
2126 // if let Some(inline) = &diagnostics.inline {
2127 // &inline.padding
2128 // } else {
2129 // &None
2130 // }
2131 // } else {
2132 // &None
2133 // }
2134 // },
2135 // pick_mut: |settings_content| {
2136 // &mut settings_content
2137 // .diagnostics
2138 // .get_or_insert_default()
2139 // .inline
2140 // .get_or_insert_default()
2141 // .padding
2142 // },
2143 // }),
2144 // metadata: None,
2145 // }),
2146 // todo(settings_ui): Needs numeric stepper
2147 // SettingsPageItem::SettingItem(SettingItem {
2148 // title: "Inline Min Column",
2149 // description: "The minimum column to display inline diagnostics",
2150 // field: Box::new(SettingField {
2151 // pick: |settings_content| {
2152 // if let Some(diagnostics) = &settings_content.diagnostics {
2153 // if let Some(inline) = &diagnostics.inline {
2154 // &inline.min_column
2155 // } else {
2156 // &None
2157 // }
2158 // } else {
2159 // &None
2160 // }
2161 // },
2162 // pick_mut: |settings_content| {
2163 // &mut settings_content
2164 // .diagnostics
2165 // .get_or_insert_default()
2166 // .inline
2167 // .get_or_insert_default()
2168 // .min_column
2169 // },
2170 // }),
2171 // metadata: None,
2172 // }),
2173 SettingsPageItem::SectionHeader("Performance"),
2174 SettingsPageItem::SettingItem(SettingItem {
2175 title: "LSP Pull Diagnostics Enabled",
2176 description: "Whether to pull for diagnostics or not",
2177 field: Box::new(SettingField {
2178 pick: |settings_content| {
2179 if let Some(diagnostics) = &settings_content.diagnostics {
2180 if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2181 &lsp_pull.enabled
2182 } else {
2183 &None
2184 }
2185 } else {
2186 &None
2187 }
2188 },
2189 pick_mut: |settings_content| {
2190 &mut settings_content
2191 .diagnostics
2192 .get_or_insert_default()
2193 .lsp_pull_diagnostics
2194 .get_or_insert_default()
2195 .enabled
2196 },
2197 }),
2198 metadata: None,
2199 }),
2200 // todo(settings_ui): Needs numeric stepper
2201 // SettingsPageItem::SettingItem(SettingItem {
2202 // title: "LSP Pull Debounce",
2203 // description: "Minimum time to wait before pulling diagnostics from the language server(s)",
2204 // field: Box::new(SettingField {
2205 // pick: |settings_content| {
2206 // if let Some(diagnostics) = &settings_content.diagnostics {
2207 // if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2208 // &lsp_pull.debounce_ms
2209 // } else {
2210 // &None
2211 // }
2212 // } else {
2213 // &None
2214 // }
2215 // },
2216 // pick_mut: |settings_content| {
2217 // &mut settings_content
2218 // .diagnostics
2219 // .get_or_insert_default()
2220 // .lsp_pull_diagnostics
2221 // .get_or_insert_default()
2222 // .debounce_ms
2223 // },
2224 // }),
2225 // metadata: None,
2226 // }),
2227 ],
2228 },
2229 SettingsPage {
2230 title: "Collaboration",
2231 items: vec![
2232 SettingsPageItem::SectionHeader("Calls"),
2233 SettingsPageItem::SettingItem(SettingItem {
2234 title: "Mute On Join",
2235 description: "Whether the microphone should be muted when joining a channel or a call",
2236 field: Box::new(SettingField {
2237 pick: |settings_content| {
2238 if let Some(calls) = &settings_content.calls {
2239 &calls.mute_on_join
2240 } else {
2241 &None
2242 }
2243 },
2244 pick_mut: |settings_content| {
2245 &mut settings_content.calls.get_or_insert_default().mute_on_join
2246 },
2247 }),
2248 metadata: None,
2249 }),
2250 SettingsPageItem::SettingItem(SettingItem {
2251 title: "Share On Join",
2252 description: "Whether your current project should be shared when joining an empty channel",
2253 field: Box::new(SettingField {
2254 pick: |settings_content| {
2255 if let Some(calls) = &settings_content.calls {
2256 &calls.share_on_join
2257 } else {
2258 &None
2259 }
2260 },
2261 pick_mut: |settings_content| {
2262 &mut settings_content.calls.get_or_insert_default().share_on_join
2263 },
2264 }),
2265 metadata: None,
2266 }),
2267 SettingsPageItem::SectionHeader("Panel"),
2268 SettingsPageItem::SettingItem(SettingItem {
2269 title: "Collaboration Panel Button",
2270 description: "Whether to show the collaboration panel button in the status bar",
2271 field: Box::new(SettingField {
2272 pick: |settings_content| {
2273 if let Some(collab) = &settings_content.collaboration_panel {
2274 &collab.button
2275 } else {
2276 &None
2277 }
2278 },
2279 pick_mut: |settings_content| {
2280 &mut settings_content
2281 .collaboration_panel
2282 .get_or_insert_default()
2283 .button
2284 },
2285 }),
2286 metadata: None,
2287 }),
2288 SettingsPageItem::SectionHeader("Experimental"),
2289 SettingsPageItem::SettingItem(SettingItem {
2290 title: "Rodio Audio",
2291 description: "Opt into the new audio system",
2292 field: Box::new(SettingField {
2293 pick: |settings_content| {
2294 if let Some(audio) = &settings_content.audio {
2295 &audio.rodio_audio
2296 } else {
2297 &None
2298 }
2299 },
2300 pick_mut: |settings_content| {
2301 &mut settings_content.audio.get_or_insert_default().rodio_audio
2302 },
2303 }),
2304 metadata: None,
2305 }),
2306 ],
2307 },
2308 SettingsPage {
2309 title: "AI",
2310 items: vec![
2311 SettingsPageItem::SectionHeader("General"),
2312 SettingsPageItem::SettingItem(SettingItem {
2313 title: "Disable AI",
2314 description: "Whether to disable all AI features in Zed",
2315 field: Box::new(SettingField {
2316 pick: |settings_content| &settings_content.disable_ai,
2317 pick_mut: |settings_content| &mut settings_content.disable_ai,
2318 }),
2319 metadata: None,
2320 }),
2321 ],
2322 },
2323 ]
2324}
2325
2326// Derive Macro, on the new ProjectSettings struct
2327
2328fn project_settings_data() -> Vec<SettingsPage> {
2329 vec![
2330 SettingsPage {
2331 title: "Project",
2332 items: vec![
2333 SettingsPageItem::SectionHeader("Worktree Settings Content"),
2334 SettingsPageItem::SettingItem(SettingItem {
2335 title: "Project Name",
2336 description: "The displayed name of this project. If not set, the root directory name",
2337 field: Box::new(SettingField {
2338 pick: |settings_content| &settings_content.project.worktree.project_name,
2339 pick_mut: |settings_content| {
2340 &mut settings_content.project.worktree.project_name
2341 },
2342 }),
2343 metadata: Some(Box::new(SettingsFieldMetadata {
2344 placeholder: Some("A new name"),
2345 })),
2346 }),
2347 ],
2348 },
2349 SettingsPage {
2350 title: "Appearance & Behavior",
2351 items: vec![
2352 SettingsPageItem::SectionHeader("Guides"),
2353 SettingsPageItem::SettingItem(SettingItem {
2354 title: "Show Wrap Guides",
2355 description: "Whether to show wrap guides (vertical rulers)",
2356 field: Box::new(SettingField {
2357 pick: |settings_content| {
2358 &settings_content
2359 .project
2360 .all_languages
2361 .defaults
2362 .show_wrap_guides
2363 },
2364 pick_mut: |settings_content| {
2365 &mut settings_content
2366 .project
2367 .all_languages
2368 .defaults
2369 .show_wrap_guides
2370 },
2371 }),
2372 metadata: None,
2373 }),
2374 // todo(settings_ui): This needs a custom component
2375 // SettingsPageItem::SettingItem(SettingItem {
2376 // title: "Wrap Guides",
2377 // description: "Character counts at which to show wrap guides",
2378 // field: Box::new(SettingField {
2379 // pick: |settings_content| {
2380 // &settings_content
2381 // .project
2382 // .all_languages
2383 // .defaults
2384 // .wrap_guides
2385 // },
2386 // pick_mut: |settings_content| {
2387 // &mut settings_content
2388 // .project
2389 // .all_languages
2390 // .defaults
2391 // .wrap_guides
2392 // },
2393 // }),
2394 // metadata: None,
2395 // }),
2396 SettingsPageItem::SectionHeader("Whitespace"),
2397 SettingsPageItem::SettingItem(SettingItem {
2398 title: "Show Whitespace",
2399 description: "Whether to show tabs and spaces",
2400 field: Box::new(SettingField {
2401 pick: |settings_content| {
2402 &settings_content
2403 .project
2404 .all_languages
2405 .defaults
2406 .show_whitespaces
2407 },
2408 pick_mut: |settings_content| {
2409 &mut settings_content
2410 .project
2411 .all_languages
2412 .defaults
2413 .show_whitespaces
2414 },
2415 }),
2416 metadata: None,
2417 }),
2418 ],
2419 },
2420 SettingsPage {
2421 title: "Editing",
2422 items: vec![
2423 SettingsPageItem::SectionHeader("Indentation"),
2424 // todo(settings_ui): Needs numeric stepper
2425 // SettingsPageItem::SettingItem(SettingItem {
2426 // title: "Tab Size",
2427 // description: "How many columns a tab should occupy",
2428 // field: Box::new(SettingField {
2429 // pick: |settings_content| &settings_content.project.all_languages.defaults.tab_size,
2430 // pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.tab_size,
2431 // }),
2432 // metadata: None,
2433 // }),
2434 SettingsPageItem::SettingItem(SettingItem {
2435 title: "Hard Tabs",
2436 description: "Whether to indent lines using tab characters, as opposed to multiple spaces",
2437 field: Box::new(SettingField {
2438 pick: |settings_content| {
2439 &settings_content.project.all_languages.defaults.hard_tabs
2440 },
2441 pick_mut: |settings_content| {
2442 &mut settings_content.project.all_languages.defaults.hard_tabs
2443 },
2444 }),
2445 metadata: None,
2446 }),
2447 SettingsPageItem::SettingItem(SettingItem {
2448 title: "Auto Indent",
2449 description: "Whether indentation should be adjusted based on the context whilst typing",
2450 field: Box::new(SettingField {
2451 pick: |settings_content| {
2452 &settings_content.project.all_languages.defaults.auto_indent
2453 },
2454 pick_mut: |settings_content| {
2455 &mut settings_content.project.all_languages.defaults.auto_indent
2456 },
2457 }),
2458 metadata: None,
2459 }),
2460 SettingsPageItem::SettingItem(SettingItem {
2461 title: "Auto Indent On Paste",
2462 description: "Whether indentation of pasted content should be adjusted based on the context",
2463 field: Box::new(SettingField {
2464 pick: |settings_content| {
2465 &settings_content
2466 .project
2467 .all_languages
2468 .defaults
2469 .auto_indent_on_paste
2470 },
2471 pick_mut: |settings_content| {
2472 &mut settings_content
2473 .project
2474 .all_languages
2475 .defaults
2476 .auto_indent_on_paste
2477 },
2478 }),
2479 metadata: None,
2480 }),
2481 SettingsPageItem::SectionHeader("Wrapping"),
2482 // todo(settings_ui): Needs numeric stepper
2483 // SettingsPageItem::SettingItem(SettingItem {
2484 // title: "Preferred Line Length",
2485 // description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled",
2486 // field: Box::new(SettingField {
2487 // pick: |settings_content| &settings_content.project.all_languages.defaults.preferred_line_length,
2488 // pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.preferred_line_length,
2489 // }),
2490 // metadata: None,
2491 // }),
2492 SettingsPageItem::SettingItem(SettingItem {
2493 title: "Soft Wrap",
2494 description: "How to soft-wrap long lines of text",
2495 field: Box::new(SettingField {
2496 pick: |settings_content| {
2497 &settings_content.project.all_languages.defaults.soft_wrap
2498 },
2499 pick_mut: |settings_content| {
2500 &mut settings_content.project.all_languages.defaults.soft_wrap
2501 },
2502 }),
2503 metadata: None,
2504 }),
2505 SettingsPageItem::SectionHeader("Auto Actions"),
2506 SettingsPageItem::SettingItem(SettingItem {
2507 title: "Use Autoclose",
2508 description: "Whether to automatically type closing characters for you",
2509 field: Box::new(SettingField {
2510 pick: |settings_content| {
2511 &settings_content
2512 .project
2513 .all_languages
2514 .defaults
2515 .use_autoclose
2516 },
2517 pick_mut: |settings_content| {
2518 &mut settings_content
2519 .project
2520 .all_languages
2521 .defaults
2522 .use_autoclose
2523 },
2524 }),
2525 metadata: None,
2526 }),
2527 SettingsPageItem::SettingItem(SettingItem {
2528 title: "Use Auto Surround",
2529 description: "Whether to automatically surround text with characters for you",
2530 field: Box::new(SettingField {
2531 pick: |settings_content| {
2532 &settings_content
2533 .project
2534 .all_languages
2535 .defaults
2536 .use_auto_surround
2537 },
2538 pick_mut: |settings_content| {
2539 &mut settings_content
2540 .project
2541 .all_languages
2542 .defaults
2543 .use_auto_surround
2544 },
2545 }),
2546 metadata: None,
2547 }),
2548 SettingsPageItem::SettingItem(SettingItem {
2549 title: "Use On Type Format",
2550 description: "Whether to use additional LSP queries to format the code after every trigger symbol input",
2551 field: Box::new(SettingField {
2552 pick: |settings_content| {
2553 &settings_content
2554 .project
2555 .all_languages
2556 .defaults
2557 .use_on_type_format
2558 },
2559 pick_mut: |settings_content| {
2560 &mut settings_content
2561 .project
2562 .all_languages
2563 .defaults
2564 .use_on_type_format
2565 },
2566 }),
2567 metadata: None,
2568 }),
2569 SettingsPageItem::SettingItem(SettingItem {
2570 title: "Always Treat Brackets As Autoclosed",
2571 description: "Controls how the editor handles the autoclosed characters",
2572 field: Box::new(SettingField {
2573 pick: |settings_content| {
2574 &settings_content
2575 .project
2576 .all_languages
2577 .defaults
2578 .always_treat_brackets_as_autoclosed
2579 },
2580 pick_mut: |settings_content| {
2581 &mut settings_content
2582 .project
2583 .all_languages
2584 .defaults
2585 .always_treat_brackets_as_autoclosed
2586 },
2587 }),
2588 metadata: None,
2589 }),
2590 SettingsPageItem::SectionHeader("Formatting"),
2591 SettingsPageItem::SettingItem(SettingItem {
2592 title: "Remove Trailing Whitespace On Save",
2593 description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it",
2594 field: Box::new(SettingField {
2595 pick: |settings_content| {
2596 &settings_content
2597 .project
2598 .all_languages
2599 .defaults
2600 .remove_trailing_whitespace_on_save
2601 },
2602 pick_mut: |settings_content| {
2603 &mut settings_content
2604 .project
2605 .all_languages
2606 .defaults
2607 .remove_trailing_whitespace_on_save
2608 },
2609 }),
2610 metadata: None,
2611 }),
2612 SettingsPageItem::SettingItem(SettingItem {
2613 title: "Ensure Final Newline On Save",
2614 description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it",
2615 field: Box::new(SettingField {
2616 pick: |settings_content| {
2617 &settings_content
2618 .project
2619 .all_languages
2620 .defaults
2621 .ensure_final_newline_on_save
2622 },
2623 pick_mut: |settings_content| {
2624 &mut settings_content
2625 .project
2626 .all_languages
2627 .defaults
2628 .ensure_final_newline_on_save
2629 },
2630 }),
2631 metadata: None,
2632 }),
2633 SettingsPageItem::SettingItem(SettingItem {
2634 title: "Extend Comment On Newline",
2635 description: "Whether to start a new line with a comment when a previous line is a comment as well",
2636 field: Box::new(SettingField {
2637 pick: |settings_content| {
2638 &settings_content
2639 .project
2640 .all_languages
2641 .defaults
2642 .extend_comment_on_newline
2643 },
2644 pick_mut: |settings_content| {
2645 &mut settings_content
2646 .project
2647 .all_languages
2648 .defaults
2649 .extend_comment_on_newline
2650 },
2651 }),
2652 metadata: None,
2653 }),
2654 SettingsPageItem::SectionHeader("Completions"),
2655 SettingsPageItem::SettingItem(SettingItem {
2656 title: "Show Completions On Input",
2657 description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it",
2658 field: Box::new(SettingField {
2659 pick: |settings_content| {
2660 &settings_content
2661 .project
2662 .all_languages
2663 .defaults
2664 .show_completions_on_input
2665 },
2666 pick_mut: |settings_content| {
2667 &mut settings_content
2668 .project
2669 .all_languages
2670 .defaults
2671 .show_completions_on_input
2672 },
2673 }),
2674 metadata: None,
2675 }),
2676 SettingsPageItem::SettingItem(SettingItem {
2677 title: "Show Completion Documentation",
2678 description: "Whether to display inline and alongside documentation for items in the completions menu",
2679 field: Box::new(SettingField {
2680 pick: |settings_content| {
2681 &settings_content
2682 .project
2683 .all_languages
2684 .defaults
2685 .show_completion_documentation
2686 },
2687 pick_mut: |settings_content| {
2688 &mut settings_content
2689 .project
2690 .all_languages
2691 .defaults
2692 .show_completion_documentation
2693 },
2694 }),
2695 metadata: None,
2696 }),
2697 ],
2698 },
2699 ]
2700}
2701
2702pub struct SettingsUiFeatureFlag;
2703
2704impl FeatureFlag for SettingsUiFeatureFlag {
2705 const NAME: &'static str = "settings-ui";
2706}
2707
2708pub fn init(cx: &mut App) {
2709 init_renderers(cx);
2710
2711 cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
2712 workspace.register_action_renderer(|div, _, _, cx| {
2713 let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsEditor>()];
2714 let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
2715 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
2716 if has_flag {
2717 filter.show_action_types(&settings_ui_actions);
2718 } else {
2719 filter.hide_action_types(&settings_ui_actions);
2720 }
2721 });
2722 if has_flag {
2723 div.on_action(cx.listener(|_, _: &OpenSettingsEditor, _, cx| {
2724 open_settings_editor(cx).ok();
2725 }))
2726 } else {
2727 div
2728 }
2729 });
2730 })
2731 .detach();
2732}
2733
2734fn init_renderers(cx: &mut App) {
2735 // fn (field: SettingsField, current_file: SettingsFile, cx) -> (currently_set_in: SettingsFile, overridden_in: Vec<SettingsFile>)
2736 cx.default_global::<SettingFieldRenderer>()
2737 .add_renderer::<bool>(|settings_field, file, _, _, cx| {
2738 render_toggle_button(*settings_field, file, cx).into_any_element()
2739 })
2740 .add_renderer::<String>(|settings_field, file, metadata, _, cx| {
2741 render_text_field(settings_field.clone(), file, metadata, cx)
2742 })
2743 .add_renderer::<SaturatingBool>(|settings_field, file, _, _, cx| {
2744 render_toggle_button(*settings_field, file, cx)
2745 })
2746 .add_renderer::<CursorShape>(|settings_field, file, _, window, cx| {
2747 render_dropdown(*settings_field, file, window, cx)
2748 })
2749 .add_renderer::<RestoreOnStartupBehavior>(|settings_field, file, _, window, cx| {
2750 render_dropdown(*settings_field, file, window, cx)
2751 })
2752 .add_renderer::<BottomDockLayout>(|settings_field, file, _, window, cx| {
2753 render_dropdown(*settings_field, file, window, cx)
2754 })
2755 .add_renderer::<OnLastWindowClosed>(|settings_field, file, _, window, cx| {
2756 render_dropdown(*settings_field, file, window, cx)
2757 })
2758 .add_renderer::<CloseWindowWhenNoItems>(|settings_field, file, _, window, cx| {
2759 render_dropdown(*settings_field, file, window, cx)
2760 })
2761 .add_renderer::<settings::FontFamilyName>(|settings_field, file, _, window, cx| {
2762 // todo(settings_ui): We need to pass in a validator for this to ensure that users that type in invalid font names
2763 render_font_picker(settings_field.clone(), file, window, cx)
2764 })
2765 .add_renderer::<settings::BufferLineHeight>(|settings_field, file, _, window, cx| {
2766 // todo(settings_ui): Do we want to expose the custom variant of buffer line height?
2767 // right now there's a manual impl of strum::VariantArray
2768 render_dropdown(*settings_field, file, window, cx)
2769 })
2770 .add_renderer::<settings::BaseKeymapContent>(|settings_field, file, _, window, cx| {
2771 render_dropdown(*settings_field, file, window, cx)
2772 })
2773 .add_renderer::<settings::MultiCursorModifier>(|settings_field, file, _, window, cx| {
2774 render_dropdown(*settings_field, file, window, cx)
2775 })
2776 .add_renderer::<settings::HideMouseMode>(|settings_field, file, _, window, cx| {
2777 render_dropdown(*settings_field, file, window, cx)
2778 })
2779 .add_renderer::<settings::CurrentLineHighlight>(|settings_field, file, _, window, cx| {
2780 render_dropdown(*settings_field, file, window, cx)
2781 })
2782 .add_renderer::<settings::ShowWhitespaceSetting>(|settings_field, file, _, window, cx| {
2783 render_dropdown(*settings_field, file, window, cx)
2784 })
2785 .add_renderer::<settings::SoftWrap>(|settings_field, file, _, window, cx| {
2786 render_dropdown(*settings_field, file, window, cx)
2787 })
2788 .add_renderer::<settings::ScrollBeyondLastLine>(|settings_field, file, _, window, cx| {
2789 render_dropdown(*settings_field, file, window, cx)
2790 })
2791 .add_renderer::<settings::SnippetSortOrder>(|settings_field, file, _, window, cx| {
2792 render_dropdown(*settings_field, file, window, cx)
2793 })
2794 .add_renderer::<settings::ClosePosition>(|settings_field, file, _, window, cx| {
2795 render_dropdown(*settings_field, file, window, cx)
2796 })
2797 .add_renderer::<settings::DockSide>(|settings_field, file, _, window, cx| {
2798 render_dropdown(*settings_field, file, window, cx)
2799 })
2800 .add_renderer::<settings::TerminalDockPosition>(|settings_field, file, _, window, cx| {
2801 render_dropdown(*settings_field, file, window, cx)
2802 })
2803 .add_renderer::<settings::GitGutterSetting>(|settings_field, file, _, window, cx| {
2804 render_dropdown(*settings_field, file, window, cx)
2805 })
2806 .add_renderer::<settings::GitHunkStyleSetting>(|settings_field, file, _, window, cx| {
2807 render_dropdown(*settings_field, file, window, cx)
2808 })
2809 .add_renderer::<settings::DiagnosticSeverityContent>(
2810 |settings_field, file, _, window, cx| {
2811 render_dropdown(*settings_field, file, window, cx)
2812 },
2813 )
2814 .add_renderer::<settings::SeedQuerySetting>(|settings_field, file, _, window, cx| {
2815 render_dropdown(*settings_field, file, window, cx)
2816 })
2817 .add_renderer::<settings::DoubleClickInMultibuffer>(
2818 |settings_field, file, _, window, cx| {
2819 render_dropdown(*settings_field, file, window, cx)
2820 },
2821 )
2822 .add_renderer::<settings::GoToDefinitionFallback>(|settings_field, file, _, window, cx| {
2823 render_dropdown(*settings_field, file, window, cx)
2824 })
2825 .add_renderer::<settings::ActivateOnClose>(|settings_field, file, _, window, cx| {
2826 render_dropdown(*settings_field, file, window, cx)
2827 })
2828 .add_renderer::<settings::ShowDiagnostics>(|settings_field, file, _, window, cx| {
2829 render_dropdown(*settings_field, file, window, cx)
2830 })
2831 .add_renderer::<settings::ShowCloseButton>(|settings_field, file, _, window, cx| {
2832 render_dropdown(*settings_field, file, window, cx)
2833 })
2834 .add_renderer::<f32>(|settings_field, file, _, window, cx| {
2835 render_numeric_stepper(*settings_field, file, window, cx)
2836 })
2837 .add_renderer::<u32>(|settings_field, file, _, window, cx| {
2838 render_numeric_stepper(*settings_field, file, window, cx)
2839 })
2840 .add_renderer::<u64>(|settings_field, file, _, window, cx| {
2841 render_numeric_stepper(*settings_field, file, window, cx)
2842 })
2843 .add_renderer::<NonZeroU32>(|settings_field, file, _, window, cx| {
2844 render_numeric_stepper(*settings_field, file, window, cx)
2845 })
2846 .add_renderer::<CodeFade>(|settings_field, file, _, window, cx| {
2847 render_numeric_stepper(*settings_field, file, window, cx)
2848 })
2849 .add_renderer::<FontWeight>(|settings_field, file, _, window, cx| {
2850 render_numeric_stepper(*settings_field, file, window, cx)
2851 });
2852
2853 // todo(settings_ui): Figure out how we want to handle discriminant unions
2854 // .add_renderer::<ThemeSelection>(|settings_field, file, _, window, cx| {
2855 // render_dropdown(*settings_field, file, window, cx)
2856 // });
2857}
2858
2859pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<SettingsWindow>> {
2860 cx.open_window(
2861 WindowOptions {
2862 titlebar: Some(TitlebarOptions {
2863 title: Some("Settings Window".into()),
2864 appears_transparent: true,
2865 traffic_light_position: Some(point(px(12.0), px(12.0))),
2866 }),
2867 focus: true,
2868 show: true,
2869 kind: gpui::WindowKind::Normal,
2870 window_background: cx.theme().window_background_appearance(),
2871 window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
2872 ..Default::default()
2873 },
2874 |window, cx| cx.new(|cx| SettingsWindow::new(window, cx)),
2875 )
2876}
2877
2878pub struct SettingsWindow {
2879 files: Vec<SettingsUiFile>,
2880 current_file: SettingsUiFile,
2881 pages: Vec<SettingsPage>,
2882 search_bar: Entity<Editor>,
2883 search_task: Option<Task<()>>,
2884 navbar_entry: usize, // Index into pages - should probably be (usize, Option<usize>) for section + page
2885 navbar_entries: Vec<NavBarEntry>,
2886 list_handle: UniformListScrollHandle,
2887 search_matches: Vec<Vec<bool>>,
2888 /// The current sub page path that is selected.
2889 /// If this is empty the selected page is rendered,
2890 /// otherwise the last sub page gets rendered.
2891 sub_page_stack: Vec<SubPage>,
2892 scroll_handle: ScrollHandle,
2893}
2894
2895struct SubPage {
2896 link: SubPageLink,
2897 section_header: &'static str,
2898}
2899
2900#[derive(PartialEq, Debug)]
2901struct NavBarEntry {
2902 title: &'static str,
2903 is_root: bool,
2904 expanded: bool,
2905 page_index: usize,
2906 item_index: Option<usize>,
2907}
2908
2909struct SettingsPage {
2910 title: &'static str,
2911 items: Vec<SettingsPageItem>,
2912}
2913
2914#[derive(PartialEq)]
2915enum SettingsPageItem {
2916 SectionHeader(&'static str),
2917 SettingItem(SettingItem),
2918 SubPageLink(SubPageLink),
2919}
2920
2921impl std::fmt::Debug for SettingsPageItem {
2922 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2923 match self {
2924 SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header),
2925 SettingsPageItem::SettingItem(setting_item) => {
2926 write!(f, "SettingItem({})", setting_item.title)
2927 }
2928 SettingsPageItem::SubPageLink(sub_page_link) => {
2929 write!(f, "SubPageLink({})", sub_page_link.title)
2930 }
2931 }
2932 }
2933}
2934
2935impl SettingsPageItem {
2936 fn render(
2937 &self,
2938 file: SettingsUiFile,
2939 section_header: &'static str,
2940 is_last: bool,
2941 window: &mut Window,
2942 cx: &mut Context<SettingsWindow>,
2943 ) -> AnyElement {
2944 match self {
2945 SettingsPageItem::SectionHeader(header) => v_flex()
2946 .w_full()
2947 .gap_1()
2948 .child(
2949 Label::new(SharedString::new_static(header))
2950 .size(LabelSize::XSmall)
2951 .color(Color::Muted)
2952 .buffer_font(cx),
2953 )
2954 .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
2955 .into_any_element(),
2956 SettingsPageItem::SettingItem(setting_item) => {
2957 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
2958 let file_set_in =
2959 SettingsUiFile::from_settings(setting_item.field.file_set_in(file.clone(), cx));
2960
2961 h_flex()
2962 .id(setting_item.title)
2963 .w_full()
2964 .gap_2()
2965 .flex_wrap()
2966 .justify_between()
2967 .map(|this| {
2968 if is_last {
2969 this.pb_6()
2970 } else {
2971 this.pb_4()
2972 .border_b_1()
2973 .border_color(cx.theme().colors().border_variant)
2974 }
2975 })
2976 .child(
2977 v_flex()
2978 .max_w_1_2()
2979 .flex_shrink()
2980 .child(
2981 h_flex()
2982 .w_full()
2983 .gap_1()
2984 .child(Label::new(SharedString::new_static(setting_item.title)))
2985 .when_some(
2986 file_set_in.filter(|file_set_in| file_set_in != &file),
2987 |this, file_set_in| {
2988 this.child(
2989 Label::new(format!(
2990 "— set in {}",
2991 file_set_in.name()
2992 ))
2993 .color(Color::Muted)
2994 .size(LabelSize::Small),
2995 )
2996 },
2997 ),
2998 )
2999 .child(
3000 Label::new(SharedString::new_static(setting_item.description))
3001 .size(LabelSize::Small)
3002 .color(Color::Muted),
3003 ),
3004 )
3005 .child(renderer.render(
3006 setting_item.field.as_ref(),
3007 file,
3008 setting_item.metadata.as_deref(),
3009 window,
3010 cx,
3011 ))
3012 .into_any_element()
3013 }
3014 SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
3015 .id(sub_page_link.title)
3016 .w_full()
3017 .gap_2()
3018 .flex_wrap()
3019 .justify_between()
3020 .when(!is_last, |this| {
3021 this.pb_4()
3022 .border_b_1()
3023 .border_color(cx.theme().colors().border_variant)
3024 })
3025 .child(
3026 v_flex()
3027 .max_w_1_2()
3028 .flex_shrink()
3029 .child(Label::new(SharedString::new_static(sub_page_link.title))),
3030 )
3031 .child(
3032 Button::new(("sub-page".into(), sub_page_link.title), "Configure")
3033 .size(ButtonSize::Medium)
3034 .icon(IconName::ChevronRight)
3035 .icon_position(IconPosition::End)
3036 .icon_color(Color::Muted)
3037 .icon_size(IconSize::Small)
3038 .style(ButtonStyle::Outlined),
3039 )
3040 .on_click({
3041 let sub_page_link = sub_page_link.clone();
3042 cx.listener(move |this, _, _, cx| {
3043 this.push_sub_page(sub_page_link.clone(), section_header, cx)
3044 })
3045 })
3046 .into_any_element(),
3047 }
3048 }
3049}
3050
3051struct SettingItem {
3052 title: &'static str,
3053 description: &'static str,
3054 field: Box<dyn AnySettingField>,
3055 metadata: Option<Box<SettingsFieldMetadata>>,
3056}
3057
3058impl PartialEq for SettingItem {
3059 fn eq(&self, other: &Self) -> bool {
3060 self.title == other.title
3061 && self.description == other.description
3062 && (match (&self.metadata, &other.metadata) {
3063 (None, None) => true,
3064 (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder,
3065 _ => false,
3066 })
3067 }
3068}
3069
3070#[derive(Clone)]
3071struct SubPageLink {
3072 title: &'static str,
3073 render: Rc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> AnyElement>,
3074}
3075
3076impl PartialEq for SubPageLink {
3077 fn eq(&self, other: &Self) -> bool {
3078 self.title == other.title
3079 }
3080}
3081
3082#[allow(unused)]
3083#[derive(Clone, PartialEq)]
3084enum SettingsUiFile {
3085 User, // Uses all settings.
3086 Local((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
3087 Server(&'static str), // Uses a special name, and the user settings
3088}
3089
3090impl SettingsUiFile {
3091 fn pages(&self) -> Vec<SettingsPage> {
3092 match self {
3093 SettingsUiFile::User => user_settings_data(),
3094 SettingsUiFile::Local(_) => project_settings_data(),
3095 SettingsUiFile::Server(_) => user_settings_data(),
3096 }
3097 }
3098
3099 fn name(&self) -> SharedString {
3100 match self {
3101 SettingsUiFile::User => SharedString::new_static("User"),
3102 // TODO is PathStyle::local() ever not appropriate?
3103 SettingsUiFile::Local((_, path)) => {
3104 format!("Local ({})", path.display(PathStyle::local())).into()
3105 }
3106 SettingsUiFile::Server(file) => format!("Server ({})", file).into(),
3107 }
3108 }
3109
3110 fn from_settings(file: settings::SettingsFile) -> Option<Self> {
3111 Some(match file {
3112 settings::SettingsFile::User => SettingsUiFile::User,
3113 settings::SettingsFile::Local(location) => SettingsUiFile::Local(location),
3114 settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
3115 settings::SettingsFile::Default => return None,
3116 })
3117 }
3118
3119 fn to_settings(&self) -> settings::SettingsFile {
3120 match self {
3121 SettingsUiFile::User => settings::SettingsFile::User,
3122 SettingsUiFile::Local(location) => settings::SettingsFile::Local(location.clone()),
3123 SettingsUiFile::Server(_) => settings::SettingsFile::Server,
3124 }
3125 }
3126}
3127
3128impl SettingsWindow {
3129 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
3130 let font_family_cache = theme::FontFamilyCache::global(cx);
3131
3132 cx.spawn(async move |this, cx| {
3133 font_family_cache.prefetch(cx).await;
3134 this.update(cx, |_, cx| {
3135 cx.notify();
3136 })
3137 })
3138 .detach();
3139
3140 let current_file = SettingsUiFile::User;
3141 let search_bar = cx.new(|cx| {
3142 let mut editor = Editor::single_line(window, cx);
3143 editor.set_placeholder_text("Search settings…", window, cx);
3144 editor
3145 });
3146
3147 cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
3148 let EditorEvent::Edited { transaction_id: _ } = event else {
3149 return;
3150 };
3151
3152 this.update_matches(cx);
3153 })
3154 .detach();
3155
3156 cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
3157 this.fetch_files(cx);
3158 cx.notify();
3159 })
3160 .detach();
3161
3162 let mut this = Self {
3163 files: vec![],
3164 current_file: current_file,
3165 pages: vec![],
3166 navbar_entries: vec![],
3167 navbar_entry: 0,
3168 list_handle: UniformListScrollHandle::default(),
3169 search_bar,
3170 search_task: None,
3171 search_matches: vec![],
3172 sub_page_stack: vec![],
3173 scroll_handle: ScrollHandle::new(),
3174 };
3175
3176 this.fetch_files(cx);
3177 this.build_ui(cx);
3178
3179 this.search_bar.update(cx, |editor, cx| {
3180 editor.focus_handle(cx).focus(window);
3181 });
3182
3183 this
3184 }
3185
3186 fn toggle_navbar_entry(&mut self, ix: usize) {
3187 // We can only toggle root entries
3188 if !self.navbar_entries[ix].is_root {
3189 return;
3190 }
3191
3192 let toggle_page_index = self.page_index_from_navbar_index(ix);
3193 let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
3194
3195 let expanded = &mut self.navbar_entries[ix].expanded;
3196 *expanded = !*expanded;
3197 // if currently selected page is a child of the parent page we are folding,
3198 // set the current page to the parent page
3199 if !*expanded && selected_page_index == toggle_page_index {
3200 self.navbar_entry = ix;
3201 }
3202 }
3203
3204 fn build_navbar(&mut self) {
3205 let mut navbar_entries = Vec::with_capacity(self.navbar_entries.len());
3206 for (page_index, page) in self.pages.iter().enumerate() {
3207 navbar_entries.push(NavBarEntry {
3208 title: page.title,
3209 is_root: true,
3210 expanded: false,
3211 page_index,
3212 item_index: None,
3213 });
3214
3215 for (item_index, item) in page.items.iter().enumerate() {
3216 let SettingsPageItem::SectionHeader(title) = item else {
3217 continue;
3218 };
3219 navbar_entries.push(NavBarEntry {
3220 title,
3221 is_root: false,
3222 expanded: false,
3223 page_index,
3224 item_index: Some(item_index),
3225 });
3226 }
3227 }
3228 self.navbar_entries = navbar_entries;
3229 }
3230
3231 fn visible_navbar_entries(&self) -> impl Iterator<Item = (usize, &NavBarEntry)> {
3232 let mut index = 0;
3233 let entries = &self.navbar_entries;
3234 let search_matches = &self.search_matches;
3235 std::iter::from_fn(move || {
3236 while index < entries.len() {
3237 let entry = &entries[index];
3238 let included_in_search = if let Some(item_index) = entry.item_index {
3239 search_matches[entry.page_index][item_index]
3240 } else {
3241 search_matches[entry.page_index].iter().any(|b| *b)
3242 || search_matches[entry.page_index].is_empty()
3243 };
3244 if included_in_search {
3245 break;
3246 }
3247 index += 1;
3248 }
3249 if index >= self.navbar_entries.len() {
3250 return None;
3251 }
3252 let entry = &entries[index];
3253 let entry_index = index;
3254
3255 index += 1;
3256 if entry.is_root && !entry.expanded {
3257 while index < entries.len() {
3258 if entries[index].is_root {
3259 break;
3260 }
3261 index += 1;
3262 }
3263 }
3264
3265 return Some((entry_index, entry));
3266 })
3267 }
3268
3269 fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
3270 self.search_task.take();
3271 let query = self.search_bar.read(cx).text(cx);
3272 if query.is_empty() {
3273 for page in &mut self.search_matches {
3274 page.fill(true);
3275 }
3276 cx.notify();
3277 return;
3278 }
3279
3280 struct ItemKey {
3281 page_index: usize,
3282 header_index: usize,
3283 item_index: usize,
3284 }
3285 let mut key_lut: Vec<ItemKey> = vec![];
3286 let mut candidates = Vec::default();
3287
3288 for (page_index, page) in self.pages.iter().enumerate() {
3289 let mut header_index = 0;
3290 for (item_index, item) in page.items.iter().enumerate() {
3291 let key_index = key_lut.len();
3292 match item {
3293 SettingsPageItem::SettingItem(item) => {
3294 candidates.push(StringMatchCandidate::new(key_index, item.title));
3295 candidates.push(StringMatchCandidate::new(key_index, item.description));
3296 }
3297 SettingsPageItem::SectionHeader(header) => {
3298 candidates.push(StringMatchCandidate::new(key_index, header));
3299 header_index = item_index;
3300 }
3301 SettingsPageItem::SubPageLink(sub_page_link) => {
3302 candidates.push(StringMatchCandidate::new(key_index, sub_page_link.title));
3303 }
3304 }
3305 key_lut.push(ItemKey {
3306 page_index,
3307 header_index,
3308 item_index,
3309 });
3310 }
3311 }
3312 let atomic_bool = AtomicBool::new(false);
3313
3314 self.search_task = Some(cx.spawn(async move |this, cx| {
3315 let string_matches = fuzzy::match_strings(
3316 candidates.as_slice(),
3317 &query,
3318 false,
3319 true,
3320 candidates.len(),
3321 &atomic_bool,
3322 cx.background_executor().clone(),
3323 );
3324 let string_matches = string_matches.await;
3325
3326 this.update(cx, |this, cx| {
3327 for page in &mut this.search_matches {
3328 page.fill(false);
3329 }
3330
3331 for string_match in string_matches {
3332 let ItemKey {
3333 page_index,
3334 header_index,
3335 item_index,
3336 } = key_lut[string_match.candidate_id];
3337 let page = &mut this.search_matches[page_index];
3338 page[header_index] = true;
3339 page[item_index] = true;
3340 }
3341 let first_navbar_entry_index = this
3342 .visible_navbar_entries()
3343 .next()
3344 .map(|e| e.0)
3345 .unwrap_or(0);
3346 this.navbar_entry = first_navbar_entry_index;
3347 cx.notify();
3348 })
3349 .ok();
3350 }));
3351 }
3352
3353 fn build_search_matches(&mut self) {
3354 self.search_matches = self
3355 .pages
3356 .iter()
3357 .map(|page| vec![true; page.items.len()])
3358 .collect::<Vec<_>>();
3359 }
3360
3361 fn build_ui(&mut self, cx: &mut Context<SettingsWindow>) {
3362 self.pages = self.current_file.pages();
3363 self.build_search_matches();
3364 self.build_navbar();
3365
3366 if !self.search_bar.read(cx).is_empty(cx) {
3367 self.update_matches(cx);
3368 }
3369
3370 cx.notify();
3371 }
3372
3373 fn fetch_files(&mut self, cx: &mut Context<SettingsWindow>) {
3374 let settings_store = cx.global::<SettingsStore>();
3375 let mut ui_files = vec![];
3376 let all_files = settings_store.get_all_files();
3377 for file in all_files {
3378 let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
3379 continue;
3380 };
3381 ui_files.push(settings_ui_file);
3382 }
3383 ui_files.reverse();
3384 self.files = ui_files;
3385 if !self.files.contains(&self.current_file) {
3386 self.change_file(0, cx);
3387 }
3388 }
3389
3390 fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
3391 if ix >= self.files.len() {
3392 self.current_file = SettingsUiFile::User;
3393 return;
3394 }
3395 if self.files[ix] == self.current_file {
3396 return;
3397 }
3398 self.current_file = self.files[ix].clone();
3399 self.navbar_entry = 0;
3400 self.build_ui(cx);
3401 }
3402
3403 fn render_files(&self, _window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3404 h_flex()
3405 .gap_1()
3406 .children(self.files.iter().enumerate().map(|(ix, file)| {
3407 Button::new(ix, file.name())
3408 .toggle_state(file == &self.current_file)
3409 .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
3410 .on_click(cx.listener(move |this, _, _window, cx| this.change_file(ix, cx)))
3411 }))
3412 }
3413
3414 fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
3415 h_flex()
3416 .py_1()
3417 .px_1p5()
3418 .gap_1p5()
3419 .rounded_sm()
3420 .bg(cx.theme().colors().editor_background)
3421 .border_1()
3422 .border_color(cx.theme().colors().border)
3423 .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
3424 .child(self.search_bar.clone())
3425 }
3426
3427 fn render_nav(
3428 &self,
3429 window: &mut Window,
3430 cx: &mut Context<SettingsWindow>,
3431 ) -> impl IntoElement {
3432 let visible_entries: Vec<_> = self.visible_navbar_entries().collect();
3433 let visible_count = visible_entries.len();
3434
3435 v_flex()
3436 .w_64()
3437 .p_2p5()
3438 .pt_10()
3439 .gap_3()
3440 .flex_none()
3441 .border_r_1()
3442 .border_color(cx.theme().colors().border)
3443 .bg(cx.theme().colors().panel_background)
3444 .child(self.render_search(window, cx))
3445 .child(
3446 v_flex()
3447 .size_full()
3448 .child(
3449 uniform_list(
3450 "settings-ui-nav-bar",
3451 visible_count,
3452 cx.processor(move |this, range: Range<usize>, _, cx| {
3453 let entries: Vec<_> = this.visible_navbar_entries().collect();
3454 range
3455 .filter_map(|ix| entries.get(ix).copied())
3456 .map(|(ix, entry)| {
3457 TreeViewItem::new(
3458 ("settings-ui-navbar-entry", ix),
3459 entry.title,
3460 )
3461 .root_item(entry.is_root)
3462 .toggle_state(this.is_navbar_entry_selected(ix))
3463 .when(entry.is_root, |item| {
3464 item.expanded(entry.expanded).on_toggle(cx.listener(
3465 move |this, _, _, cx| {
3466 this.toggle_navbar_entry(ix);
3467 cx.notify();
3468 },
3469 ))
3470 })
3471 .on_click(cx.listener(move |this, _, _, cx| {
3472 this.navbar_entry = ix;
3473 cx.notify();
3474 }))
3475 .into_any_element()
3476 })
3477 .collect()
3478 }),
3479 )
3480 .track_scroll(self.list_handle.clone())
3481 .flex_grow(),
3482 )
3483 .vertical_scrollbar_for(self.list_handle.clone(), window, cx),
3484 )
3485 }
3486
3487 fn page_items(&self) -> impl Iterator<Item = &SettingsPageItem> {
3488 let page_idx = self.current_page_index();
3489
3490 self.current_page()
3491 .items
3492 .iter()
3493 .enumerate()
3494 .filter_map(move |(item_index, item)| {
3495 self.search_matches[page_idx][item_index].then_some(item)
3496 })
3497 }
3498
3499 fn render_sub_page_breadcrumbs(&self) -> impl IntoElement {
3500 let mut items = vec![];
3501 items.push(self.current_page().title);
3502 items.extend(
3503 self.sub_page_stack
3504 .iter()
3505 .flat_map(|page| [page.section_header, page.link.title]),
3506 );
3507
3508 let last = items.pop().unwrap();
3509 h_flex()
3510 .gap_1()
3511 .children(
3512 items
3513 .into_iter()
3514 .flat_map(|item| [item, "/"])
3515 .map(|item| Label::new(item).color(Color::Muted)),
3516 )
3517 .child(Label::new(last))
3518 }
3519
3520 fn render_page(
3521 &mut self,
3522 window: &mut Window,
3523 cx: &mut Context<SettingsWindow>,
3524 ) -> impl IntoElement {
3525 let mut page = v_flex()
3526 .w_full()
3527 .pt_4()
3528 .pb_6()
3529 .px_6()
3530 .gap_4()
3531 .bg(cx.theme().colors().editor_background)
3532 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx);
3533
3534 let mut page_content = v_flex()
3535 .id("settings-ui-page")
3536 .size_full()
3537 .gap_4()
3538 .overflow_y_scroll()
3539 .track_scroll(&self.scroll_handle);
3540
3541 if self.sub_page_stack.len() == 0 {
3542 page = page.child(self.render_files(window, cx));
3543
3544 let items: Vec<_> = self.page_items().collect();
3545 let items_len = items.len();
3546 let mut section_header = None;
3547
3548 let search_query = self.search_bar.read(cx).text(cx);
3549 let has_active_search = !search_query.is_empty();
3550 let has_no_results = items_len == 0 && has_active_search;
3551
3552 if has_no_results {
3553 page_content = page_content.child(
3554 v_flex()
3555 .size_full()
3556 .items_center()
3557 .justify_center()
3558 .gap_1()
3559 .child(div().child("No Results"))
3560 .child(
3561 div()
3562 .text_sm()
3563 .text_color(cx.theme().colors().text_muted)
3564 .child(format!("No settings match \"{}\"", search_query)),
3565 ),
3566 )
3567 } else {
3568 let last_non_header_index = items
3569 .iter()
3570 .enumerate()
3571 .rev()
3572 .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
3573 .map(|(index, _)| index);
3574
3575 page_content = page_content.children(items.clone().into_iter().enumerate().map(
3576 |(index, item)| {
3577 let no_bottom_border = items
3578 .get(index + 1)
3579 .map(|next_item| {
3580 matches!(next_item, SettingsPageItem::SectionHeader(_))
3581 })
3582 .unwrap_or(false);
3583 let is_last = Some(index) == last_non_header_index;
3584
3585 if let SettingsPageItem::SectionHeader(header) = item {
3586 section_header = Some(*header);
3587 }
3588 item.render(
3589 self.current_file.clone(),
3590 section_header.expect("All items rendered after a section header"),
3591 no_bottom_border || is_last,
3592 window,
3593 cx,
3594 )
3595 },
3596 ))
3597 }
3598 } else {
3599 page = page.child(
3600 h_flex()
3601 .ml_neg_1p5()
3602 .gap_1()
3603 .child(
3604 IconButton::new("back-btn", IconName::ArrowLeft)
3605 .icon_size(IconSize::Small)
3606 .shape(IconButtonShape::Square)
3607 .on_click(cx.listener(|this, _, _, cx| {
3608 this.pop_sub_page(cx);
3609 })),
3610 )
3611 .child(self.render_sub_page_breadcrumbs()),
3612 );
3613
3614 let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone();
3615 page_content = page_content.child((active_page_render_fn)(self, window, cx));
3616 }
3617
3618 return page.child(page_content);
3619 }
3620
3621 fn current_page_index(&self) -> usize {
3622 self.page_index_from_navbar_index(self.navbar_entry)
3623 }
3624
3625 fn current_page(&self) -> &SettingsPage {
3626 &self.pages[self.current_page_index()]
3627 }
3628
3629 fn page_index_from_navbar_index(&self, index: usize) -> usize {
3630 if self.navbar_entries.is_empty() {
3631 return 0;
3632 }
3633
3634 self.navbar_entries[index].page_index
3635 }
3636
3637 fn is_navbar_entry_selected(&self, ix: usize) -> bool {
3638 ix == self.navbar_entry
3639 }
3640
3641 fn push_sub_page(
3642 &mut self,
3643 sub_page_link: SubPageLink,
3644 section_header: &'static str,
3645 cx: &mut Context<SettingsWindow>,
3646 ) {
3647 self.sub_page_stack.push(SubPage {
3648 link: sub_page_link,
3649 section_header,
3650 });
3651 cx.notify();
3652 }
3653
3654 fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
3655 self.sub_page_stack.pop();
3656 cx.notify();
3657 }
3658}
3659
3660impl Render for SettingsWindow {
3661 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3662 let ui_font = theme::setup_ui_font(window, cx);
3663
3664 div()
3665 .key_context("SettingsWindow")
3666 .flex()
3667 .flex_row()
3668 .size_full()
3669 .font(ui_font)
3670 .bg(cx.theme().colors().background)
3671 .text_color(cx.theme().colors().text)
3672 .child(self.render_nav(window, cx))
3673 .child(self.render_page(window, cx))
3674 }
3675}
3676
3677fn update_settings_file(
3678 file: SettingsUiFile,
3679 cx: &mut App,
3680 update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
3681) -> Result<()> {
3682 match file {
3683 SettingsUiFile::Local((worktree_id, rel_path)) => {
3684 fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
3685 workspace::AppState::global(cx)
3686 .upgrade()
3687 .map(|app_state| {
3688 app_state
3689 .workspace_store
3690 .read(cx)
3691 .workspaces()
3692 .iter()
3693 .filter_map(|workspace| {
3694 Some(workspace.read(cx).ok()?.project().clone())
3695 })
3696 })
3697 .into_iter()
3698 .flatten()
3699 }
3700 let rel_path = rel_path.join(paths::local_settings_file_relative_path());
3701 let project = all_projects(cx).find(|project| {
3702 project.read_with(cx, |project, cx| {
3703 project.contains_local_settings_file(worktree_id, &rel_path, cx)
3704 })
3705 });
3706 let Some(project) = project else {
3707 anyhow::bail!(
3708 "Could not find worktree containing settings file: {}",
3709 &rel_path.display(PathStyle::local())
3710 );
3711 };
3712 project.update(cx, |project, cx| {
3713 project.update_local_settings_file(worktree_id, rel_path, cx, update);
3714 });
3715 return Ok(());
3716 }
3717 SettingsUiFile::User => {
3718 // todo(settings_ui) error?
3719 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
3720 Ok(())
3721 }
3722 SettingsUiFile::Server(_) => unimplemented!(),
3723 }
3724}
3725
3726fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
3727 field: SettingField<T>,
3728 file: SettingsUiFile,
3729 metadata: Option<&SettingsFieldMetadata>,
3730 cx: &mut App,
3731) -> AnyElement {
3732 let (_, initial_text) =
3733 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3734 let initial_text = Some(initial_text.clone()).filter(|s| !s.as_ref().is_empty());
3735
3736 SettingsEditor::new()
3737 .when_some(initial_text, |editor, text| {
3738 editor.with_initial_text(text.into())
3739 })
3740 .when_some(
3741 metadata.and_then(|metadata| metadata.placeholder),
3742 |editor, placeholder| editor.with_placeholder(placeholder),
3743 )
3744 .on_confirm({
3745 move |new_text, cx| {
3746 update_settings_file(file.clone(), cx, move |settings, _cx| {
3747 *(field.pick_mut)(settings) = new_text.map(Into::into);
3748 })
3749 .log_err(); // todo(settings_ui) don't log err
3750 }
3751 })
3752 .into_any_element()
3753}
3754
3755fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
3756 field: SettingField<B>,
3757 file: SettingsUiFile,
3758 cx: &mut App,
3759) -> AnyElement {
3760 let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3761
3762 let toggle_state = if value.into() {
3763 ToggleState::Selected
3764 } else {
3765 ToggleState::Unselected
3766 };
3767
3768 Switch::new("toggle_button", toggle_state)
3769 .color(ui::SwitchColor::Accent)
3770 .on_click({
3771 move |state, _window, cx| {
3772 let state = *state == ui::ToggleState::Selected;
3773 update_settings_file(file.clone(), cx, move |settings, _cx| {
3774 *(field.pick_mut)(settings) = Some(state.into());
3775 })
3776 .log_err(); // todo(settings_ui) don't log err
3777 }
3778 })
3779 .color(SwitchColor::Accent)
3780 .into_any_element()
3781}
3782
3783fn render_font_picker(
3784 field: SettingField<settings::FontFamilyName>,
3785 file: SettingsUiFile,
3786 window: &mut Window,
3787 cx: &mut App,
3788) -> AnyElement {
3789 let current_value = SettingsStore::global(cx)
3790 .get_value_from_file(file.to_settings(), field.pick)
3791 .1
3792 .clone();
3793
3794 let font_picker = cx.new(|cx| {
3795 ui_input::font_picker(
3796 current_value.clone().into(),
3797 move |font_name, cx| {
3798 update_settings_file(file.clone(), cx, move |settings, _cx| {
3799 *(field.pick_mut)(settings) = Some(font_name.into());
3800 })
3801 .log_err(); // todo(settings_ui) don't log err
3802 },
3803 window,
3804 cx,
3805 )
3806 });
3807
3808 div()
3809 .child(
3810 PopoverMenu::new("font-picker")
3811 .menu(move |_window, _cx| Some(font_picker.clone()))
3812 .trigger(
3813 ButtonLike::new("font-family-button")
3814 .style(ButtonStyle::Outlined)
3815 .size(ButtonSize::Medium)
3816 .full_width()
3817 .child(
3818 h_flex()
3819 .w_full()
3820 .justify_between()
3821 .child(Label::new(current_value))
3822 .child(
3823 Icon::new(IconName::ChevronUpDown)
3824 .color(Color::Muted)
3825 .size(IconSize::XSmall),
3826 ),
3827 ),
3828 )
3829 .full_width(true)
3830 .anchor(gpui::Corner::TopLeft)
3831 .offset(gpui::Point {
3832 x: px(0.0),
3833 y: px(4.0),
3834 })
3835 .with_handle(ui::PopoverMenuHandle::default()),
3836 )
3837 .into_any_element()
3838}
3839
3840fn render_numeric_stepper<T: NumericStepperType + Send + Sync>(
3841 field: SettingField<T>,
3842 file: SettingsUiFile,
3843 window: &mut Window,
3844 cx: &mut App,
3845) -> AnyElement {
3846 let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3847
3848 NumericStepper::new("numeric_stepper", value, window, cx)
3849 .on_change({
3850 move |value, _window, cx| {
3851 let value = *value;
3852 update_settings_file(file.clone(), cx, move |settings, _cx| {
3853 *(field.pick_mut)(settings) = Some(value);
3854 })
3855 .log_err(); // todo(settings_ui) don't log err
3856 }
3857 })
3858 .style(NumericStepperStyle::Outlined)
3859 .into_any_element()
3860}
3861
3862fn render_dropdown<T>(
3863 field: SettingField<T>,
3864 file: SettingsUiFile,
3865 window: &mut Window,
3866 cx: &mut App,
3867) -> AnyElement
3868where
3869 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
3870{
3871 let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
3872 let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
3873
3874 let (_, ¤t_value) =
3875 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3876
3877 let current_value_label =
3878 labels()[variants().iter().position(|v| *v == current_value).unwrap()];
3879
3880 DropdownMenu::new(
3881 "dropdown",
3882 current_value_label,
3883 ContextMenu::build(window, cx, move |mut menu, _, _| {
3884 for (&value, &label) in std::iter::zip(variants(), labels()) {
3885 let file = file.clone();
3886 menu = menu.toggleable_entry(
3887 label,
3888 value == current_value,
3889 IconPosition::Start,
3890 None,
3891 move |_, cx| {
3892 if value == current_value {
3893 return;
3894 }
3895 update_settings_file(file.clone(), cx, move |settings, _cx| {
3896 *(field.pick_mut)(settings) = Some(value);
3897 })
3898 .log_err(); // todo(settings_ui) don't log err
3899 },
3900 );
3901 }
3902 menu
3903 }),
3904 )
3905 .trigger_size(ButtonSize::Medium)
3906 .style(DropdownStyle::Outlined)
3907 .offset(gpui::Point {
3908 x: px(0.0),
3909 y: px(2.0),
3910 })
3911 .into_any_element()
3912}
3913
3914#[cfg(test)]
3915mod test {
3916
3917 use super::*;
3918
3919 impl SettingsWindow {
3920 fn navbar_entry(&self) -> usize {
3921 self.navbar_entry
3922 }
3923
3924 fn new_builder(window: &mut Window, cx: &mut Context<Self>) -> Self {
3925 let mut this = Self::new(window, cx);
3926 this.navbar_entries.clear();
3927 this.pages.clear();
3928 this
3929 }
3930
3931 fn build(mut self) -> Self {
3932 self.build_search_matches();
3933 self.build_navbar();
3934 self
3935 }
3936
3937 fn add_page(
3938 mut self,
3939 title: &'static str,
3940 build_page: impl Fn(SettingsPage) -> SettingsPage,
3941 ) -> Self {
3942 let page = SettingsPage {
3943 title,
3944 items: Vec::default(),
3945 };
3946
3947 self.pages.push(build_page(page));
3948 self
3949 }
3950
3951 fn search(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<Self>) {
3952 self.search_task.take();
3953 self.search_bar.update(cx, |editor, cx| {
3954 editor.set_text(search_query, window, cx);
3955 });
3956 self.update_matches(cx);
3957 }
3958
3959 fn assert_search_results(&self, other: &Self) {
3960 // page index could be different because of filtered out pages
3961 #[derive(Debug, PartialEq)]
3962 struct EntryMinimal {
3963 is_root: bool,
3964 title: &'static str,
3965 }
3966 pretty_assertions::assert_eq!(
3967 other
3968 .visible_navbar_entries()
3969 .map(|(_, entry)| EntryMinimal {
3970 is_root: entry.is_root,
3971 title: entry.title,
3972 })
3973 .collect::<Vec<_>>(),
3974 self.visible_navbar_entries()
3975 .map(|(_, entry)| EntryMinimal {
3976 is_root: entry.is_root,
3977 title: entry.title,
3978 })
3979 .collect::<Vec<_>>(),
3980 );
3981 assert_eq!(
3982 self.current_page().items.iter().collect::<Vec<_>>(),
3983 other.page_items().collect::<Vec<_>>()
3984 );
3985 }
3986 }
3987
3988 impl SettingsPage {
3989 fn item(mut self, item: SettingsPageItem) -> Self {
3990 self.items.push(item);
3991 self
3992 }
3993 }
3994
3995 impl SettingsPageItem {
3996 fn basic_item(title: &'static str, description: &'static str) -> Self {
3997 SettingsPageItem::SettingItem(SettingItem {
3998 title,
3999 description,
4000 field: Box::new(SettingField {
4001 pick: |settings_content| &settings_content.auto_update,
4002 pick_mut: |settings_content| &mut settings_content.auto_update,
4003 }),
4004 metadata: None,
4005 })
4006 }
4007 }
4008
4009 fn register_settings(cx: &mut App) {
4010 settings::init(cx);
4011 theme::init(theme::LoadThemes::JustBase, cx);
4012 workspace::init_settings(cx);
4013 project::Project::init_settings(cx);
4014 language::init(cx);
4015 editor::init(cx);
4016 menu::init();
4017 }
4018
4019 fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
4020 let mut pages: Vec<SettingsPage> = Vec::new();
4021 let mut expanded_pages = Vec::new();
4022 let mut selected_idx = None;
4023 let mut index = 0;
4024 let mut in_expanded_section = false;
4025
4026 for mut line in input
4027 .lines()
4028 .map(|line| line.trim())
4029 .filter(|line| !line.is_empty())
4030 {
4031 if let Some(pre) = line.strip_suffix('*') {
4032 assert!(selected_idx.is_none(), "Only one selected entry allowed");
4033 selected_idx = Some(index);
4034 line = pre;
4035 }
4036 let (kind, title) = line.split_once(" ").unwrap();
4037 assert_eq!(kind.len(), 1);
4038 let kind = kind.chars().next().unwrap();
4039 if kind == 'v' {
4040 let page_idx = pages.len();
4041 expanded_pages.push(page_idx);
4042 pages.push(SettingsPage {
4043 title,
4044 items: vec![],
4045 });
4046 index += 1;
4047 in_expanded_section = true;
4048 } else if kind == '>' {
4049 pages.push(SettingsPage {
4050 title,
4051 items: vec![],
4052 });
4053 index += 1;
4054 in_expanded_section = false;
4055 } else if kind == '-' {
4056 pages
4057 .last_mut()
4058 .unwrap()
4059 .items
4060 .push(SettingsPageItem::SectionHeader(title));
4061 if selected_idx == Some(index) && !in_expanded_section {
4062 panic!("Items in unexpanded sections cannot be selected");
4063 }
4064 index += 1;
4065 } else {
4066 panic!(
4067 "Entries must start with one of 'v', '>', or '-'\n line: {}",
4068 line
4069 );
4070 }
4071 }
4072
4073 let mut settings_window = SettingsWindow {
4074 files: Vec::default(),
4075 current_file: crate::SettingsUiFile::User,
4076 pages,
4077 search_bar: cx.new(|cx| Editor::single_line(window, cx)),
4078 navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
4079 navbar_entries: Vec::default(),
4080 list_handle: UniformListScrollHandle::default(),
4081 search_matches: vec![],
4082 search_task: None,
4083 sub_page_stack: vec![],
4084 scroll_handle: ScrollHandle::new(),
4085 };
4086
4087 settings_window.build_search_matches();
4088 settings_window.build_navbar();
4089 for expanded_page_index in expanded_pages {
4090 for entry in &mut settings_window.navbar_entries {
4091 if entry.page_index == expanded_page_index && entry.is_root {
4092 entry.expanded = true;
4093 }
4094 }
4095 }
4096 settings_window
4097 }
4098
4099 #[track_caller]
4100 fn check_navbar_toggle(
4101 before: &'static str,
4102 toggle_page: &'static str,
4103 after: &'static str,
4104 window: &mut Window,
4105 cx: &mut App,
4106 ) {
4107 let mut settings_window = parse(before, window, cx);
4108 let toggle_page_idx = settings_window
4109 .pages
4110 .iter()
4111 .position(|page| page.title == toggle_page)
4112 .expect("page not found");
4113 let toggle_idx = settings_window
4114 .navbar_entries
4115 .iter()
4116 .position(|entry| entry.page_index == toggle_page_idx)
4117 .expect("page not found");
4118 settings_window.toggle_navbar_entry(toggle_idx);
4119
4120 let expected_settings_window = parse(after, window, cx);
4121
4122 pretty_assertions::assert_eq!(
4123 settings_window
4124 .visible_navbar_entries()
4125 .map(|(_, entry)| entry)
4126 .collect::<Vec<_>>(),
4127 expected_settings_window
4128 .visible_navbar_entries()
4129 .map(|(_, entry)| entry)
4130 .collect::<Vec<_>>(),
4131 );
4132 pretty_assertions::assert_eq!(
4133 settings_window.navbar_entries[settings_window.navbar_entry()],
4134 expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()],
4135 );
4136 }
4137
4138 macro_rules! check_navbar_toggle {
4139 ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => {
4140 #[gpui::test]
4141 fn $name(cx: &mut gpui::TestAppContext) {
4142 let window = cx.add_empty_window();
4143 window.update(|window, cx| {
4144 register_settings(cx);
4145 check_navbar_toggle($before, $toggle_page, $after, window, cx);
4146 });
4147 }
4148 };
4149 }
4150
4151 check_navbar_toggle!(
4152 navbar_basic_open,
4153 before: r"
4154 v General
4155 - General
4156 - Privacy*
4157 v Project
4158 - Project Settings
4159 ",
4160 toggle_page: "General",
4161 after: r"
4162 > General*
4163 v Project
4164 - Project Settings
4165 "
4166 );
4167
4168 check_navbar_toggle!(
4169 navbar_basic_close,
4170 before: r"
4171 > General*
4172 - General
4173 - Privacy
4174 v Project
4175 - Project Settings
4176 ",
4177 toggle_page: "General",
4178 after: r"
4179 v General*
4180 - General
4181 - Privacy
4182 v Project
4183 - Project Settings
4184 "
4185 );
4186
4187 check_navbar_toggle!(
4188 navbar_basic_second_root_entry_close,
4189 before: r"
4190 > General
4191 - General
4192 - Privacy
4193 v Project
4194 - Project Settings*
4195 ",
4196 toggle_page: "Project",
4197 after: r"
4198 > General
4199 > Project*
4200 "
4201 );
4202
4203 check_navbar_toggle!(
4204 navbar_toggle_subroot,
4205 before: r"
4206 v General Page
4207 - General
4208 - Privacy
4209 v Project
4210 - Worktree Settings Content*
4211 v AI
4212 - General
4213 > Appearance & Behavior
4214 ",
4215 toggle_page: "Project",
4216 after: r"
4217 v General Page
4218 - General
4219 - Privacy
4220 > Project*
4221 v AI
4222 - General
4223 > Appearance & Behavior
4224 "
4225 );
4226
4227 check_navbar_toggle!(
4228 navbar_toggle_close_propagates_selected_index,
4229 before: r"
4230 v General Page
4231 - General
4232 - Privacy
4233 v Project
4234 - Worktree Settings Content
4235 v AI
4236 - General*
4237 > Appearance & Behavior
4238 ",
4239 toggle_page: "General Page",
4240 after: r"
4241 > General Page
4242 v Project
4243 - Worktree Settings Content
4244 v AI
4245 - General*
4246 > Appearance & Behavior
4247 "
4248 );
4249
4250 check_navbar_toggle!(
4251 navbar_toggle_expand_propagates_selected_index,
4252 before: r"
4253 > General Page
4254 - General
4255 - Privacy
4256 v Project
4257 - Worktree Settings Content
4258 v AI
4259 - General*
4260 > Appearance & Behavior
4261 ",
4262 toggle_page: "General Page",
4263 after: r"
4264 v General Page
4265 - General
4266 - Privacy
4267 v Project
4268 - Worktree Settings Content
4269 v AI
4270 - General*
4271 > Appearance & Behavior
4272 "
4273 );
4274
4275 #[gpui::test]
4276 fn test_basic_search(cx: &mut gpui::TestAppContext) {
4277 let cx = cx.add_empty_window();
4278 let (actual, expected) = cx.update(|window, cx| {
4279 register_settings(cx);
4280
4281 let expected = cx.new(|cx| {
4282 SettingsWindow::new_builder(window, cx)
4283 .add_page("General", |page| {
4284 page.item(SettingsPageItem::SectionHeader("General settings"))
4285 .item(SettingsPageItem::basic_item("test title", "General test"))
4286 })
4287 .build()
4288 });
4289
4290 let actual = cx.new(|cx| {
4291 SettingsWindow::new_builder(window, cx)
4292 .add_page("General", |page| {
4293 page.item(SettingsPageItem::SectionHeader("General settings"))
4294 .item(SettingsPageItem::basic_item("test title", "General test"))
4295 })
4296 .add_page("Theme", |page| {
4297 page.item(SettingsPageItem::SectionHeader("Theme settings"))
4298 })
4299 .build()
4300 });
4301
4302 actual.update(cx, |settings, cx| settings.search("gen", window, cx));
4303
4304 (actual, expected)
4305 });
4306
4307 cx.cx.run_until_parked();
4308
4309 cx.update(|_window, cx| {
4310 let expected = expected.read(cx);
4311 let actual = actual.read(cx);
4312 expected.assert_search_results(&actual);
4313 })
4314 }
4315
4316 #[gpui::test]
4317 fn test_search_render_page_with_filtered_out_navbar_entries(cx: &mut gpui::TestAppContext) {
4318 let cx = cx.add_empty_window();
4319 let (actual, expected) = cx.update(|window, cx| {
4320 register_settings(cx);
4321
4322 let actual = cx.new(|cx| {
4323 SettingsWindow::new_builder(window, cx)
4324 .add_page("General", |page| {
4325 page.item(SettingsPageItem::SectionHeader("General settings"))
4326 .item(SettingsPageItem::basic_item(
4327 "Confirm Quit",
4328 "Whether to confirm before quitting Zed",
4329 ))
4330 .item(SettingsPageItem::basic_item(
4331 "Auto Update",
4332 "Automatically update Zed",
4333 ))
4334 })
4335 .add_page("AI", |page| {
4336 page.item(SettingsPageItem::basic_item(
4337 "Disable AI",
4338 "Whether to disable all AI features in Zed",
4339 ))
4340 })
4341 .add_page("Appearance & Behavior", |page| {
4342 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4343 SettingsPageItem::basic_item(
4344 "Cursor Shape",
4345 "Cursor shape for the editor",
4346 ),
4347 )
4348 })
4349 .build()
4350 });
4351
4352 let expected = cx.new(|cx| {
4353 SettingsWindow::new_builder(window, cx)
4354 .add_page("Appearance & Behavior", |page| {
4355 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4356 SettingsPageItem::basic_item(
4357 "Cursor Shape",
4358 "Cursor shape for the editor",
4359 ),
4360 )
4361 })
4362 .build()
4363 });
4364
4365 actual.update(cx, |settings, cx| settings.search("cursor", window, cx));
4366
4367 (actual, expected)
4368 });
4369
4370 cx.cx.run_until_parked();
4371
4372 cx.update(|_window, cx| {
4373 let expected = expected.read(cx);
4374 let actual = actual.read(cx);
4375 expected.assert_search_results(&actual);
4376 })
4377 }
4378}