Compare commits
10 Commits
inline-ass
...
inspect-na
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66302a4943 | ||
|
|
5495885573 | ||
|
|
5b0af08c64 | ||
|
|
1d0c89ee8e | ||
|
|
68d795387a | ||
|
|
d4bee57aac | ||
|
|
0b09b27b95 | ||
|
|
fbe43d500f | ||
|
|
746aacb066 | ||
|
|
1e9c9025cd |
5
Cargo.lock
generated
5
Cargo.lock
generated
@@ -8912,6 +8912,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"command_palette_hooks",
|
||||
"convert_case 0.8.0",
|
||||
"editor",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
@@ -8919,6 +8920,8 @@ dependencies = [
|
||||
"project",
|
||||
"serde_json",
|
||||
"serde_json_lenient",
|
||||
"strum 0.27.1",
|
||||
"text",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
@@ -18590,7 +18593,9 @@ dependencies = [
|
||||
name = "util_macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case 0.8.0",
|
||||
"perf",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.101",
|
||||
"workspace-hack",
|
||||
|
||||
@@ -697,6 +697,16 @@ pub struct Background {
|
||||
pad: u32,
|
||||
}
|
||||
|
||||
impl Background {
|
||||
/// Convert this background to a solid color, if it is one.
|
||||
pub fn as_solid(&self) -> Option<Hsla> {
|
||||
match self.tag {
|
||||
BackgroundTag::Solid => Some(self.solid),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for Background {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
match self.tag {
|
||||
|
||||
@@ -14,6 +14,7 @@ path = "src/inspector_ui.rs"
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
convert_case.workspace = true
|
||||
editor.workspace = true
|
||||
fuzzy.workspace = true
|
||||
gpui.workspace = true
|
||||
@@ -21,6 +22,8 @@ language.workspace = true
|
||||
project.workspace = true
|
||||
serde_json.workspace = true
|
||||
serde_json_lenient.workspace = true
|
||||
strum.workspace = true
|
||||
text.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use anyhow::{Result, anyhow};
|
||||
use convert_case::{Case, Casing};
|
||||
use editor::{
|
||||
Bias, CompletionProvider, Editor, EditorEvent, EditorMode, ExcerptId, MinimapVisibility,
|
||||
MultiBuffer,
|
||||
};
|
||||
use fuzzy::StringMatch;
|
||||
use gpui::Hsla;
|
||||
use gpui::{
|
||||
AsyncWindowContext, DivInspectorState, Entity, InspectorElementId, IntoElement,
|
||||
StyleRefinement, Task, Window, inspector_reflection::FunctionReflection, styled_reflection,
|
||||
@@ -11,7 +13,7 @@ use gpui::{
|
||||
use language::language_settings::SoftWrap;
|
||||
use language::{
|
||||
Anchor, Buffer, BufferSnapshot, CodeLabel, Diagnostic, DiagnosticEntry, DiagnosticSet,
|
||||
DiagnosticSeverity, LanguageServerId, Point, ToOffset as _, ToPoint as _,
|
||||
DiagnosticSeverity, LanguageServerId, ToOffset as _,
|
||||
};
|
||||
use project::lsp_store::CompletionDocumentation;
|
||||
use project::{
|
||||
@@ -23,8 +25,10 @@ use std::ops::Range;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::sync::LazyLock;
|
||||
use strum::IntoEnumIterator;
|
||||
use theme::{StatusColorField, ThemeColorField};
|
||||
use ui::{Label, LabelSize, Tooltip, prelude::*, styled_ext_reflection, v_flex};
|
||||
use util::split_str_with_ranges;
|
||||
use util::{FieldAccessByEnum, TakeUntilExt, split_str_with_ranges};
|
||||
|
||||
/// Path used for unsaved buffer that contains style json. To support the json language server, this
|
||||
/// matches the name used in the generated schemas.
|
||||
@@ -210,6 +214,7 @@ impl DivInspector {
|
||||
Ok(new_style) => {
|
||||
let (rust_style, _) = this.style_from_rust_buffer_snapshot(
|
||||
&rust_style_buffer.read(cx).snapshot(),
|
||||
cx,
|
||||
);
|
||||
|
||||
let mut unconvertible_plus_rust = this.unconvertible_style.clone();
|
||||
@@ -301,11 +306,11 @@ impl DivInspector {
|
||||
}
|
||||
};
|
||||
|
||||
let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style);
|
||||
let (rust_code, rust_style) = guess_rust_code_from_style(&self.initial_style, cx);
|
||||
rust_style_buffer.update(cx, |rust_style_buffer, cx| {
|
||||
rust_style_buffer.set_text(rust_code, cx);
|
||||
let snapshot = rust_style_buffer.snapshot();
|
||||
let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
|
||||
let (_, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot, cx);
|
||||
Self::set_rust_buffer_diagnostics(
|
||||
unrecognized_ranges,
|
||||
rust_style_buffer,
|
||||
@@ -348,7 +353,8 @@ impl DivInspector {
|
||||
) {
|
||||
let rust_style = rust_style_buffer.update(cx, |rust_style_buffer, cx| {
|
||||
let snapshot = rust_style_buffer.snapshot();
|
||||
let (rust_style, unrecognized_ranges) = self.style_from_rust_buffer_snapshot(&snapshot);
|
||||
let (rust_style, unrecognized_ranges) =
|
||||
self.style_from_rust_buffer_snapshot(&snapshot, cx);
|
||||
Self::set_rust_buffer_diagnostics(
|
||||
unrecognized_ranges,
|
||||
rust_style_buffer,
|
||||
@@ -385,6 +391,7 @@ impl DivInspector {
|
||||
fn style_from_rust_buffer_snapshot(
|
||||
&self,
|
||||
snapshot: &BufferSnapshot,
|
||||
cx: &App,
|
||||
) -> (StyleRefinement, Vec<Range<Anchor>>) {
|
||||
let method_names = if let Some((completion, completion_range)) = self
|
||||
.rust_completion
|
||||
@@ -418,18 +425,7 @@ impl DivInspector {
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let mut style = StyleRefinement::default();
|
||||
let mut unrecognized_ranges = Vec::new();
|
||||
for (range, name) in method_names {
|
||||
if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
|
||||
style = method.invoke(style);
|
||||
} else if let Some(range) = range {
|
||||
unrecognized_ranges
|
||||
.push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
|
||||
}
|
||||
}
|
||||
|
||||
(style, unrecognized_ranges)
|
||||
guess_style_from_rust_code(method_names, snapshot, cx)
|
||||
}
|
||||
|
||||
fn set_rust_buffer_diagnostics(
|
||||
@@ -602,7 +598,59 @@ static STYLE_METHODS: LazyLock<Vec<(Box<StyleRefinement>, FunctionReflection<Sty
|
||||
.collect()
|
||||
});
|
||||
|
||||
fn guess_rust_code_from_style(goal_style: &StyleRefinement) -> (String, StyleRefinement) {
|
||||
static COLOR_METHODS: &[ColorMethod] = &[
|
||||
ColorMethod {
|
||||
name: "border_color",
|
||||
set: |style, color| style.border_color(color),
|
||||
get: |style| style.border_color,
|
||||
},
|
||||
ColorMethod {
|
||||
name: "text_color",
|
||||
set: |style, color| style.text_color(color),
|
||||
get: |style| style.text.as_ref().and_then(|text_style| text_style.color),
|
||||
},
|
||||
ColorMethod {
|
||||
name: "text_decoration_color",
|
||||
set: |style, color| style.text_decoration_color(color),
|
||||
get: |style| {
|
||||
style.text.as_ref().and_then(|text_style| {
|
||||
text_style
|
||||
.underline
|
||||
.as_ref()
|
||||
.and_then(|underline| underline.color)
|
||||
})
|
||||
},
|
||||
},
|
||||
ColorMethod {
|
||||
name: "text_bg",
|
||||
set: |style, color| style.text_bg(color),
|
||||
get: |style| {
|
||||
style
|
||||
.text
|
||||
.as_ref()
|
||||
.and_then(|text_style| text_style.background_color)
|
||||
},
|
||||
},
|
||||
ColorMethod {
|
||||
name: "bg",
|
||||
set: |style, color| style.bg(color),
|
||||
get: |style| {
|
||||
style.background.as_ref().and_then(|background| {
|
||||
background
|
||||
.color()
|
||||
.and_then(|background| background.as_solid())
|
||||
})
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
struct ColorMethod {
|
||||
name: &'static str,
|
||||
set: fn(StyleRefinement, Hsla) -> StyleRefinement,
|
||||
get: fn(&StyleRefinement) -> Option<Hsla>,
|
||||
}
|
||||
|
||||
fn guess_rust_code_from_style(goal_style: &StyleRefinement, cx: &App) -> (String, StyleRefinement) {
|
||||
let mut subset_methods = Vec::new();
|
||||
for (style, method) in STYLE_METHODS.iter() {
|
||||
if goal_style.is_superset_of(style) {
|
||||
@@ -610,20 +658,98 @@ fn guess_rust_code_from_style(goal_style: &StyleRefinement) -> (String, StyleRef
|
||||
}
|
||||
}
|
||||
|
||||
let mut code = "fn build() -> Div {\n div()".to_string();
|
||||
let mut code = String::new();
|
||||
let mut style = StyleRefinement::default();
|
||||
for method in subset_methods {
|
||||
let before_change = style.clone();
|
||||
style = method.invoke(style);
|
||||
if before_change != style {
|
||||
let _ = write!(code, "\n .{}()", &method.name);
|
||||
let _ = write!(code, ".{}()\n", &method.name);
|
||||
}
|
||||
}
|
||||
|
||||
let theme = cx.theme();
|
||||
for color_method in COLOR_METHODS {
|
||||
if let Some(color) = (color_method.get)(&goal_style) {
|
||||
let mut found_match = false;
|
||||
for theme_color_field in ThemeColorField::iter() {
|
||||
// TODO: If proper color provenance information is added, then the
|
||||
// `make_colors_unique` hack in the theme loading code can be removed.
|
||||
if *theme.colors().get_field_by_enum(theme_color_field) == color {
|
||||
found_match = true;
|
||||
let _ = write!(
|
||||
code,
|
||||
".{}(colors.{})\n",
|
||||
color_method.name,
|
||||
theme_color_field.as_ref().to_case(Case::Snake)
|
||||
);
|
||||
}
|
||||
}
|
||||
for status_color_field in StatusColorField::iter() {
|
||||
if *theme.status().get_field_by_enum(status_color_field) == color {
|
||||
found_match = true;
|
||||
let _ = write!(
|
||||
code,
|
||||
".{}(status.{})\n",
|
||||
color_method.name,
|
||||
status_color_field.as_ref().to_case(Case::Snake)
|
||||
);
|
||||
}
|
||||
}
|
||||
if found_match {
|
||||
style = (color_method.set)(style, color);
|
||||
}
|
||||
}
|
||||
}
|
||||
code.push_str("\n}");
|
||||
|
||||
(code, style)
|
||||
}
|
||||
|
||||
fn guess_style_from_rust_code(
|
||||
method_names: Vec<(Option<Range<usize>>, String)>,
|
||||
snapshot: &BufferSnapshot,
|
||||
cx: &App,
|
||||
) -> (StyleRefinement, Vec<Range<Anchor>>) {
|
||||
let theme = cx.theme();
|
||||
let mut style = StyleRefinement::default();
|
||||
let mut unrecognized_ranges = Vec::new();
|
||||
let mut preceded_by_color_method: Option<&ColorMethod> = None;
|
||||
for (range, name) in method_names {
|
||||
if name == "colors" || name == "status" {
|
||||
continue;
|
||||
}
|
||||
if let Some(color_method) = preceded_by_color_method {
|
||||
if let Some(field) = ThemeColorField::iter()
|
||||
.find(|field| name == field.as_ref().to_case(Case::Snake).as_str())
|
||||
{
|
||||
style = (color_method.set)(style, *theme.colors().get_field_by_enum(field));
|
||||
continue;
|
||||
}
|
||||
if let Some(field) = StatusColorField::iter()
|
||||
.find(|field| name == field.as_ref().to_case(Case::Snake).as_str())
|
||||
{
|
||||
style = (color_method.set)(style, *theme.status().get_field_by_enum(field));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if let Some((_, method)) = STYLE_METHODS.iter().find(|(_, m)| m.name == name) {
|
||||
preceded_by_color_method = None;
|
||||
style = method.invoke(style);
|
||||
continue;
|
||||
}
|
||||
if let Some(color_method) = COLOR_METHODS.iter().find(|m| m.name == name) {
|
||||
preceded_by_color_method = Some(color_method);
|
||||
continue;
|
||||
}
|
||||
if let Some(range) = range {
|
||||
unrecognized_ranges
|
||||
.push(snapshot.anchor_before(range.start)..snapshot.anchor_before(range.end));
|
||||
}
|
||||
}
|
||||
|
||||
(style, unrecognized_ranges)
|
||||
}
|
||||
|
||||
fn is_not_identifier_char(c: char) -> bool {
|
||||
!c.is_alphanumeric() && c != '_'
|
||||
}
|
||||
@@ -642,34 +768,102 @@ impl CompletionProvider for RustStyleCompletionProvider {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> Task<Result<Vec<CompletionResponse>>> {
|
||||
let Some(replace_range) = completion_replace_range(&buffer.read(cx).snapshot(), &position)
|
||||
else {
|
||||
let snapshot: &text::BufferSnapshot = &buffer.read(cx);
|
||||
let Some(replace_range) = completion_replace_range(snapshot, &position) else {
|
||||
return Task::ready(Ok(Vec::new()));
|
||||
};
|
||||
|
||||
let preceded_by_colors;
|
||||
let preceded_by_status;
|
||||
{
|
||||
let rev_chars_before = snapshot
|
||||
.reversed_chars_at(position)
|
||||
.take(50)
|
||||
.collect::<String>();
|
||||
let mut rev_words_before = rev_chars_before.split(is_not_identifier_char);
|
||||
let rev_first_word_before = rev_words_before.next();
|
||||
let rev_second_word_before = rev_words_before.next();
|
||||
preceded_by_colors =
|
||||
rev_first_word_before == Some("sroloc") || rev_second_word_before == Some("sroloc");
|
||||
preceded_by_status =
|
||||
rev_first_word_before == Some("sutats") || rev_second_word_before == Some("sutats");
|
||||
}
|
||||
|
||||
self.div_inspector.update(cx, |div_inspector, _cx| {
|
||||
div_inspector.rust_completion_replace_range = Some(replace_range.clone());
|
||||
});
|
||||
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: STYLE_METHODS
|
||||
.iter()
|
||||
.map(|(_, method)| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}()", method.name),
|
||||
label: CodeLabel::plain(method.name.to_string(), None),
|
||||
icon_path: None,
|
||||
documentation: method.documentation.map(|documentation| {
|
||||
CompletionDocumentation::MultiLineMarkdown(documentation.into())
|
||||
}),
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
.collect(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
if preceded_by_colors {
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: ThemeColorField::iter()
|
||||
.map(|color_field| {
|
||||
let name = color_field.as_ref().to_case(Case::Snake);
|
||||
Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}", name),
|
||||
label: CodeLabel::plain(name, None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
} else if preceded_by_status {
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: StatusColorField::iter()
|
||||
.map(|color_field| {
|
||||
let name = color_field.as_ref().to_case(Case::Snake);
|
||||
Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}", name),
|
||||
label: CodeLabel::plain(name, None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
} else {
|
||||
Task::ready(Ok(vec![CompletionResponse {
|
||||
completions: STYLE_METHODS
|
||||
.iter()
|
||||
.map(|(_, method)| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}()", method.name),
|
||||
label: CodeLabel::plain(method.name.to_string(), None),
|
||||
icon_path: None,
|
||||
documentation: method.documentation.map(|documentation| {
|
||||
CompletionDocumentation::MultiLineMarkdown(documentation.into())
|
||||
}),
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
})
|
||||
.chain(COLOR_METHODS.iter().map(|method| Completion {
|
||||
replace_range: replace_range.clone(),
|
||||
new_text: format!(".{}(colors.", method.name),
|
||||
label: CodeLabel::plain(method.name.to_string(), None),
|
||||
icon_path: None,
|
||||
documentation: None,
|
||||
source: CompletionSource::Custom,
|
||||
insert_text_mode: None,
|
||||
confirm: None,
|
||||
}))
|
||||
.collect(),
|
||||
display_options: CompletionDisplayOptions::default(),
|
||||
is_incomplete: false,
|
||||
}]))
|
||||
}
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
@@ -681,7 +875,8 @@ impl CompletionProvider for RustStyleCompletionProvider {
|
||||
_menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()
|
||||
let snapshot: &text::BufferSnapshot = &buffer.read(cx);
|
||||
completion_replace_range(snapshot, &position).is_some()
|
||||
}
|
||||
|
||||
fn selection_changed(&self, mat: Option<&StringMatch>, _window: &mut Window, cx: &mut App) {
|
||||
@@ -699,27 +894,21 @@ impl CompletionProvider for RustStyleCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn completion_replace_range(snapshot: &BufferSnapshot, anchor: &Anchor) -> Option<Range<Anchor>> {
|
||||
let point = anchor.to_point(snapshot);
|
||||
let offset = point.to_offset(snapshot);
|
||||
let line_start = Point::new(point.row, 0).to_offset(snapshot);
|
||||
let line_end = Point::new(point.row, snapshot.line_len(point.row)).to_offset(snapshot);
|
||||
let mut lines = snapshot.text_for_range(line_start..line_end).lines();
|
||||
let line = lines.next()?;
|
||||
|
||||
let start_in_line = &line[..offset - line_start]
|
||||
.rfind(|c| is_not_identifier_char(c) && c != '.')
|
||||
.map(|ix| ix + 1)
|
||||
.unwrap_or(0);
|
||||
let end_in_line = &line[offset - line_start..]
|
||||
.rfind(|c| is_not_identifier_char(c) && c != '(' && c != ')')
|
||||
.unwrap_or(line_end - line_start);
|
||||
|
||||
if end_in_line > start_in_line {
|
||||
let replace_start = snapshot.anchor_before(line_start + start_in_line);
|
||||
let replace_end = snapshot.anchor_after(line_start + end_in_line);
|
||||
Some(replace_start..replace_end)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
fn completion_replace_range(
|
||||
snapshot: &text::BufferSnapshot,
|
||||
anchor: &Anchor,
|
||||
) -> Option<Range<Anchor>> {
|
||||
let offset = anchor.to_offset(snapshot);
|
||||
let start: usize = snapshot
|
||||
.reversed_chars_at(offset)
|
||||
.take_until(|c| is_not_identifier_char(*c))
|
||||
.map(|char| char.len_utf8())
|
||||
.sum();
|
||||
let end: usize = snapshot
|
||||
.chars_at(offset)
|
||||
.take_until(|c| is_not_identifier_char(*c))
|
||||
.skip(1)
|
||||
.map(|char| char.len_utf8())
|
||||
.sum();
|
||||
Some(snapshot.anchor_after(offset - start)..snapshot.anchor_before(offset + end))
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -238,6 +238,14 @@ impl ThemeFamily {
|
||||
.map(Into::into)
|
||||
.unwrap_or_default();
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
use collections::HashMap;
|
||||
let mut color_to_count: HashMap<Hsla, u32> = HashMap::default();
|
||||
make_colors_unique(&mut refined_status_colors, &mut color_to_count);
|
||||
make_colors_unique(&mut refined_theme_colors, &mut color_to_count);
|
||||
}
|
||||
|
||||
Theme {
|
||||
id: uuid::Uuid::new_v4().to_string(),
|
||||
name: theme.name.clone().into(),
|
||||
@@ -280,6 +288,33 @@ pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFam
|
||||
theme_family
|
||||
}
|
||||
|
||||
/// Make theme colors unique via tiny adjustments to luminance. This is a hack to provide color
|
||||
/// provenance information to the inspector (which is only available in debug builds).
|
||||
///
|
||||
/// Since luminance is an f32, `next_down` / `next_up` should rarely affect RGB.
|
||||
#[cfg(debug_assertions)]
|
||||
fn make_colors_unique<T, F>(colors: &mut T, color_to_count: &mut collections::HashMap<Hsla, u32>)
|
||||
where
|
||||
F: Copy + strum::IntoEnumIterator,
|
||||
T: util::FieldAccessByEnum<Hsla, Field = F>,
|
||||
{
|
||||
for field in F::iter() {
|
||||
let mut color = *colors.get_field_by_enum(field);
|
||||
let count = color_to_count.entry(color).or_default();
|
||||
*count += 1;
|
||||
if color.l > 0.5 {
|
||||
for _ in 0..*count {
|
||||
color.l = color.l.next_down();
|
||||
}
|
||||
} else {
|
||||
for _ in 0..*count {
|
||||
color.l = color.l.next_up();
|
||||
}
|
||||
}
|
||||
colors.set_field_by_enum(field, color);
|
||||
}
|
||||
}
|
||||
|
||||
/// A theme is the primary mechanism for defining the appearance of the UI.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Theme {
|
||||
|
||||
@@ -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,6 +31,7 @@ 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};
|
||||
|
||||
@@ -1104,6 +1105,53 @@ pub fn some_or_debug_panic<T>(option: Option<T>) -> Option<T> {
|
||||
option
|
||||
}
|
||||
|
||||
/// Trait that provides field access specified by enum.
|
||||
///
|
||||
/// The derive macro only supports structs with named fields that all have the same type.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[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:
|
||||
/// ```ignore
|
||||
/// #[derive(Debug, Clone, Copy, EnumIter, AsRefStr)]
|
||||
/// #[strum(serialize_all = "snake_case")]
|
||||
/// enum ColorField {
|
||||
/// Background,
|
||||
/// Foreground,
|
||||
/// BorderColor,
|
||||
/// }
|
||||
///
|
||||
/// impl FieldAccessByEnum<Hsla> for Theme {
|
||||
/// type Field = ColorField;
|
||||
/// // ... get and set methods
|
||||
/// }
|
||||
/// ```
|
||||
pub trait FieldAccessByEnum<V> {
|
||||
/// The enum type representing the available fields
|
||||
type Field;
|
||||
|
||||
/// Get a reference to the value of the specified field
|
||||
fn get_field_by_enum(&self, field: Self::Field) -> &V;
|
||||
/// Set the value of the specified field
|
||||
fn set_field_by_enum(&mut self, field: Self::Field, value: V);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -14,9 +14,11 @@ proc-macro = true
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
convert_case.workspace = true
|
||||
quote.workspace = true
|
||||
syn.workspace = true
|
||||
perf.workspace = true
|
||||
proc-macro2.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
#![cfg_attr(not(target_os = "windows"), allow(unused))]
|
||||
#![allow(clippy::test_attr_in_doctest)]
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use perf::*;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{ItemFn, LitStr, parse_macro_input, parse_quote};
|
||||
use proc_macro2::TokenStream as TokenStream2;
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::{
|
||||
Data, DeriveInput, Expr, ExprArray, ExprLit, Fields, ItemFn, Lit, LitStr, MetaNameValue, Token,
|
||||
parse_macro_input, parse_quote, 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
|
||||
@@ -23,19 +27,26 @@ use syn::{ItemFn, LitStr, parse_macro_input, parse_quote};
|
||||
#[proc_macro]
|
||||
pub fn path(input: TokenStream) -> TokenStream {
|
||||
let path = parse_macro_input!(input as LitStr);
|
||||
let mut path = path.value();
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let mut path = path.value();
|
||||
path = path.replace("/", "\\");
|
||||
if path.starts_with("\\") {
|
||||
path = format!("C:{}", path);
|
||||
}
|
||||
return TokenStream::from(quote! {
|
||||
#path
|
||||
});
|
||||
}
|
||||
|
||||
TokenStream::from(quote! {
|
||||
#path
|
||||
})
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
{
|
||||
let path = path.value();
|
||||
return TokenStream::from(quote! {
|
||||
#path
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// This macro replaces the path prefix `file:///` with `file:///C:/` for Windows.
|
||||
@@ -280,3 +291,121 @@ pub fn perf(our_attr: TokenStream, input: TokenStream) -> TokenStream {
|
||||
.flat_map(|f| TokenStream::from(f.into_token_stream()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[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"),
|
||||
}
|
||||
} else {
|
||||
if let Some(ident) = name_value.path.get_ident() {
|
||||
panic!("Unrecognized argument name {}", ident);
|
||||
} else {
|
||||
panic!("Unrecognized argument {:?}", name_value.path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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<#field_value_type> for #struct_name {
|
||||
type Field = #enum_ident;
|
||||
|
||||
fn get_field_by_enum(&self, field: Self::Field) -> &#field_value_type {
|
||||
match field {
|
||||
#(#get_match_arms)*
|
||||
}
|
||||
}
|
||||
|
||||
fn set_field_by_enum(&mut self, field: Self::Field, value: #field_value_type) {
|
||||
match field {
|
||||
#(#set_match_arms)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user