settings_ui: Handle enums with fields (#37945)
Closes #ISSUE
Adds handling for Enums with fields (i.e. not `enum Foo { Yes, No }`) in
Settings UI. Accomplished by creating default values for each element
with fields (in the derive macro), and rendering a toggle button group
with a button for each variant where switching the active variant sets
the value in the settings JSON to the default for the new active
variant.
Release Notes:
- N/A *or* Added/Fixed/Improved ...
---------
Co-authored-by: Conrad <conrad@zed.dev>
This commit is contained in:
@@ -23,7 +23,7 @@ impl FeatureFlags {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.staff && T::enabled_for_staff() {
|
||||
if (cfg!(debug_assertions) || self.staff) && !*ZED_DISABLE_STAFF && T::enabled_for_staff() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -210,7 +210,10 @@ impl FeatureFlagAppExt for App {
|
||||
fn has_flag<T: FeatureFlag>(&self) -> bool {
|
||||
self.try_global::<FeatureFlags>()
|
||||
.map(|flags| flags.has_flag::<T>())
|
||||
.unwrap_or(T::enabled_for_all())
|
||||
.unwrap_or_else(|| {
|
||||
(cfg!(debug_assertions) && T::enabled_for_staff() && !*ZED_DISABLE_STAFF)
|
||||
|| T::enabled_for_all()
|
||||
})
|
||||
}
|
||||
|
||||
fn is_staff(&self) -> bool {
|
||||
|
||||
@@ -981,11 +981,17 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
}
|
||||
|
||||
/// Controls which formatters should be used when formatting code.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
|
||||
#[serde(untagged)]
|
||||
pub enum FormatterList {
|
||||
Single(Formatter),
|
||||
Vec(Vec<Formatter>),
|
||||
Vec(#[settings_ui(skip)] Vec<Formatter>),
|
||||
}
|
||||
|
||||
impl Default for FormatterList {
|
||||
fn default() -> Self {
|
||||
Self::Single(Formatter::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[Formatter]> for FormatterList {
|
||||
@@ -998,22 +1004,28 @@ impl AsRef<[Formatter]> for FormatterList {
|
||||
}
|
||||
|
||||
/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Formatter {
|
||||
/// Format code using the current language server.
|
||||
LanguageServer { name: Option<String> },
|
||||
LanguageServer {
|
||||
#[settings_ui(skip)]
|
||||
name: Option<String>,
|
||||
},
|
||||
/// Format code using Zed's Prettier integration.
|
||||
#[default]
|
||||
Prettier,
|
||||
/// Format code using an external command.
|
||||
External {
|
||||
/// The external program to run.
|
||||
#[settings_ui(skip)]
|
||||
command: Arc<str>,
|
||||
/// The arguments to pass to the program.
|
||||
#[settings_ui(skip)]
|
||||
arguments: Option<Arc<[String]>>,
|
||||
},
|
||||
/// Files should be formatted using code actions executed by language servers.
|
||||
CodeActions(HashMap<String, bool>),
|
||||
CodeActions(#[settings_ui(skip)] HashMap<String, bool>),
|
||||
}
|
||||
|
||||
/// The settings for indent guides.
|
||||
|
||||
@@ -108,10 +108,19 @@ impl<T: serde::Serialize> SettingsValue<T> {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsUiItemUnion {
|
||||
pub options: Vec<SettingsUiEntry>,
|
||||
/// Must be the same length as `labels` and `options`
|
||||
pub defaults: Box<[serde_json::Value]>,
|
||||
/// Must be the same length as defaults` and `options`
|
||||
pub labels: &'static [&'static str],
|
||||
/// Must be the same length as `defaults` and `labels`
|
||||
pub options: Box<[Option<SettingsUiEntry>]>,
|
||||
pub determine_option: fn(&serde_json::Value, &App) -> usize,
|
||||
}
|
||||
|
||||
// todo(settings_ui): use in ToggleGroup and Dropdown
|
||||
#[derive(Clone)]
|
||||
pub struct SettingsEnumVariants {}
|
||||
|
||||
pub struct SettingsUiEntryMetaData {
|
||||
pub title: SharedString,
|
||||
pub path: SharedString,
|
||||
@@ -136,6 +145,7 @@ pub enum SettingsUiItem {
|
||||
Single(SettingsUiItemSingle),
|
||||
Union(SettingsUiItemUnion),
|
||||
DynamicMap(SettingsUiItemDynamicMap),
|
||||
// Array(SettingsUiItemArray), // code-actions: array of objects, array of string
|
||||
None,
|
||||
}
|
||||
|
||||
|
||||
@@ -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 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>());
|
||||
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);
|
||||
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.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,148 +380,177 @@ 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),
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
} 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,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
index = tree[sub_child_index].next_sibling;
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
},
|
||||
)));
|
||||
// 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((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()),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
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;
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
return element;
|
||||
}
|
||||
|
||||
impl Render for SettingsPage {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let scroll_handle = window.use_state(cx, |_, _| ScrollHandle::new());
|
||||
@@ -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)
|
||||
});
|
||||
|
||||
let (variants, labels): (Vec<_>, Vec<_>) = variants.unzip();
|
||||
|
||||
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),*] })
|
||||
}
|
||||
variants.push(variant_name);
|
||||
labels.push(title);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let options = data_enum.variants.iter().map(|variant| {
|
||||
if variant.fields.is_empty() {
|
||||
return quote! {None};
|
||||
}
|
||||
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! {
|
||||
|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(())
|
||||
})
|
||||
|
||||
@@ -456,7 +456,7 @@ Project panel can be shown/hidden with {#action project_panel::ToggleFocus} ({#k
|
||||
"button": true, // Show/hide the icon in the status bar
|
||||
"dock": "right", // Where to dock: left, right, bottom
|
||||
"default_width": 640, // Default width (left/right docked)
|
||||
"default_height": 320, // Default height (bottom dockeed)
|
||||
"default_height": 320, // Default height (bottom docked)
|
||||
},
|
||||
"agent_font_size": 16
|
||||
```
|
||||
@@ -471,7 +471,7 @@ See [Zed AI Documentation](./ai/overview.md) for additional non-visual AI settin
|
||||
"dock": "bottom", // Where to dock: left, right, bottom
|
||||
"button": true, // Show/hide status bar icon
|
||||
"default_width": 640, // Default width (left/right docked)
|
||||
"default_height": 320, // Default height (bottom dockeed)
|
||||
"default_height": 320, // Default height (bottom docked)
|
||||
|
||||
// Set the cursor blinking behavior in the terminal (on, off, terminal_controlled)
|
||||
"blinking": "terminal_controlled",
|
||||
|
||||
Reference in New Issue
Block a user