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