Detailed changes
@@ -748,6 +748,7 @@ pub struct ScrollbarAxesContent {
#[derive(
Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi,
)]
+#[settings_ui(group = "Gutter")]
pub struct GutterContent {
/// Whether to show line numbers in the gutter.
///
@@ -208,7 +208,9 @@ impl LanguageSettings {
}
/// The provider that supplies edit predictions.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionProvider {
None,
@@ -231,13 +233,14 @@ impl EditPredictionProvider {
/// The settings for edit predictions, such as [GitHub Copilot](https://github.com/features/copilot)
/// or [Supermaven](https://supermaven.com).
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, SettingsUi)]
pub struct EditPredictionSettings {
/// The provider that supplies edit predictions.
pub provider: EditPredictionProvider,
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
+ #[settings_ui(skip)]
pub disabled_globs: Vec<DisabledGlob>,
/// Configures how edit predictions are displayed in the buffer.
pub mode: EditPredictionsMode,
@@ -269,7 +272,9 @@ pub struct DisabledGlob {
}
/// The mode in which edit predictions should be displayed.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
#[serde(rename_all = "snake_case")]
pub enum EditPredictionsMode {
/// If provider supports it, display inline when holding modifier key (e.g., alt).
@@ -282,13 +287,15 @@ pub enum EditPredictionsMode {
Eager,
}
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Debug, Default, SettingsUi)]
pub struct CopilotSettings {
/// HTTP/HTTPS proxy to use for Copilot.
+ #[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for proxy (not recommended).
pub proxy_no_verify: Option<bool>,
/// Enterprise URI for Copilot.
+ #[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
@@ -297,6 +304,7 @@ pub struct CopilotSettings {
Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey,
)]
#[settings_key(None)]
+#[settings_ui(group = "Default Language Settings")]
pub struct AllLanguageSettingsContent {
/// The settings for enabling/disabling features.
#[serde(default)]
@@ -309,10 +317,12 @@ pub struct AllLanguageSettingsContent {
pub defaults: LanguageSettingsContent,
/// The settings for individual languages.
#[serde(default)]
+ #[settings_ui(skip)]
pub languages: LanguageToSettingsMap,
/// Settings for associating file extensions and filenames
/// with languages.
#[serde(default)]
+ #[settings_ui(skip)]
pub file_types: HashMap<Arc<str>, Vec<String>>,
}
@@ -345,7 +355,7 @@ inventory::submit! {
}
/// Controls how completions are processed for this language.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub struct CompletionSettings {
/// Controls how words are completed.
@@ -420,7 +430,7 @@ fn default_3() -> usize {
}
/// The settings for a particular language.
-#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, SettingsUi)]
pub struct LanguageSettingsContent {
/// How many columns a tab should occupy.
///
@@ -617,12 +627,13 @@ pub enum RewrapBehavior {
}
/// The contents of the edit prediction settings.
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
pub struct EditPredictionSettingsContent {
/// A list of globs representing files that edit predictions should be disabled for.
/// This list adds to a pre-existing, sensible default set of globs.
/// Any additional ones you add are combined with them.
#[serde(default)]
+ #[settings_ui(skip)]
pub disabled_globs: Option<Vec<String>>,
/// The mode used to display edit predictions in the buffer.
/// Provider support required.
@@ -637,12 +648,13 @@ pub struct EditPredictionSettingsContent {
pub enabled_in_text_threads: bool,
}
-#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq)]
+#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, SettingsUi)]
pub struct CopilotSettingsContent {
/// HTTP/HTTPS proxy to use for Copilot.
///
/// Default: none
#[serde(default)]
+ #[settings_ui(skip)]
pub proxy: Option<String>,
/// Disable certificate verification for the proxy (not recommended).
///
@@ -653,19 +665,21 @@ pub struct CopilotSettingsContent {
///
/// Default: none
#[serde(default)]
+ #[settings_ui(skip)]
pub enterprise_uri: Option<String>,
}
/// The settings for enabling/disabling features.
-#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema)]
+#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
+#[settings_ui(group = "Features")]
pub struct FeaturesContent {
/// Determines which edit prediction provider to use.
pub edit_prediction_provider: Option<EditPredictionProvider>,
}
/// Controls the soft-wrapping behavior in the editor.
-#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
+#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, SettingsUi)]
#[serde(rename_all = "snake_case")]
pub enum SoftWrap {
/// Prefer a single line generally, unless an overly long line is encountered.
@@ -934,7 +948,9 @@ pub enum Formatter {
}
/// The settings for indent guides.
-#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
+#[derive(
+ Default, Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, SettingsUi,
+)]
pub struct IndentGuideSettings {
/// Whether to display indent guides in the editor.
///
@@ -19,6 +19,7 @@ pub trait SettingsUi {
path: None,
title: "None entry",
item: SettingsUiItem::None,
+ documentation: None,
}
}
}
@@ -29,6 +30,8 @@ pub struct SettingsUiEntry {
pub path: Option<&'static str>,
/// What is displayed for the text for this entry
pub title: &'static str,
+ /// documentation for this entry. Constructed from the documentation comment above the struct or field
+ pub documentation: Option<&'static str>,
pub item: SettingsUiItem,
}
@@ -54,6 +57,7 @@ pub enum SettingsUiItemSingle {
pub struct SettingsValue<T> {
pub title: &'static str,
+ pub documentation: Option<&'static str>,
pub path: SmallVec<[&'static str; 1]>,
pub value: Option<T>,
pub default_value: T,
@@ -128,7 +132,9 @@ pub enum NumType {
U64 = 0,
U32 = 1,
F32 = 2,
+ USIZE = 3,
}
+
pub static NUM_TYPE_NAMES: std::sync::LazyLock<[&'static str; NumType::COUNT]> =
std::sync::LazyLock::new(|| NumType::ALL.map(NumType::type_name));
pub static NUM_TYPE_IDS: std::sync::LazyLock<[TypeId; NumType::COUNT]> =
@@ -143,6 +149,7 @@ impl NumType {
NumType::U64 => TypeId::of::<u64>(),
NumType::U32 => TypeId::of::<u32>(),
NumType::F32 => TypeId::of::<f32>(),
+ NumType::USIZE => TypeId::of::<usize>(),
}
}
@@ -151,6 +158,7 @@ impl NumType {
NumType::U64 => std::any::type_name::<u64>(),
NumType::U32 => std::any::type_name::<u32>(),
NumType::F32 => std::any::type_name::<f32>(),
+ NumType::USIZE => std::any::type_name::<usize>(),
}
}
}
@@ -175,3 +183,4 @@ numeric_stepper_for_num_type!(u64, U64);
numeric_stepper_for_num_type!(u32, U32);
// todo(settings_ui) is there a better ui for f32?
numeric_stepper_for_num_type!(f32, F32);
+numeric_stepper_for_num_type!(usize, USIZE);
@@ -1,14 +1,13 @@
mod appearance_settings_controls;
use std::any::TypeId;
-use std::collections::VecDeque;
use std::ops::{Not, Range};
use anyhow::Context as _;
use command_palette_hooks::CommandPaletteFilter;
use editor::EditorSettingsControls;
use feature_flags::{FeatureFlag, FeatureFlagViewExt};
-use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, actions};
+use gpui::{App, Entity, EventEmitter, FocusHandle, Focusable, ReadGlobal, ScrollHandle, actions};
use settings::{
NumType, SettingsStore, SettingsUiEntry, SettingsUiItem, SettingsUiItemDynamic,
SettingsUiItemGroup, SettingsUiItemSingle, SettingsValue,
@@ -138,6 +137,7 @@ impl Item for SettingsPage {
struct UiEntry {
title: &'static str,
path: Option<&'static str>,
+ documentation: Option<&'static str>,
_depth: usize,
// a
// b < a descendant range < a total descendant range
@@ -195,6 +195,7 @@ fn build_tree_item(
tree.push(UiEntry {
title: entry.title,
path: entry.path,
+ documentation: entry.documentation,
_depth: depth,
descendant_range: index + 1..index + 1,
total_descendant_range: index + 1..index + 1,
@@ -354,32 +355,29 @@ fn render_content(
tree: &SettingsUiTree,
window: &mut Window,
cx: &mut Context<SettingsPage>,
-) -> impl IntoElement {
- let Some(active_entry) = tree.entries.get(tree.active_entry_index) else {
- return div()
- .size_full()
- .child(Label::new(SharedString::new_static("No settings found")).color(Color::Error));
- };
- let mut content = v_flex().size_full().gap_4().overflow_hidden();
+) -> Div {
+ let content = v_flex().size_full().gap_4();
let mut path = smallvec::smallvec![];
- if let Some(active_entry_path) = active_entry.path {
- path.push(active_entry_path);
- }
- let mut entry_index_queue = VecDeque::new();
-
- if let Some(child_index) = active_entry.first_descendant_index() {
- entry_index_queue.push_back(child_index);
- let mut index = child_index;
- while let Some(next_sibling_index) = tree.entries[index].next_sibling {
- entry_index_queue.push_back(next_sibling_index);
- index = next_sibling_index;
- }
- };
- while let Some(index) = entry_index_queue.pop_front() {
+ fn render_recursive(
+ tree: &SettingsUiTree,
+ index: usize,
+ path: &mut SmallVec<[&'static str; 1]>,
+ mut element: Div,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Div {
+ let Some(child) = tree.entries.get(index) else {
+ return element.child(
+ Label::new(SharedString::new_static("No settings found")).color(Color::Error),
+ );
+ };
+
+ element =
+ element.child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large));
+
// todo(settings_ui): subgroups?
- let child = &tree.entries[index];
let mut pushed_path = false;
if let Some(child_path) = child.path {
path.push(child_path);
@@ -388,37 +386,56 @@ fn render_content(
let settings_value = settings_value_from_settings_and_path(
path.clone(),
child.title,
+ child.documentation,
// 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 = select_descendant(settings_value.read(), cx);
- if let Some(descendant_index) =
- child.nth_descendant_index(&tree.entries, selected_descendant)
- {
- entry_index_queue.push_front(descendant_index);
+ let selected_descendant = child
+ .nth_descendant_index(&tree.entries, select_descendant(settings_value.read(), cx));
+ if let Some(descendant_index) = selected_descendant {
+ element = render_recursive(&tree, descendant_index, path, element, window, cx);
}
}
+ 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, window, cx);
+ index = tree.entries[sub_child_index].next_sibling;
+ }
+ } else {
+ element =
+ element.child(div().child(Label::new("// skipped (for now)").color(Color::Muted)))
+ }
+
if pushed_path {
path.pop();
}
- let Some(child_render) = child.render.as_ref() else {
- continue;
- };
- content = content.child(
- div()
- .child(Label::new(SharedString::new_static(child.title)).size(LabelSize::Large))
- .child(render_item_single(settings_value, child_render, window, cx)),
- );
+ return element;
}
- return content;
+ return render_recursive(
+ tree,
+ tree.active_entry_index,
+ &mut path,
+ content,
+ window,
+ cx,
+ );
}
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());
div()
.grid()
.grid_cols(16)
@@ -427,15 +444,19 @@ impl Render for SettingsPage {
.size_full()
.child(
div()
+ .id("settings-ui-nav")
.col_span(2)
.h_full()
.child(render_nav(&self.settings_tree, window, cx)),
)
- .child(div().col_span(4).h_full().child(render_content(
- &self.settings_tree,
- window,
- cx,
- )))
+ .child(
+ div().col_span(6).h_full().child(
+ render_content(&self.settings_tree, window, cx)
+ .id("settings-ui-content")
+ .track_scroll(scroll_handle.read(cx))
+ .overflow_y_scroll(),
+ ),
+ )
}
}
@@ -530,6 +551,7 @@ fn downcast_any_item<T: serde::de::DeserializeOwned>(
let deserialized_setting_value = SettingsValue {
title: settings_value.title,
path: settings_value.path,
+ documentation: settings_value.documentation,
value,
default_value,
};
@@ -586,6 +608,17 @@ fn render_any_numeric_stepper(
window,
cx,
),
+ NumType::USIZE => render_numeric_stepper::<usize>(
+ downcast_any_item(settings_value),
+ usize::saturating_sub,
+ usize::saturating_add,
+ |n| {
+ serde_json::Number::try_from(n)
+ .context("Failed to convert usize to serde_json::Number")
+ },
+ window,
+ cx,
+ ),
}
}
@@ -640,7 +673,7 @@ fn render_switch_field(
SwitchField::new(
id,
SharedString::new_static(value.title),
- None,
+ value.documentation.map(SharedString::new_static),
match value.read() {
true => ToggleState::Selected,
false => ToggleState::Unselected,
@@ -731,6 +764,7 @@ fn render_toggle_button_group(
fn settings_value_from_settings_and_path(
path: SmallVec<[&'static str; 1]>,
title: &'static str,
+ documentation: Option<&'static str>,
user_settings: &serde_json::Value,
default_settings: &serde_json::Value,
) -> SettingsValue<serde_json::Value> {
@@ -743,6 +777,7 @@ fn settings_value_from_settings_and_path(
let settings_value = SettingsValue {
default_value,
value,
+ documentation,
path: path.clone(),
// todo(settings_ui) is title required inside SettingsValue?
title,
@@ -1,3 +1,5 @@
+use std::ops::Not;
+
use heck::{ToSnakeCase as _, ToTitleCase as _};
use proc_macro2::TokenStream;
use quote::{ToTokens, quote};
@@ -63,12 +65,19 @@ 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);
// 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());
- let ui_entry_fn_body = map_ui_item_to_entry(path_name.as_deref(), &title, quote! { Self });
+ let ui_entry_fn_body = map_ui_item_to_entry(
+ path_name.as_deref(),
+ &title,
+ doc_str.as_deref(),
+ quote! { Self },
+ );
let expanded = quote! {
impl #impl_generics settings::SettingsUi for #name #ty_generics #where_clause {
@@ -111,14 +120,22 @@ fn option_inner_type(ty: TokenStream) -> Option<TokenStream> {
return Some(ty.to_token_stream());
}
-fn map_ui_item_to_entry(path: Option<&str>, title: &str, ty: TokenStream) -> TokenStream {
+fn map_ui_item_to_entry(
+ path: Option<&str>,
+ title: &str,
+ 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)});
quote! {
settings::SettingsUiEntry {
title: #title,
path: #path,
item: #ty::settings_ui_item(),
+ documentation: #doc_str,
}
}
}
@@ -134,6 +151,7 @@ fn generate_ui_item_body(
settings::SettingsUiItem::None
},
(Some(_), _, Data::Struct(data_struct)) => {
+ let struct_serde_attrs = parse_serde_attributes(&input.attrs);
let fields = data_struct
.fields
.iter()
@@ -153,48 +171,37 @@ fn generate_ui_item_body(
})
})
.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);
+
(
- field.ident.clone().expect("tuple fields").to_string(),
+ 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(|(name, ty)| map_ui_item_to_entry(Some(&name), &name.to_title_case(), ty));
+ .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),*] })
}
}
(None, _, Data::Enum(data_enum)) => {
- let mut lowercase = false;
- let mut snake_case = false;
- for attr in &input.attrs {
- if attr.path().is_ident("serde") {
- attr.parse_nested_meta(|meta| {
- if meta.path.is_ident("rename_all") {
- meta.input.parse::<Token![=]>()?;
- let lit = meta.input.parse::<LitStr>()?.value();
- lowercase = lit == "lowercase";
- snake_case = lit == "snake_case";
- }
- Ok(())
- })
- .ok();
- }
- }
+ 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 title = string.to_title_case();
- let string = if lowercase {
- string.to_lowercase()
- } else if snake_case {
- string.to_snake_case()
- } else {
- string
- };
+ let string = serde_attrs.rename_all.apply(&string);
(string, title)
});
@@ -218,6 +225,113 @@ fn generate_ui_item_body(
}
}
+struct SerdeOptions {
+ rename_all: SerdeRenameAll,
+ rename: Option<String>,
+ flatten: bool,
+ _alias: Option<String>, // todo(settings_ui)
+}
+
+#[derive(PartialEq)]
+enum SerdeRenameAll {
+ Lowercase,
+ SnakeCase,
+ None,
+}
+
+impl SerdeRenameAll {
+ fn apply(&self, name: &str) -> String {
+ match self {
+ SerdeRenameAll::Lowercase => name.to_lowercase(),
+ SerdeRenameAll::SnakeCase => name.to_snake_case(),
+ SerdeRenameAll::None => name.to_string(),
+ }
+ }
+}
+
+impl SerdeOptions {
+ fn apply_rename_to_field(&self, field_options: &Self, name: &str) -> String {
+ // field renames take precedence over struct rename all cases
+ if let Some(rename) = &field_options.rename {
+ return rename.clone();
+ }
+ return self.rename_all.apply(name);
+ }
+}
+
+fn parse_serde_attributes(attrs: &[syn::Attribute]) -> SerdeOptions {
+ let mut options = SerdeOptions {
+ rename_all: SerdeRenameAll::None,
+ rename: None,
+ flatten: false,
+ _alias: None,
+ };
+
+ for attr in attrs {
+ if !attr.path().is_ident("serde") {
+ continue;
+ }
+ attr.parse_nested_meta(|meta| {
+ if meta.path.is_ident("rename_all") {
+ meta.input.parse::<Token![=]>()?;
+ let lit = meta.input.parse::<LitStr>()?.value();
+
+ if options.rename_all != SerdeRenameAll::None {
+ return Err(meta.error("duplicate `rename_all` attribute"));
+ } else if lit == "lowercase" {
+ options.rename_all = SerdeRenameAll::Lowercase;
+ } else if lit == "snake_case" {
+ options.rename_all = SerdeRenameAll::SnakeCase;
+ } else {
+ return Err(meta.error(format!("invalid `rename_all` attribute: {}", lit)));
+ }
+ // todo(settings_ui): Other options?
+ } else if meta.path.is_ident("flatten") {
+ options.flatten = true;
+ } else if meta.path.is_ident("rename") {
+ if options.rename.is_some() {
+ return Err(meta.error("Can only have one rename attribute"));
+ }
+
+ meta.input.parse::<Token![=]>()?;
+ let lit = meta.input.parse::<LitStr>()?.value();
+ options.rename = Some(lit);
+ }
+ Ok(())
+ })
+ .unwrap();
+ }
+
+ return options;
+}
+
+fn parse_documentation_from_attrs(attrs: &[syn::Attribute]) -> Option<String> {
+ let mut doc_str = Option::<String>::None;
+ for attr in attrs {
+ if attr.path().is_ident("doc") {
+ // /// ...
+ // becomes
+ // #[doc = "..."]
+ use syn::{Expr::Lit, ExprLit, Lit::Str, Meta, MetaNameValue};
+ if let Meta::NameValue(MetaNameValue {
+ value:
+ Lit(ExprLit {
+ lit: Str(ref lit_str),
+ ..
+ }),
+ ..
+ }) = attr.meta
+ {
+ let doc = lit_str.value();
+ let doc_str = doc_str.get_or_insert_default();
+ doc_str.push_str(doc.trim());
+ doc_str.push('\n');
+ }
+ }
+ }
+ return doc_str;
+}
+
struct SettingsKey {
key: Option<String>,
fallback_key: Option<String>,
@@ -290,7 +404,7 @@ pub fn derive_settings_key(input: proc_macro::TokenStream) -> proc_macro::TokenS
if parsed_settings_key.is_some() && settings_key.is_some() {
panic!("Duplicate #[settings_key] attribute");
}
- settings_key = parsed_settings_key;
+ settings_key = settings_key.or(parsed_settings_key);
}
let Some(SettingsKey { key, fallback_key }) = settings_key else {