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