settings_ui.rs

  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                        &current_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}