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.editor.status_bar {
1422 &status_bar.active_language_button
1423 } else {
1424 &None
1425 }
1426 },
1427 pick_mut: |settings_content| {
1428 &mut settings_content
1429 .editor
1430 .status_bar
1431 .get_or_insert_default()
1432 .active_language_button
1433 },
1434 }),
1435 metadata: None,
1436 }),
1437 SettingsPageItem::SettingItem(SettingItem {
1438 title: "Cursor Position Button",
1439 description: "Whether to show the cursor position button in the status bar",
1440 field: Box::new(SettingField {
1441 pick: |settings_content| {
1442 if let Some(status_bar) = &settings_content.editor.status_bar {
1443 &status_bar.cursor_position_button
1444 } else {
1445 &None
1446 }
1447 },
1448 pick_mut: |settings_content| {
1449 &mut settings_content
1450 .editor
1451 .status_bar
1452 .get_or_insert_default()
1453 .cursor_position_button
1454 },
1455 }),
1456 metadata: None,
1457 }),
1458 SettingsPageItem::SectionHeader("Terminal"),
1459 SettingsPageItem::SettingItem(SettingItem {
1460 title: "Terminal Button",
1461 description: "Whether to show the terminal button in the status bar",
1462 field: Box::new(SettingField {
1463 pick: |settings_content| {
1464 if let Some(terminal) = &settings_content.terminal {
1465 &terminal.button
1466 } else {
1467 &None
1468 }
1469 },
1470 pick_mut: |settings_content| {
1471 &mut settings_content.terminal.get_or_insert_default().button
1472 },
1473 }),
1474 metadata: None,
1475 }),
1476 SettingsPageItem::SettingItem(SettingItem {
1477 title: "Show Navigation History Buttons",
1478 description: "Whether or not to show the navigation history buttons in the tab bar",
1479 field: Box::new(SettingField {
1480 pick: |settings_content| {
1481 if let Some(tab_bar) = &settings_content.tab_bar {
1482 &tab_bar.show_nav_history_buttons
1483 } else {
1484 &None
1485 }
1486 },
1487 pick_mut: |settings_content| {
1488 &mut settings_content
1489 .tab_bar
1490 .get_or_insert_default()
1491 .show_nav_history_buttons
1492 },
1493 }),
1494 metadata: None,
1495 }),
1496 ],
1497 },
1498 SettingsPage {
1499 title: "Panels & Tools",
1500 expanded: false,
1501 items: vec![
1502 SettingsPageItem::SectionHeader("Project Panel"),
1503 SettingsPageItem::SettingItem(SettingItem {
1504 title: "Project Panel Button",
1505 description: "Whether to show the project panel button in the status bar",
1506 field: Box::new(SettingField {
1507 pick: |settings_content| {
1508 if let Some(project_panel) = &settings_content.project_panel {
1509 &project_panel.button
1510 } else {
1511 &None
1512 }
1513 },
1514 pick_mut: |settings_content| {
1515 &mut settings_content
1516 .project_panel
1517 .get_or_insert_default()
1518 .button
1519 },
1520 }),
1521 metadata: None,
1522 }),
1523 SettingsPageItem::SettingItem(SettingItem {
1524 title: "Project Panel Dock",
1525 description: "Where to dock the project panel",
1526 field: Box::new(SettingField {
1527 pick: |settings_content| {
1528 if let Some(project_panel) = &settings_content.project_panel {
1529 &project_panel.dock
1530 } else {
1531 &None
1532 }
1533 },
1534 pick_mut: |settings_content| {
1535 &mut settings_content.project_panel.get_or_insert_default().dock
1536 },
1537 }),
1538 metadata: None,
1539 }),
1540 // todo(settings_ui): Needs numeric stepper
1541 // SettingsPageItem::SettingItem(SettingItem {
1542 // title: "Project Panel Default Width",
1543 // description: "Default width of the project panel in pixels",
1544 // field: Box::new(SettingField {
1545 // pick: |settings_content| {
1546 // if let Some(project_panel) = &settings_content.project_panel {
1547 // &project_panel.default_width
1548 // } else {
1549 // &None
1550 // }
1551 // },
1552 // pick_mut: |settings_content| {
1553 // &mut settings_content
1554 // .project_panel
1555 // .get_or_insert_default()
1556 // .default_width
1557 // },
1558 // }),
1559 // metadata: None,
1560 // }),
1561 SettingsPageItem::SectionHeader("Terminal"),
1562 SettingsPageItem::SettingItem(SettingItem {
1563 title: "Terminal Dock",
1564 description: "Where to dock the terminal panel",
1565 field: Box::new(SettingField {
1566 pick: |settings_content| {
1567 if let Some(terminal) = &settings_content.terminal {
1568 &terminal.dock
1569 } else {
1570 &None
1571 }
1572 },
1573 pick_mut: |settings_content| {
1574 &mut settings_content.terminal.get_or_insert_default().dock
1575 },
1576 }),
1577 metadata: None,
1578 }),
1579 SettingsPageItem::SectionHeader("Tab Settings"),
1580 SettingsPageItem::SettingItem(SettingItem {
1581 title: "Activate On Close",
1582 description: "What to do after closing the current tab",
1583 field: Box::new(SettingField {
1584 pick: |settings_content| {
1585 if let Some(tabs) = &settings_content.tabs {
1586 &tabs.activate_on_close
1587 } else {
1588 &None
1589 }
1590 },
1591 pick_mut: |settings_content| {
1592 &mut settings_content
1593 .tabs
1594 .get_or_insert_default()
1595 .activate_on_close
1596 },
1597 }),
1598 metadata: None,
1599 }),
1600 SettingsPageItem::SettingItem(SettingItem {
1601 title: "Tab Show Diagnostics",
1602 description: "Which files containing diagnostic errors/warnings to mark in the tabs",
1603 field: Box::new(SettingField {
1604 pick: |settings_content| {
1605 if let Some(tabs) = &settings_content.tabs {
1606 &tabs.show_diagnostics
1607 } else {
1608 &None
1609 }
1610 },
1611 pick_mut: |settings_content| {
1612 &mut settings_content
1613 .tabs
1614 .get_or_insert_default()
1615 .show_diagnostics
1616 },
1617 }),
1618 metadata: None,
1619 }),
1620 SettingsPageItem::SettingItem(SettingItem {
1621 title: "Show Close Button",
1622 description: "Controls the appearance behavior of the tab's close button",
1623 field: Box::new(SettingField {
1624 pick: |settings_content| {
1625 if let Some(tabs) = &settings_content.tabs {
1626 &tabs.show_close_button
1627 } else {
1628 &None
1629 }
1630 },
1631 pick_mut: |settings_content| {
1632 &mut settings_content
1633 .tabs
1634 .get_or_insert_default()
1635 .show_close_button
1636 },
1637 }),
1638 metadata: None,
1639 }),
1640 SettingsPageItem::SectionHeader("Preview Tabs"),
1641 SettingsPageItem::SettingItem(SettingItem {
1642 title: "Preview Tabs Enabled",
1643 description: "Whether to show opened editors as preview tabs",
1644 field: Box::new(SettingField {
1645 pick: |settings_content| {
1646 if let Some(preview_tabs) = &settings_content.preview_tabs {
1647 &preview_tabs.enabled
1648 } else {
1649 &None
1650 }
1651 },
1652 pick_mut: |settings_content| {
1653 &mut settings_content
1654 .preview_tabs
1655 .get_or_insert_default()
1656 .enabled
1657 },
1658 }),
1659 metadata: None,
1660 }),
1661 SettingsPageItem::SettingItem(SettingItem {
1662 title: "Enable Preview From File Finder",
1663 description: "Whether to open tabs in preview mode when selected from the file finder",
1664 field: Box::new(SettingField {
1665 pick: |settings_content| {
1666 if let Some(preview_tabs) = &settings_content.preview_tabs {
1667 &preview_tabs.enable_preview_from_file_finder
1668 } else {
1669 &None
1670 }
1671 },
1672 pick_mut: |settings_content| {
1673 &mut settings_content
1674 .preview_tabs
1675 .get_or_insert_default()
1676 .enable_preview_from_file_finder
1677 },
1678 }),
1679 metadata: None,
1680 }),
1681 SettingsPageItem::SettingItem(SettingItem {
1682 title: "Enable Preview From Code Navigation",
1683 description: "Whether a preview tab gets replaced when code navigation is used to navigate away from the tab",
1684 field: Box::new(SettingField {
1685 pick: |settings_content| {
1686 if let Some(preview_tabs) = &settings_content.preview_tabs {
1687 &preview_tabs.enable_preview_from_code_navigation
1688 } else {
1689 &None
1690 }
1691 },
1692 pick_mut: |settings_content| {
1693 &mut settings_content
1694 .preview_tabs
1695 .get_or_insert_default()
1696 .enable_preview_from_code_navigation
1697 },
1698 }),
1699 metadata: None,
1700 }),
1701 ],
1702 },
1703 SettingsPage {
1704 title: "Version Control",
1705 expanded: false,
1706 items: vec![
1707 SettingsPageItem::SectionHeader("Git"),
1708 SettingsPageItem::SettingItem(SettingItem {
1709 title: "Git Gutter",
1710 description: "Control whether the git gutter is shown",
1711 field: Box::new(SettingField {
1712 pick: |settings_content| {
1713 if let Some(git) = &settings_content.git {
1714 &git.git_gutter
1715 } else {
1716 &None
1717 }
1718 },
1719 pick_mut: |settings_content| {
1720 &mut settings_content.git.get_or_insert_default().git_gutter
1721 },
1722 }),
1723 metadata: None,
1724 }),
1725 // todo(settings_ui): Needs numeric stepper
1726 // SettingsPageItem::SettingItem(SettingItem {
1727 // title: "Gutter Debounce",
1728 // description: "Debounce threshold in milliseconds after which changes are reflected in the git gutter",
1729 // field: Box::new(SettingField {
1730 // pick: |settings_content| {
1731 // if let Some(git) = &settings_content.git {
1732 // &git.gutter_debounce
1733 // } else {
1734 // &None
1735 // }
1736 // },
1737 // pick_mut: |settings_content| {
1738 // &mut settings_content.git.get_or_insert_default().gutter_debounce
1739 // },
1740 // }),
1741 // metadata: None,
1742 // }),
1743 SettingsPageItem::SettingItem(SettingItem {
1744 title: "Inline Blame Enabled",
1745 description: "Whether or not to show git blame data inline in the currently focused line",
1746 field: Box::new(SettingField {
1747 pick: |settings_content| {
1748 if let Some(git) = &settings_content.git {
1749 if let Some(inline_blame) = &git.inline_blame {
1750 &inline_blame.enabled
1751 } else {
1752 &None
1753 }
1754 } else {
1755 &None
1756 }
1757 },
1758 pick_mut: |settings_content| {
1759 &mut settings_content
1760 .git
1761 .get_or_insert_default()
1762 .inline_blame
1763 .get_or_insert_default()
1764 .enabled
1765 },
1766 }),
1767 metadata: None,
1768 }),
1769 SettingsPageItem::SettingItem(SettingItem {
1770 title: "Show Commit Summary",
1771 description: "Whether to show commit summary as part of the inline blame",
1772 field: Box::new(SettingField {
1773 pick: |settings_content| {
1774 if let Some(git) = &settings_content.git {
1775 if let Some(inline_blame) = &git.inline_blame {
1776 &inline_blame.show_commit_summary
1777 } else {
1778 &None
1779 }
1780 } else {
1781 &None
1782 }
1783 },
1784 pick_mut: |settings_content| {
1785 &mut settings_content
1786 .git
1787 .get_or_insert_default()
1788 .inline_blame
1789 .get_or_insert_default()
1790 .show_commit_summary
1791 },
1792 }),
1793 metadata: None,
1794 }),
1795 SettingsPageItem::SettingItem(SettingItem {
1796 title: "Show Avatar",
1797 description: "Whether to show the avatar of the author of the commit",
1798 field: Box::new(SettingField {
1799 pick: |settings_content| {
1800 if let Some(git) = &settings_content.git {
1801 if let Some(blame) = &git.blame {
1802 &blame.show_avatar
1803 } else {
1804 &None
1805 }
1806 } else {
1807 &None
1808 }
1809 },
1810 pick_mut: |settings_content| {
1811 &mut settings_content
1812 .git
1813 .get_or_insert_default()
1814 .blame
1815 .get_or_insert_default()
1816 .show_avatar
1817 },
1818 }),
1819 metadata: None,
1820 }),
1821 SettingsPageItem::SettingItem(SettingItem {
1822 title: "Show Author Name In Branch Picker",
1823 description: "Whether to show author name as part of the commit information in branch picker",
1824 field: Box::new(SettingField {
1825 pick: |settings_content| {
1826 if let Some(git) = &settings_content.git {
1827 if let Some(branch_picker) = &git.branch_picker {
1828 &branch_picker.show_author_name
1829 } else {
1830 &None
1831 }
1832 } else {
1833 &None
1834 }
1835 },
1836 pick_mut: |settings_content| {
1837 &mut settings_content
1838 .git
1839 .get_or_insert_default()
1840 .branch_picker
1841 .get_or_insert_default()
1842 .show_author_name
1843 },
1844 }),
1845 metadata: None,
1846 }),
1847 SettingsPageItem::SettingItem(SettingItem {
1848 title: "Hunk Style",
1849 description: "How git hunks are displayed visually in the editor",
1850 field: Box::new(SettingField {
1851 pick: |settings_content| {
1852 if let Some(git) = &settings_content.git {
1853 &git.hunk_style
1854 } else {
1855 &None
1856 }
1857 },
1858 pick_mut: |settings_content| {
1859 &mut settings_content.git.get_or_insert_default().hunk_style
1860 },
1861 }),
1862 metadata: None,
1863 }),
1864 ],
1865 },
1866 SettingsPage {
1867 title: "System & Network",
1868 expanded: false,
1869 items: vec![
1870 SettingsPageItem::SectionHeader("Network"),
1871 // todo(settings_ui): Proxy needs a default
1872 // SettingsPageItem::SettingItem(SettingItem {
1873 // title: "Proxy",
1874 // description: "The proxy to use for network requests",
1875 // field: Box::new(SettingField {
1876 // pick: |settings_content| &settings_content.proxy,
1877 // pick_mut: |settings_content| &mut settings_content.proxy,
1878 // }),
1879 // metadata: Some(Box::new(SettingsFieldMetadata {
1880 // placeholder: Some("socks5h://localhost:10808"),
1881 // })),
1882 // }),
1883 SettingsPageItem::SettingItem(SettingItem {
1884 title: "Server URL",
1885 description: "The URL of the Zed server to connect to",
1886 field: Box::new(SettingField {
1887 pick: |settings_content| &settings_content.server_url,
1888 pick_mut: |settings_content| &mut settings_content.server_url,
1889 }),
1890 metadata: Some(Box::new(SettingsFieldMetadata {
1891 placeholder: Some("https://zed.dev"),
1892 })),
1893 }),
1894 SettingsPageItem::SectionHeader("System"),
1895 SettingsPageItem::SettingItem(SettingItem {
1896 title: "Auto Update",
1897 description: "Whether or not to automatically check for updates",
1898 field: Box::new(SettingField {
1899 pick: |settings_content| &settings_content.auto_update,
1900 pick_mut: |settings_content| &mut settings_content.auto_update,
1901 }),
1902 metadata: None,
1903 }),
1904 ],
1905 },
1906 SettingsPage {
1907 title: "Diagnostics & Errors",
1908 expanded: false,
1909 items: vec![
1910 SettingsPageItem::SectionHeader("Display"),
1911 SettingsPageItem::SettingItem(SettingItem {
1912 title: "Diagnostics Button",
1913 description: "Whether to show the project diagnostics button in the status bar",
1914 field: Box::new(SettingField {
1915 pick: |settings_content| {
1916 if let Some(diagnostics) = &settings_content.diagnostics {
1917 &diagnostics.button
1918 } else {
1919 &None
1920 }
1921 },
1922 pick_mut: |settings_content| {
1923 &mut settings_content.diagnostics.get_or_insert_default().button
1924 },
1925 }),
1926 metadata: None,
1927 }),
1928 SettingsPageItem::SectionHeader("Filtering"),
1929 SettingsPageItem::SettingItem(SettingItem {
1930 title: "Max Severity",
1931 description: "Which level to use to filter out diagnostics displayed in the editor",
1932 field: Box::new(SettingField {
1933 pick: |settings_content| &settings_content.editor.diagnostics_max_severity,
1934 pick_mut: |settings_content| {
1935 &mut settings_content.editor.diagnostics_max_severity
1936 },
1937 }),
1938 metadata: None,
1939 }),
1940 SettingsPageItem::SettingItem(SettingItem {
1941 title: "Include Warnings",
1942 description: "Whether to show warnings or not by default",
1943 field: Box::new(SettingField {
1944 pick: |settings_content| {
1945 if let Some(diagnostics) = &settings_content.diagnostics {
1946 &diagnostics.include_warnings
1947 } else {
1948 &None
1949 }
1950 },
1951 pick_mut: |settings_content| {
1952 &mut settings_content
1953 .diagnostics
1954 .get_or_insert_default()
1955 .include_warnings
1956 },
1957 }),
1958 metadata: None,
1959 }),
1960 SettingsPageItem::SectionHeader("Inline"),
1961 SettingsPageItem::SettingItem(SettingItem {
1962 title: "Inline Diagnostics Enabled",
1963 description: "Whether to show diagnostics inline or not",
1964 field: Box::new(SettingField {
1965 pick: |settings_content| {
1966 if let Some(diagnostics) = &settings_content.diagnostics {
1967 if let Some(inline) = &diagnostics.inline {
1968 &inline.enabled
1969 } else {
1970 &None
1971 }
1972 } else {
1973 &None
1974 }
1975 },
1976 pick_mut: |settings_content| {
1977 &mut settings_content
1978 .diagnostics
1979 .get_or_insert_default()
1980 .inline
1981 .get_or_insert_default()
1982 .enabled
1983 },
1984 }),
1985 metadata: None,
1986 }),
1987 // todo(settings_ui): Needs numeric stepper
1988 // SettingsPageItem::SettingItem(SettingItem {
1989 // title: "Inline Update Debounce",
1990 // description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update",
1991 // field: Box::new(SettingField {
1992 // pick: |settings_content| {
1993 // if let Some(diagnostics) = &settings_content.diagnostics {
1994 // if let Some(inline) = &diagnostics.inline {
1995 // &inline.update_debounce_ms
1996 // } else {
1997 // &None
1998 // }
1999 // } else {
2000 // &None
2001 // }
2002 // },
2003 // pick_mut: |settings_content| {
2004 // &mut settings_content
2005 // .diagnostics
2006 // .get_or_insert_default()
2007 // .inline
2008 // .get_or_insert_default()
2009 // .update_debounce_ms
2010 // },
2011 // }),
2012 // metadata: None,
2013 // }),
2014 // todo(settings_ui): Needs numeric stepper
2015 // SettingsPageItem::SettingItem(SettingItem {
2016 // title: "Inline Padding",
2017 // description: "The amount of padding between the end of the source line and the start of the inline diagnostic",
2018 // field: Box::new(SettingField {
2019 // pick: |settings_content| {
2020 // if let Some(diagnostics) = &settings_content.diagnostics {
2021 // if let Some(inline) = &diagnostics.inline {
2022 // &inline.padding
2023 // } else {
2024 // &None
2025 // }
2026 // } else {
2027 // &None
2028 // }
2029 // },
2030 // pick_mut: |settings_content| {
2031 // &mut settings_content
2032 // .diagnostics
2033 // .get_or_insert_default()
2034 // .inline
2035 // .get_or_insert_default()
2036 // .padding
2037 // },
2038 // }),
2039 // metadata: None,
2040 // }),
2041 // todo(settings_ui): Needs numeric stepper
2042 // SettingsPageItem::SettingItem(SettingItem {
2043 // title: "Inline Min Column",
2044 // description: "The minimum column to display inline diagnostics",
2045 // field: Box::new(SettingField {
2046 // pick: |settings_content| {
2047 // if let Some(diagnostics) = &settings_content.diagnostics {
2048 // if let Some(inline) = &diagnostics.inline {
2049 // &inline.min_column
2050 // } else {
2051 // &None
2052 // }
2053 // } else {
2054 // &None
2055 // }
2056 // },
2057 // pick_mut: |settings_content| {
2058 // &mut settings_content
2059 // .diagnostics
2060 // .get_or_insert_default()
2061 // .inline
2062 // .get_or_insert_default()
2063 // .min_column
2064 // },
2065 // }),
2066 // metadata: None,
2067 // }),
2068 SettingsPageItem::SectionHeader("Performance"),
2069 SettingsPageItem::SettingItem(SettingItem {
2070 title: "LSP Pull Diagnostics Enabled",
2071 description: "Whether to pull for diagnostics or not",
2072 field: Box::new(SettingField {
2073 pick: |settings_content| {
2074 if let Some(diagnostics) = &settings_content.diagnostics {
2075 if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2076 &lsp_pull.enabled
2077 } else {
2078 &None
2079 }
2080 } else {
2081 &None
2082 }
2083 },
2084 pick_mut: |settings_content| {
2085 &mut settings_content
2086 .diagnostics
2087 .get_or_insert_default()
2088 .lsp_pull_diagnostics
2089 .get_or_insert_default()
2090 .enabled
2091 },
2092 }),
2093 metadata: None,
2094 }),
2095 // todo(settings_ui): Needs numeric stepper
2096 // SettingsPageItem::SettingItem(SettingItem {
2097 // title: "LSP Pull Debounce",
2098 // description: "Minimum time to wait before pulling diagnostics from the language server(s)",
2099 // field: Box::new(SettingField {
2100 // pick: |settings_content| {
2101 // if let Some(diagnostics) = &settings_content.diagnostics {
2102 // if let Some(lsp_pull) = &diagnostics.lsp_pull_diagnostics {
2103 // &lsp_pull.debounce_ms
2104 // } else {
2105 // &None
2106 // }
2107 // } else {
2108 // &None
2109 // }
2110 // },
2111 // pick_mut: |settings_content| {
2112 // &mut settings_content
2113 // .diagnostics
2114 // .get_or_insert_default()
2115 // .lsp_pull_diagnostics
2116 // .get_or_insert_default()
2117 // .debounce_ms
2118 // },
2119 // }),
2120 // metadata: None,
2121 // }),
2122 ],
2123 },
2124 SettingsPage {
2125 title: "Collaboration",
2126 expanded: false,
2127 items: vec![
2128 SettingsPageItem::SectionHeader("Calls"),
2129 SettingsPageItem::SettingItem(SettingItem {
2130 title: "Mute On Join",
2131 description: "Whether the microphone should be muted when joining a channel or a call",
2132 field: Box::new(SettingField {
2133 pick: |settings_content| {
2134 if let Some(calls) = &settings_content.calls {
2135 &calls.mute_on_join
2136 } else {
2137 &None
2138 }
2139 },
2140 pick_mut: |settings_content| {
2141 &mut settings_content.calls.get_or_insert_default().mute_on_join
2142 },
2143 }),
2144 metadata: None,
2145 }),
2146 SettingsPageItem::SettingItem(SettingItem {
2147 title: "Share On Join",
2148 description: "Whether your current project should be shared when joining an empty channel",
2149 field: Box::new(SettingField {
2150 pick: |settings_content| {
2151 if let Some(calls) = &settings_content.calls {
2152 &calls.share_on_join
2153 } else {
2154 &None
2155 }
2156 },
2157 pick_mut: |settings_content| {
2158 &mut settings_content.calls.get_or_insert_default().share_on_join
2159 },
2160 }),
2161 metadata: None,
2162 }),
2163 SettingsPageItem::SectionHeader("Panel"),
2164 SettingsPageItem::SettingItem(SettingItem {
2165 title: "Collaboration Panel Button",
2166 description: "Whether to show the collaboration panel button in the status bar",
2167 field: Box::new(SettingField {
2168 pick: |settings_content| {
2169 if let Some(collab) = &settings_content.collaboration_panel {
2170 &collab.button
2171 } else {
2172 &None
2173 }
2174 },
2175 pick_mut: |settings_content| {
2176 &mut settings_content
2177 .collaboration_panel
2178 .get_or_insert_default()
2179 .button
2180 },
2181 }),
2182 metadata: None,
2183 }),
2184 SettingsPageItem::SectionHeader("Experimental"),
2185 SettingsPageItem::SettingItem(SettingItem {
2186 title: "Rodio Audio",
2187 description: "Opt into the new audio system",
2188 field: Box::new(SettingField {
2189 pick: |settings_content| {
2190 if let Some(audio) = &settings_content.audio {
2191 &audio.rodio_audio
2192 } else {
2193 &None
2194 }
2195 },
2196 pick_mut: |settings_content| {
2197 &mut settings_content.audio.get_or_insert_default().rodio_audio
2198 },
2199 }),
2200 metadata: None,
2201 }),
2202 ],
2203 },
2204 SettingsPage {
2205 title: "AI",
2206 expanded: false,
2207 items: vec![
2208 SettingsPageItem::SectionHeader("General"),
2209 SettingsPageItem::SettingItem(SettingItem {
2210 title: "Disable AI",
2211 description: "Whether to disable all AI features in Zed",
2212 field: Box::new(SettingField {
2213 pick: |settings_content| &settings_content.disable_ai,
2214 pick_mut: |settings_content| &mut settings_content.disable_ai,
2215 }),
2216 metadata: None,
2217 }),
2218 ],
2219 },
2220 ]
2221}
2222
2223// Derive Macro, on the new ProjectSettings struct
2224
2225fn project_settings_data() -> Vec<SettingsPage> {
2226 vec![
2227 SettingsPage {
2228 title: "Project",
2229 expanded: false,
2230 items: vec![
2231 SettingsPageItem::SectionHeader("Worktree Settings Content"),
2232 SettingsPageItem::SettingItem(SettingItem {
2233 title: "Project Name",
2234 description: "The displayed name of this project. If not set, the root directory name",
2235 field: Box::new(SettingField {
2236 pick: |settings_content| &settings_content.project.worktree.project_name,
2237 pick_mut: |settings_content| {
2238 &mut settings_content.project.worktree.project_name
2239 },
2240 }),
2241 metadata: Some(Box::new(SettingsFieldMetadata {
2242 placeholder: Some("A new name"),
2243 })),
2244 }),
2245 ],
2246 },
2247 SettingsPage {
2248 title: "Appearance & Behavior",
2249 expanded: false,
2250 items: vec![
2251 SettingsPageItem::SectionHeader("Guides"),
2252 SettingsPageItem::SettingItem(SettingItem {
2253 title: "Show Wrap Guides",
2254 description: "Whether to show wrap guides (vertical rulers)",
2255 field: Box::new(SettingField {
2256 pick: |settings_content| {
2257 &settings_content
2258 .project
2259 .all_languages
2260 .defaults
2261 .show_wrap_guides
2262 },
2263 pick_mut: |settings_content| {
2264 &mut settings_content
2265 .project
2266 .all_languages
2267 .defaults
2268 .show_wrap_guides
2269 },
2270 }),
2271 metadata: None,
2272 }),
2273 // todo(settings_ui): This needs a custom component
2274 // SettingsPageItem::SettingItem(SettingItem {
2275 // title: "Wrap Guides",
2276 // description: "Character counts at which to show wrap guides",
2277 // field: Box::new(SettingField {
2278 // pick: |settings_content| {
2279 // &settings_content
2280 // .project
2281 // .all_languages
2282 // .defaults
2283 // .wrap_guides
2284 // },
2285 // pick_mut: |settings_content| {
2286 // &mut settings_content
2287 // .project
2288 // .all_languages
2289 // .defaults
2290 // .wrap_guides
2291 // },
2292 // }),
2293 // metadata: None,
2294 // }),
2295 SettingsPageItem::SectionHeader("Whitespace"),
2296 SettingsPageItem::SettingItem(SettingItem {
2297 title: "Show Whitespace",
2298 description: "Whether to show tabs and spaces",
2299 field: Box::new(SettingField {
2300 pick: |settings_content| {
2301 &settings_content
2302 .project
2303 .all_languages
2304 .defaults
2305 .show_whitespaces
2306 },
2307 pick_mut: |settings_content| {
2308 &mut settings_content
2309 .project
2310 .all_languages
2311 .defaults
2312 .show_whitespaces
2313 },
2314 }),
2315 metadata: None,
2316 }),
2317 ],
2318 },
2319 SettingsPage {
2320 title: "Editing",
2321 expanded: false,
2322 items: vec![
2323 SettingsPageItem::SectionHeader("Indentation"),
2324 // todo(settings_ui): Needs numeric stepper
2325 // SettingsPageItem::SettingItem(SettingItem {
2326 // title: "Tab Size",
2327 // description: "How many columns a tab should occupy",
2328 // field: Box::new(SettingField {
2329 // pick: |settings_content| &settings_content.project.all_languages.defaults.tab_size,
2330 // pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.tab_size,
2331 // }),
2332 // metadata: None,
2333 // }),
2334 SettingsPageItem::SettingItem(SettingItem {
2335 title: "Hard Tabs",
2336 description: "Whether to indent lines using tab characters, as opposed to multiple spaces",
2337 field: Box::new(SettingField {
2338 pick: |settings_content| {
2339 &settings_content.project.all_languages.defaults.hard_tabs
2340 },
2341 pick_mut: |settings_content| {
2342 &mut settings_content.project.all_languages.defaults.hard_tabs
2343 },
2344 }),
2345 metadata: None,
2346 }),
2347 SettingsPageItem::SettingItem(SettingItem {
2348 title: "Auto Indent",
2349 description: "Whether indentation should be adjusted based on the context whilst typing",
2350 field: Box::new(SettingField {
2351 pick: |settings_content| {
2352 &settings_content.project.all_languages.defaults.auto_indent
2353 },
2354 pick_mut: |settings_content| {
2355 &mut settings_content.project.all_languages.defaults.auto_indent
2356 },
2357 }),
2358 metadata: None,
2359 }),
2360 SettingsPageItem::SettingItem(SettingItem {
2361 title: "Auto Indent On Paste",
2362 description: "Whether indentation of pasted content should be adjusted based on the context",
2363 field: Box::new(SettingField {
2364 pick: |settings_content| {
2365 &settings_content
2366 .project
2367 .all_languages
2368 .defaults
2369 .auto_indent_on_paste
2370 },
2371 pick_mut: |settings_content| {
2372 &mut settings_content
2373 .project
2374 .all_languages
2375 .defaults
2376 .auto_indent_on_paste
2377 },
2378 }),
2379 metadata: None,
2380 }),
2381 SettingsPageItem::SectionHeader("Wrapping"),
2382 // todo(settings_ui): Needs numeric stepper
2383 // SettingsPageItem::SettingItem(SettingItem {
2384 // title: "Preferred Line Length",
2385 // description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled",
2386 // field: Box::new(SettingField {
2387 // pick: |settings_content| &settings_content.project.all_languages.defaults.preferred_line_length,
2388 // pick_mut: |settings_content| &mut settings_content.project.all_languages.defaults.preferred_line_length,
2389 // }),
2390 // metadata: None,
2391 // }),
2392 SettingsPageItem::SettingItem(SettingItem {
2393 title: "Soft Wrap",
2394 description: "How to soft-wrap long lines of text",
2395 field: Box::new(SettingField {
2396 pick: |settings_content| {
2397 &settings_content.project.all_languages.defaults.soft_wrap
2398 },
2399 pick_mut: |settings_content| {
2400 &mut settings_content.project.all_languages.defaults.soft_wrap
2401 },
2402 }),
2403 metadata: None,
2404 }),
2405 SettingsPageItem::SectionHeader("Auto Actions"),
2406 SettingsPageItem::SettingItem(SettingItem {
2407 title: "Use Autoclose",
2408 description: "Whether to automatically type closing characters for you",
2409 field: Box::new(SettingField {
2410 pick: |settings_content| {
2411 &settings_content
2412 .project
2413 .all_languages
2414 .defaults
2415 .use_autoclose
2416 },
2417 pick_mut: |settings_content| {
2418 &mut settings_content
2419 .project
2420 .all_languages
2421 .defaults
2422 .use_autoclose
2423 },
2424 }),
2425 metadata: None,
2426 }),
2427 SettingsPageItem::SettingItem(SettingItem {
2428 title: "Use Auto Surround",
2429 description: "Whether to automatically surround text with characters for you",
2430 field: Box::new(SettingField {
2431 pick: |settings_content| {
2432 &settings_content
2433 .project
2434 .all_languages
2435 .defaults
2436 .use_auto_surround
2437 },
2438 pick_mut: |settings_content| {
2439 &mut settings_content
2440 .project
2441 .all_languages
2442 .defaults
2443 .use_auto_surround
2444 },
2445 }),
2446 metadata: None,
2447 }),
2448 SettingsPageItem::SettingItem(SettingItem {
2449 title: "Use On Type Format",
2450 description: "Whether to use additional LSP queries to format the code after every trigger symbol input",
2451 field: Box::new(SettingField {
2452 pick: |settings_content| {
2453 &settings_content
2454 .project
2455 .all_languages
2456 .defaults
2457 .use_on_type_format
2458 },
2459 pick_mut: |settings_content| {
2460 &mut settings_content
2461 .project
2462 .all_languages
2463 .defaults
2464 .use_on_type_format
2465 },
2466 }),
2467 metadata: None,
2468 }),
2469 SettingsPageItem::SettingItem(SettingItem {
2470 title: "Always Treat Brackets As Autoclosed",
2471 description: "Controls how the editor handles the autoclosed characters",
2472 field: Box::new(SettingField {
2473 pick: |settings_content| {
2474 &settings_content
2475 .project
2476 .all_languages
2477 .defaults
2478 .always_treat_brackets_as_autoclosed
2479 },
2480 pick_mut: |settings_content| {
2481 &mut settings_content
2482 .project
2483 .all_languages
2484 .defaults
2485 .always_treat_brackets_as_autoclosed
2486 },
2487 }),
2488 metadata: None,
2489 }),
2490 SettingsPageItem::SectionHeader("Formatting"),
2491 SettingsPageItem::SettingItem(SettingItem {
2492 title: "Remove Trailing Whitespace On Save",
2493 description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it",
2494 field: Box::new(SettingField {
2495 pick: |settings_content| {
2496 &settings_content
2497 .project
2498 .all_languages
2499 .defaults
2500 .remove_trailing_whitespace_on_save
2501 },
2502 pick_mut: |settings_content| {
2503 &mut settings_content
2504 .project
2505 .all_languages
2506 .defaults
2507 .remove_trailing_whitespace_on_save
2508 },
2509 }),
2510 metadata: None,
2511 }),
2512 SettingsPageItem::SettingItem(SettingItem {
2513 title: "Ensure Final Newline On Save",
2514 description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it",
2515 field: Box::new(SettingField {
2516 pick: |settings_content| {
2517 &settings_content
2518 .project
2519 .all_languages
2520 .defaults
2521 .ensure_final_newline_on_save
2522 },
2523 pick_mut: |settings_content| {
2524 &mut settings_content
2525 .project
2526 .all_languages
2527 .defaults
2528 .ensure_final_newline_on_save
2529 },
2530 }),
2531 metadata: None,
2532 }),
2533 SettingsPageItem::SettingItem(SettingItem {
2534 title: "Extend Comment On Newline",
2535 description: "Whether to start a new line with a comment when a previous line is a comment as well",
2536 field: Box::new(SettingField {
2537 pick: |settings_content| {
2538 &settings_content
2539 .project
2540 .all_languages
2541 .defaults
2542 .extend_comment_on_newline
2543 },
2544 pick_mut: |settings_content| {
2545 &mut settings_content
2546 .project
2547 .all_languages
2548 .defaults
2549 .extend_comment_on_newline
2550 },
2551 }),
2552 metadata: None,
2553 }),
2554 SettingsPageItem::SectionHeader("Completions"),
2555 SettingsPageItem::SettingItem(SettingItem {
2556 title: "Show Completions On Input",
2557 description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it",
2558 field: Box::new(SettingField {
2559 pick: |settings_content| {
2560 &settings_content
2561 .project
2562 .all_languages
2563 .defaults
2564 .show_completions_on_input
2565 },
2566 pick_mut: |settings_content| {
2567 &mut settings_content
2568 .project
2569 .all_languages
2570 .defaults
2571 .show_completions_on_input
2572 },
2573 }),
2574 metadata: None,
2575 }),
2576 SettingsPageItem::SettingItem(SettingItem {
2577 title: "Show Completion Documentation",
2578 description: "Whether to display inline and alongside documentation for items in the completions menu",
2579 field: Box::new(SettingField {
2580 pick: |settings_content| {
2581 &settings_content
2582 .project
2583 .all_languages
2584 .defaults
2585 .show_completion_documentation
2586 },
2587 pick_mut: |settings_content| {
2588 &mut settings_content
2589 .project
2590 .all_languages
2591 .defaults
2592 .show_completion_documentation
2593 },
2594 }),
2595 metadata: None,
2596 }),
2597 ],
2598 },
2599 ]
2600}
2601
2602pub struct SettingsUiFeatureFlag;
2603
2604impl FeatureFlag for SettingsUiFeatureFlag {
2605 const NAME: &'static str = "settings-ui";
2606}
2607
2608actions!(
2609 zed,
2610 [
2611 /// Opens Settings Editor.
2612 OpenSettingsEditor
2613 ]
2614);
2615
2616pub fn init(cx: &mut App) {
2617 init_renderers(cx);
2618
2619 cx.observe_new(|workspace: &mut workspace::Workspace, _, _| {
2620 workspace.register_action_renderer(|div, _, _, cx| {
2621 let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsEditor>()];
2622 let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
2623 command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
2624 if has_flag {
2625 filter.show_action_types(&settings_ui_actions);
2626 } else {
2627 filter.hide_action_types(&settings_ui_actions);
2628 }
2629 });
2630 if has_flag {
2631 div.on_action(cx.listener(|_, _: &OpenSettingsEditor, _, cx| {
2632 open_settings_editor(cx).ok();
2633 }))
2634 } else {
2635 div
2636 }
2637 });
2638 })
2639 .detach();
2640}
2641
2642fn init_renderers(cx: &mut App) {
2643 // fn (field: SettingsField, current_file: SettingsFile, cx) -> (currently_set_in: SettingsFile, overridden_in: Vec<SettingsFile>)
2644 cx.default_global::<SettingFieldRenderer>()
2645 .add_renderer::<bool>(|settings_field, file, _, _, cx| {
2646 render_toggle_button(*settings_field, file, cx).into_any_element()
2647 })
2648 .add_renderer::<String>(|settings_field, file, metadata, _, cx| {
2649 render_text_field(settings_field.clone(), file, metadata, cx)
2650 })
2651 .add_renderer::<SaturatingBool>(|settings_field, file, _, _, cx| {
2652 render_toggle_button(*settings_field, file, cx)
2653 })
2654 .add_renderer::<CursorShape>(|settings_field, file, _, window, cx| {
2655 render_dropdown(*settings_field, file, window, cx)
2656 })
2657 .add_renderer::<RestoreOnStartupBehavior>(|settings_field, file, _, window, cx| {
2658 render_dropdown(*settings_field, file, window, cx)
2659 })
2660 .add_renderer::<BottomDockLayout>(|settings_field, file, _, window, cx| {
2661 render_dropdown(*settings_field, file, window, cx)
2662 })
2663 .add_renderer::<OnLastWindowClosed>(|settings_field, file, _, window, cx| {
2664 render_dropdown(*settings_field, file, window, cx)
2665 })
2666 .add_renderer::<CloseWindowWhenNoItems>(|settings_field, file, _, window, cx| {
2667 render_dropdown(*settings_field, file, window, cx)
2668 })
2669 .add_renderer::<settings::FontFamilyName>(|settings_field, file, metadata, _, cx| {
2670 // todo(settings_ui): We need to pass in a validator for this to ensure that users that type in invalid font names
2671 render_text_field(settings_field.clone(), file, metadata, cx)
2672 })
2673 .add_renderer::<settings::BufferLineHeight>(|settings_field, file, _, window, cx| {
2674 // todo(settings_ui): Do we want to expose the custom variant of buffer line height?
2675 // right now there's a manual impl of strum::VariantArray
2676 render_dropdown(*settings_field, file, window, cx)
2677 })
2678 .add_renderer::<settings::BaseKeymapContent>(|settings_field, file, _, window, cx| {
2679 render_dropdown(*settings_field, file, window, cx)
2680 })
2681 .add_renderer::<settings::MultiCursorModifier>(|settings_field, file, _, window, cx| {
2682 render_dropdown(*settings_field, file, window, cx)
2683 })
2684 .add_renderer::<settings::HideMouseMode>(|settings_field, file, _, window, cx| {
2685 render_dropdown(*settings_field, file, window, cx)
2686 })
2687 .add_renderer::<settings::CurrentLineHighlight>(|settings_field, file, _, window, cx| {
2688 render_dropdown(*settings_field, file, window, cx)
2689 })
2690 .add_renderer::<settings::ShowWhitespaceSetting>(|settings_field, file, _, window, cx| {
2691 render_dropdown(*settings_field, file, window, cx)
2692 })
2693 .add_renderer::<settings::SoftWrap>(|settings_field, file, _, window, cx| {
2694 render_dropdown(*settings_field, file, window, cx)
2695 })
2696 .add_renderer::<settings::ScrollBeyondLastLine>(|settings_field, file, _, window, cx| {
2697 render_dropdown(*settings_field, file, window, cx)
2698 })
2699 .add_renderer::<settings::SnippetSortOrder>(|settings_field, file, _, window, cx| {
2700 render_dropdown(*settings_field, file, window, cx)
2701 })
2702 .add_renderer::<settings::ClosePosition>(|settings_field, file, _, window, cx| {
2703 render_dropdown(*settings_field, file, window, cx)
2704 })
2705 .add_renderer::<settings::DockSide>(|settings_field, file, _, window, cx| {
2706 render_dropdown(*settings_field, file, window, cx)
2707 })
2708 .add_renderer::<settings::TerminalDockPosition>(|settings_field, file, _, window, cx| {
2709 render_dropdown(*settings_field, file, window, cx)
2710 })
2711 .add_renderer::<settings::GitGutterSetting>(|settings_field, file, _, window, cx| {
2712 render_dropdown(*settings_field, file, window, cx)
2713 })
2714 .add_renderer::<settings::GitHunkStyleSetting>(|settings_field, file, _, window, cx| {
2715 render_dropdown(*settings_field, file, window, cx)
2716 })
2717 .add_renderer::<settings::DiagnosticSeverityContent>(
2718 |settings_field, file, _, window, cx| {
2719 render_dropdown(*settings_field, file, window, cx)
2720 },
2721 )
2722 .add_renderer::<settings::SeedQuerySetting>(|settings_field, file, _, window, cx| {
2723 render_dropdown(*settings_field, file, window, cx)
2724 })
2725 .add_renderer::<settings::DoubleClickInMultibuffer>(
2726 |settings_field, file, _, window, cx| {
2727 render_dropdown(*settings_field, file, window, cx)
2728 },
2729 )
2730 .add_renderer::<settings::GoToDefinitionFallback>(|settings_field, file, _, window, cx| {
2731 render_dropdown(*settings_field, file, window, cx)
2732 })
2733 .add_renderer::<settings::ActivateOnClose>(|settings_field, file, _, window, cx| {
2734 render_dropdown(*settings_field, file, window, cx)
2735 })
2736 .add_renderer::<settings::ShowDiagnostics>(|settings_field, file, _, window, cx| {
2737 render_dropdown(*settings_field, file, window, cx)
2738 })
2739 .add_renderer::<settings::ShowCloseButton>(|settings_field, file, _, window, cx| {
2740 render_dropdown(*settings_field, file, window, cx)
2741 });
2742
2743 // todo(settings_ui): Figure out how we want to handle discriminant unions
2744 // .add_renderer::<ThemeSelection>(|settings_field, file, _, window, cx| {
2745 // render_dropdown(*settings_field, file, window, cx)
2746 // });
2747}
2748
2749pub fn open_settings_editor(cx: &mut App) -> anyhow::Result<WindowHandle<SettingsWindow>> {
2750 cx.open_window(
2751 WindowOptions {
2752 titlebar: Some(TitlebarOptions {
2753 title: Some("Settings Window".into()),
2754 appears_transparent: true,
2755 traffic_light_position: Some(point(px(12.0), px(12.0))),
2756 }),
2757 focus: true,
2758 show: true,
2759 kind: gpui::WindowKind::Normal,
2760 window_background: cx.theme().window_background_appearance(),
2761 window_min_size: Some(size(px(800.), px(600.))), // 4:3 Aspect Ratio
2762 ..Default::default()
2763 },
2764 |window, cx| cx.new(|cx| SettingsWindow::new(window, cx)),
2765 )
2766}
2767
2768pub struct SettingsWindow {
2769 files: Vec<SettingsUiFile>,
2770 current_file: SettingsUiFile,
2771 pages: Vec<SettingsPage>,
2772 search_bar: Entity<Editor>,
2773 search_task: Option<Task<()>>,
2774 navbar_entry: usize, // Index into pages - should probably be (usize, Option<usize>) for section + page
2775 navbar_entries: Vec<NavBarEntry>,
2776 list_handle: UniformListScrollHandle,
2777 search_matches: Vec<Vec<bool>>,
2778 /// The current sub page path that is selected.
2779 /// If this is empty the selected page is rendered,
2780 /// otherwise the last sub page gets rendered.
2781 sub_page_stack: Vec<SubPage>,
2782}
2783
2784struct SubPage {
2785 link: SubPageLink,
2786 section_header: &'static str,
2787}
2788
2789#[derive(PartialEq, Debug)]
2790struct NavBarEntry {
2791 title: &'static str,
2792 is_root: bool,
2793 page_index: usize,
2794}
2795
2796struct SettingsPage {
2797 title: &'static str,
2798 expanded: bool,
2799 items: Vec<SettingsPageItem>,
2800}
2801
2802#[derive(PartialEq)]
2803enum SettingsPageItem {
2804 SectionHeader(&'static str),
2805 SettingItem(SettingItem),
2806 SubPageLink(SubPageLink),
2807}
2808
2809impl std::fmt::Debug for SettingsPageItem {
2810 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2811 match self {
2812 SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header),
2813 SettingsPageItem::SettingItem(setting_item) => {
2814 write!(f, "SettingItem({})", setting_item.title)
2815 }
2816 SettingsPageItem::SubPageLink(sub_page_link) => {
2817 write!(f, "SubPageLink({})", sub_page_link.title)
2818 }
2819 }
2820 }
2821}
2822
2823impl SettingsPageItem {
2824 fn render(
2825 &self,
2826 file: SettingsUiFile,
2827 section_header: &'static str,
2828 is_last: bool,
2829 window: &mut Window,
2830 cx: &mut Context<SettingsWindow>,
2831 ) -> AnyElement {
2832 match self {
2833 SettingsPageItem::SectionHeader(header) => v_flex()
2834 .w_full()
2835 .gap_1()
2836 .child(
2837 Label::new(SharedString::new_static(header))
2838 .size(LabelSize::XSmall)
2839 .color(Color::Muted)
2840 .buffer_font(cx),
2841 )
2842 .child(Divider::horizontal().color(ui::DividerColor::BorderVariant))
2843 .into_any_element(),
2844 SettingsPageItem::SettingItem(setting_item) => {
2845 let renderer = cx.default_global::<SettingFieldRenderer>().clone();
2846 let file_set_in =
2847 SettingsUiFile::from_settings(setting_item.field.file_set_in(file.clone(), cx));
2848
2849 h_flex()
2850 .id(setting_item.title)
2851 .w_full()
2852 .gap_2()
2853 .flex_wrap()
2854 .justify_between()
2855 .when(!is_last, |this| {
2856 this.pb_4()
2857 .border_b_1()
2858 .border_color(cx.theme().colors().border_variant)
2859 })
2860 .child(
2861 v_flex()
2862 .max_w_1_2()
2863 .flex_shrink()
2864 .child(
2865 h_flex()
2866 .w_full()
2867 .gap_4()
2868 .child(
2869 Label::new(SharedString::new_static(setting_item.title))
2870 .size(LabelSize::Default),
2871 )
2872 .when_some(
2873 file_set_in.filter(|file_set_in| file_set_in != &file),
2874 |elem, file_set_in| {
2875 elem.child(
2876 Label::new(format!(
2877 "set in {}",
2878 file_set_in.name()
2879 ))
2880 .color(Color::Muted),
2881 )
2882 },
2883 ),
2884 )
2885 .child(
2886 Label::new(SharedString::new_static(setting_item.description))
2887 .size(LabelSize::Small)
2888 .color(Color::Muted),
2889 ),
2890 )
2891 .child(renderer.render(
2892 setting_item.field.as_ref(),
2893 file,
2894 setting_item.metadata.as_deref(),
2895 window,
2896 cx,
2897 ))
2898 .into_any_element()
2899 }
2900 SettingsPageItem::SubPageLink(sub_page_link) => h_flex()
2901 .id(sub_page_link.title)
2902 .w_full()
2903 .gap_2()
2904 .flex_wrap()
2905 .justify_between()
2906 .when(!is_last, |this| {
2907 this.pb_4()
2908 .border_b_1()
2909 .border_color(cx.theme().colors().border_variant)
2910 })
2911 .child(
2912 v_flex().max_w_1_2().flex_shrink().child(
2913 Label::new(SharedString::new_static(sub_page_link.title))
2914 .size(LabelSize::Default),
2915 ),
2916 )
2917 .child(
2918 Button::new(("sub-page".into(), sub_page_link.title), "Configure")
2919 .icon(Some(IconName::ChevronRight))
2920 .icon_position(Some(IconPosition::End))
2921 .style(ButtonStyle::Outlined),
2922 )
2923 .on_click({
2924 let sub_page_link = sub_page_link.clone();
2925 cx.listener(move |this, _, _, cx| {
2926 this.push_sub_page(sub_page_link.clone(), section_header, cx)
2927 })
2928 })
2929 .into_any_element(),
2930 }
2931 }
2932}
2933
2934struct SettingItem {
2935 title: &'static str,
2936 description: &'static str,
2937 field: Box<dyn AnySettingField>,
2938 metadata: Option<Box<SettingsFieldMetadata>>,
2939}
2940
2941impl PartialEq for SettingItem {
2942 fn eq(&self, other: &Self) -> bool {
2943 self.title == other.title
2944 && self.description == other.description
2945 && (match (&self.metadata, &other.metadata) {
2946 (None, None) => true,
2947 (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder,
2948 _ => false,
2949 })
2950 }
2951}
2952
2953#[derive(Clone)]
2954struct SubPageLink {
2955 title: &'static str,
2956 render: Rc<dyn Fn(&mut SettingsWindow, &mut Window, &mut App) -> AnyElement>,
2957}
2958
2959impl PartialEq for SubPageLink {
2960 fn eq(&self, other: &Self) -> bool {
2961 self.title == other.title
2962 }
2963}
2964
2965#[allow(unused)]
2966#[derive(Clone, PartialEq)]
2967enum SettingsUiFile {
2968 User, // Uses all settings.
2969 Local((WorktreeId, Arc<RelPath>)), // Has a special name, and special set of settings
2970 Server(&'static str), // Uses a special name, and the user settings
2971}
2972
2973impl SettingsUiFile {
2974 fn pages(&self) -> Vec<SettingsPage> {
2975 match self {
2976 SettingsUiFile::User => user_settings_data(),
2977 SettingsUiFile::Local(_) => project_settings_data(),
2978 SettingsUiFile::Server(_) => user_settings_data(),
2979 }
2980 }
2981
2982 fn name(&self) -> SharedString {
2983 match self {
2984 SettingsUiFile::User => SharedString::new_static("User"),
2985 // TODO is PathStyle::local() ever not appropriate?
2986 SettingsUiFile::Local((_, path)) => {
2987 format!("Local ({})", path.display(PathStyle::local())).into()
2988 }
2989 SettingsUiFile::Server(file) => format!("Server ({})", file).into(),
2990 }
2991 }
2992
2993 fn from_settings(file: settings::SettingsFile) -> Option<Self> {
2994 Some(match file {
2995 settings::SettingsFile::User => SettingsUiFile::User,
2996 settings::SettingsFile::Local(location) => SettingsUiFile::Local(location),
2997 settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"),
2998 settings::SettingsFile::Default => return None,
2999 })
3000 }
3001
3002 fn to_settings(&self) -> settings::SettingsFile {
3003 match self {
3004 SettingsUiFile::User => settings::SettingsFile::User,
3005 SettingsUiFile::Local(location) => settings::SettingsFile::Local(location.clone()),
3006 SettingsUiFile::Server(_) => settings::SettingsFile::Server,
3007 }
3008 }
3009}
3010
3011impl SettingsWindow {
3012 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
3013 let current_file = SettingsUiFile::User;
3014 let search_bar = cx.new(|cx| {
3015 let mut editor = Editor::single_line(window, cx);
3016 editor.set_placeholder_text("Search settings…", window, cx);
3017 editor
3018 });
3019
3020 cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| {
3021 let EditorEvent::Edited { transaction_id: _ } = event else {
3022 return;
3023 };
3024
3025 this.update_matches(cx);
3026 })
3027 .detach();
3028
3029 cx.observe_global_in::<SettingsStore>(window, move |this, _, cx| {
3030 this.fetch_files(cx);
3031 cx.notify();
3032 })
3033 .detach();
3034
3035 let mut this = Self {
3036 files: vec![],
3037 current_file: current_file,
3038 pages: vec![],
3039 navbar_entries: vec![],
3040 navbar_entry: 0,
3041 list_handle: UniformListScrollHandle::default(),
3042 search_bar,
3043 search_task: None,
3044 search_matches: vec![],
3045 sub_page_stack: vec![],
3046 };
3047
3048 this.fetch_files(cx);
3049 this.build_ui(cx);
3050
3051 this
3052 }
3053
3054 fn toggle_navbar_entry(&mut self, ix: usize) {
3055 // We can only toggle root entries
3056 if !self.navbar_entries[ix].is_root {
3057 return;
3058 }
3059
3060 let toggle_page_index = self.page_index_from_navbar_index(ix);
3061 let selected_page_index = self.page_index_from_navbar_index(self.navbar_entry);
3062
3063 let expanded = &mut self.page_for_navbar_index(ix).expanded;
3064 *expanded = !*expanded;
3065 let expanded = *expanded;
3066 // if currently selected page is a child of the parent page we are folding,
3067 // set the current page to the parent page
3068 if selected_page_index == toggle_page_index {
3069 self.navbar_entry = ix;
3070 } else if selected_page_index > toggle_page_index {
3071 let sub_items_count = self.pages[toggle_page_index]
3072 .items
3073 .iter()
3074 .filter(|item| matches!(item, SettingsPageItem::SectionHeader(_)))
3075 .count();
3076 if expanded {
3077 self.navbar_entry += sub_items_count;
3078 } else {
3079 self.navbar_entry -= sub_items_count;
3080 }
3081 }
3082
3083 self.build_navbar();
3084 }
3085
3086 fn build_navbar(&mut self) {
3087 let mut navbar_entries = Vec::with_capacity(self.navbar_entries.len());
3088 for (page_index, page) in self.pages.iter().enumerate() {
3089 if !self.search_matches[page_index]
3090 .iter()
3091 .any(|is_match| *is_match)
3092 && !self.search_matches[page_index].is_empty()
3093 {
3094 continue;
3095 }
3096 navbar_entries.push(NavBarEntry {
3097 title: page.title,
3098 is_root: true,
3099 page_index,
3100 });
3101 if !page.expanded {
3102 continue;
3103 }
3104
3105 for (item_index, item) in page.items.iter().enumerate() {
3106 let SettingsPageItem::SectionHeader(title) = item else {
3107 continue;
3108 };
3109 if !self.search_matches[page_index][item_index] {
3110 continue;
3111 }
3112
3113 navbar_entries.push(NavBarEntry {
3114 title,
3115 is_root: false,
3116 page_index,
3117 });
3118 }
3119 }
3120 self.navbar_entries = navbar_entries;
3121 }
3122
3123 fn update_matches(&mut self, cx: &mut Context<SettingsWindow>) {
3124 self.search_task.take();
3125 let query = self.search_bar.read(cx).text(cx);
3126 if query.is_empty() {
3127 for page in &mut self.search_matches {
3128 page.fill(true);
3129 }
3130 self.build_navbar();
3131 cx.notify();
3132 return;
3133 }
3134
3135 struct ItemKey {
3136 page_index: usize,
3137 header_index: usize,
3138 item_index: usize,
3139 }
3140 let mut key_lut: Vec<ItemKey> = vec![];
3141 let mut candidates = Vec::default();
3142
3143 for (page_index, page) in self.pages.iter().enumerate() {
3144 let mut header_index = 0;
3145 for (item_index, item) in page.items.iter().enumerate() {
3146 let key_index = key_lut.len();
3147 match item {
3148 SettingsPageItem::SettingItem(item) => {
3149 candidates.push(StringMatchCandidate::new(key_index, item.title));
3150 candidates.push(StringMatchCandidate::new(key_index, item.description));
3151 }
3152 SettingsPageItem::SectionHeader(header) => {
3153 candidates.push(StringMatchCandidate::new(key_index, header));
3154 header_index = item_index;
3155 }
3156 SettingsPageItem::SubPageLink(sub_page_link) => {
3157 candidates.push(StringMatchCandidate::new(key_index, sub_page_link.title));
3158 }
3159 }
3160 key_lut.push(ItemKey {
3161 page_index,
3162 header_index,
3163 item_index,
3164 });
3165 }
3166 }
3167 let atomic_bool = AtomicBool::new(false);
3168
3169 self.search_task = Some(cx.spawn(async move |this, cx| {
3170 let string_matches = fuzzy::match_strings(
3171 candidates.as_slice(),
3172 &query,
3173 false,
3174 false,
3175 candidates.len(),
3176 &atomic_bool,
3177 cx.background_executor().clone(),
3178 );
3179 let string_matches = string_matches.await;
3180
3181 this.update(cx, |this, cx| {
3182 for page in &mut this.search_matches {
3183 page.fill(false);
3184 }
3185
3186 for string_match in string_matches {
3187 let ItemKey {
3188 page_index,
3189 header_index,
3190 item_index,
3191 } = key_lut[string_match.candidate_id];
3192 let page = &mut this.search_matches[page_index];
3193 page[header_index] = true;
3194 page[item_index] = true;
3195 }
3196 this.build_navbar();
3197 this.navbar_entry = 0;
3198 cx.notify();
3199 })
3200 .ok();
3201 }));
3202 }
3203
3204 fn build_ui(&mut self, cx: &mut Context<SettingsWindow>) {
3205 self.pages = self.current_file.pages();
3206 self.search_matches = self
3207 .pages
3208 .iter()
3209 .map(|page| vec![true; page.items.len()])
3210 .collect::<Vec<_>>();
3211 self.build_navbar();
3212
3213 if !self.search_bar.read(cx).is_empty(cx) {
3214 self.update_matches(cx);
3215 }
3216
3217 cx.notify();
3218 }
3219
3220 fn fetch_files(&mut self, cx: &mut Context<SettingsWindow>) {
3221 let settings_store = cx.global::<SettingsStore>();
3222 let mut ui_files = vec![];
3223 let all_files = settings_store.get_all_files();
3224 for file in all_files {
3225 let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else {
3226 continue;
3227 };
3228 ui_files.push(settings_ui_file);
3229 }
3230 ui_files.reverse();
3231 self.files = ui_files;
3232 if !self.files.contains(&self.current_file) {
3233 self.change_file(0, cx);
3234 }
3235 }
3236
3237 fn change_file(&mut self, ix: usize, cx: &mut Context<SettingsWindow>) {
3238 if ix >= self.files.len() {
3239 self.current_file = SettingsUiFile::User;
3240 return;
3241 }
3242 if self.files[ix] == self.current_file {
3243 return;
3244 }
3245 self.current_file = self.files[ix].clone();
3246 self.navbar_entry = 0;
3247 self.build_ui(cx);
3248 }
3249
3250 fn render_files(&self, _window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3251 h_flex()
3252 .gap_1()
3253 .children(self.files.iter().enumerate().map(|(ix, file)| {
3254 Button::new(ix, file.name())
3255 .on_click(cx.listener(move |this, _, _window, cx| this.change_file(ix, cx)))
3256 }))
3257 }
3258
3259 fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div {
3260 h_flex()
3261 .pt_1()
3262 .px_1p5()
3263 .gap_1p5()
3264 .rounded_sm()
3265 .bg(cx.theme().colors().editor_background)
3266 .border_1()
3267 .border_color(cx.theme().colors().border)
3268 .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
3269 .child(self.search_bar.clone())
3270 }
3271
3272 fn render_nav(&self, window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3273 v_flex()
3274 .w_64()
3275 .p_2p5()
3276 .pt_10()
3277 .gap_3()
3278 .flex_none()
3279 .border_r_1()
3280 .border_color(cx.theme().colors().border)
3281 .bg(cx.theme().colors().panel_background)
3282 .child(self.render_search(window, cx).pb_1())
3283 .child(
3284 uniform_list(
3285 "settings-ui-nav-bar",
3286 self.navbar_entries.len(),
3287 cx.processor(|this, range: Range<usize>, _, cx| {
3288 range
3289 .into_iter()
3290 .map(|ix| {
3291 let entry = &this.navbar_entries[ix];
3292
3293 TreeViewItem::new(("settings-ui-navbar-entry", ix), entry.title)
3294 .root_item(entry.is_root)
3295 .toggle_state(this.is_navbar_entry_selected(ix))
3296 .when(entry.is_root, |item| {
3297 item.toggle(
3298 this.pages[this.page_index_from_navbar_index(ix)]
3299 .expanded,
3300 )
3301 .on_toggle(
3302 cx.listener(move |this, _, _, cx| {
3303 this.toggle_navbar_entry(ix);
3304 cx.notify();
3305 }),
3306 )
3307 })
3308 .on_click(cx.listener(move |this, _, _, cx| {
3309 this.navbar_entry = ix;
3310 cx.notify();
3311 }))
3312 .into_any_element()
3313 })
3314 .collect()
3315 }),
3316 )
3317 .track_scroll(self.list_handle.clone())
3318 .size_full()
3319 .flex_grow(),
3320 )
3321 }
3322
3323 fn page_items(&self) -> impl Iterator<Item = &SettingsPageItem> {
3324 let page_idx = self.current_page_index();
3325
3326 self.current_page()
3327 .items
3328 .iter()
3329 .enumerate()
3330 .filter_map(move |(item_index, item)| {
3331 self.search_matches[page_idx][item_index].then_some(item)
3332 })
3333 }
3334
3335 fn render_sub_page_breadcrumbs(&self) -> impl IntoElement {
3336 let mut items = vec![];
3337 items.push(self.current_page().title);
3338 items.extend(
3339 self.sub_page_stack
3340 .iter()
3341 .flat_map(|page| [page.section_header, page.link.title]),
3342 );
3343
3344 let last = items.pop().unwrap();
3345 h_flex()
3346 .gap_1()
3347 .children(
3348 items
3349 .into_iter()
3350 .flat_map(|item| [item, "/"])
3351 .map(|item| Label::new(item).color(Color::Muted)),
3352 )
3353 .child(Label::new(last))
3354 }
3355
3356 fn render_page(&mut self, window: &mut Window, cx: &mut Context<SettingsWindow>) -> Div {
3357 let mut page = v_flex()
3358 .w_full()
3359 .pt_4()
3360 .px_6()
3361 .gap_4()
3362 .bg(cx.theme().colors().editor_background);
3363 let mut page_content = v_flex()
3364 .id("settings-ui-page")
3365 .gap_4()
3366 .overflow_y_scroll()
3367 .track_scroll(
3368 window
3369 .use_state(cx, |_, _| ScrollHandle::default())
3370 .read(cx),
3371 );
3372 if self.sub_page_stack.len() == 0 {
3373 page = page.child(self.render_files(window, cx));
3374
3375 let items: Vec<_> = self.page_items().collect();
3376 let items_len = items.len();
3377 let mut section_header = None;
3378
3379 page_content =
3380 page_content.children(items.into_iter().enumerate().map(|(index, item)| {
3381 let is_last = index == items_len - 1;
3382 if let SettingsPageItem::SectionHeader(header) = item {
3383 section_header = Some(*header);
3384 }
3385 item.render(
3386 self.current_file.clone(),
3387 section_header.expect("All items rendered after a section header"),
3388 is_last,
3389 window,
3390 cx,
3391 )
3392 }))
3393 } else {
3394 page = page.child(
3395 h_flex()
3396 .gap_2()
3397 .child(IconButton::new("back-btn", IconName::ChevronLeft).on_click(
3398 cx.listener(|this, _, _, cx| {
3399 this.pop_sub_page(cx);
3400 }),
3401 ))
3402 .child(self.render_sub_page_breadcrumbs()),
3403 );
3404
3405 let active_page_render_fn = self.sub_page_stack.last().unwrap().link.render.clone();
3406 page_content = page_content.child((active_page_render_fn)(self, window, cx));
3407 }
3408
3409 return page.child(page_content);
3410 }
3411
3412 fn current_page_index(&self) -> usize {
3413 self.page_index_from_navbar_index(self.navbar_entry)
3414 }
3415
3416 fn current_page(&self) -> &SettingsPage {
3417 &self.pages[self.current_page_index()]
3418 }
3419
3420 fn page_index_from_navbar_index(&self, index: usize) -> usize {
3421 if self.navbar_entries.is_empty() {
3422 return 0;
3423 }
3424
3425 self.navbar_entries[index].page_index
3426 }
3427
3428 fn page_for_navbar_index(&mut self, index: usize) -> &mut SettingsPage {
3429 let index = self.page_index_from_navbar_index(index);
3430 &mut self.pages[index]
3431 }
3432
3433 fn is_navbar_entry_selected(&self, ix: usize) -> bool {
3434 ix == self.navbar_entry
3435 }
3436
3437 fn push_sub_page(
3438 &mut self,
3439 sub_page_link: SubPageLink,
3440 section_header: &'static str,
3441 cx: &mut Context<SettingsWindow>,
3442 ) {
3443 self.sub_page_stack.push(SubPage {
3444 link: sub_page_link,
3445 section_header,
3446 });
3447 cx.notify();
3448 }
3449
3450 fn pop_sub_page(&mut self, cx: &mut Context<SettingsWindow>) {
3451 self.sub_page_stack.pop();
3452 cx.notify();
3453 }
3454}
3455
3456impl Render for SettingsWindow {
3457 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3458 let ui_font = theme::setup_ui_font(window, cx);
3459
3460 div()
3461 .flex()
3462 .flex_row()
3463 .size_full()
3464 .font(ui_font)
3465 .bg(cx.theme().colors().background)
3466 .text_color(cx.theme().colors().text)
3467 .child(self.render_nav(window, cx))
3468 .child(self.render_page(window, cx))
3469 }
3470}
3471
3472fn update_settings_file(
3473 file: SettingsUiFile,
3474 cx: &mut App,
3475 update: impl 'static + Send + FnOnce(&mut SettingsContent, &App),
3476) -> Result<()> {
3477 match file {
3478 SettingsUiFile::Local((worktree_id, rel_path)) => {
3479 fn all_projects(cx: &App) -> impl Iterator<Item = Entity<project::Project>> {
3480 workspace::AppState::global(cx)
3481 .upgrade()
3482 .map(|app_state| {
3483 app_state
3484 .workspace_store
3485 .read(cx)
3486 .workspaces()
3487 .iter()
3488 .filter_map(|workspace| {
3489 Some(workspace.read(cx).ok()?.project().clone())
3490 })
3491 })
3492 .into_iter()
3493 .flatten()
3494 }
3495 let rel_path = rel_path.join(paths::local_settings_file_relative_path());
3496 let project = all_projects(cx).find(|project| {
3497 project.read_with(cx, |project, cx| {
3498 project.contains_local_settings_file(worktree_id, &rel_path, cx)
3499 })
3500 });
3501 let Some(project) = project else {
3502 anyhow::bail!(
3503 "Could not find worktree containing settings file: {}",
3504 &rel_path.display(PathStyle::local())
3505 );
3506 };
3507 project.update(cx, |project, cx| {
3508 project.update_local_settings_file(worktree_id, rel_path, cx, update);
3509 });
3510 return Ok(());
3511 }
3512 SettingsUiFile::User => {
3513 // todo(settings_ui) error?
3514 SettingsStore::global(cx).update_settings_file(<dyn fs::Fs>::global(cx), update);
3515 Ok(())
3516 }
3517 SettingsUiFile::Server(_) => unimplemented!(),
3518 }
3519}
3520
3521fn render_text_field<T: From<String> + Into<String> + AsRef<str> + Clone>(
3522 field: SettingField<T>,
3523 file: SettingsUiFile,
3524 metadata: Option<&SettingsFieldMetadata>,
3525 cx: &mut App,
3526) -> AnyElement {
3527 let (_, initial_text) =
3528 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3529 let initial_text = Some(initial_text.clone()).filter(|s| !s.as_ref().is_empty());
3530
3531 SettingsEditor::new()
3532 .when_some(initial_text, |editor, text| {
3533 editor.with_initial_text(text.into())
3534 })
3535 .when_some(
3536 metadata.and_then(|metadata| metadata.placeholder),
3537 |editor, placeholder| editor.with_placeholder(placeholder),
3538 )
3539 .on_confirm({
3540 move |new_text, cx| {
3541 update_settings_file(file.clone(), cx, move |settings, _cx| {
3542 *(field.pick_mut)(settings) = new_text.map(Into::into);
3543 })
3544 .log_err(); // todo(settings_ui) don't log err
3545 }
3546 })
3547 .into_any_element()
3548}
3549
3550fn render_toggle_button<B: Into<bool> + From<bool> + Copy>(
3551 field: SettingField<B>,
3552 file: SettingsUiFile,
3553 cx: &mut App,
3554) -> AnyElement {
3555 let (_, &value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3556
3557 let toggle_state = if value.into() {
3558 ToggleState::Selected
3559 } else {
3560 ToggleState::Unselected
3561 };
3562
3563 Switch::new("toggle_button", toggle_state)
3564 .color(ui::SwitchColor::Accent)
3565 .on_click({
3566 move |state, _window, cx| {
3567 let state = *state == ui::ToggleState::Selected;
3568 update_settings_file(file.clone(), cx, move |settings, _cx| {
3569 *(field.pick_mut)(settings) = Some(state.into());
3570 })
3571 .log_err(); // todo(settings_ui) don't log err
3572 }
3573 })
3574 .color(SwitchColor::Accent)
3575 .into_any_element()
3576}
3577
3578fn render_dropdown<T>(
3579 field: SettingField<T>,
3580 file: SettingsUiFile,
3581 window: &mut Window,
3582 cx: &mut App,
3583) -> AnyElement
3584where
3585 T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static,
3586{
3587 let variants = || -> &'static [T] { <T as strum::VariantArray>::VARIANTS };
3588 let labels = || -> &'static [&'static str] { <T as strum::VariantNames>::VARIANTS };
3589
3590 let (_, ¤t_value) =
3591 SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick);
3592
3593 let current_value_label =
3594 labels()[variants().iter().position(|v| *v == current_value).unwrap()];
3595
3596 DropdownMenu::new(
3597 "dropdown",
3598 current_value_label,
3599 ContextMenu::build(window, cx, move |mut menu, _, _| {
3600 for (&value, &label) in std::iter::zip(variants(), labels()) {
3601 let file = file.clone();
3602 menu = menu.toggleable_entry(
3603 label,
3604 value == current_value,
3605 IconPosition::Start,
3606 None,
3607 move |_, cx| {
3608 if value == current_value {
3609 return;
3610 }
3611 update_settings_file(file.clone(), cx, move |settings, _cx| {
3612 *(field.pick_mut)(settings) = Some(value);
3613 })
3614 .log_err(); // todo(settings_ui) don't log err
3615 },
3616 );
3617 }
3618 menu
3619 }),
3620 )
3621 .style(DropdownStyle::Outlined)
3622 .into_any_element()
3623}
3624
3625#[cfg(test)]
3626mod test {
3627
3628 use super::*;
3629
3630 impl SettingsWindow {
3631 fn navbar(&self) -> &[NavBarEntry] {
3632 self.navbar_entries.as_slice()
3633 }
3634
3635 fn navbar_entry(&self) -> usize {
3636 self.navbar_entry
3637 }
3638
3639 fn new_builder(window: &mut Window, cx: &mut Context<Self>) -> Self {
3640 let mut this = Self::new(window, cx);
3641 this.navbar_entries.clear();
3642 this.pages.clear();
3643 this
3644 }
3645
3646 fn build(mut self) -> Self {
3647 self.build_navbar();
3648 self
3649 }
3650
3651 fn add_page(
3652 mut self,
3653 title: &'static str,
3654 build_page: impl Fn(SettingsPage) -> SettingsPage,
3655 ) -> Self {
3656 let page = SettingsPage {
3657 title,
3658 expanded: false,
3659 items: Vec::default(),
3660 };
3661
3662 self.pages.push(build_page(page));
3663 self
3664 }
3665
3666 fn search(&mut self, search_query: &str, window: &mut Window, cx: &mut Context<Self>) {
3667 self.search_task.take();
3668 self.search_bar.update(cx, |editor, cx| {
3669 editor.set_text(search_query, window, cx);
3670 });
3671 self.update_matches(cx);
3672 }
3673
3674 fn assert_search_results(&self, other: &Self) {
3675 // page index could be different because of filtered out pages
3676 assert!(
3677 self.navbar_entries
3678 .iter()
3679 .zip(other.navbar_entries.iter())
3680 .all(|(entry, other)| {
3681 entry.is_root == other.is_root && entry.title == other.title
3682 })
3683 );
3684 assert_eq!(
3685 self.current_page().items.iter().collect::<Vec<_>>(),
3686 other.page_items().collect::<Vec<_>>()
3687 );
3688 }
3689 }
3690
3691 impl SettingsPage {
3692 fn item(mut self, item: SettingsPageItem) -> Self {
3693 self.items.push(item);
3694 self
3695 }
3696 }
3697
3698 impl SettingsPageItem {
3699 fn basic_item(title: &'static str, description: &'static str) -> Self {
3700 SettingsPageItem::SettingItem(SettingItem {
3701 title,
3702 description,
3703 field: Box::new(SettingField {
3704 pick: |settings_content| &settings_content.auto_update,
3705 pick_mut: |settings_content| &mut settings_content.auto_update,
3706 }),
3707 metadata: None,
3708 })
3709 }
3710 }
3711
3712 fn register_settings(cx: &mut App) {
3713 settings::init(cx);
3714 theme::init(theme::LoadThemes::JustBase, cx);
3715 workspace::init_settings(cx);
3716 project::Project::init_settings(cx);
3717 language::init(cx);
3718 editor::init(cx);
3719 menu::init();
3720 }
3721
3722 fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow {
3723 let mut pages: Vec<SettingsPage> = Vec::new();
3724 let mut current_page = None;
3725 let mut selected_idx = None;
3726 let mut ix = 0;
3727 let mut in_closed_subentry = false;
3728
3729 for mut line in input
3730 .lines()
3731 .map(|line| line.trim())
3732 .filter(|line| !line.is_empty())
3733 {
3734 let mut is_selected = false;
3735 if line.ends_with("*") {
3736 assert!(
3737 selected_idx.is_none(),
3738 "Can only have one selected navbar entry at a time"
3739 );
3740 selected_idx = Some(ix);
3741 line = &line[..line.len() - 1];
3742 is_selected = true;
3743 }
3744
3745 if line.starts_with("v") || line.starts_with(">") {
3746 if let Some(current_page) = current_page.take() {
3747 pages.push(current_page);
3748 }
3749
3750 let expanded = line.starts_with("v");
3751 in_closed_subentry = !expanded;
3752 ix += 1;
3753
3754 current_page = Some(SettingsPage {
3755 title: line.split_once(" ").unwrap().1,
3756 expanded,
3757 items: Vec::default(),
3758 });
3759 } else if line.starts_with("-") {
3760 if !in_closed_subentry {
3761 ix += 1;
3762 } else if is_selected && in_closed_subentry {
3763 panic!("Can't select sub entry if it's parent is closed");
3764 }
3765
3766 let Some(current_page) = current_page.as_mut() else {
3767 panic!("Sub entries must be within a page");
3768 };
3769
3770 current_page.items.push(SettingsPageItem::SectionHeader(
3771 line.split_once(" ").unwrap().1,
3772 ));
3773 } else {
3774 panic!(
3775 "Entries must start with one of 'v', '>', or '-'\n line: {}",
3776 line
3777 );
3778 }
3779 }
3780
3781 if let Some(current_page) = current_page.take() {
3782 pages.push(current_page);
3783 }
3784
3785 let search_matches = pages
3786 .iter()
3787 .map(|page| vec![true; page.items.len()])
3788 .collect::<Vec<_>>();
3789
3790 let mut settings_window = SettingsWindow {
3791 files: Vec::default(),
3792 current_file: crate::SettingsUiFile::User,
3793 pages,
3794 search_bar: cx.new(|cx| Editor::single_line(window, cx)),
3795 navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
3796 navbar_entries: Vec::default(),
3797 list_handle: UniformListScrollHandle::default(),
3798 search_matches,
3799 search_task: None,
3800 sub_page_stack: vec![],
3801 };
3802
3803 settings_window.build_navbar();
3804 settings_window
3805 }
3806
3807 #[track_caller]
3808 fn check_navbar_toggle(
3809 before: &'static str,
3810 toggle_idx: usize,
3811 after: &'static str,
3812 window: &mut Window,
3813 cx: &mut App,
3814 ) {
3815 let mut settings_window = parse(before, window, cx);
3816 settings_window.toggle_navbar_entry(toggle_idx);
3817
3818 let expected_settings_window = parse(after, window, cx);
3819
3820 assert_eq!(settings_window.navbar(), expected_settings_window.navbar());
3821 assert_eq!(
3822 settings_window.navbar_entry(),
3823 expected_settings_window.navbar_entry()
3824 );
3825 }
3826
3827 macro_rules! check_navbar_toggle {
3828 ($name:ident, before: $before:expr, toggle_idx: $toggle_idx:expr, after: $after:expr) => {
3829 #[gpui::test]
3830 fn $name(cx: &mut gpui::TestAppContext) {
3831 let window = cx.add_empty_window();
3832 window.update(|window, cx| {
3833 register_settings(cx);
3834 check_navbar_toggle($before, $toggle_idx, $after, window, cx);
3835 });
3836 }
3837 };
3838 }
3839
3840 check_navbar_toggle!(
3841 navbar_basic_open,
3842 before: r"
3843 v General
3844 - General
3845 - Privacy*
3846 v Project
3847 - Project Settings
3848 ",
3849 toggle_idx: 0,
3850 after: r"
3851 > General*
3852 v Project
3853 - Project Settings
3854 "
3855 );
3856
3857 check_navbar_toggle!(
3858 navbar_basic_close,
3859 before: r"
3860 > General*
3861 - General
3862 - Privacy
3863 v Project
3864 - Project Settings
3865 ",
3866 toggle_idx: 0,
3867 after: r"
3868 v General*
3869 - General
3870 - Privacy
3871 v Project
3872 - Project Settings
3873 "
3874 );
3875
3876 check_navbar_toggle!(
3877 navbar_basic_second_root_entry_close,
3878 before: r"
3879 > General
3880 - General
3881 - Privacy
3882 v Project
3883 - Project Settings*
3884 ",
3885 toggle_idx: 1,
3886 after: r"
3887 > General
3888 > Project*
3889 "
3890 );
3891
3892 check_navbar_toggle!(
3893 navbar_toggle_subroot,
3894 before: r"
3895 v General Page
3896 - General
3897 - Privacy
3898 v Project
3899 - Worktree Settings Content*
3900 v AI
3901 - General
3902 > Appearance & Behavior
3903 ",
3904 toggle_idx: 3,
3905 after: r"
3906 v General Page
3907 - General
3908 - Privacy
3909 > Project*
3910 v AI
3911 - General
3912 > Appearance & Behavior
3913 "
3914 );
3915
3916 check_navbar_toggle!(
3917 navbar_toggle_close_propagates_selected_index,
3918 before: r"
3919 v General Page
3920 - General
3921 - Privacy
3922 v Project
3923 - Worktree Settings Content
3924 v AI
3925 - General*
3926 > Appearance & Behavior
3927 ",
3928 toggle_idx: 0,
3929 after: r"
3930 > General Page
3931 v Project
3932 - Worktree Settings Content
3933 v AI
3934 - General*
3935 > Appearance & Behavior
3936 "
3937 );
3938
3939 check_navbar_toggle!(
3940 navbar_toggle_expand_propagates_selected_index,
3941 before: r"
3942 > General Page
3943 - General
3944 - Privacy
3945 v Project
3946 - Worktree Settings Content
3947 v AI
3948 - General*
3949 > Appearance & Behavior
3950 ",
3951 toggle_idx: 0,
3952 after: r"
3953 v General Page
3954 - General
3955 - Privacy
3956 v Project
3957 - Worktree Settings Content
3958 v AI
3959 - General*
3960 > Appearance & Behavior
3961 "
3962 );
3963
3964 check_navbar_toggle!(
3965 navbar_toggle_sub_entry_does_nothing,
3966 before: r"
3967 > General Page
3968 - General
3969 - Privacy
3970 v Project
3971 - Worktree Settings Content
3972 v AI
3973 - General*
3974 > Appearance & Behavior
3975 ",
3976 toggle_idx: 4,
3977 after: r"
3978 > General Page
3979 - General
3980 - Privacy
3981 v Project
3982 - Worktree Settings Content
3983 v AI
3984 - General*
3985 > Appearance & Behavior
3986 "
3987 );
3988
3989 #[gpui::test]
3990 fn test_basic_search(cx: &mut gpui::TestAppContext) {
3991 let cx = cx.add_empty_window();
3992 let (actual, expected) = cx.update(|window, cx| {
3993 register_settings(cx);
3994
3995 let expected = cx.new(|cx| {
3996 SettingsWindow::new_builder(window, cx)
3997 .add_page("General", |page| {
3998 page.item(SettingsPageItem::SectionHeader("General settings"))
3999 .item(SettingsPageItem::basic_item("test title", "General test"))
4000 })
4001 .build()
4002 });
4003
4004 let actual = cx.new(|cx| {
4005 SettingsWindow::new_builder(window, cx)
4006 .add_page("General", |page| {
4007 page.item(SettingsPageItem::SectionHeader("General settings"))
4008 .item(SettingsPageItem::basic_item("test title", "General test"))
4009 })
4010 .add_page("Theme", |page| {
4011 page.item(SettingsPageItem::SectionHeader("Theme settings"))
4012 })
4013 .build()
4014 });
4015
4016 actual.update(cx, |settings, cx| settings.search("gen", window, cx));
4017
4018 (actual, expected)
4019 });
4020
4021 cx.cx.run_until_parked();
4022
4023 cx.update(|_window, cx| {
4024 let expected = expected.read(cx);
4025 let actual = actual.read(cx);
4026 expected.assert_search_results(&actual);
4027 })
4028 }
4029
4030 #[gpui::test]
4031 fn test_search_render_page_with_filtered_out_navbar_entries(cx: &mut gpui::TestAppContext) {
4032 let cx = cx.add_empty_window();
4033 let (actual, expected) = cx.update(|window, cx| {
4034 register_settings(cx);
4035
4036 let actual = cx.new(|cx| {
4037 SettingsWindow::new_builder(window, cx)
4038 .add_page("General", |page| {
4039 page.item(SettingsPageItem::SectionHeader("General settings"))
4040 .item(SettingsPageItem::basic_item(
4041 "Confirm Quit",
4042 "Whether to confirm before quitting Zed",
4043 ))
4044 .item(SettingsPageItem::basic_item(
4045 "Auto Update",
4046 "Automatically update Zed",
4047 ))
4048 })
4049 .add_page("AI", |page| {
4050 page.item(SettingsPageItem::basic_item(
4051 "Disable AI",
4052 "Whether to disable all AI features in Zed",
4053 ))
4054 })
4055 .add_page("Appearance & Behavior", |page| {
4056 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4057 SettingsPageItem::basic_item(
4058 "Cursor Shape",
4059 "Cursor shape for the editor",
4060 ),
4061 )
4062 })
4063 .build()
4064 });
4065
4066 let expected = cx.new(|cx| {
4067 SettingsWindow::new_builder(window, cx)
4068 .add_page("Appearance & Behavior", |page| {
4069 page.item(SettingsPageItem::SectionHeader("Cursor")).item(
4070 SettingsPageItem::basic_item(
4071 "Cursor Shape",
4072 "Cursor shape for the editor",
4073 ),
4074 )
4075 })
4076 .build()
4077 });
4078
4079 actual.update(cx, |settings, cx| settings.search("cursor", window, cx));
4080
4081 (actual, expected)
4082 });
4083
4084 cx.cx.run_until_parked();
4085
4086 cx.update(|_window, cx| {
4087 let expected = expected.read(cx);
4088 let actual = actual.read(cx);
4089 expected.assert_search_results(&actual);
4090 })
4091 }
4092}