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",
Michael Sloan created
Cargo.lock | 2
crates/theme/src/styles/colors.rs | 254 +---------------------------
crates/theme/src/styles/status.rs | 11 +
crates/util/Cargo.toml | 5
crates/util/src/util.rs | 45 +++++
crates/util_macros/Cargo.toml | 2
crates/util_macros/src/util_macros.rs | 161 +++++++++++++++++
7 files changed, 229 insertions(+), 251 deletions(-)
@@ -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",
@@ -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<Item = (ThemeColorField, Hsla)> + '_ {
- 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))
})
@@ -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.
@@ -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
@@ -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)* ) => {
@@ -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
@@ -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<TokenStream2> = Vec::new();
+
+ for attr in &input.attrs {
+ if attr.path().is_ident("field_access_by_enum") {
+ let name_values: Punctuated<MetaNameValue, Token![,]> =
+ 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)
+}