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