mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-26 02:18:11 +08:00
Generate a visualization of the editor's hierarchical message system tree (#2499)
* Feat: implement the hierarchical tree for visualization * rename HierarchicalTree trait function * feat: change the HierarchicalTree from String to DebugMessageTree struct * Nits * feat: impliment proc macro to extract field from messagedata structs * update the hierarchical-tree for hanlder data * feat: added message handler struct to hierarchical tree * feat: add the line number to message handler struct * feat: added handler path to tree and NITS * clean the white spaces in type string * fixes some white spaces * feat: added path to message enum in hierarchical tree * feat: add file creation of hierarchical message system tree * cleanup * Code review * Add todo comment for deferred change --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
co-authored by
Keavon Chambers
parent
d0e1d8982f
commit
00236c8136
@@ -61,7 +61,7 @@ pub fn combined_message_attrs_impl(attr: TokenStream, input_item: TokenStream) -
|
||||
<#parent as ToDiscriminant>::Discriminant
|
||||
};
|
||||
|
||||
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant, TransitiveChild)] });
|
||||
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant, TransitiveChild, HierarchicalTree)] });
|
||||
input.attrs.push(syn::parse_quote! { #[parent(#parent, #parent::#variant)] });
|
||||
if parent_is_top {
|
||||
input.attrs.push(syn::parse_quote! { #[parent_is_top] });
|
||||
@@ -97,7 +97,7 @@ pub fn combined_message_attrs_impl(attr: TokenStream, input_item: TokenStream) -
|
||||
fn top_level_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
|
||||
let mut input = syn::parse2::<ItemEnum>(input_item)?;
|
||||
|
||||
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant)] });
|
||||
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant, HierarchicalTree)] });
|
||||
input.attrs.push(syn::parse_quote! { #[discriminant_attr(derive(Debug, Copy, Clone, PartialEq, Eq, Hash, AsMessage))] });
|
||||
|
||||
for var in &mut input.variants {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::helpers::clean_rust_type_syntax;
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{ToTokens, format_ident, quote};
|
||||
use syn::{Data, DeriveInput, Fields, Type, parse2};
|
||||
|
||||
pub fn derive_extract_field_impl(input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let input = parse2::<DeriveInput>(input)?;
|
||||
let struct_name = &input.ident;
|
||||
let generics = &input.generics;
|
||||
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
|
||||
|
||||
let fields = match &input.data {
|
||||
Data::Struct(data) => match &data.fields {
|
||||
Fields::Named(fields) => &fields.named,
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "ExtractField only works on structs with named fields")),
|
||||
},
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "ExtractField only works on structs")),
|
||||
};
|
||||
|
||||
let mut field_line = Vec::new();
|
||||
// Extract field names and types as strings at compile time
|
||||
let field_info = fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
let ident = field.ident.as_ref().unwrap();
|
||||
let name = ident.to_string();
|
||||
let ty = clean_rust_type_syntax(field.ty.to_token_stream().to_string());
|
||||
let line = ident.span().start().line;
|
||||
field_line.push(line);
|
||||
(name, ty)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let field_str = field_info.into_iter().map(|(name, ty)| (format!("{}: {}", name, ty)));
|
||||
|
||||
let res = quote! {
|
||||
impl #impl_generics #struct_name #ty_generics #where_clause {
|
||||
pub fn field_types() -> Vec<(String, usize)> {
|
||||
vec![
|
||||
#((String::from(#field_str), #field_line)),*
|
||||
]
|
||||
}
|
||||
|
||||
pub fn print_field_types() {
|
||||
for (field, line) in Self::field_types() {
|
||||
println!("{} at line {}", field, line);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path() -> &'static str {
|
||||
file!()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -42,6 +42,58 @@ pub fn two_segment_path(left_ident: Ident, right_ident: Ident) -> Path {
|
||||
Path { leading_colon: None, segments }
|
||||
}
|
||||
|
||||
pub fn clean_rust_type_syntax(input: String) -> String {
|
||||
let mut result = String::new();
|
||||
let mut chars = input.chars().peekable();
|
||||
|
||||
while let Some(c) = chars.next() {
|
||||
match c {
|
||||
'&' => {
|
||||
result.push('&');
|
||||
while let Some(' ') = chars.peek() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
'<' => {
|
||||
while let Some(' ') = result.chars().rev().next() {
|
||||
result.pop();
|
||||
}
|
||||
result.push('<');
|
||||
while let Some(' ') = chars.peek() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
'>' => {
|
||||
while let Some(' ') = result.chars().rev().next() {
|
||||
result.pop();
|
||||
}
|
||||
result.push('>');
|
||||
while let Some(' ') = chars.peek() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
':' => {
|
||||
if let Some(':') = chars.peek() {
|
||||
while let Some(' ') = result.chars().rev().next() {
|
||||
result.pop();
|
||||
}
|
||||
}
|
||||
result.push(':');
|
||||
chars.next();
|
||||
result.push(':');
|
||||
while let Some(' ') = chars.peek() {
|
||||
chars.next();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
result.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{Data, DeriveInput, Fields, Type, parse2};
|
||||
|
||||
pub fn generate_hierarchical_tree(input: TokenStream) -> syn::Result<TokenStream> {
|
||||
let input = parse2::<DeriveInput>(input)?;
|
||||
let input_type = &input.ident;
|
||||
|
||||
let data = match &input.data {
|
||||
Data::Enum(data) => data,
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "Tried to derive HierarchicalTree for non-enum")),
|
||||
};
|
||||
|
||||
let build_message_tree = data.variants.iter().map(|variant| {
|
||||
let variant_type = &variant.ident;
|
||||
|
||||
let has_child = variant
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().get_ident().is_some_and(|ident| ident == "sub_discriminant" || ident == "child"));
|
||||
|
||||
if has_child {
|
||||
if let Fields::Unnamed(fields) = &variant.fields {
|
||||
let field_type = &fields.unnamed.first().unwrap().ty;
|
||||
quote! {
|
||||
{
|
||||
let mut variant_tree = DebugMessageTree::new(stringify!(#variant_type));
|
||||
let field_name = stringify!(#field_type);
|
||||
const message_string: &str = "Message";
|
||||
if message_string == &field_name[field_name.len().saturating_sub(message_string.len())..] {
|
||||
// The field is a Message type, recursively build its tree
|
||||
let sub_tree = #field_type::build_message_tree();
|
||||
variant_tree.add_variant(sub_tree);
|
||||
}
|
||||
message_tree.add_variant(variant_tree);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
message_tree.add_variant(DebugMessageTree::new(stringify!(#variant_type)));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
message_tree.add_variant(DebugMessageTree::new(stringify!(#variant_type)));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let res = quote! {
|
||||
impl HierarchicalTree for #input_type {
|
||||
fn build_message_tree() -> DebugMessageTree {
|
||||
let mut message_tree = DebugMessageTree::new(stringify!(#input_type));
|
||||
#(#build_message_tree)*
|
||||
let message_handler_str = #input_type::message_handler_str();
|
||||
if message_handler_str.fields().len() > 0 {
|
||||
message_tree.add_message_handler_field(message_handler_str);
|
||||
}
|
||||
|
||||
let message_handler_data_str = #input_type::message_handler_data_str();
|
||||
if message_handler_data_str.fields().len() > 0 {
|
||||
message_tree.add_message_handler_data_field(message_handler_data_str);
|
||||
}
|
||||
|
||||
message_tree.set_path(file!());
|
||||
|
||||
message_tree
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
@@ -3,17 +3,23 @@
|
||||
mod as_message;
|
||||
mod combined_message_attrs;
|
||||
mod discriminant;
|
||||
mod extract_fields;
|
||||
mod helper_structs;
|
||||
mod helpers;
|
||||
mod hierarchical_tree;
|
||||
mod hint;
|
||||
mod message_handler_data_attr;
|
||||
mod transitive_child;
|
||||
mod widget_builder;
|
||||
|
||||
use crate::as_message::derive_as_message_impl;
|
||||
use crate::combined_message_attrs::combined_message_attrs_impl;
|
||||
use crate::discriminant::derive_discriminant_impl;
|
||||
use crate::extract_fields::derive_extract_field_impl;
|
||||
use crate::helper_structs::AttrInnerSingleString;
|
||||
use crate::hierarchical_tree::generate_hierarchical_tree;
|
||||
use crate::hint::derive_hint_impl;
|
||||
use crate::message_handler_data_attr::message_handler_data_attr_impl;
|
||||
use crate::transitive_child::derive_transitive_child_impl;
|
||||
use crate::widget_builder::derive_widget_builder_impl;
|
||||
use proc_macro::TokenStream;
|
||||
@@ -281,6 +287,21 @@ pub fn derive_widget_builder(input_item: TokenStream) -> TokenStream {
|
||||
TokenStream::from(derive_widget_builder_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[proc_macro_derive(HierarchicalTree)]
|
||||
pub fn derive_hierarchical_tree(input_item: TokenStream) -> TokenStream {
|
||||
TokenStream::from(generate_hierarchical_tree(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[proc_macro_derive(ExtractField)]
|
||||
pub fn derive_extract_field(input_item: TokenStream) -> TokenStream {
|
||||
TokenStream::from(derive_extract_field_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[proc_macro_attribute]
|
||||
pub fn message_handler_data(attr: TokenStream, input_item: TokenStream) -> TokenStream {
|
||||
TokenStream::from(message_handler_data_attr_impl(attr.into(), input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
use crate::helpers::{call_site_ident, clean_rust_type_syntax};
|
||||
use proc_macro2::{Span, TokenStream};
|
||||
use quote::{ToTokens, quote};
|
||||
use syn::{ItemImpl, Type, parse2, spanned::Spanned};
|
||||
|
||||
pub fn message_handler_data_attr_impl(attr: TokenStream, input_item: TokenStream) -> syn::Result<TokenStream> {
|
||||
// Parse the input as an impl block
|
||||
let impl_block = parse2::<ItemImpl>(input_item.clone())?;
|
||||
|
||||
let self_ty = &impl_block.self_ty;
|
||||
|
||||
let path = match &**self_ty {
|
||||
Type::Path(path) => &path.path,
|
||||
_ => return Err(syn::Error::new(Span::call_site(), "Expected impl implementation")),
|
||||
};
|
||||
|
||||
let input_type = path.segments.last().map(|s| &s.ident).unwrap();
|
||||
|
||||
// Extract the message type from the trait path
|
||||
let trait_path = match &impl_block.trait_ {
|
||||
Some((_, path, _)) => path,
|
||||
None => return Err(syn::Error::new(Span::call_site(), "Expected trait implementation")),
|
||||
};
|
||||
|
||||
// Get the trait generics (should be MessageHandler<M, D>)
|
||||
if let Some(segment) = trait_path.segments.last() {
|
||||
if segment.ident != "MessageHandler" {
|
||||
return Err(syn::Error::new(segment.ident.span(), "Expected MessageHandler trait"));
|
||||
}
|
||||
if let syn::PathArguments::AngleBracketed(args) = &segment.arguments {
|
||||
if args.args.len() >= 2 {
|
||||
// Extract the message type (M) and data type (D) from the trait params
|
||||
let message_type = &args.args[0];
|
||||
let data_type = &args.args[1];
|
||||
|
||||
// Check if the attribute is "CustomData"
|
||||
let is_custom_data = attr.to_string().contains("CustomData");
|
||||
|
||||
let impl_item = match data_type {
|
||||
syn::GenericArgument::Type(t) => {
|
||||
match t {
|
||||
syn::Type::Path(type_path) if !type_path.path.segments.is_empty() => {
|
||||
// Get just the base identifier (ToolMessageData) without generics
|
||||
let type_name = &type_path.path.segments.first().unwrap().ident;
|
||||
|
||||
if is_custom_data {
|
||||
quote! {
|
||||
#input_item
|
||||
impl #message_type {
|
||||
pub fn message_handler_data_str() -> MessageData {
|
||||
custom_data()
|
||||
}
|
||||
pub fn message_handler_str() -> MessageData {
|
||||
MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#input_item
|
||||
impl #message_type {
|
||||
pub fn message_handler_data_str() -> MessageData
|
||||
{
|
||||
MessageData::new(format!("{}",stringify!(#type_name)), #type_name::field_types(), #type_name::path())
|
||||
|
||||
}
|
||||
pub fn message_handler_str() -> MessageData {
|
||||
MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
syn::Type::Tuple(_) => quote! {
|
||||
#input_item
|
||||
impl #message_type {
|
||||
pub fn message_handler_str() -> MessageData {
|
||||
MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path())
|
||||
}
|
||||
}
|
||||
},
|
||||
syn::Type::Reference(type_reference) => {
|
||||
let message_type = call_site_ident(format!("{input_type}Message"));
|
||||
let type_ident = match &*type_reference.elem {
|
||||
syn::Type::Path(type_path) => &type_path.path.segments.first().unwrap().ident,
|
||||
_ => return Err(syn::Error::new(type_reference.elem.span(), "Expected type path")),
|
||||
};
|
||||
let tr = clean_rust_type_syntax(type_reference.to_token_stream().to_string());
|
||||
quote! {
|
||||
#input_item
|
||||
impl #message_type {
|
||||
pub fn message_handler_data_str() -> MessageData {
|
||||
MessageData::new(format!("{}", #tr),#type_ident::field_types(), #type_ident::path())
|
||||
}
|
||||
|
||||
pub fn message_handler_str() -> MessageData {
|
||||
MessageData::new(format!("{}",stringify!(#input_type)), #input_type::field_types(), #input_type::path())
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => return Err(syn::Error::new(t.span(), "Unsupported type format")),
|
||||
}
|
||||
}
|
||||
|
||||
_ => quote! {
|
||||
#input_item
|
||||
},
|
||||
};
|
||||
return Ok(impl_item);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(input_item)
|
||||
}
|
||||
Reference in New Issue
Block a user