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 ContextMenu, Divider, DropdownMenu, DropdownStyle, IconButtonShape, Switch, SwitchColor,
28 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, metadata, _, 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_text_field(settings_field.clone(), file, metadata, 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 current_file = SettingsUiFile::User;
3131 let search_bar = cx.new(|cx| {
3132 let mut editor = Editor::single_line(window, cx);
3133 editor.set_placeholder_text("Search settings…", window, cx);
3134 editor
3135 });
3136
3137 cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
3138 let EditorEvent::Edited { transaction_id: _ } = event else {
3139 return;
3140 };
3141
3142 this.update_matches(cx);
3143 })
3144 .detach();
3145
3146 cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
3147 this.fetch_files(cx);
3148 cx.notify();
3149 })
3150 .detach();
3151
3152 let mut this = Self {
3153 files: vec![],
3154 current_file: current_file,
3155 pages: vec![],
3156 navbar_entries: vec![],
3157 navbar_entry: 0,
3158 list_handle: UniformListScrollHandle::default(),
3159 search_bar,
3160 search_task: None,
3161 search_matches: vec![],
3162 sub_page_stack: vec![],
3163 scroll_handle: ScrollHandle::new(),
3164 };
3165
3166 this.fetch_files(cx);
3167 this.build_ui(cx);
3168
3169 this.search_bar.update(cx, |editor, cx| {
3170 editor.focus_handle(cx).focus(window);
3171 });
3172
3173 this
3174 }
3175
3176 fn toggle_navbar_entry(&mut self, ix: usize) {
3177 // We can only toggle root entries
3178 if !self.navbar_entries[ix].is_root {
3179 return;
3180 }
3181
3182 let toggle_page_index = self.page_index_from_navbar_index(ix);
3183 let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
3184
3185 let expanded = &mut self.navbar_entries[ix].expanded;
3186 *expanded = !*expanded;
3187 // if currently selected page is a child of the parent page we are folding,
3188 // set the current page to the parent page
3189 if !*expanded && selected_page_index == toggle_page_index {
3190 self.navbar_entry = ix;
3191 }
3192 }
3193
3194 fn build_navbar(&mut self) {
3195 let mut navbar_entries = Vec::with_capacity(self.navbar_entries.len());
3196 for (page_index, page) in self.pages.iter().enumerate() {
3197 navbar_entries.push(NavBarEntry {
3198 title: page.title,
3199 is_root: true,
3200 expanded: false,
3201 page_index,
3202 item_index: None,
3203 });
3204
3205 for (item_index, item) in page.items.iter().enumerate() {
3206 let SettingsPageItem::SectionHeader(title) = item else {
3207 continue;
3208 };
3209 navbar_entries.push(NavBarEntry {
3210 title,
3211 is_root: false,
3212 expanded: false,
3213 page_index,
3214 item_index: Some(item_index),
3215 });
3216 }
3217 }
3218 self.navbar_entries = navbar_entries;
3219 }
3220
3221 fn visible_navbar_entries(&self) -> impl Iterator<Item = (usize, &NavBarEntry)> {
3222 let mut index = 0;
3223 let entries = &self.navbar_entries;
3224 let search_matches = &self.search_matches;
3225 std::iter::from_fn(move || {
3226 while index < entries.len() {
3227 let entry = &entries[index];
3228 let included_in_search = if let Some(item_index) = entry.item_index {
3229 search_matches[entry.page_index][item_index]
3230 } else {
3231 search_matches[entry.page_index].iter().any(|b| *b)
3232 || search_matches[entry.page_index].is_empty()
3233 };
3234 if included_in_search {
3235 break;
3236 }
3237 index += 1;
3238 }
3239 if index >= self.navbar_entries.len() {
3240 return None;
3241 }
3242 let entry = &entries[index];
3243 let entry_index = index;
3244
3245 index += 1;
3246 if entry.is_root && !entry.expanded {
3247 while index < entries.len() {
3248 if entries[index].is_root {
3249 break;
3250 }
3251 index += 1;
3252 }
3253 }
3254
3255 return Some((entry_index, entry));
3256 })
3257 }
3258
3259 fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
3260 self.search_task.take();
3261 let query = self.search_bar.read(cx).text(cx);
3262 if query.is_empty() {
3263 for page in &mut self.search_matches {
3264 page.fill(true);
3265 }
3266 cx.notify();
3267 return;
3268 }
3269
3270 struct ItemKey {
3271 page_index: usize,
3272 header_index: usize,
3273 item_index: usize,
3274 }
3275 let mut key_lut: Vec<ItemKey> = vec![];
3276 let mut candidates = Vec::default();
3277
3278 for (page_index, page) in self.pages.iter().enumerate() {
3279 let mut header_index = 0;
3280 for (item_index, item) in page.items.iter().enumerate() {
3281 let key_index = key_lut.len();
3282 match item {
3283 SettingsPageItem::SettingItem(item) => {
3284 candidates.push(StringMatchCandidate::new(key_index, item.title));
3285 candidates.push(StringMatchCandidate::new(key_index, item.description));
3286 }
3287 SettingsPageItem::SectionHeader(header) => {
3288 candidates.push(StringMatchCandidate::new(key_index, header));
3289 header_index = item_index;
3290 }
3291 SettingsPageItem::SubPageLink(sub_page_link) => {
3292 candidates.push(StringMatchCandidate::new(key_index, sub_page_link.title));
3293 }
3294 }
3295 key_lut.push(ItemKey {
3296 page_index,
3297 header_index,
3298 item_index,
3299 });
3300 }
3301 }
3302 let atomic_bool = AtomicBool::new(false);
3303
3304 self.search_task = Some(cx.spawn(async move |this, cx| {
3305 let string_matches = fuzzy::match_strings(
3306 candidates.as_slice(),
3307 &query,
3308 false,
3309 true,
3310 candidates.len(),
3311 &atomic_bool,
3312 cx.background_executor().clone(),
3313 );
3314 let string_matches = string_matches.await;
3315
3316 this.update(cx, |this, cx| {
3317 for page in &mut this.search_matches {
3318 page.fill(false);
3319 }
3320
3321 for string_match in string_matches {
3322 let ItemKey {
3323 page_index,
3324 header_index,
3325 item_index,
3326 } = key_lut[string_match.candidate_id];
3327 let page = &mut this.search_matches[page_index];
3328 page[header_index] = true;
3329 page[item_index] = true;
3330 }
3331 let first_navbar_entry_index = this
3332 .visible_navbar_entries()
3333 .next()
3334 .map(|e| e.0)
3335 .unwrap_or(0);
3336 this.navbar_entry = first_navbar_entry_index;
3337 cx.notify();
3338 })
3339 .ok();
3340 }));
3341 }
3342
3343 fn build_search_matches(&mut self) {
3344 self.search_matches = self
3345 .pages
3346 .iter()
3347 .map(|page| vec![true; page.items.len()])
3348 .collect::<Vec<_>>();
3349 }
3350
3351 fn build_ui(&mut self, cx: &mut Context<SettingsWindow>) {
3352 self.pages = self.current_file.pages();
3353 self.build_search_matches();
3354 self.build_navbar();
3355
3356 if !self.search_bar.read(cx).is_empty(cx) {
3357 self.update_matches(cx);
3358 }
3359
3360 cx.notify();
3361 }
3362
3363 fn fetch_files(&mut self, cx: &mut Context<SettingsWindow>) {
3364 let settings_store = cx.global::<SettingsStore>();
3365 let mut ui_files = vec![];
3366 let all_files = settings_store.get_all_files();
3367 for file in all_files {
3368 let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
3369 continue;
3370 };
3371 ui_files.push(settings_ui_file);
3372 }
3373 ui_files.reverse();
3374 self.files = ui_files;
3375 if !self.files.contains(&self.current_file) {
3376 self.change_file(0, cx);
3377 }
3378 }
3379
3380 fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
3381 if ix >= self.files.len() {
3382 self.current_file = SettingsUiFile::User;
3383 return;
3384 }
3385 if self.files[ix] == self.current_file {
3386 return;
3387 }
3388 self.current_file = self.files[ix].clone();
3389 self.navbar_entry = 0;
3390 self.build_ui(cx);
3391 }
3392
3393 fn render_files(&self, _window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3394 h_flex()
3395 .gap_1()
3396 .children(self.files.iter().enumerate().map(|(ix, file)| {
3397 Button::new(ix, file.name())
3398 .toggle_state(file == &self.current_file)
3399 .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent))
3400 .on_click(cx.listener(move |this, _, _window, cx| this.change_file(ix, cx)))
3401 }))
3402 }
3403
3404 fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
3405 h_flex()
3406 .py_1()
3407 .px_1p5()
3408 .gap_1p5()
3409 .rounded_sm()
3410 .bg(cx.theme().colors().editor_background)
3411 .border_1()
3412 .border_color(cx.theme().colors().border)
3413 .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
3414 .child(self.search_bar.clone())
3415 }
3416
3417 fn render_nav(
3418 &self,
3419 window: &mut Window,
3420 cx: &mut Context<SettingsWindow>,
3421 ) -> impl IntoElement {
3422 let visible_entries: Vec<_> = self.visible_navbar_entries().collect();
3423 let visible_count = visible_entries.len();
3424
3425 v_flex()
3426 .w_64()
3427 .p_2p5()
3428 .pt_10()
3429 .gap_3()
3430 .flex_none()
3431 .border_r_1()
3432 .border_color(cx.theme().colors().border)
3433 .bg(cx.theme().colors().panel_background)
3434 .child(self.render_search(window, cx))
3435 .child(
3436 v_flex()
3437 .size_full()
3438 .child(
3439 uniform_list(
3440 "settings-ui-nav-bar",
3441 visible_count,
3442 cx.processor(move |this, range: Range<usize>, _, cx| {
3443 let entries: Vec<_> = this.visible_navbar_entries().collect();
3444 range
3445 .filter_map(|ix| entries.get(ix).copied())
3446 .map(|(ix, entry)| {
3447 TreeViewItem::new(
3448 ("settings-ui-navbar-entry", ix),
3449 entry.title,
3450 )
3451 .root_item(entry.is_root)
3452 .toggle_state(this.is_navbar_entry_selected(ix))
3453 .when(entry.is_root, |item| {
3454 item.expanded(entry.expanded).on_toggle(cx.listener(
3455 move |this, _, _, cx| {
3456 this.toggle_navbar_entry(ix);
3457 cx.notify();
3458 },
3459 ))
3460 })
3461 .on_click(cx.listener(move |this, _, _, cx| {
3462 this.navbar_entry = ix;
3463 cx.notify();
3464 }))
3465 .into_any_element()
3466 })
3467 .collect()
3468 }),
3469 )
3470 .track_scroll(self.list_handle.clone())
3471 .flex_grow(),
3472 )
3473 .vertical_scrollbar_for(self.list_handle.clone(), window, cx),
3474 )
3475 }
3476
3477 fn page_items(&self) -> impl Iterator<Item = &SettingsPageItem> {
3478 let page_idx = self.current_page_index();
3479
3480 self.current_page()
3481 .items
3482 .iter()
3483 .enumerate()
3484 .filter_map(move |(item_index, item)| {
3485 self.search_matches[page_idx][item_index].then_some(item)
3486 })
3487 }
3488
3489 fn render_sub_page_breadcrumbs(&self) -> impl IntoElement {
3490 let mut items = vec![];
3491 items.push(self.current_page().title);
3492 items.extend(
3493 self.sub_page_stack
3494 .iter()
3495 .flat_map(|page| [page.section_header, page.link.title]),
3496 );
3497
3498 let last = items.pop().unwrap();
3499 h_flex()
3500 .gap_1()
3501 .children(
3502 items
3503 .into_iter()
3504 .flat_map(|item| [item, "/"])
3505 .map(|item| Label::new(item).color(Color::Muted)),
3506 )
3507 .child(Label::new(last))
3508 }
3509
3510 fn render_page(
3511 &mut self,
3512 window: &mut Window,
3513 cx: &mut Context<SettingsWindow>,
3514 ) -> impl IntoElement {
3515 let mut page = v_flex()
3516 .w_full()
3517 .pt_4()
3518 .pb_6()
3519 .px_6()
3520 .gap_4()
3521 .bg(cx.theme().colors().editor_background)
3522 .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx);
3523
3524 let mut page_content = v_flex()
3525 .id("settings-ui-page")
3526 .size_full()
3527 .gap_4()
3528 .overflow_y_scroll()
3529 .track_scroll(&self.scroll_handle);
3530
3531 if self.sub_page_stack.len() == 0 {
3532 page = page.child(self.render_files(window, cx));
3533
3534 let items: Vec<_> = self.page_items().collect();
3535 let items_len = items.len();
3536 let mut section_header = None;
3537
3538 let search_query = self.search_bar.read(cx).text(cx);
3539 let has_active_search = !search_query.is_empty();
3540 let has_no_results = items_len == 0 && has_active_search;
3541
3542 if has_no_results {
3543 page_content = page_content.child(
3544 v_flex()
3545 .size_full()
3546 .items_center()
3547 .justify_center()
3548 .gap_1()
3549 .child(div().child("No Results"))
3550 .child(
3551 div()
3552 .text_sm()
3553 .text_color(cx.theme().colors().text_muted)
3554 .child(format!("No settings match \"{}\"", search_query)),
3555 ),
3556 )
3557 } else {
3558 let last_non_header_index = items
3559 .iter()
3560 .enumerate()
3561 .rev()
3562 .find(|(_, item)| !matches!(item, SettingsPageItem::SectionHeader(_)))
3563 .map(|(index, _)| index);
3564
3565 page_content = page_content.children(items.clone().into_iter().enumerate().map(
3566 |(index, item)| {
3567 let no_bottom_border = items
3568 .get(index + 1)
3569 .map(|next_item| {
3570 matches!(next_item, SettingsPageItem::SectionHeader(_))
3571 })
3572 .unwrap_or(false);
3573 let is_last = Some(index) == last_non_header_index;
3574
3575 if let SettingsPageItem::SectionHeader(header) = item {
3576 section_header = Some(*header);
3577 }
3578 item.render(
3579 self.current_file.clone(),
3580 section_header.expect("All items rendered after a section header"),
3581 no_bottom_border || is_last,
3582 window,
3583 cx,
3584 )
3585 },
3586 ))
3587 }
3588 } else {
3589 page = page.child(
3590 h_flex()
3591 .ml_neg_1p5()
3592 .gap_1()
3593 .child(
3594 IconButton::new("back-btn", IconName::ArrowLeft)
3595 .icon_size(IconSize::Small)
3596 .shape(IconButtonShape::Square)
3597 .on_click(cx.listener(|this, _, _, cx| {
3598 this.pop_sub_page(cx);
3599 })),
3600 )
3601 .child(self.render_sub_page_breadcrumbs()),
3602 );
3603
3604 let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone();
3605 page_content = page_content.child((active_page_render_fn)(self, window, cx));
3606 }
3607
3608 return page.child(page_content);
3609 }
3610
3611 fn current_page_index(&self) -> usize {
3612 self.page_index_from_navbar_index(self.navbar_entry)
3613 }
3614
3615 fn current_page(&self) -> &SettingsPage {
3616 &self.pages[self.current_page_index()]
3617 }
3618
3619 fn page_index_from_navbar_index(&self, index: usize) -> usize {
3620 if self.navbar_entries.is_empty() {
3621 return 0;
3622 }
3623
3624 self.navbar_entries[index].page_index
3625 }
3626
3627 fn is_navbar_entry_selected(&self, ix: usize) -> bool {
3628 ix == self.navbar_entry
3629 }
3630
3631 fn push_sub_page(
3632 &mut self,
3633 sub_page_link: SubPageLink,
3634 section_header: &'static str,
3635 cx: &mut Context<SettingsWindow>,
3636 ) {
3637 self.sub_page_stack.push(SubPage {
3638 link: sub_page_link,
3639 section_header,
3640 });
3641 cx.notify();
3642 }
3643
3644 fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
3645 self.sub_page_stack.pop();
3646 cx.notify();
3647 }
3648}
3649
3650impl Render for SettingsWindow {
3651 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3652 let ui_font = theme::setup_ui_font(window, cx);
3653
3654 div()
3655 .key_context("SettingsWindow")
3656 .flex()
3657 .flex_row()
3658 .size_full()
3659 .font(ui_font)
3660 .bg(cx.theme().colors().background)
3661 .text_color(cx.theme().colors().text)
3662 .child(self.render_nav(window, cx))
3663 .child(self.render_page(window, cx))
3664 }
3665}
3666
3667fn update_settings_file(
3668 file: SettingsUiFile,
3669 cx: &mut App,
3670 update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
3671) -> Result<()> {
3672 match file {
3673 SettingsUiFile::Local((worktree_id, rel_path)) => {
3674 fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
3675 workspace::AppState::global(cx)
3676 .upgrade()
3677 .map(|app_state| {
3678 app_state
3679 .workspace_store
3680 .read(cx)
3681 .workspaces()
3682 .iter()
3683 .filter_map(|workspace| {
3684 Some(workspace.read(cx).ok()?.project().clone())
3685 })
3686 })
3687 .into_iter()
3688 .flatten()
3689 }
3690 let rel_path = rel_path.join(paths::local_settings_file_relative_path());
3691 let project = all_projects(cx).find(|project| {
3692 project.read_with(cx, |project, cx| {
3693 project.contains_local_settings_file(worktree_id, &rel_path, cx)
3694 })
3695 });
3696 let Some(project) = project else {
3697 anyhow::bail!(
3698 "Could not find worktree containing settings file: {}",
3699 &rel_path.display(PathStyle::local())
3700 );
3701 };
3702 project.update(cx, |project, cx| {
3703 project.update_local_settings_file(worktree_id, rel_path, cx, update);
3704 });
3705 return Ok(());
3706 }
3707 SettingsUiFile::User => {
3708 // todo(settings_ui) error?
3709 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
3710 Ok(())
3711 }
3712 SettingsUiFile::Server(_) => unimplemented!(),
3713 }
3714}
3715
3716fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
3717 field: SettingField<T>,
3718 file: SettingsUiFile,
3719 metadata: Option<&SettingsFieldMetadata>,
3720 cx: &mut App,
3721) -> AnyElement {
3722 let (_, initial_text) =
3723 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3724 let initial_text = Some(initial_text.clone()).filter(|s| !s.as_ref().is_empty());
3725
3726 SettingsEditor::new()
3727 .when_some(initial_text, |editor, text| {
3728 editor.with_initial_text(text.into())
3729 })
3730 .when_some(
3731 metadata.and_then(|metadata| metadata.placeholder),
3732 |editor, placeholder| editor.with_placeholder(placeholder),
3733 )
3734 .on_confirm({
3735 move |new_text, cx| {
3736 update_settings_file(file.clone(), cx, move |settings, _cx| {
3737 *(field.pick_mut)(settings) = new_text.map(Into::into);
3738 })
3739 .log_err(); // todo(settings_ui) don't log err
3740 }
3741 })
3742 .into_any_element()
3743}
3744
3745fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
3746 field: SettingField<B>,
3747 file: SettingsUiFile,
3748 cx: &mut App,
3749) -> AnyElement {
3750 let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3751
3752 let toggle_state = if value.into() {
3753 ToggleState::Selected
3754 } else {
3755 ToggleState::Unselected
3756 };
3757
3758 Switch::new("toggle_button", toggle_state)
3759 .color(ui::SwitchColor::Accent)
3760 .on_click({
3761 move |state, _window, cx| {
3762 let state = *state == ui::ToggleState::Selected;
3763 update_settings_file(file.clone(), cx, move |settings, _cx| {
3764 *(field.pick_mut)(settings) = Some(state.into());
3765 })
3766 .log_err(); // todo(settings_ui) don't log err
3767 }
3768 })
3769 .color(SwitchColor::Accent)
3770 .into_any_element()
3771}
3772
3773fn render_numeric_stepper<T: NumericStepperType + Send + Sync>(
3774 field: SettingField<T>,
3775 file: SettingsUiFile,
3776 window: &mut Window,
3777 cx: &mut App,
3778) -> AnyElement {
3779 let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3780
3781 NumericStepper::new("numeric_stepper", value, window, cx)
3782 .on_change({
3783 move |value, _window, cx| {
3784 let value = *value;
3785 update_settings_file(file.clone(), cx, move |settings, _cx| {
3786 *(field.pick_mut)(settings) = Some(value);
3787 })
3788 .log_err(); // todo(settings_ui) don't log err
3789 }
3790 })
3791 .style(NumericStepperStyle::Outlined)
3792 .into_any_element()
3793}
3794
3795fn render_dropdown<T>(
3796 field: SettingField<T>,
3797 file: SettingsUiFile,
3798 window: &mut Window,
3799 cx: &mut App,
3800) -> AnyElement
3801where
3802 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
3803{
3804 let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
3805 let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
3806
3807 let (_, ¤t_value) =
3808 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3809
3810 let current_value_label =
3811 labels()[variants().iter().position(|v| *v == current_value).unwrap()];
3812
3813 DropdownMenu::new(
3814 "dropdown",
3815 current_value_label,
3816 ContextMenu::build(window, cx, move |mut menu, _, _| {
3817 for (&value, &label) in std::iter::zip(variants(), labels()) {
3818 let file = file.clone();
3819 menu = menu.toggleable_entry(
3820 label,
3821 value == current_value,
3822 IconPosition::Start,
3823 None,
3824 move |_, cx| {
3825 if value == current_value {
3826 return;
3827 }
3828 update_settings_file(file.clone(), cx, move |settings, _cx| {
3829 *(field.pick_mut)(settings) = Some(value);
3830 })
3831 .log_err(); // todo(settings_ui) don't log err
3832 },
3833 );
3834 }
3835 menu
3836 }),
3837 )
3838 .trigger_size(ButtonSize::Medium)
3839 .style(DropdownStyle::Outlined)
3840 .offset(gpui::Point {
3841 x: px(0.0),
3842 y: px(2.0),
3843 })
3844 .into_any_element()
3845}
3846
3847#[cfg(test)]
3848mod test {
3849
3850 use super::*;
3851
3852 impl SettingsWindow {
3853 fn navbar_entry(&self) -> usize {
3854 self.navbar_entry
3855 }
3856
3857 fn new_builder(window: &mut Window, cx: &mut Context<Self>) -> Self {
3858 let mut this = Self::new(window, cx);
3859 this.navbar_entries.clear();
3860 this.pages.clear();
3861 this
3862 }
3863
3864 fn build(mut self) -> Self {
3865 self.build_search_matches();
3866 self.build_navbar();
3867 self
3868 }
3869
3870 fn add_page(
3871 mut self,
3872 title: &'static str,
3873 build_page: impl Fn(SettingsPage) -> SettingsPage,
3874 ) -> Self {
3875 let page = SettingsPage {
3876 title,
3877 items: Vec::default(),
3878 };
3879
3880 self.pages.push(build_page(page));
3881 self
3882 }
3883
3884 fn search(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<Self>) {
3885 self.search_task.take();
3886 self.search_bar.update(cx, |editor, cx| {
3887 editor.set_text(search_query, window, cx);
3888 });
3889 self.update_matches(cx);
3890 }
3891
3892 fn assert_search_results(&self, other: &Self) {
3893 // page index could be different because of filtered out pages
3894 #[derive(Debug, PartialEq)]
3895 struct EntryMinimal {
3896 is_root: bool,
3897 title: &'static str,
3898 }
3899 pretty_assertions::assert_eq!(
3900 other
3901 .visible_navbar_entries()
3902 .map(|(_, entry)| EntryMinimal {
3903 is_root: entry.is_root,
3904 title: entry.title,
3905 })
3906 .collect::<Vec<_>>(),
3907 self.visible_navbar_entries()
3908 .map(|(_, entry)| EntryMinimal {
3909 is_root: entry.is_root,
3910 title: entry.title,
3911 })
3912 .collect::<Vec<_>>(),
3913 );
3914 assert_eq!(
3915 self.current_page().items.iter().collect::<Vec<_>>(),
3916 other.page_items().collect::<Vec<_>>()
3917 );
3918 }
3919 }
3920
3921 impl SettingsPage {
3922 fn item(mut self, item: SettingsPageItem) -> Self {
3923 self.items.push(item);
3924 self
3925 }
3926 }
3927
3928 impl SettingsPageItem {
3929 fn basic_item(title: &'static str, description: &'static str) -> Self {
3930 SettingsPageItem::SettingItem(SettingItem {
3931 title,
3932 description,
3933 field: Box::new(SettingField {
3934 pick: |settings_content| &settings_content.auto_update,
3935 pick_mut: |settings_content| &mut settings_content.auto_update,
3936 }),
3937 metadata: None,
3938 })
3939 }
3940 }
3941
3942 fn register_settings(cx: &mut App) {
3943 settings::init(cx);
3944 theme::init(theme::LoadThemes::JustBase, cx);
3945 workspace::init_settings(cx);
3946 project::Project::init_settings(cx);
3947 language::init(cx);
3948 editor::init(cx);
3949 menu::init();
3950 }
3951
3952 fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
3953 let mut pages: Vec<SettingsPage> = Vec::new();
3954 let mut expanded_pages = Vec::new();
3955 let mut selected_idx = None;
3956 let mut index = 0;
3957 let mut in_expanded_section = false;
3958
3959 for mut line in input
3960 .lines()
3961 .map(|line| line.trim())
3962 .filter(|line| !line.is_empty())
3963 {
3964 if let Some(pre) = line.strip_suffix('*') {
3965 assert!(selected_idx.is_none(), "Only one selected entry allowed");
3966 selected_idx = Some(index);
3967 line = pre;
3968 }
3969 let (kind, title) = line.split_once(" ").unwrap();
3970 assert_eq!(kind.len(), 1);
3971 let kind = kind.chars().next().unwrap();
3972 if kind == 'v' {
3973 let page_idx = pages.len();
3974 expanded_pages.push(page_idx);
3975 pages.push(SettingsPage {
3976 title,
3977 items: vec![],
3978 });
3979 index += 1;
3980 in_expanded_section = true;
3981 } else if kind == '>' {
3982 pages.push(SettingsPage {
3983 title,
3984 items: vec![],
3985 });
3986 index += 1;
3987 in_expanded_section = false;
3988 } else if kind == '-' {
3989 pages
3990 .last_mut()
3991 .unwrap()
3992 .items
3993 .push(SettingsPageItem::SectionHeader(title));
3994 if selected_idx == Some(index) && !in_expanded_section {
3995 panic!("Items in unexpanded sections cannot be selected");
3996 }
3997 index += 1;
3998 } else {
3999 panic!(
4000 "Entries must start with one of 'v', '>', or '-'\n line: {}",
4001 line
4002 );
4003 }
4004 }
4005
4006 let mut settings_window = SettingsWindow {
4007 files: Vec::default(),
4008 current_file: crate::SettingsUiFile::User,
4009 pages,
4010 search_bar: cx.new(|cx| Editor::single_line(window, cx)),
4011 navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
4012 navbar_entries: Vec::default(),
4013 list_handle: UniformListScrollHandle::default(),
4014 search_matches: vec![],
4015 search_task: None,
4016 sub_page_stack: vec![],
4017 scroll_handle: ScrollHandle::new(),
4018 };
4019
4020 settings_window.build_search_matches();
4021 settings_window.build_navbar();
4022 for expanded_page_index in expanded_pages {
4023 for entry in &mut settings_window.navbar_entries {
4024 if entry.page_index == expanded_page_index && entry.is_root {
4025 entry.expanded = true;
4026 }
4027 }
4028 }
4029 settings_window
4030 }
4031
4032 #[track_caller]
4033 fn check_navbar_toggle(
4034 before: &'static str,
4035 toggle_page: &'static str,
4036 after: &'static str,
4037 window: &mut Window,
4038 cx: &mut App,
4039 ) {
4040 let mut settings_window = parse(before, window, cx);
4041 let toggle_page_idx = settings_window
4042 .pages
4043 .iter()
4044 .position(|page| page.title == toggle_page)
4045 .expect("page not found");
4046 let toggle_idx = settings_window
4047 .navbar_entries
4048 .iter()
4049 .position(|entry| entry.page_index == toggle_page_idx)
4050 .expect("page not found");
4051 settings_window.toggle_navbar_entry(toggle_idx);
4052
4053 let expected_settings_window = parse(after, window, cx);
4054
4055 pretty_assertions::assert_eq!(
4056 settings_window
4057 .visible_navbar_entries()
4058 .map(|(_, entry)| entry)
4059 .collect::<Vec<_>>(),
4060 expected_settings_window
4061 .visible_navbar_entries()
4062 .map(|(_, entry)| entry)
4063 .collect::<Vec<_>>(),
4064 );
4065 pretty_assertions::assert_eq!(
4066 settings_window.navbar_entries[settings_window.navbar_entry()],
4067 expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()],
4068 );
4069 }
4070
4071 macro_rules! check_navbar_toggle {
4072 ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => {
4073 #[gpui::test]
4074 fn $name(cx: &mut gpui::TestAppContext) {
4075 let window = cx.add_empty_window();
4076 window.update(|window, cx| {
4077 register_settings(cx);
4078 check_navbar_toggle($before, $toggle_page, $after, window, cx);
4079 });
4080 }
4081 };
4082 }
4083
4084 check_navbar_toggle!(
4085 navbar_basic_open,
4086 before: r"
4087 v General
4088 - General
4089 - Privacy*
4090 v Project
4091 - Project Settings
4092 ",
4093 toggle_page: "General",
4094 after: r"
4095 > General*
4096 v Project
4097 - Project Settings
4098 "
4099 );
4100
4101 check_navbar_toggle!(
4102 navbar_basic_close,
4103 before: r"
4104 > General*
4105 - General
4106 - Privacy
4107 v Project
4108 - Project Settings
4109 ",
4110 toggle_page: "General",
4111 after: r"
4112 v General*
4113 - General
4114 - Privacy
4115 v Project
4116 - Project Settings
4117 "
4118 );
4119
4120 check_navbar_toggle!(
4121 navbar_basic_second_root_entry_close,
4122 before: r"
4123 > General
4124 - General
4125 - Privacy
4126 v Project
4127 - Project Settings*
4128 ",
4129 toggle_page: "Project",
4130 after: r"
4131 > General
4132 > Project*
4133 "
4134 );
4135
4136 check_navbar_toggle!(
4137 navbar_toggle_subroot,
4138 before: r"
4139 v General Page
4140 - General
4141 - Privacy
4142 v Project
4143 - Worktree Settings Content*
4144 v AI
4145 - General
4146 > Appearance & Behavior
4147 ",
4148 toggle_page: "Project",
4149 after: r"
4150 v General Page
4151 - General
4152 - Privacy
4153 > Project*
4154 v AI
4155 - General
4156 > Appearance & Behavior
4157 "
4158 );
4159
4160 check_navbar_toggle!(
4161 navbar_toggle_close_propagates_selected_index,
4162 before: r"
4163 v General Page
4164 - General
4165 - Privacy
4166 v Project
4167 - Worktree Settings Content
4168 v AI
4169 - General*
4170 > Appearance & Behavior
4171 ",
4172 toggle_page: "General Page",
4173 after: r"
4174 > General Page
4175 v Project
4176 - Worktree Settings Content
4177 v AI
4178 - General*
4179 > Appearance & Behavior
4180 "
4181 );
4182
4183 check_navbar_toggle!(
4184 navbar_toggle_expand_propagates_selected_index,
4185 before: r"
4186 > General Page
4187 - General
4188 - Privacy
4189 v Project
4190 - Worktree Settings Content
4191 v AI
4192 - General*
4193 > Appearance & Behavior
4194 ",
4195 toggle_page: "General Page",
4196 after: r"
4197 v General Page
4198 - General
4199 - Privacy
4200 v Project
4201 - Worktree Settings Content
4202 v AI
4203 - General*
4204 > Appearance & Behavior
4205 "
4206 );
4207
4208 #[gpui::test]
4209 fn test_basic_search(cx: &mut gpui::TestAppContext) {
4210 let cx = cx.add_empty_window();
4211 let (actual, expected) = cx.update(|window, cx| {
4212 register_settings(cx);
4213
4214 let expected = cx.new(|cx| {
4215 SettingsWindow::new_builder(window, cx)
4216 .add_page("General", |page| {
4217 page.item(SettingsPageItem::SectionHeader("General settings"))
4218 .item(SettingsPageItem::basic_item("test title", "General test"))
4219 })
4220 .build()
4221 });
4222
4223 let actual = cx.new(|cx| {
4224 SettingsWindow::new_builder(window, cx)
4225 .add_page("General", |page| {
4226 page.item(SettingsPageItem::SectionHeader("General settings"))
4227 .item(SettingsPageItem::basic_item("test title", "General test"))
4228 })
4229 .add_page("Theme", |page| {
4230 page.item(SettingsPageItem::SectionHeader("Theme settings"))
4231 })
4232 .build()
4233 });
4234
4235 actual.update(cx, |settings, cx| settings.search("gen", window, cx));
4236
4237 (actual, expected)
4238 });
4239
4240 cx.cx.run_until_parked();
4241
4242 cx.update(|_window, cx| {
4243 let expected = expected.read(cx);
4244 let actual = actual.read(cx);
4245 expected.assert_search_results(&actual);
4246 })
4247 }
4248
4249 #[gpui::test]
4250 fn test_search_render_page_with_filtered_out_navbar_entries(cx: &mut gpui::TestAppContext) {
4251 let cx = cx.add_empty_window();
4252 let (actual, expected) = cx.update(|window, cx| {
4253 register_settings(cx);
4254
4255 let actual = cx.new(|cx| {
4256 SettingsWindow::new_builder(window, cx)
4257 .add_page("General", |page| {
4258 page.item(SettingsPageItem::SectionHeader("General settings"))
4259 .item(SettingsPageItem::basic_item(
4260 "Confirm Quit",
4261 "Whether to confirm before quitting Zed",
4262 ))
4263 .item(SettingsPageItem::basic_item(
4264 "Auto Update",
4265 "Automatically update Zed",
4266 ))
4267 })
4268 .add_page("AI", |page| {
4269 page.item(SettingsPageItem::basic_item(
4270 "Disable AI",
4271 "Whether to disable all AI features in Zed",
4272 ))
4273 })
4274 .add_page("Appearance & Behavior", |page| {
4275 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4276 SettingsPageItem::basic_item(
4277 "Cursor Shape",
4278 "Cursor shape for the editor",
4279 ),
4280 )
4281 })
4282 .build()
4283 });
4284
4285 let expected = cx.new(|cx| {
4286 SettingsWindow::new_builder(window, cx)
4287 .add_page("Appearance & Behavior", |page| {
4288 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4289 SettingsPageItem::basic_item(
4290 "Cursor Shape",
4291 "Cursor shape for the editor",
4292 ),
4293 )
4294 })
4295 .build()
4296 });
4297
4298 actual.update(cx, |settings, cx| settings.search("cursor", window, cx));
4299
4300 (actual, expected)
4301 });
4302
4303 cx.cx.run_until_parked();
4304
4305 cx.update(|_window, cx| {
4306 let expected = expected.read(cx);
4307 let actual = actual.read(cx);
4308 expected.assert_search_results(&actual);
4309 })
4310 }
4311}