diff --git a/Cargo.lock b/Cargo.lock index 5654d206701f5efd5d91cd33c9ab456701b1e667..90e17983ace9c592d2f5eada9e2c033444d74677 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18579,6 +18579,8 @@ dependencies = [ name = "util_macros" version = "0.1.0" dependencies = [ + "convert_case 0.8.0", + "proc-macro2", "quote", "syn 2.0.101", "workspace-hack", diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 198ad97adb5d964a1d8f62c5bde99d1d5be5adf7..86d35c3cd285d46a1d3e95090a3d8c5c14b54e14 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -4,14 +4,22 @@ use gpui::{App, Hsla, SharedString, WindowBackgroundAppearance}; use refineable::Refineable; use std::sync::Arc; use strum::{AsRefStr, EnumIter, IntoEnumIterator}; +use util::FieldAccessByEnum; use crate::{ AccentColors, ActiveTheme, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, SystemColors, }; -#[derive(Refineable, Clone, Debug, PartialEq)] +#[derive(Refineable, FieldAccessByEnum, Clone, Debug, PartialEq)] #[refineable(Debug, serde::Deserialize)] +#[field_access_by_enum( + enum_name = "ThemeColorField", + enum_attrs = [ + derive(Debug, Clone, Copy, EnumIter, AsRefStr), + strum(serialize_all = "snake_case") + ] +)] pub struct ThemeColors { /// Border color. Used for most borders, is usually a high contrast color. pub border: Hsla, @@ -288,249 +296,9 @@ pub struct ThemeColors { pub version_control_conflict_marker_theirs: Hsla, } -#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)] -#[strum(serialize_all = "snake_case")] -pub enum ThemeColorField { - Border, - BorderVariant, - BorderFocused, - BorderSelected, - BorderTransparent, - BorderDisabled, - ElevatedSurfaceBackground, - SurfaceBackground, - Background, - ElementBackground, - ElementHover, - ElementActive, - ElementSelected, - ElementDisabled, - DropTargetBackground, - DropTargetBorder, - GhostElementBackground, - GhostElementHover, - GhostElementActive, - GhostElementSelected, - GhostElementDisabled, - Text, - TextMuted, - TextPlaceholder, - TextDisabled, - TextAccent, - Icon, - IconMuted, - IconDisabled, - IconPlaceholder, - IconAccent, - StatusBarBackground, - TitleBarBackground, - TitleBarInactiveBackground, - ToolbarBackground, - TabBarBackground, - TabInactiveBackground, - TabActiveBackground, - SearchMatchBackground, - PanelBackground, - PanelFocusedBorder, - PanelIndentGuide, - PanelIndentGuideHover, - PanelIndentGuideActive, - PanelOverlayBackground, - PanelOverlayHover, - PaneFocusedBorder, - PaneGroupBorder, - ScrollbarThumbBackground, - ScrollbarThumbHoverBackground, - ScrollbarThumbActiveBackground, - ScrollbarThumbBorder, - ScrollbarTrackBackground, - ScrollbarTrackBorder, - MinimapThumbBackground, - MinimapThumbHoverBackground, - MinimapThumbActiveBackground, - MinimapThumbBorder, - EditorForeground, - EditorBackground, - EditorGutterBackground, - EditorSubheaderBackground, - EditorActiveLineBackground, - EditorHighlightedLineBackground, - EditorLineNumber, - EditorActiveLineNumber, - EditorInvisible, - EditorWrapGuide, - EditorActiveWrapGuide, - EditorIndentGuide, - EditorIndentGuideActive, - EditorDocumentHighlightReadBackground, - EditorDocumentHighlightWriteBackground, - EditorDocumentHighlightBracketBackground, - TerminalBackground, - TerminalForeground, - TerminalBrightForeground, - TerminalDimForeground, - TerminalAnsiBackground, - TerminalAnsiBlack, - TerminalAnsiBrightBlack, - TerminalAnsiDimBlack, - TerminalAnsiRed, - TerminalAnsiBrightRed, - TerminalAnsiDimRed, - TerminalAnsiGreen, - TerminalAnsiBrightGreen, - TerminalAnsiDimGreen, - TerminalAnsiYellow, - TerminalAnsiBrightYellow, - TerminalAnsiDimYellow, - TerminalAnsiBlue, - TerminalAnsiBrightBlue, - TerminalAnsiDimBlue, - TerminalAnsiMagenta, - TerminalAnsiBrightMagenta, - TerminalAnsiDimMagenta, - TerminalAnsiCyan, - TerminalAnsiBrightCyan, - TerminalAnsiDimCyan, - TerminalAnsiWhite, - TerminalAnsiBrightWhite, - TerminalAnsiDimWhite, - LinkTextHover, - VersionControlAdded, - VersionControlDeleted, - VersionControlModified, - VersionControlRenamed, - VersionControlConflict, - VersionControlIgnored, -} - impl ThemeColors { - pub fn color(&self, field: ThemeColorField) -> Hsla { - match field { - ThemeColorField::Border => self.border, - ThemeColorField::BorderVariant => self.border_variant, - ThemeColorField::BorderFocused => self.border_focused, - ThemeColorField::BorderSelected => self.border_selected, - ThemeColorField::BorderTransparent => self.border_transparent, - ThemeColorField::BorderDisabled => self.border_disabled, - ThemeColorField::ElevatedSurfaceBackground => self.elevated_surface_background, - ThemeColorField::SurfaceBackground => self.surface_background, - ThemeColorField::Background => self.background, - ThemeColorField::ElementBackground => self.element_background, - ThemeColorField::ElementHover => self.element_hover, - ThemeColorField::ElementActive => self.element_active, - ThemeColorField::ElementSelected => self.element_selected, - ThemeColorField::ElementDisabled => self.element_disabled, - ThemeColorField::DropTargetBackground => self.drop_target_background, - ThemeColorField::DropTargetBorder => self.drop_target_border, - ThemeColorField::GhostElementBackground => self.ghost_element_background, - ThemeColorField::GhostElementHover => self.ghost_element_hover, - ThemeColorField::GhostElementActive => self.ghost_element_active, - ThemeColorField::GhostElementSelected => self.ghost_element_selected, - ThemeColorField::GhostElementDisabled => self.ghost_element_disabled, - ThemeColorField::Text => self.text, - ThemeColorField::TextMuted => self.text_muted, - ThemeColorField::TextPlaceholder => self.text_placeholder, - ThemeColorField::TextDisabled => self.text_disabled, - ThemeColorField::TextAccent => self.text_accent, - ThemeColorField::Icon => self.icon, - ThemeColorField::IconMuted => self.icon_muted, - ThemeColorField::IconDisabled => self.icon_disabled, - ThemeColorField::IconPlaceholder => self.icon_placeholder, - ThemeColorField::IconAccent => self.icon_accent, - ThemeColorField::StatusBarBackground => self.status_bar_background, - ThemeColorField::TitleBarBackground => self.title_bar_background, - ThemeColorField::TitleBarInactiveBackground => self.title_bar_inactive_background, - ThemeColorField::ToolbarBackground => self.toolbar_background, - ThemeColorField::TabBarBackground => self.tab_bar_background, - ThemeColorField::TabInactiveBackground => self.tab_inactive_background, - ThemeColorField::TabActiveBackground => self.tab_active_background, - ThemeColorField::SearchMatchBackground => self.search_match_background, - ThemeColorField::PanelBackground => self.panel_background, - ThemeColorField::PanelFocusedBorder => self.panel_focused_border, - ThemeColorField::PanelIndentGuide => self.panel_indent_guide, - ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, - ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, - ThemeColorField::PanelOverlayBackground => self.panel_overlay_background, - ThemeColorField::PanelOverlayHover => self.panel_overlay_hover, - ThemeColorField::PaneFocusedBorder => self.pane_focused_border, - ThemeColorField::PaneGroupBorder => self.pane_group_border, - ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, - ThemeColorField::ScrollbarThumbHoverBackground => self.scrollbar_thumb_hover_background, - ThemeColorField::ScrollbarThumbActiveBackground => { - self.scrollbar_thumb_active_background - } - ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border, - ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background, - ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border, - ThemeColorField::MinimapThumbBackground => self.minimap_thumb_background, - ThemeColorField::MinimapThumbHoverBackground => self.minimap_thumb_hover_background, - ThemeColorField::MinimapThumbActiveBackground => self.minimap_thumb_active_background, - ThemeColorField::MinimapThumbBorder => self.minimap_thumb_border, - ThemeColorField::EditorForeground => self.editor_foreground, - ThemeColorField::EditorBackground => self.editor_background, - ThemeColorField::EditorGutterBackground => self.editor_gutter_background, - ThemeColorField::EditorSubheaderBackground => self.editor_subheader_background, - ThemeColorField::EditorActiveLineBackground => self.editor_active_line_background, - ThemeColorField::EditorHighlightedLineBackground => { - self.editor_highlighted_line_background - } - ThemeColorField::EditorLineNumber => self.editor_line_number, - ThemeColorField::EditorActiveLineNumber => self.editor_active_line_number, - ThemeColorField::EditorInvisible => self.editor_invisible, - ThemeColorField::EditorWrapGuide => self.editor_wrap_guide, - ThemeColorField::EditorActiveWrapGuide => self.editor_active_wrap_guide, - ThemeColorField::EditorIndentGuide => self.editor_indent_guide, - ThemeColorField::EditorIndentGuideActive => self.editor_indent_guide_active, - ThemeColorField::EditorDocumentHighlightReadBackground => { - self.editor_document_highlight_read_background - } - ThemeColorField::EditorDocumentHighlightWriteBackground => { - self.editor_document_highlight_write_background - } - ThemeColorField::EditorDocumentHighlightBracketBackground => { - self.editor_document_highlight_bracket_background - } - ThemeColorField::TerminalBackground => self.terminal_background, - ThemeColorField::TerminalForeground => self.terminal_foreground, - ThemeColorField::TerminalBrightForeground => self.terminal_bright_foreground, - ThemeColorField::TerminalDimForeground => self.terminal_dim_foreground, - ThemeColorField::TerminalAnsiBackground => self.terminal_ansi_background, - ThemeColorField::TerminalAnsiBlack => self.terminal_ansi_black, - ThemeColorField::TerminalAnsiBrightBlack => self.terminal_ansi_bright_black, - ThemeColorField::TerminalAnsiDimBlack => self.terminal_ansi_dim_black, - ThemeColorField::TerminalAnsiRed => self.terminal_ansi_red, - ThemeColorField::TerminalAnsiBrightRed => self.terminal_ansi_bright_red, - ThemeColorField::TerminalAnsiDimRed => self.terminal_ansi_dim_red, - ThemeColorField::TerminalAnsiGreen => self.terminal_ansi_green, - ThemeColorField::TerminalAnsiBrightGreen => self.terminal_ansi_bright_green, - ThemeColorField::TerminalAnsiDimGreen => self.terminal_ansi_dim_green, - ThemeColorField::TerminalAnsiYellow => self.terminal_ansi_yellow, - ThemeColorField::TerminalAnsiBrightYellow => self.terminal_ansi_bright_yellow, - ThemeColorField::TerminalAnsiDimYellow => self.terminal_ansi_dim_yellow, - ThemeColorField::TerminalAnsiBlue => self.terminal_ansi_blue, - ThemeColorField::TerminalAnsiBrightBlue => self.terminal_ansi_bright_blue, - ThemeColorField::TerminalAnsiDimBlue => self.terminal_ansi_dim_blue, - ThemeColorField::TerminalAnsiMagenta => self.terminal_ansi_magenta, - ThemeColorField::TerminalAnsiBrightMagenta => self.terminal_ansi_bright_magenta, - ThemeColorField::TerminalAnsiDimMagenta => self.terminal_ansi_dim_magenta, - ThemeColorField::TerminalAnsiCyan => self.terminal_ansi_cyan, - ThemeColorField::TerminalAnsiBrightCyan => self.terminal_ansi_bright_cyan, - ThemeColorField::TerminalAnsiDimCyan => self.terminal_ansi_dim_cyan, - ThemeColorField::TerminalAnsiWhite => self.terminal_ansi_white, - ThemeColorField::TerminalAnsiBrightWhite => self.terminal_ansi_bright_white, - ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white, - ThemeColorField::LinkTextHover => self.link_text_hover, - ThemeColorField::VersionControlAdded => self.version_control_added, - ThemeColorField::VersionControlDeleted => self.version_control_deleted, - ThemeColorField::VersionControlModified => self.version_control_modified, - ThemeColorField::VersionControlRenamed => self.version_control_renamed, - ThemeColorField::VersionControlConflict => self.version_control_conflict, - ThemeColorField::VersionControlIgnored => self.version_control_ignored, - } - } - pub fn iter(&self) -> impl Iterator + '_ { - ThemeColorField::iter().map(move |field| (field, self.color(field))) + ThemeColorField::iter().map(move |field| (field, *self.get_field_by_enum(field))) } pub fn to_vec(&self) -> Vec<(ThemeColorField, Hsla)> { @@ -542,7 +310,7 @@ pub fn all_theme_colors(cx: &mut App) -> Vec<(Hsla, SharedString)> { let theme = cx.theme(); ThemeColorField::iter() .map(|field| { - let color = theme.colors().color(field); + let color = *theme.colors().get_field_by_enum(field); let name = field.as_ref().to_string(); (color, SharedString::from(name)) }) diff --git a/crates/theme/src/styles/status.rs b/crates/theme/src/styles/status.rs index ab333ecb3ee2d0be32aa76e99e073fb37873bcf9..f9d1db752e3ba10a14a8c1ac98203df399ca73ea 100644 --- a/crates/theme/src/styles/status.rs +++ b/crates/theme/src/styles/status.rs @@ -2,11 +2,20 @@ use gpui::Hsla; use refineable::Refineable; +use strum::{AsRefStr, EnumIter}; +use util::FieldAccessByEnum; use crate::{blue, grass, neutral, red, yellow}; -#[derive(Refineable, Clone, Debug, PartialEq)] +#[derive(Refineable, FieldAccessByEnum, Clone, Debug, PartialEq)] #[refineable(Debug, serde::Deserialize)] +#[field_access_by_enum( + enum_name = "StatusColorField", + enum_attrs = [ + derive(Debug, Clone, Copy, EnumIter, AsRefStr), + strum(serialize_all = "snake_case") + ] +)] pub struct StatusColors { /// Indicates some kind of conflict, like a file changed on disk while it was open, or /// merge conflicts in a Git repository. diff --git a/crates/util/Cargo.toml b/crates/util/Cargo.toml index 825d6471b2d0585803d2f5567eadef11ffdc9d1b..2e07a9e23b98af3010e457addf07ae20c62fdcfa 100644 --- a/crates/util/Cargo.toml +++ b/crates/util/Cargo.toml @@ -13,7 +13,7 @@ path = "src/util.rs" doctest = true [features] -test-support = ["git2", "rand", "util_macros"] +test-support = ["git2", "rand"] [dependencies] anyhow.workspace = true @@ -39,7 +39,7 @@ smol.workspace = true take-until.workspace = true tempfile.workspace = true unicase.workspace = true -util_macros = { workspace = true, optional = true } +util_macros = { workspace = true } walkdir.workspace = true workspace-hack.workspace = true @@ -56,4 +56,3 @@ dunce = "1.0" git2.workspace = true indoc.workspace = true rand.workspace = true -util_macros.workspace = true diff --git a/crates/util/src/util.rs b/crates/util/src/util.rs index ee18093784e4b0f2b78db9240d918685b6f01b6f..15393d76f941d57458655956f3883871c949dbbc 100644 --- a/crates/util/src/util.rs +++ b/crates/util/src/util.rs @@ -31,9 +31,54 @@ use std::{ use unicase::UniCase; pub use take_until::*; +pub use util_macros::FieldAccessByEnum; #[cfg(any(test, feature = "test-support"))] pub use util_macros::{line_endings, path, uri}; +// todo! dedupe docs + +/// Trait for types that can access their fields via an enum. +/// +/// This trait provides a way to dynamically access and modify fields of a struct +/// using an enum that represents the field names. It's particularly useful for +/// implementing generic field access patterns, such as theme color management +/// or configuration field manipulation. +/// +/// # Example +/// +/// ```rust +/// use util::FieldAccessByEnum; +/// +/// #[derive(util_macros::FieldAccessByEnum)] +/// #[field_access_by_enum(enum_name = "ColorField", field_type = "String")] +/// struct Theme { +/// background: String, +/// foreground: String, +/// border: String, +/// } +/// +/// let mut theme = Theme { +/// background: "white".to_string(), +/// foreground: "black".to_string(), +/// border: "gray".to_string(), +/// }; +/// +/// // Access field via enum +/// let bg = theme.get(ColorField::Background); +/// theme.set(ColorField::Foreground, "blue".to_string()); +/// ``` +pub trait FieldAccessByEnum { + /// The enum type representing the available fields + type Field; + /// The type of values stored in the fields + type FieldValue; + + /// Get a reference to the value of the specified field + fn get_field_by_enum(&self, field: Self::Field) -> &Self::FieldValue; + /// Set the value of the specified field + fn set_field_by_enum(&mut self, field: Self::Field, value: Self::FieldValue); +} + #[macro_export] macro_rules! debug_panic { ( $($fmt_arg:tt)* ) => { diff --git a/crates/util_macros/Cargo.toml b/crates/util_macros/Cargo.toml index 996eefcb303ee5959f0e7fa920f1a91a509407eb..771c27a3c12f3cb9b29a354f35b95bbdb275a636 100644 --- a/crates/util_macros/Cargo.toml +++ b/crates/util_macros/Cargo.toml @@ -14,6 +14,8 @@ proc-macro = true doctest = false [dependencies] +convert_case.workspace = true quote.workspace = true syn.workspace = true workspace-hack.workspace = true +proc-macro2.workspace = true diff --git a/crates/util_macros/src/util_macros.rs b/crates/util_macros/src/util_macros.rs index 9d0b06ab10a7454d6c0d19fd54722fd98db4ac25..11bdfded7c8cad9655da192dea72fe4c07e7aa2c 100644 --- a/crates/util_macros/src/util_macros.rs +++ b/crates/util_macros/src/util_macros.rs @@ -1,8 +1,11 @@ -#![cfg_attr(not(target_os = "windows"), allow(unused))] - +use convert_case::{Case, Casing}; use proc_macro::TokenStream; -use quote::quote; -use syn::{LitStr, parse_macro_input}; +use proc_macro2::TokenStream as TokenStream2; +use quote::{format_ident, quote}; +use syn::{ + Data, DeriveInput, Expr, ExprArray, ExprLit, Fields, Lit, LitStr, MetaNameValue, Token, + parse_macro_input, punctuated::Punctuated, +}; /// A macro used in tests for cross-platform path string literals in tests. On Windows it replaces /// `/` with `\\` and adds `C:` to the beginning of absolute paths. On other platforms, the path is @@ -87,3 +90,153 @@ pub fn line_endings(input: TokenStream) -> TokenStream { #text }) } + +/// Derive macro that generates an enum and implements `FieldAccessByEnum`. Only works for structs +/// with named fields where every field has the same type. +/// +/// # Example +/// +/// ```rust +/// #[derive(FieldAccessByEnum)] +/// #[field_access_by_enum( +/// enum_name = "ColorField", +/// enum_attrs = [ +/// derive(Debug, Clone, Copy, EnumIter, AsRefStr), +/// strum(serialize_all = "snake_case") +/// ], +/// )] +/// struct Theme { +/// background: Hsla, +/// foreground: Hsla, +/// border_color: Hsla, +/// } +/// ``` +/// +/// This generates: +/// ```rust +/// #[derive(Debug, Clone, Copy, EnumIter, AsRefStr)] +/// #[strum(serialize_all = "snake_case")] +/// enum ColorField { +/// Background, +/// Foreground, +/// BorderColor, +/// } +/// +/// impl FieldAccessByEnum for Theme { +/// type Field = ColorField; +/// type FieldValue = Hsla; +/// // ... get and set methods +/// } +/// ``` +#[proc_macro_derive(FieldAccessByEnum, attributes(field_access_by_enum))] +pub fn derive_field_access_by_enum(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let struct_name = &input.ident; + + let mut enum_name = None; + let mut enum_attrs: Vec = Vec::new(); + + for attr in &input.attrs { + if attr.path().is_ident("field_access_by_enum") { + let name_values: Punctuated = + attr.parse_args_with(Punctuated::parse_terminated).unwrap(); + for name_value in name_values { + if name_value.path.is_ident("enum_name") { + let value = name_value.value; + match value { + Expr::Lit(ExprLit { + lit: Lit::Str(name), + .. + }) => enum_name = Some(name.value()), + _ => panic!("Expected string literal in enum_name attribute"), + } + } else if name_value.path.is_ident("enum_attrs") { + let value = name_value.value; + match value { + Expr::Array(ExprArray { elems, .. }) => { + for elem in elems { + enum_attrs.push(quote!(#[#elem])); + } + } + _ => panic!("Expected array literal in enum_attr attribute"), + } + } + } + } + } + let Some(enum_name) = enum_name else { + panic!("#[field_access_by_enum(enum_name = \"...\")] attribute is required"); + }; + let enum_ident = format_ident!("{}", enum_name); + + let fields = match input.data { + Data::Struct(data_struct) => match data_struct.fields { + Fields::Named(fields) => fields.named, + _ => panic!("FieldAccessByEnum can only be derived for structs with named fields"), + }, + _ => panic!("FieldAccessByEnum can only be derived for structs"), + }; + + if fields.is_empty() { + panic!("FieldAccessByEnum cannot be derived for structs with no fields"); + } + + let mut enum_variants = Vec::new(); + let mut get_match_arms = Vec::new(); + let mut set_match_arms = Vec::new(); + let mut field_types = Vec::new(); + + for field in fields.iter() { + let field_name = field.ident.as_ref().unwrap(); + let variant_name = field_name.to_string().to_case(Case::Pascal); + let variant_ident = format_ident!("{}", variant_name); + let field_type = &field.ty; + + enum_variants.push(variant_ident.clone()); + field_types.push(field_type); + + get_match_arms.push(quote! { + #enum_ident::#variant_ident => &self.#field_name, + }); + + set_match_arms.push(quote! { + #enum_ident::#variant_ident => self.#field_name = value, + }); + } + + let first_type = &field_types[0]; + let all_same_type = field_types + .iter() + .all(|ty| quote!(#ty).to_string() == quote!(#first_type).to_string()); + if !all_same_type { + panic!("Fields have different types."); + } + let field_value_type = quote! { #first_type }; + + let expanded = quote! { + #(#enum_attrs)* + pub enum #enum_ident { + #(#enum_variants),* + } + + impl util::FieldAccessByEnum for #struct_name { + type Field = #enum_ident; + type FieldValue = #field_value_type; + + fn get_field_by_enum(&self, field: Self::Field) -> &Self::FieldValue { + match field { + #(#get_match_arms)* + } + } + + fn set_field_by_enum(&mut self, field: Self::Field, value: Self::FieldValue) { + match field { + #(#set_match_arms)* + } + } + } + }; + + TokenStream::from(expanded) +}