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