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