1mod appearance_settings_controls;
2
3use std::any::TypeId;
4use std::ops::{Not, Range};
5
6use anyhow::Context as _;
7use command_palette_hooks::CommandPaletteFilter;
8use editor::EditorSettingsControls;
9use feature_flags::{FeatureFlag, FeatureFlagViewExt};
10use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
11use settings::{
12 NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
13 SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
14};
15use smallvec::SmallVec;
16use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, prelude::*};
17use workspace::{
18 Workspace,
19 item::{Item, ItemEvent},
20 with_active_or_new_workspace,
21};
22
23use crate::appearance_settings_controls::AppearanceSettingsControls;
24
25pub struct SettingsUiFeatureFlag;
26
27impl FeatureFlag for SettingsUiFeatureFlag {
28 const NAME: &'static str = "settings-ui";
29}
30
31actions!(
32 zed,
33 [
34 /// Opens the settings editor.
35 OpenSettingsEditor
36 ]
37);
38
39pub fn init(cx: &mut App) {
40 cx.on_action(|_: &OpenSettingsEditor, cx| {
41 with_active_or_new_workspace(cx, move |workspace, window, cx| {
42 let existing = workspace
43 .active_pane()
44 .read(cx)
45 .items()
46 .find_map(|item| item.downcast::<SettingsPage>());
47
48 if let Some(existing) = existing {
49 workspace.activate_item(&existing, true, true, window, cx);
50 } else {
51 let settings_page = SettingsPage::new(workspace, cx);
52 workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
53 }
54 });
55 });
56
57 cx.observe_new(|_workspace: &mut Workspace, window, cx| {
58 let Some(window) = window else {
59 return;
60 };
61
62 let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
63
64 CommandPaletteFilter::update_global(cx, |filter, _cx| {
65 filter.hide_action_types(&settings_ui_actions);
66 });
67
68 cx.observe_flag::<SettingsUiFeatureFlag, _>(
69 window,
70 move |is_enabled, _workspace, _, cx| {
71 if is_enabled {
72 CommandPaletteFilter::update_global(cx, |filter, _cx| {
73 filter.show_action_types(&settings_ui_actions);
74 });
75 } else {
76 CommandPaletteFilter::update_global(cx, |filter, _cx| {
77 filter.hide_action_types(&settings_ui_actions);
78 });
79 }
80 },
81 )
82 .detach();
83 })
84 .detach();
85}
86
87pub struct SettingsPage {
88 focus_handle: FocusHandle,
89 settings_tree: SettingsUiTree,
90}
91
92impl SettingsPage {
93 pub fn new(_workspace: &Workspace, cx: &mut Context<Workspace>) -> Entity<Self> {
94 cx.new(|cx| Self {
95 focus_handle: cx.focus_handle(),
96 settings_tree: SettingsUiTree::new(cx),
97 })
98 }
99}
100
101impl EventEmitter<ItemEvent> for SettingsPage {}
102
103impl Focusable for SettingsPage {
104 fn focus_handle(&self, _cx: &App) -> FocusHandle {
105 self.focus_handle.clone()
106 }
107}
108
109impl Item for SettingsPage {
110 type Event = ItemEvent;
111
112 fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
113 Some(Icon::new(IconName::Settings))
114 }
115
116 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
117 "Settings".into()
118 }
119
120 fn show_toolbar(&self) -> bool {
121 false
122 }
123
124 fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
125 f(*event)
126 }
127}
128
129// We want to iterate over the side bar with root groups
130// - this is a loop over top level groups, and if any are expanded, recursively displaying their items
131// - Should be able to get all items from a group (flatten a group)
132// - Should be able to toggle/untoggle groups in UI (at least in sidebar)
133// - Search should be available
134// - there should be an index of text -> item mappings, for using fuzzy::match
135// - Do we want to show the parent groups when a item is matched?
136
137struct UiEntry {
138 title: &'static str,
139 path: Option<&'static str>,
140 documentation: Option<&'static str>,
141 _depth: usize,
142 // a
143 // b < a descendant range < a total descendant range
144 // f | |
145 // g | |
146 // c < |
147 // d |
148 // e <
149 descendant_range: Range<usize>,
150 total_descendant_range: Range<usize>,
151 next_sibling: Option<usize>,
152 // expanded: bool,
153 render: Option<SettingsUiItemSingle>,
154 /// For dynamic items this is a way to select a value from a list of values
155 /// this is always none for non-dynamic items
156 select_descendant: Option<fn(&serde_json::Value, &App) -> usize>,
157}
158
159impl UiEntry {
160 fn first_descendant_index(&self) -> Option<usize> {
161 return self
162 .descendant_range
163 .is_empty()
164 .not()
165 .then_some(self.descendant_range.start);
166 }
167
168 fn nth_descendant_index(&self, tree: &[UiEntry], n: usize) -> Option<usize> {
169 let first_descendant_index = self.first_descendant_index()?;
170 let mut current_index = 0;
171 let mut current_descendant_index = Some(first_descendant_index);
172 while let Some(descendant_index) = current_descendant_index
173 && current_index < n
174 {
175 current_index += 1;
176 current_descendant_index = tree[descendant_index].next_sibling;
177 }
178 current_descendant_index
179 }
180}
181
182pub struct SettingsUiTree {
183 root_entry_indices: Vec<usize>,
184 entries: Vec<UiEntry>,
185 active_entry_index: usize,
186}
187
188fn build_tree_item(
189 tree: &mut Vec<UiEntry>,
190 entry: SettingsUiEntry,
191 depth: usize,
192 prev_index: Option<usize>,
193) {
194 let index = tree.len();
195 tree.push(UiEntry {
196 title: entry.title,
197 path: entry.path,
198 documentation: entry.documentation,
199 _depth: depth,
200 descendant_range: index + 1..index + 1,
201 total_descendant_range: index + 1..index + 1,
202 render: None,
203 next_sibling: None,
204 select_descendant: None,
205 });
206 if let Some(prev_index) = prev_index {
207 tree[prev_index].next_sibling = Some(index);
208 }
209 match entry.item {
210 SettingsUiItem::Group(SettingsUiItemGroup { items: group_items }) => {
211 for group_item in group_items {
212 let prev_index = tree[index]
213 .descendant_range
214 .is_empty()
215 .not()
216 .then_some(tree[index].descendant_range.end - 1);
217 tree[index].descendant_range.end = tree.len() + 1;
218 build_tree_item(tree, group_item, depth + 1, prev_index);
219 tree[index].total_descendant_range.end = tree.len();
220 }
221 }
222 SettingsUiItem::Single(item) => {
223 tree[index].render = Some(item);
224 }
225 SettingsUiItem::Dynamic(SettingsUiItemDynamic {
226 options,
227 determine_option,
228 }) => {
229 tree[index].select_descendant = Some(determine_option);
230 for option in options {
231 let prev_index = tree[index]
232 .descendant_range
233 .is_empty()
234 .not()
235 .then_some(tree[index].descendant_range.end - 1);
236 tree[index].descendant_range.end = tree.len() + 1;
237 build_tree_item(tree, option, depth + 1, prev_index);
238 tree[index].total_descendant_range.end = tree.len();
239 }
240 }
241 SettingsUiItem::None => {
242 return;
243 }
244 }
245}
246
247impl SettingsUiTree {
248 pub fn new(cx: &App) -> Self {
249 let settings_store = SettingsStore::global(cx);
250 let mut tree = vec![];
251 let mut root_entry_indices = vec![];
252 for item in settings_store.settings_ui_items() {
253 if matches!(item.item, SettingsUiItem::None)
254 // todo(settings_ui): How to handle top level single items? BaseKeymap is in this category. Probably need a way to
255 // link them to other groups
256 || matches!(item.item, SettingsUiItem::Single(_))
257 {
258 continue;
259 }
260
261 let prev_root_entry_index = root_entry_indices.last().copied();
262 root_entry_indices.push(tree.len());
263 build_tree_item(&mut tree, item, 0, prev_root_entry_index);
264 }
265
266 root_entry_indices.sort_by_key(|i| tree[*i].title);
267
268 let active_entry_index = root_entry_indices[0];
269 Self {
270 entries: tree,
271 root_entry_indices,
272 active_entry_index,
273 }
274 }
275
276 // todo(settings_ui): Make sure `Item::None` paths are added to the paths tree,
277 // so that we can keep none/skip and still test in CI that all settings have
278 #[cfg(feature = "test-support")]
279 pub fn all_paths(&self, cx: &App) -> Vec<Vec<&'static str>> {
280 fn all_paths_rec(
281 tree: &[UiEntry],
282 paths: &mut Vec<Vec<&'static str>>,
283 current_path: &mut Vec<&'static str>,
284 idx: usize,
285 cx: &App,
286 ) {
287 let child = &tree[idx];
288 let mut pushed_path = false;
289 if let Some(path) = child.path.as_ref() {
290 current_path.push(path);
291 paths.push(current_path.clone());
292 pushed_path = true;
293 }
294 // todo(settings_ui): handle dynamic nodes here
295 let selected_descendant_index = child
296 .select_descendant
297 .map(|select_descendant| {
298 read_settings_value_from_path(
299 SettingsStore::global(cx).raw_default_settings(),
300 ¤t_path,
301 )
302 .map(|value| select_descendant(value, cx))
303 })
304 .and_then(|selected_descendant_index| {
305 selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
306 });
307
308 if let Some(selected_descendant_index) = selected_descendant_index {
309 // just silently fail if we didn't find a setting value for the path
310 if let Some(descendant_index) = selected_descendant_index {
311 all_paths_rec(tree, paths, current_path, descendant_index, cx);
312 }
313 } else if let Some(desc_idx) = child.first_descendant_index() {
314 let mut desc_idx = Some(desc_idx);
315 while let Some(descendant_index) = desc_idx {
316 all_paths_rec(&tree, paths, current_path, descendant_index, cx);
317 desc_idx = tree[descendant_index].next_sibling;
318 }
319 }
320 if pushed_path {
321 current_path.pop();
322 }
323 }
324
325 let mut paths = Vec::new();
326 for &index in &self.root_entry_indices {
327 all_paths_rec(&self.entries, &mut paths, &mut Vec::new(), index, cx);
328 }
329 paths
330 }
331}
332
333fn render_nav(tree: &SettingsUiTree, _window: &mut Window, cx: &mut Context<SettingsPage>) -> Div {
334 let mut nav = v_flex().p_4().gap_2();
335 for &index in &tree.root_entry_indices {
336 nav = nav.child(
337 div()
338 .id(index)
339 .on_click(cx.listener(move |settings, _, _, _| {
340 settings.settings_tree.active_entry_index = index;
341 }))
342 .child(
343 Label::new(SharedString::new_static(tree.entries[index].title))
344 .size(LabelSize::Large)
345 .when(tree.active_entry_index == index, |this| {
346 this.color(Color::Selected)
347 }),
348 ),
349 );
350 }
351 nav
352}
353
354fn render_content(
355 tree: &SettingsUiTree,
356 window: &mut Window,
357 cx: &mut Context<SettingsPage>,
358) -> Div {
359 let content = v_flex().size_full().gap_4();
360
361 let mut path = smallvec::smallvec![];
362
363 fn render_recursive(
364 tree: &SettingsUiTree,
365 index: usize,
366 path: &mut SmallVec<[&'static str; 1]>,
367 mut element: Div,
368 window: &mut Window,
369 cx: &mut App,
370 ) -> Div {
371 let Some(child) = tree.entries.get(index) else {
372 return element.child(
373 Label::new(SharedString::new_static("No settings found")).color(Color::Error),
374 );
375 };
376
377 element =
378 element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large));
379
380 // todo(settings_ui): subgroups?
381 let mut pushed_path = false;
382 if let Some(child_path) = child.path {
383 path.push(child_path);
384 pushed_path = true;
385 }
386 let settings_value = settings_value_from_settings_and_path(
387 path.clone(),
388 child.title,
389 child.documentation,
390 // PERF: how to structure this better? There feels like there's a way to avoid the clone
391 // and every value lookup
392 SettingsStore::global(cx).raw_user_settings(),
393 SettingsStore::global(cx).raw_default_settings(),
394 );
395 if let Some(select_descendant) = child.select_descendant {
396 let selected_descendant = child
397 .nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx));
398 if let Some(descendant_index) = selected_descendant {
399 element = render_recursive(&tree, descendant_index, path, element, window, cx);
400 }
401 }
402 if let Some(child_render) = child.render.as_ref() {
403 element = element.child(div().child(render_item_single(
404 settings_value,
405 child_render,
406 window,
407 cx,
408 )));
409 } else if let Some(child_index) = child.first_descendant_index() {
410 let mut index = Some(child_index);
411 while let Some(sub_child_index) = index {
412 element = render_recursive(tree, sub_child_index, path, element, window, cx);
413 index = tree.entries[sub_child_index].next_sibling;
414 }
415 } else {
416 element =
417 element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
418 }
419
420 if pushed_path {
421 path.pop();
422 }
423 return element;
424 }
425
426 return render_recursive(
427 tree,
428 tree.active_entry_index,
429 &mut path,
430 content,
431 window,
432 cx,
433 );
434}
435
436impl Render for SettingsPage {
437 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
438 let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
439 div()
440 .grid()
441 .grid_cols(16)
442 .p_4()
443 .bg(cx.theme().colors().editor_background)
444 .size_full()
445 .child(
446 div()
447 .id("settings-ui-nav")
448 .col_span(2)
449 .h_full()
450 .child(render_nav(&self.settings_tree, window, cx)),
451 )
452 .child(
453 div().col_span(6).h_full().child(
454 render_content(&self.settings_tree, window, cx)
455 .id("settings-ui-content")
456 .track_scroll(scroll_handle.read(cx))
457 .overflow_y_scroll(),
458 ),
459 )
460 }
461}
462
463// todo(settings_ui): remove, only here as inspiration
464#[allow(dead_code)]
465fn render_old_appearance_settings(cx: &mut App) -> impl IntoElement {
466 v_flex()
467 .p_4()
468 .size_full()
469 .gap_4()
470 .child(Label::new("Settings").size(LabelSize::Large))
471 .child(
472 v_flex().gap_1().child(Label::new("Appearance")).child(
473 v_flex()
474 .elevation_2(cx)
475 .child(AppearanceSettingsControls::new()),
476 ),
477 )
478 .child(
479 v_flex().gap_1().child(Label::new("Editor")).child(
480 v_flex()
481 .elevation_2(cx)
482 .child(EditorSettingsControls::new()),
483 ),
484 )
485}
486
487fn element_id_from_path(path: &[&'static str]) -> ElementId {
488 if path.len() == 0 {
489 panic!("Path length must not be zero");
490 } else if path.len() == 1 {
491 ElementId::Name(SharedString::new_static(path[0]))
492 } else {
493 ElementId::from((
494 ElementId::from(SharedString::new_static(path[path.len() - 2])),
495 SharedString::new_static(path[path.len() - 1]),
496 ))
497 }
498}
499
500fn render_item_single(
501 settings_value: SettingsValue<serde_json::Value>,
502 item: &SettingsUiItemSingle,
503 window: &mut Window,
504 cx: &mut App,
505) -> AnyElement {
506 match item {
507 SettingsUiItemSingle::Custom(_) => div()
508 .child(format!("Item: {}", settings_value.path.join(".")))
509 .into_any_element(),
510 SettingsUiItemSingle::SwitchField => {
511 render_any_item(settings_value, render_switch_field, window, cx)
512 }
513 SettingsUiItemSingle::NumericStepper(num_type) => {
514 render_any_numeric_stepper(settings_value, *num_type, window, cx)
515 }
516 SettingsUiItemSingle::ToggleGroup {
517 variants: values,
518 labels: titles,
519 } => render_toggle_button_group(settings_value, values, titles, window, cx),
520 SettingsUiItemSingle::DropDown { .. } => {
521 unimplemented!("This")
522 }
523 }
524}
525
526pub fn read_settings_value_from_path<'a>(
527 settings_contents: &'a serde_json::Value,
528 path: &[&str],
529) -> Option<&'a serde_json::Value> {
530 // todo(settings_ui) make non recursive, and move to `settings` alongside SettingsValue, and add method to SettingsValue to get nested
531 let Some((key, remaining)) = path.split_first() else {
532 return Some(settings_contents);
533 };
534 let Some(value) = settings_contents.get(key) else {
535 return None;
536 };
537
538 read_settings_value_from_path(value, remaining)
539}
540
541fn downcast_any_item<T: serde::de::DeserializeOwned>(
542 settings_value: SettingsValue<serde_json::Value>,
543) -> SettingsValue<T> {
544 let value = settings_value
545 .value
546 .map(|value| serde_json::from_value::<T>(value).expect("value is not a T"));
547 // todo(settings_ui) Create test that constructs UI tree, and asserts that all elements have default values
548 let default_value = serde_json::from_value::<T>(settings_value.default_value)
549 .with_context(|| format!("path: {:?}", settings_value.path.join(".")))
550 .expect("default value is not an Option<T>");
551 let deserialized_setting_value = SettingsValue {
552 title: settings_value.title,
553 path: settings_value.path,
554 documentation: settings_value.documentation,
555 value,
556 default_value,
557 };
558 deserialized_setting_value
559}
560
561fn render_any_item<T: serde::de::DeserializeOwned>(
562 settings_value: SettingsValue<serde_json::Value>,
563 render_fn: impl Fn(SettingsValue<T>, &mut Window, &mut App) -> AnyElement + 'static,
564 window: &mut Window,
565 cx: &mut App,
566) -> AnyElement {
567 let deserialized_setting_value = downcast_any_item(settings_value);
568 render_fn(deserialized_setting_value, window, cx)
569}
570
571fn render_any_numeric_stepper(
572 settings_value: SettingsValue<serde_json::Value>,
573 num_type: NumType,
574 window: &mut Window,
575 cx: &mut App,
576) -> AnyElement {
577 match num_type {
578 NumType::U64 => render_numeric_stepper::<u64>(
579 downcast_any_item(settings_value),
580 u64::saturating_sub,
581 u64::saturating_add,
582 |n| {
583 serde_json::Number::try_from(n)
584 .context("Failed to convert u64 to serde_json::Number")
585 },
586 window,
587 cx,
588 ),
589 NumType::U32 => render_numeric_stepper::<u32>(
590 downcast_any_item(settings_value),
591 u32::saturating_sub,
592 u32::saturating_add,
593 |n| {
594 serde_json::Number::try_from(n)
595 .context("Failed to convert u32 to serde_json::Number")
596 },
597 window,
598 cx,
599 ),
600 NumType::F32 => render_numeric_stepper::<f32>(
601 downcast_any_item(settings_value),
602 |a, b| a - b,
603 |a, b| a + b,
604 |n| {
605 serde_json::Number::from_f64(n as f64)
606 .context("Failed to convert f32 to serde_json::Number")
607 },
608 window,
609 cx,
610 ),
611 NumType::USIZE => render_numeric_stepper::<usize>(
612 downcast_any_item(settings_value),
613 usize::saturating_sub,
614 usize::saturating_add,
615 |n| {
616 serde_json::Number::try_from(n)
617 .context("Failed to convert usize to serde_json::Number")
618 },
619 window,
620 cx,
621 ),
622 }
623}
624
625fn render_numeric_stepper<
626 T: serde::de::DeserializeOwned + std::fmt::Display + Copy + From<u8> + 'static,
627>(
628 value: SettingsValue<T>,
629 saturating_sub: fn(T, T) -> T,
630 saturating_add: fn(T, T) -> T,
631 to_serde_number: fn(T) -> anyhow::Result<serde_json::Number>,
632 _window: &mut Window,
633 _cx: &mut App,
634) -> AnyElement {
635 let id = element_id_from_path(&value.path);
636 let path = value.path.clone();
637 let num = *value.read();
638
639 NumericStepper::new(
640 id,
641 num.to_string(),
642 {
643 let path = value.path.clone();
644 move |_, _, cx| {
645 let Some(number) = to_serde_number(saturating_sub(num, 1.into())).ok() else {
646 return;
647 };
648 let new_value = serde_json::Value::Number(number);
649 SettingsValue::write_value(&path, new_value, cx);
650 }
651 },
652 move |_, _, cx| {
653 let Some(number) = to_serde_number(saturating_add(num, 1.into())).ok() else {
654 return;
655 };
656
657 let new_value = serde_json::Value::Number(number);
658
659 SettingsValue::write_value(&path, new_value, cx);
660 },
661 )
662 .style(ui::NumericStepperStyle::Outlined)
663 .into_any_element()
664}
665
666fn render_switch_field(
667 value: SettingsValue<bool>,
668 _window: &mut Window,
669 _cx: &mut App,
670) -> AnyElement {
671 let id = element_id_from_path(&value.path);
672 let path = value.path.clone();
673 SwitchField::new(
674 id,
675 SharedString::new_static(value.title),
676 value.documentation.map(SharedString::new_static),
677 match value.read() {
678 true => ToggleState::Selected,
679 false => ToggleState::Unselected,
680 },
681 move |toggle_state, _, cx| {
682 let new_value = serde_json::Value::Bool(match toggle_state {
683 ToggleState::Indeterminate => {
684 return;
685 }
686 ToggleState::Selected => true,
687 ToggleState::Unselected => false,
688 });
689
690 SettingsValue::write_value(&path, new_value, cx);
691 },
692 )
693 .into_any_element()
694}
695
696fn render_toggle_button_group(
697 value: SettingsValue<serde_json::Value>,
698 variants: &'static [&'static str],
699 labels: &'static [&'static str],
700 _: &mut Window,
701 _: &mut App,
702) -> AnyElement {
703 let value = downcast_any_item::<String>(value);
704
705 fn make_toggle_group<const LEN: usize>(
706 group_name: &'static str,
707 value: SettingsValue<String>,
708 variants: &'static [&'static str],
709 labels: &'static [&'static str],
710 ) -> AnyElement {
711 let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN];
712 for i in 0..LEN {
713 variants_array[i] = (variants[i], labels[i]);
714 }
715 let active_value = value.read();
716
717 let selected_idx = variants_array
718 .iter()
719 .enumerate()
720 .find_map(|(idx, (variant, _))| {
721 if variant == &active_value {
722 Some(idx)
723 } else {
724 None
725 }
726 });
727
728 let mut idx = 0;
729 ToggleButtonGroup::single_row(
730 group_name,
731 variants_array.map(|(variant, label)| {
732 let path = value.path.clone();
733 idx += 1;
734 ToggleButtonSimple::new(label, move |_, _, cx| {
735 SettingsValue::write_value(
736 &path,
737 serde_json::Value::String(variant.to_string()),
738 cx,
739 );
740 })
741 }),
742 )
743 .when_some(selected_idx, |this, ix| this.selected_index(ix))
744 .style(ui::ToggleButtonGroupStyle::Filled)
745 .into_any_element()
746 }
747
748 macro_rules! templ_toggl_with_const_param {
749 ($len:expr) => {
750 if variants.len() == $len {
751 return make_toggle_group::<$len>(value.title, value, variants, labels);
752 }
753 };
754 }
755 templ_toggl_with_const_param!(1);
756 templ_toggl_with_const_param!(2);
757 templ_toggl_with_const_param!(3);
758 templ_toggl_with_const_param!(4);
759 templ_toggl_with_const_param!(5);
760 templ_toggl_with_const_param!(6);
761 unreachable!("Too many variants");
762}
763
764fn settings_value_from_settings_and_path(
765 path: SmallVec<[&'static str; 1]>,
766 title: &'static str,
767 documentation: Option<&'static str>,
768 user_settings: &serde_json::Value,
769 default_settings: &serde_json::Value,
770) -> SettingsValue<serde_json::Value> {
771 let default_value = read_settings_value_from_path(default_settings, &path)
772 .with_context(|| format!("No default value for item at path {:?}", path.join(".")))
773 .expect("Default value set for item")
774 .clone();
775
776 let value = read_settings_value_from_path(user_settings, &path).cloned();
777 let settings_value = SettingsValue {
778 default_value,
779 value,
780 documentation,
781 path: path.clone(),
782 // todo(settings_ui) is title required inside SettingsValue?
783 title,
784 };
785 return settings_value;
786}