Improve macro to show items

Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
Anthony 2025-08-26 15:36:53 -04:00
parent c1631b6e8c
commit 1fd0f7a46b
5 changed files with 148 additions and 23 deletions

View file

@ -1,13 +1,17 @@
use std::fmt::{Display, Formatter};
use crate::{Settings, SettingsSources, SettingsUI, VsCodeSettings};
use crate as settings;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources, VsCodeSettings};
use settings_ui_macros::SettingsUI;
/// Base key bindings scheme. Base keymaps can be overridden with user keymaps.
///
/// Default: VSCode
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
#[derive(
Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, SettingsUI,
)]
pub enum BaseKeymap {
#[default]
VSCode,
@ -96,8 +100,6 @@ impl BaseKeymap {
}
}
impl SettingsUI for BaseKeymap {}
impl Settings for BaseKeymap {
const KEY: Option<&'static str> = Some("base_keymap");

View file

@ -32,8 +32,8 @@ pub type EditorconfigProperties = ec4rs::Properties;
use crate::{
ActiveSettingsProfileName, ParameterizedJsonSchema, SettingsJsonSchemaParams, SettingsUIItem,
VsCodeSettings, WorktreeId, parse_json_with_comments, settings_ui::SettingsUI,
update_value_in_json_text,
SettingsUIRender, VsCodeSettings, WorktreeId, parse_json_with_comments,
settings_ui::SettingsUI, update_value_in_json_text,
};
/// A value that can be defined as a user setting.
@ -1508,7 +1508,7 @@ impl<T: Settings> AnySettingValue for SettingValue<T> {
}
fn settings_ui_item(&self) -> SettingsUIItem {
T::ui_item()
<T as SettingsUI>::settings_ui_item()
}
}

View file

@ -3,22 +3,23 @@ use std::any::Any;
use gpui::{AnyElement, App, Window};
pub trait SettingsUI {
fn ui_item() -> SettingsUIItem {
SettingsUIItem {
item: SettingsUIItemVariant::None,
}
fn settings_ui_render() -> SettingsUIRender {
SettingsUIRender::None
}
fn settings_ui_item() -> SettingsUIItem;
}
pub struct SettingsUIItem {
// TODO: move this back here once there isn't a None variant
// pub path: &'static str,
// pub title: &'static str,
pub item: SettingsUIItemVariant,
}
pub enum SettingsUIItemVariant {
Group {
path: &'static str,
title: &'static str,
group: SettingsUIItemGroup,
},
Item {
@ -35,9 +36,48 @@ pub struct SettingsUIItemGroup {
pub enum SettingsUIItemSingle {
// TODO: default/builtin variants
SwitchField,
Custom(Box<dyn Fn(&dyn Any, &mut Window, &mut App) -> AnyElement>),
}
pub enum SettingsUIRender {
Group {
title: &'static str,
items: Vec<SettingsUIItem>,
},
Item(SettingsUIItemSingle),
None,
}
impl SettingsUI for bool {
fn settings_ui_render() -> SettingsUIRender {
SettingsUIRender::Item(SettingsUIItemSingle::SwitchField)
}
fn settings_ui_item() -> SettingsUIItem {
SettingsUIItem {
item: SettingsUIItemVariant::None,
}
}
}
/*
#[derive(SettingsUI)]
#[settings_ui(group = "Foo")]
struct Foo {
// #[settings_ui(render = "my_render_function")]
pub toggle: bool,
pub font_size: u32,
Group(vec![Item {path: "toggle", item: SwitchField}])
}
macro code:
settings_ui_item() {
group.items = struct.fields.map((field_name, field_type) => quote! { SettingsUIItem::Item {path: #field_type::settings_ui_path().unwrap_or_else(|| #field_name), item: if field.attrs.render { #render } else field::settings_ui_render()}})
}
*/
/* NOTES:
# Root Group

View file

@ -126,11 +126,33 @@ impl Render for SettingsPage {
.settings_ui_items()
.into_iter()
.flat_map(|item| match item.item {
settings::SettingsUIItemVariant::Group { path, group } => Some(path),
settings::SettingsUIItemVariant::Group { title, path, group } => Some(
div()
.child(Label::new(title).size(LabelSize::Large))
.children(group.items.iter().map(|item| {
match &item.item {
settings::SettingsUIItemVariant::Group {
path,
title,
group,
} => div()
.child(format!("Subgroup: {}", title))
.into_any_element(),
settings::SettingsUIItemVariant::Item { path, item } => {
div()
.child(format!("Item: {}", path))
.into_any_element()
}
settings::SettingsUIItemVariant::None => {
div().child("None").into_any_element()
}
}
})),
),
settings::SettingsUIItemVariant::Item { path, item } => todo!(),
settings::SettingsUIItemVariant::None => None,
})
.map(|group_name| Label::new(group_name).size(LabelSize::Large)),
}),
)
.child(Label::new("Settings").size(LabelSize::Large))
.child(

View file

@ -1,5 +1,5 @@
use proc_macro::TokenStream;
use quote::quote;
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
use syn::{DeriveInput, LitStr, Token, parse_macro_input};
/// Derive macro for the `SettingsUI` marker trait.
@ -22,7 +22,7 @@ use syn::{DeriveInput, LitStr, Token, parse_macro_input};
/// }
/// ```
#[proc_macro_derive(SettingsUI, attributes(settings_ui))]
pub fn derive_settings_ui(input: TokenStream) -> TokenStream {
pub fn derive_settings_ui(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
@ -48,23 +48,84 @@ pub fn derive_settings_ui(input: TokenStream) -> TokenStream {
}
}
let ui_item_fn_body = if let Some(group_name) = group_name {
// let mut root_item = vec![];
// for field in struct {
//
// match field::settings_ui_render()
// Group(items) => {
// let item = items.map(|item| something);
// item
// items.push(item::settings_ui_render());
// root_item.push(Group(items));
// },
// Leaf(item) => {
// root_item.push(item);
// }
// }
// }
//
// group.items = struct.fields.map((field_name, field_type) => quote! { SettingsUIItem::Item {path: #field_type::settings_ui_path().unwrap_or_else(|| #field_name), item: if field.attrs.render { #render } else field::settings_ui_render()}})
// }
fn map_ui_item_to_render(path: &str, ty: TokenStream) -> TokenStream {
quote! {
settings::SettingsUIItem { item: settings::SettingsUIItemVariant::Group{ path: #group_name, group: settings::SettingsUIItemGroup{ items: Default::default() } } }
settings::SettingsUIItem {
item: match #ty::settings_ui_render() {
settings::SettingsUIRender::Group{title, items} => settings::SettingsUIItemVariant::Group {
title,
path: #path,
group: settings::SettingsUIItemGroup { items },
},
settings::SettingsUIRender::Item(item) => settings::SettingsUIItemVariant::Item {
path: #path,
item,
},
settings::SettingsUIRender::None => settings::SettingsUIItemVariant::None,
}
}
}
}
let ui_render_fn_body = if let Some(group_name) = group_name {
let fields = match input.data {
syn::Data::Struct(data_struct) => data_struct
.fields
.iter()
.map(|field| {
(
field.ident.clone().expect("tuple fields").to_string(),
field.ty.to_token_stream(),
)
})
.collect(),
syn::Data::Enum(data_enum) => vec![], // todo! enums
syn::Data::Union(data_union) => unimplemented!("Derive SettingsUI for unions"),
};
let items = fields
.into_iter()
.map(|(name, ty)| map_ui_item_to_render(&name, ty));
quote! {
settings::SettingsUIRender::Group{ title: #group_name, items: vec![#(#items),*] }
}
} else {
quote! {
settings::SettingsUIItem { item: settings::SettingsUIItemVariant::None }
settings::SettingsUIRender::None
}
};
let settings_ui_item_fn_body = map_ui_item_to_render("todo! define path", quote! { Self });
let expanded = quote! {
impl #impl_generics settings::SettingsUI for #name #ty_generics #where_clause {
fn ui_item() -> settings::SettingsUIItem {
#ui_item_fn_body
fn settings_ui_render() -> settings::SettingsUIRender {
#ui_render_fn_body
}
fn settings_ui_item() -> settings::SettingsUIItem {
#settings_ui_item_fn_body
}
}
};
TokenStream::from(expanded)
proc_macro::TokenStream::from(expanded)
}