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