@@ -1,13 +1,14 @@
mod appearance_settings_controls;
-use std::any::TypeId;
-use std::num::NonZeroU32;
-use std::ops::{Not, Range};
+use std::{
+ num::NonZeroU32,
+ ops::{Not, Range},
+ rc::Rc,
+};
use anyhow::Context as _;
-use command_palette_hooks::CommandPaletteFilter;
use editor::{Editor, EditorSettingsControls};
-use feature_flags::{FeatureFlag, FeatureFlagViewExt};
+use feature_flags::{FeatureFlag, FeatureFlagAppExt};
use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
use settings::{
NumType, SettingsStore, SettingsUiEntry, SettingsUiEntryMetaData, SettingsUiItem,
@@ -19,7 +20,6 @@ use ui::{NumericStepper, SwitchField, ToggleButtonGroup, ToggleButtonSimple, pre
use workspace::{
Workspace,
item::{Item, ItemEvent},
- with_active_or_new_workspace,
};
use crate::appearance_settings_controls::AppearanceSettingsControls;
@@ -38,50 +38,45 @@ actions!(
]
);
+pub fn open_settings_editor(
+ workspace: &mut Workspace,
+ _: &OpenSettingsEditor,
+ window: &mut Window,
+ cx: &mut Context<Workspace>,
+) {
+ // todo(settings_ui) open in a local workspace if this is remote.
+ let existing = workspace
+ .active_pane()
+ .read(cx)
+ .items()
+ .find_map(|item| item.downcast::<SettingsPage>());
+
+ if let Some(existing) = existing {
+ workspace.activate_item(&existing, true, true, window, cx);
+ } else {
+ let settings_page = SettingsPage::new(workspace, cx);
+ workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
+ }
+}
+
pub fn init(cx: &mut App) {
- cx.on_action(|_: &OpenSettingsEditor, cx| {
- with_active_or_new_workspace(cx, move |workspace, window, cx| {
- let existing = workspace
- .active_pane()
- .read(cx)
- .items()
- .find_map(|item| item.downcast::<SettingsPage>());
-
- if let Some(existing) = existing {
- workspace.activate_item(&existing, true, true, window, cx);
+ cx.observe_new(|workspace: &mut Workspace, _, _| {
+ workspace.register_action_renderer(|div, _, _, cx| {
+ let settings_ui_actions = [std::any::TypeId::of::<OpenSettingsEditor>()];
+ let has_flag = cx.has_flag::<SettingsUiFeatureFlag>();
+ command_palette_hooks::CommandPaletteFilter::update_global(cx, |filter, _| {
+ if has_flag {
+ filter.show_action_types(&settings_ui_actions);
+ } else {
+ filter.hide_action_types(&settings_ui_actions);
+ }
+ });
+ if has_flag {
+ div.on_action(cx.listener(open_settings_editor))
} else {
- let settings_page = SettingsPage::new(workspace, cx);
- workspace.add_item_to_active_pane(Box::new(settings_page), None, true, window, cx)
+ div
}
});
- });
-
- cx.observe_new(|_workspace: &mut Workspace, window, cx| {
- let Some(window) = window else {
- return;
- };
-
- let settings_ui_actions = [TypeId::of::<OpenSettingsEditor>()];
-
- CommandPaletteFilter::update_global(cx, |filter, _cx| {
- filter.hide_action_types(&settings_ui_actions);
- });
-
- cx.observe_flag::<SettingsUiFeatureFlag, _>(
- window,
- move |is_enabled, _workspace, _, cx| {
- if is_enabled {
- CommandPaletteFilter::update_global(cx, |filter, _cx| {
- filter.show_action_types(&settings_ui_actions);
- });
- } else {
- CommandPaletteFilter::update_global(cx, |filter, _cx| {
- filter.hide_action_types(&settings_ui_actions);
- });
- }
- },
- )
- .detach();
})
.detach();
}
@@ -153,9 +148,7 @@ struct UiEntry {
next_sibling: Option<usize>,
// expanded: bool,
render: Option<SettingsUiItemSingle>,
- /// For dynamic items this is a way to select a value from a list of values
- /// this is always none for non-dynamic items
- select_descendant: Option<fn(&serde_json::Value, &App) -> usize>,
+ dynamic_render: Option<SettingsUiItemUnion>,
generate_items: Option<(
SettingsUiItem,
fn(&serde_json::Value, &App) -> Vec<SettingsUiEntryMetaData>,
@@ -198,6 +191,7 @@ fn build_tree_item(
depth: usize,
prev_index: Option<usize>,
) {
+ // let tree: HashMap<Path, UiEntry>;
let index = tree.len();
tree.push(UiEntry {
title: entry.title.into(),
@@ -208,7 +202,7 @@ fn build_tree_item(
total_descendant_range: index + 1..index + 1,
render: None,
next_sibling: None,
- select_descendant: None,
+ dynamic_render: None,
generate_items: None,
});
if let Some(prev_index) = prev_index {
@@ -230,12 +224,14 @@ fn build_tree_item(
SettingsUiItem::Single(item) => {
tree[index].render = Some(item);
}
- SettingsUiItem::Union(SettingsUiItemUnion {
- options,
- determine_option,
- }) => {
- tree[index].select_descendant = Some(determine_option);
+ SettingsUiItem::Union(dynamic_render) => {
+ // todo(settings_ui) take from item and store other fields instead of clone
+ // will also require replacing usage in render_recursive so it can know
+ // which options were actually rendered
+ let options = dynamic_render.options.clone();
+ tree[index].dynamic_render = Some(dynamic_render);
for option in options {
+ let Some(option) = option else { continue };
let prev_index = tree[index]
.descendant_range
.is_empty()
@@ -316,13 +312,14 @@ impl SettingsUiTree {
}
// todo(settings_ui): handle dynamic nodes here
let selected_descendant_index = child
- .select_descendant
- .map(|select_descendant| {
+ .dynamic_render
+ .as_ref()
+ .map(|dynamic_render| {
read_settings_value_from_path(
SettingsStore::global(cx).raw_default_settings(),
¤t_path,
)
- .map(|value| select_descendant(value, cx))
+ .map(|value| (dynamic_render.determine_option)(value, cx))
})
.and_then(|selected_descendant_index| {
selected_descendant_index.map(|index| child.nth_descendant_index(tree, index))
@@ -383,146 +380,175 @@ fn render_content(
let mut path = smallvec::smallvec![];
- fn render_recursive(
- tree: &[UiEntry],
- index: usize,
- path: &mut SmallVec<[SharedString; 1]>,
- mut element: Div,
- // todo(settings_ui): can this be a ref without cx borrow issues?
- fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
- window: &mut Window,
- cx: &mut App,
- ) -> Div {
- let Some(child) = tree.get(index) else {
- return element.child(
- Label::new(SharedString::new_static("No settings found")).color(Color::Error),
- );
- };
+ return render_recursive(
+ &tree.entries,
+ tree.active_entry_index,
+ &mut path,
+ content,
+ &mut None,
+ true,
+ window,
+ cx,
+ );
+}
+fn render_recursive(
+ tree: &[UiEntry],
+ index: usize,
+ path: &mut SmallVec<[SharedString; 1]>,
+ mut element: Div,
+ fallback_path: &mut Option<SmallVec<[SharedString; 1]>>,
+ render_next_title: bool,
+ window: &mut Window,
+ cx: &mut App,
+) -> Div {
+ let Some(child) = tree.get(index) else {
+ return element
+ .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
+ };
+
+ if render_next_title {
element = element.child(Label::new(child.title.clone()).size(LabelSize::Large));
+ }
- // todo(settings_ui): subgroups?
- let mut pushed_path = false;
- if let Some(child_path) = child.path.as_ref() {
- path.push(child_path.clone());
- if let Some(fallback_path) = fallback_path.as_mut() {
- fallback_path.push(child_path.clone());
- }
- pushed_path = true;
+ // todo(settings_ui): subgroups?
+ let mut pushed_path = false;
+ if let Some(child_path) = child.path.as_ref() {
+ path.push(child_path.clone());
+ if let Some(fallback_path) = fallback_path.as_mut() {
+ fallback_path.push(child_path.clone());
}
- // let fallback_path_copy = fallback_path.cloned();
- let settings_value = settings_value_from_settings_and_path(
- path.clone(),
- fallback_path.as_ref().map(|path| path.as_slice()),
- child.title.clone(),
- child.documentation.clone(),
- // PERF: how to structure this better? There feels like there's a way to avoid the clone
- // and every value lookup
- SettingsStore::global(cx).raw_user_settings(),
- SettingsStore::global(cx).raw_default_settings(),
- );
- if let Some(select_descendant) = child.select_descendant {
- let selected_descendant =
- child.nth_descendant_index(tree, select_descendant(settings_value.read(), cx));
- if let Some(descendant_index) = selected_descendant {
- element = render_recursive(
- tree,
- descendant_index,
- path,
- element,
- fallback_path,
- window,
- cx,
- );
- }
- } else if let Some((settings_ui_item, generate_items, defaults_path)) =
- child.generate_items.as_ref()
- {
- let generated_items = generate_items(settings_value.read(), cx);
- let mut ui_items = Vec::with_capacity(generated_items.len());
- for item in generated_items {
- let settings_ui_entry = SettingsUiEntry {
- path: None,
- title: "",
- documentation: None,
- item: settings_ui_item.clone(),
- };
- let prev_index = if ui_items.is_empty() {
- None
- } else {
- Some(ui_items.len() - 1)
- };
- let item_index = ui_items.len();
- build_tree_item(
- &mut ui_items,
- settings_ui_entry,
- child._depth + 1,
- prev_index,
- );
- if item_index < ui_items.len() {
- ui_items[item_index].path = None;
- ui_items[item_index].title = item.title.clone();
- ui_items[item_index].documentation = item.documentation.clone();
-
- // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
- // when we recurse
- path.push(item.path.clone());
- element = render_recursive(
- &ui_items,
- item_index,
- path,
- element,
- &mut Some(defaults_path.clone()),
- window,
- cx,
- );
- path.pop();
+ pushed_path = true;
+ }
+ let settings_value = settings_value_from_settings_and_path(
+ path.clone(),
+ fallback_path.as_ref().map(|path| path.as_slice()),
+ child.title.clone(),
+ child.documentation.clone(),
+ // PERF: how to structure this better? There feels like there's a way to avoid the clone
+ // and every value lookup
+ SettingsStore::global(cx).raw_user_settings(),
+ SettingsStore::global(cx).raw_default_settings(),
+ );
+ if let Some(dynamic_render) = child.dynamic_render.as_ref() {
+ let value = settings_value.read();
+ let selected_index = (dynamic_render.determine_option)(value, cx);
+ element = element.child(div().child(render_toggle_button_group_inner(
+ settings_value.title.clone(),
+ dynamic_render.labels,
+ Some(selected_index),
+ {
+ let path = settings_value.path.clone();
+ let defaults = dynamic_render.defaults.clone();
+ move |idx, cx| {
+ if idx == selected_index {
+ return;
+ }
+ let default = defaults.get(idx).cloned().unwrap_or_default();
+ SettingsValue::write_value(&path, default, cx);
}
- }
- } else if let Some(child_render) = child.render.as_ref() {
- element = element.child(div().child(render_item_single(
- settings_value,
- child_render,
+ },
+ )));
+ // we don't add descendants for unit options, so we adjust the selected index
+ // by the number of options we didn't add descendants for, to get the descendant index
+ let selected_descendant_index = selected_index
+ - dynamic_render.options[..selected_index]
+ .iter()
+ .filter(|option| option.is_none())
+ .count();
+ if dynamic_render.options[selected_index].is_some()
+ && let Some(descendant_index) =
+ child.nth_descendant_index(tree, selected_descendant_index)
+ {
+ element = render_recursive(
+ tree,
+ descendant_index,
+ path,
+ element,
+ fallback_path,
+ false,
window,
cx,
- )));
- } else if let Some(child_index) = child.first_descendant_index() {
- let mut index = Some(child_index);
- while let Some(sub_child_index) = index {
+ );
+ }
+ } else if let Some((settings_ui_item, generate_items, defaults_path)) =
+ child.generate_items.as_ref()
+ {
+ let generated_items = generate_items(settings_value.read(), cx);
+ let mut ui_items = Vec::with_capacity(generated_items.len());
+ for item in generated_items {
+ let settings_ui_entry = SettingsUiEntry {
+ path: None,
+ title: "",
+ documentation: None,
+ item: settings_ui_item.clone(),
+ };
+ let prev_index = if ui_items.is_empty() {
+ None
+ } else {
+ Some(ui_items.len() - 1)
+ };
+ let item_index = ui_items.len();
+ build_tree_item(
+ &mut ui_items,
+ settings_ui_entry,
+ child._depth + 1,
+ prev_index,
+ );
+ if item_index < ui_items.len() {
+ ui_items[item_index].path = None;
+ ui_items[item_index].title = item.title.clone();
+ ui_items[item_index].documentation = item.documentation.clone();
+
+ // push path instead of setting path on ui item so that the path isn't pushed to default_path as well
+ // when we recurse
+ path.push(item.path.clone());
element = render_recursive(
- tree,
- sub_child_index,
+ &ui_items,
+ item_index,
path,
element,
- fallback_path,
+ &mut Some(defaults_path.clone()),
+ true,
window,
cx,
);
- index = tree[sub_child_index].next_sibling;
+ path.pop();
}
- } else {
- element =
- element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
}
-
- if pushed_path {
- path.pop();
- if let Some(fallback_path) = fallback_path.as_mut() {
- fallback_path.pop();
- }
+ } else if let Some(child_render) = child.render.as_ref() {
+ element = element.child(div().child(render_item_single(
+ settings_value,
+ child_render,
+ window,
+ cx,
+ )));
+ } else if let Some(child_index) = child.first_descendant_index() {
+ let mut index = Some(child_index);
+ while let Some(sub_child_index) = index {
+ element = render_recursive(
+ tree,
+ sub_child_index,
+ path,
+ element,
+ fallback_path,
+ true,
+ window,
+ cx,
+ );
+ index = tree[sub_child_index].next_sibling;
}
- return element;
+ } else {
+ element = element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
}
- return render_recursive(
- &tree.entries,
- tree.active_entry_index,
- &mut path,
- content,
- &mut None,
- window,
- cx,
- );
+ if pushed_path {
+ path.pop();
+ if let Some(fallback_path) = fallback_path.as_mut() {
+ fallback_path.pop();
+ }
+ }
+ return element;
}
impl Render for SettingsPage {
@@ -855,41 +881,47 @@ fn render_toggle_button_group(
_: &mut App,
) -> AnyElement {
let value = downcast_any_item::<String>(value);
+ let active_value = value.read();
+ let selected_idx = variants.iter().position(|v| v == &active_value);
+
+ return render_toggle_button_group_inner(value.title, labels, selected_idx, {
+ let path = value.path.clone();
+ move |variant_index, cx| {
+ SettingsValue::write_value(
+ &path,
+ serde_json::Value::String(variants[variant_index].to_string()),
+ cx,
+ );
+ }
+ });
+}
+fn render_toggle_button_group_inner(
+ title: SharedString,
+ labels: &'static [&'static str],
+ selected_idx: Option<usize>,
+ on_write: impl Fn(usize, &mut App) + 'static,
+) -> AnyElement {
fn make_toggle_group<const LEN: usize>(
- value: SettingsValue<String>,
- variants: &'static [&'static str],
+ title: SharedString,
+ selected_idx: Option<usize>,
+ on_write: Rc<dyn Fn(usize, &mut App)>,
labels: &'static [&'static str],
) -> AnyElement {
- let mut variants_array: [(&'static str, &'static str); LEN] = [("unused", "unused"); LEN];
- for i in 0..LEN {
- variants_array[i] = (variants[i], labels[i]);
- }
- let active_value = value.read();
-
- let selected_idx = variants_array
- .iter()
- .enumerate()
- .find_map(|(idx, (variant, _))| {
- if variant == &active_value {
- Some(idx)
- } else {
- None
- }
- });
+ let labels_array: [&'static str; LEN] = {
+ let mut arr = ["unused"; LEN];
+ arr.copy_from_slice(labels);
+ arr
+ };
let mut idx = 0;
ToggleButtonGroup::single_row(
- value.title.clone(),
- variants_array.map(|(variant, label)| {
- let path = value.path.clone();
+ title,
+ labels_array.map(|label| {
idx += 1;
+ let on_write = on_write.clone();
ToggleButtonSimple::new(label, move |_, _, cx| {
- SettingsValue::write_value(
- &path,
- serde_json::Value::String(variant.to_string()),
- cx,
- );
+ on_write(idx - 1, cx);
})
}),
)
@@ -898,10 +930,12 @@ fn render_toggle_button_group(
.into_any_element()
}
+ let on_write = Rc::new(on_write);
+
macro_rules! templ_toggl_with_const_param {
($len:expr) => {
- if variants.len() == $len {
- return make_toggle_group::<$len>(value, variants, labels);
+ if labels.len() == $len {
+ return make_toggle_group::<$len>(title.clone(), selected_idx, on_write, labels);
}
};
}
@@ -1,5 +1,3 @@
-use std::ops::Not;
-
use heck::{ToSnakeCase as _, ToTitleCase as _};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
@@ -67,7 +65,7 @@ pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenSt
let doc_str = parse_documentation_from_attrs(&input.attrs);
- let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), path_name.as_ref(), &input);
+ let ui_item_fn_body = generate_ui_item_body(group_name.as_ref(), &input);
// todo(settings_ui): make group name optional, repurpose group as tag indicating item is group, and have "title" tag for custom title
let title = group_name.unwrap_or(input.ident.to_string().to_title_case());
@@ -126,109 +124,221 @@ fn map_ui_item_to_entry(
doc_str: Option<&str>,
ty: TokenStream,
) -> TokenStream {
- let ty = extract_type_from_option(ty);
// todo(settings_ui): does quote! just work with options?
let path = path.map_or_else(|| quote! {None}, |path| quote! {Some(#path)});
let doc_str = doc_str.map_or_else(|| quote! {None}, |doc_str| quote! {Some(#doc_str)});
+ let item = ui_item_from_type(ty);
quote! {
settings::SettingsUiEntry {
title: #title,
path: #path,
- item: #ty::settings_ui_item(),
+ item: #item,
documentation: #doc_str,
}
}
}
-fn generate_ui_item_body(
- group_name: Option<&String>,
- path_name: Option<&String>,
- input: &syn::DeriveInput,
+fn ui_item_from_type(ty: TokenStream) -> TokenStream {
+ let ty = extract_type_from_option(ty);
+ return trait_method_call(ty, quote! {settings::SettingsUi}, quote! {settings_ui_item});
+}
+
+fn trait_method_call(
+ ty: TokenStream,
+ trait_name: TokenStream,
+ method_name: TokenStream,
) -> TokenStream {
- match (group_name, path_name, &input.data) {
- (_, _, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"),
- (None, _, Data::Struct(_)) => quote! {
+ // doing the <ty as settings::SettingsUi> makes the error message better:
+ // -> "#ty Doesn't implement settings::SettingsUi" instead of "no item "settings_ui_item" for #ty"
+ // and ensures safety against name conflicts
+ //
+ // todo(settings_ui): Turn `Vec<T>` into `Vec::<T>` here as well
+ quote! {
+ <#ty as #trait_name>::#method_name()
+ }
+}
+
+fn generate_ui_item_body(group_name: Option<&String>, input: &syn::DeriveInput) -> TokenStream {
+ match (group_name, &input.data) {
+ (_, Data::Union(_)) => unimplemented!("Derive SettingsUi for Unions"),
+ (None, Data::Struct(_)) => quote! {
settings::SettingsUiItem::None
},
- (Some(_), _, Data::Struct(data_struct)) => {
- let struct_serde_attrs = parse_serde_attributes(&input.attrs);
- let fields = data_struct
- .fields
- .iter()
- .filter(|field| {
- !field.attrs.iter().any(|attr| {
- let mut has_skip = false;
- if attr.path().is_ident("settings_ui") {
- let _ = attr.parse_nested_meta(|meta| {
- if meta.path.is_ident("skip") {
- has_skip = true;
- }
- Ok(())
- });
- }
-
- has_skip
- })
- })
- .map(|field| {
- let field_serde_attrs = parse_serde_attributes(&field.attrs);
- let name = field.ident.clone().expect("tuple fields").to_string();
- let doc_str = parse_documentation_from_attrs(&field.attrs);
-
- (
- name.to_title_case(),
- doc_str,
- field_serde_attrs.flatten.not().then(|| {
- struct_serde_attrs.apply_rename_to_field(&field_serde_attrs, &name)
- }),
- field.ty.to_token_stream(),
- )
- })
- // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr
- .map(|(title, doc_str, path, ty)| {
- map_ui_item_to_entry(path.as_deref(), &title, doc_str.as_deref(), ty)
- });
-
- quote! {
- settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#fields),*] })
- }
+ (Some(_), Data::Struct(data_struct)) => {
+ let parent_serde_attrs = parse_serde_attributes(&input.attrs);
+ item_group_from_fields(&data_struct.fields, &parent_serde_attrs)
}
- (None, _, Data::Enum(data_enum)) => {
+ (None, Data::Enum(data_enum)) => {
let serde_attrs = parse_serde_attributes(&input.attrs);
let length = data_enum.variants.len();
- let variants = data_enum.variants.iter().map(|variant| {
- let string = variant.ident.clone().to_string();
+ let mut variants = Vec::with_capacity(length);
+ let mut labels = Vec::with_capacity(length);
- let title = string.to_title_case();
- let string = serde_attrs.rename_all.apply(&string);
+ for variant in &data_enum.variants {
+ // todo(settings_ui): Can #[serde(rename = )] be on enum variants?
+ let ident = variant.ident.clone().to_string();
+ let variant_name = serde_attrs.rename_all.apply(&ident);
+ let title = variant_name.to_title_case();
- (string, title)
- });
+ variants.push(variant_name);
+ labels.push(title);
+ }
- let (variants, labels): (Vec<_>, Vec<_>) = variants.unzip();
+ let is_not_union = data_enum.variants.iter().all(|v| v.fields.is_empty());
+ if is_not_union {
+ return if length > 6 {
+ quote! {
+ settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+ }
+ } else {
+ quote! {
+ settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+ }
+ };
+ }
+ // else: Union!
+ let enum_name = &input.ident;
- if length > 6 {
- quote! {
- settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::DropDown{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+ let options = data_enum.variants.iter().map(|variant| {
+ if variant.fields.is_empty() {
+ return quote! {None};
}
- } else {
+ let name = &variant.ident;
+ let item = item_group_from_fields(&variant.fields, &serde_attrs);
+ // todo(settings_ui): documentation
+ return quote! {
+ Some(settings::SettingsUiEntry {
+ path: None,
+ title: stringify!(#name),
+ documentation: None,
+ item: #item,
+ })
+ };
+ });
+ let defaults = data_enum.variants.iter().map(|variant| {
+ let variant_name = &variant.ident;
+ if variant.fields.is_empty() {
+ quote! {
+ serde_json::to_value(#enum_name::#variant_name).expect("Failed to serialize default value for #enum_name::#variant_name")
+ }
+ } else {
+ let fields = variant.fields.iter().enumerate().map(|(index, field)| {
+ let field_name = field.ident.as_ref().map_or_else(|| syn::Index::from(index).into_token_stream(), |ident| ident.to_token_stream());
+ let field_type_is_option = option_inner_type(field.ty.to_token_stream()).is_some();
+ let field_default = if field_type_is_option {
+ quote! {
+ None
+ }
+ } else {
+ quote! {
+ ::std::default::Default::default()
+ }
+ };
+
+ quote!{
+ #field_name: #field_default
+ }
+ });
+ quote! {
+ serde_json::to_value(#enum_name::#variant_name {
+ #(#fields),*
+ }).expect("Failed to serialize default value for #enum_name::#variant_name")
+ }
+ }
+ });
+ // todo(settings_ui): Identify #[default] attr and use it for index, defaulting to 0
+ let default_variant_index: usize = 0;
+ let determine_option_fn = {
+ let match_arms = data_enum
+ .variants
+ .iter()
+ .enumerate()
+ .map(|(index, variant)| {
+ let variant_name = &variant.ident;
+ quote! {
+ Ok(#variant_name {..}) => #index
+ }
+ });
quote! {
- settings::SettingsUiItem::Single(settings::SettingsUiItemSingle::ToggleGroup{ variants: &[#(#variants),*], labels: &[#(#labels),*] })
+ |value: &serde_json::Value, _cx: &gpui::App| -> usize {
+ use #enum_name::*;
+ match serde_json::from_value::<#enum_name>(value.clone()) {
+ #(#match_arms),*,
+ Err(_) => #default_variant_index,
+ }
+ }
}
- }
+ };
+ // todo(settings_ui) should probably always use toggle group for unions, dropdown makes less sense
+ return quote! {
+ settings::SettingsUiItem::Union(settings::SettingsUiItemUnion {
+ defaults: Box::new([#(#defaults),*]),
+ labels: &[#(#labels),*],
+ options: Box::new([#(#options),*]),
+ determine_option: #determine_option_fn,
+ })
+ };
+ // panic!("Unhandled");
}
// todo(settings_ui) discriminated unions
- (_, _, Data::Enum(_)) => quote! {
+ (_, Data::Enum(_)) => quote! {
settings::SettingsUiItem::None
},
}
}
+fn item_group_from_fields(fields: &syn::Fields, parent_serde_attrs: &SerdeOptions) -> TokenStream {
+ let group_items = fields
+ .iter()
+ .filter(|field| {
+ !field.attrs.iter().any(|attr| {
+ let mut has_skip = false;
+ if attr.path().is_ident("settings_ui") {
+ let _ = attr.parse_nested_meta(|meta| {
+ if meta.path.is_ident("skip") {
+ has_skip = true;
+ }
+ Ok(())
+ });
+ }
+
+ has_skip
+ })
+ })
+ .map(|field| {
+ let field_serde_attrs = parse_serde_attributes(&field.attrs);
+ let name = field.ident.as_ref().map(ToString::to_string);
+ let title = name.as_ref().map_or_else(
+ || "todo(settings_ui): Titles for tuple fields".to_string(),
+ |name| name.to_title_case(),
+ );
+ let doc_str = parse_documentation_from_attrs(&field.attrs);
+
+ (
+ title,
+ doc_str,
+ name.filter(|_| !field_serde_attrs.flatten).map(|name| {
+ parent_serde_attrs.apply_rename_to_field(&field_serde_attrs, &name)
+ }),
+ field.ty.to_token_stream(),
+ )
+ })
+ // todo(settings_ui): Re-format field name as nice title, and support setting different title with attr
+ .map(|(title, doc_str, path, ty)| {
+ map_ui_item_to_entry(path.as_deref(), &title, doc_str.as_deref(), ty)
+ });
+
+ quote! {
+ settings::SettingsUiItem::Group(settings::SettingsUiItemGroup{ items: vec![#(#group_items),*] })
+ }
+}
+
struct SerdeOptions {
rename_all: SerdeRenameAll,
rename: Option<String>,
flatten: bool,
+ untagged: bool,
_alias: Option<String>, // todo(settings_ui)
}
@@ -264,6 +374,7 @@ fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions {
rename_all: SerdeRenameAll::None,
rename: None,
flatten: false,
+ untagged: false,
_alias: None,
};
@@ -296,6 +407,8 @@ fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions {
meta.input.parse::<Token![=]>()?;
let lit = meta.input.parse::<LitStr>()?.value();
options.rename = Some(lit);
+ } else if meta.path.is_ident("untagged") {
+ options.untagged = true;
}
Ok(())
})