Restructure project directories (#333)

`/client/web` -> `/frontend`
`/client/cli` -> *delete for now*
`/client/native` -> *delete for now*
`/core/editor` -> `/editor`
`/core/document` -> `/graphene`
`/core/renderer` -> `/charcoal`
`/core/proc-macro` -> `/proc-macros` *(now plural)*
This commit is contained in:
Keavon Chambers
2021-08-07 05:17:18 -07:00
parent 434695d578
commit 53ad105f57
239 changed files with 197 additions and 224 deletions

19
proc-macros/Cargo.toml Normal file
View File

@@ -0,0 +1,19 @@
[package]
name = "graphite-proc-macros"
version = "0.1.0"
authors = ["Graphite Authors <contact@graphite.design>"]
edition = "2018"
publish = false
[lib]
path = "src/lib.rs"
proc-macro = true
[dependencies]
proc-macro2 = "1.0.26"
syn = { version = "1.0.68", features = ["full"] }
quote = "1.0.9"
[dev-dependencies.editor]
path = "../editor"
package = "graphite-editor"

View File

@@ -0,0 +1,55 @@
use proc_macro2::{Span, TokenStream};
use syn::{Data, DeriveInput};
pub fn derive_as_message_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let data = match input.data {
Data::Enum(data) => data,
_ => return Err(syn::Error::new(Span::call_site(), "Tried to derive AsMessage for non-enum")),
};
let input_type = input.ident;
let (globs, names) = data
.variants
.iter()
.map(|var| {
let var_name = &var.ident;
let var_name_s = var.ident.to_string();
if var.attrs.iter().any(|a| a.path.is_ident("child")) {
(
quote::quote! {
#input_type::#var_name(child)
},
quote::quote! {
format!("{}.{}", #var_name_s, child.local_name())
},
)
} else {
(
quote::quote! {
#input_type::#var_name { .. }
},
quote::quote! {
#var_name_s.to_string()
},
)
}
})
.unzip::<_, _, Vec<_>, Vec<_>>();
let res = quote::quote! {
impl AsMessage for #input_type {
fn local_name(self) -> String {
match self {
#(
#globs => #names
),*
}
}
}
};
Ok(res)
}

View File

@@ -0,0 +1,124 @@
use crate::helpers::call_site_ident;
use proc_macro2::Ident;
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::parse::{Parse, ParseStream};
use syn::Token;
use syn::{ItemEnum, TypePath};
struct MessageArgs {
pub _top_parent: TypePath,
pub _comma1: Token![,],
pub parent: TypePath,
pub _comma2: Token![,],
pub variant: Ident,
}
impl Parse for MessageArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
_top_parent: input.parse()?,
_comma1: input.parse()?,
parent: input.parse()?,
_comma2: input.parse()?,
variant: input.parse()?,
})
}
}
struct TopLevelMessageArgs {
pub parent: TypePath,
pub _comma2: Token![,],
pub variant: Ident,
}
impl Parse for TopLevelMessageArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
parent: input.parse()?,
_comma2: input.parse()?,
variant: input.parse()?,
})
}
}
pub fn combined_message_attrs_impl(attr: TokenStream, input_item: TokenStream) -> syn::Result<TokenStream> {
if attr.is_empty() {
return top_level_impl(input_item);
}
let mut input = syn::parse2::<ItemEnum>(input_item)?;
let (parent_is_top, parent, variant) = match syn::parse2::<MessageArgs>(attr.clone()) {
Ok(x) => (false, x.parent, x.variant),
Err(_) => {
let x = syn::parse2::<TopLevelMessageArgs>(attr)?;
(true, x.parent, x.variant)
}
};
let parent_discriminant = quote::quote! {
<#parent as ToDiscriminant>::Discriminant
};
input.attrs.push(syn::parse_quote! { #[derive(ToDiscriminant, TransitiveChild)] });
input.attrs.push(syn::parse_quote! { #[parent(#parent, #parent::#variant)] });
if parent_is_top {
input.attrs.push(syn::parse_quote! { #[parent_is_top] });
}
input
.attrs
.push(syn::parse_quote! { #[discriminant_attr(derive(Debug, Copy, Clone, PartialEq, Eq, Hash, AsMessage, TransitiveChild))] });
input
.attrs
.push(syn::parse_quote! { #[discriminant_attr(parent(#parent_discriminant, #parent_discriminant::#variant))] });
if parent_is_top {
input.attrs.push(syn::parse_quote! { #[discriminant_attr(parent_is_top)] });
}
for var in &mut input.variants {
if let Some(attr) = var.attrs.iter_mut().find(|a| a.path.is_ident("child")) {
let last_segment = attr.path.segments.last_mut().unwrap();
last_segment.ident = call_site_ident("sub_discriminant");
var.attrs.push(syn::parse_quote! {
#[discriminant_attr(child)]
});
}
}
Ok(input.into_token_stream())
}
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! { #[discriminant_attr(derive(Debug, Copy, Clone, PartialEq, Eq, Hash, AsMessage))] });
for var in &mut input.variants {
if let Some(attr) = var.attrs.iter_mut().find(|a| a.path.is_ident("child")) {
let last_segment = attr.path.segments.last_mut().unwrap();
last_segment.ident = call_site_ident("sub_discriminant");
var.attrs.push(syn::parse_quote! {
#[discriminant_attr(child)]
});
}
}
let input_type = &input.ident;
let discriminant = call_site_ident(format!("{}Discriminant", input_type));
Ok(quote::quote! {
#input
impl TransitiveChild for #input_type {
type TopParent = Self;
type Parent = Self;
}
impl TransitiveChild for #discriminant {
type TopParent = Self;
type Parent = Self;
}
})
}

View File

@@ -0,0 +1,139 @@
use crate::helper_structs::ParenthesizedTokens;
use crate::helpers::call_site_ident;
use proc_macro2::{Ident, Span, TokenStream};
use syn::spanned::Spanned;
use syn::{Attribute, Data, DeriveInput, Field, Fields, ItemEnum};
pub fn derive_discriminant_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let mut data = match input.data {
Data::Enum(data) => data,
_ => return Err(syn::Error::new(Span::call_site(), "Tried to derive a discriminant for non-enum")),
};
let mut is_sub_discriminant = vec![];
let mut attr_errs = vec![];
for var in &mut data.variants {
if var.attrs.iter().any(|a| a.path.is_ident("sub_discriminant")) {
match var.fields.len() {
1 => {
let Field { ty, .. } = var.fields.iter_mut().next().unwrap();
*ty = syn::parse_quote! {
<#ty as ToDiscriminant>::Discriminant
};
is_sub_discriminant.push(true);
}
n => unimplemented!("#[sub_discriminant] on variants with {} fields is not supported (for now)", n),
}
} else {
var.fields = Fields::Unit;
is_sub_discriminant.push(false);
}
let mut retain = vec![];
for (i, a) in var.attrs.iter_mut().enumerate() {
if a.path.is_ident("discriminant_attr") {
match syn::parse2::<ParenthesizedTokens>(a.tokens.clone()) {
Ok(ParenthesizedTokens { tokens, .. }) => {
let attr: Attribute = syn::parse_quote! {
#[#tokens]
};
*a = attr;
retain.push(i);
}
Err(e) => {
attr_errs.push(syn::Error::new(a.span(), e));
}
}
}
}
var.attrs = var.attrs.iter().enumerate().filter_map(|(i, x)| retain.contains(&i).then(|| x.clone())).collect();
}
let attrs = input
.attrs
.iter()
.cloned()
.filter_map(|a| {
let a_span = a.span();
a.path
.is_ident("discriminant_attr")
.then(|| match syn::parse2::<ParenthesizedTokens>(a.tokens) {
Ok(ParenthesizedTokens { tokens, .. }) => {
let attr: Attribute = syn::parse_quote! {
#[#tokens]
};
Some(attr)
}
Err(e) => {
attr_errs.push(syn::Error::new(a_span, e));
None
}
})
.and_then(|opt| opt)
})
.collect::<Vec<Attribute>>();
if !attr_errs.is_empty() {
return Err(attr_errs
.into_iter()
.reduce(|mut l, r| {
l.combine(r);
l
})
.unwrap());
}
let discriminant = ItemEnum {
attrs,
vis: input.vis,
enum_token: data.enum_token,
ident: call_site_ident(format!("{}Discriminant", input.ident)),
generics: input.generics,
brace_token: data.brace_token,
variants: data.variants,
};
let input_type = &input.ident;
let discriminant_type = &discriminant.ident;
let variant = &discriminant.variants.iter().map(|var| &var.ident).collect::<Vec<&Ident>>();
let (pattern, value) = is_sub_discriminant
.into_iter()
.map(|b| {
(
if b {
quote::quote! { (x) }
} else {
quote::quote! { { .. } }
},
b.then(|| quote::quote! { (x.to_discriminant()) }).unwrap_or_default(),
)
})
.unzip::<_, _, Vec<_>, Vec<_>>();
let res = quote::quote! {
#discriminant
impl ToDiscriminant for #input_type {
type Discriminant = #discriminant_type;
fn to_discriminant(&self) -> #discriminant_type {
match self {
#(
#input_type::#variant #pattern => #discriminant_type::#variant #value
),*
}
}
}
impl From<&#input_type> for #discriminant_type {
fn from(x: &#input_type) -> #discriminant_type {
x.to_discriminant()
}
}
};
Ok(res)
}

View File

@@ -0,0 +1,207 @@
use proc_macro2::{Ident, TokenStream};
use std::collections::HashMap;
use syn::parse::{Parse, ParseStream};
use syn::punctuated::Punctuated;
use syn::token::Paren;
use syn::{parenthesized, LitStr, Token};
pub struct IdentList {
pub parts: Punctuated<Ident, Token![,]>,
}
impl Parse for IdentList {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
parts: Punctuated::parse_terminated(&content)?,
})
}
}
/// Parses `("some text")`
pub struct AttrInnerSingleString {
_paren_token: Paren,
pub content: LitStr,
}
impl Parse for AttrInnerSingleString {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
_paren_token,
content: content.parse()?,
})
}
}
/// Parses `key="value"`
pub struct KeyEqString {
key: Ident,
_eq_token: Token![=],
lit: LitStr,
}
impl Parse for KeyEqString {
fn parse(input: ParseStream) -> syn::Result<Self> {
Ok(Self {
key: input.parse()?,
_eq_token: input.parse()?,
lit: input.parse()?,
})
}
}
/// Parses `(key="value", key="value", …)`
pub struct AttrInnerKeyStringMap {
_paren_token: Paren,
parts: Punctuated<KeyEqString, Token![,]>,
}
impl Parse for AttrInnerKeyStringMap {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let _paren_token = parenthesized!(content in input);
Ok(Self {
_paren_token,
parts: Punctuated::parse_terminated(&content)?,
})
}
}
impl AttrInnerKeyStringMap {
pub fn multi_into_iter(iter: impl IntoIterator<Item = Self>) -> impl Iterator<Item = (Ident, Vec<LitStr>)> {
use std::collections::hash_map::Entry;
let mut res = Vec::<(Ident, Vec<LitStr>)>::new();
let mut idx = HashMap::<Ident, usize>::new();
for part in iter.into_iter().flat_map(|x: Self| x.parts) {
match idx.entry(part.key) {
Entry::Occupied(occ) => {
res[*occ.get()].1.push(part.lit);
}
Entry::Vacant(vac) => {
let ident = vac.key().clone();
vac.insert(res.len());
res.push((ident, vec![part.lit]));
}
}
}
res.into_iter()
}
}
/// Parses `(left, right)`
pub struct Pair<F, S> {
pub paren_token: Paren,
pub first: F,
pub sep: Token![,],
pub second: S,
}
impl<F, S> Parse for Pair<F, S>
where
F: Parse,
S: Parse,
{
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let paren_token = parenthesized!(content in input);
Ok(Self {
paren_token,
first: content.parse()?,
sep: content.parse()?,
second: content.parse()?,
})
}
}
/// parses `(...)`
pub struct ParenthesizedTokens {
pub paren: Paren,
pub tokens: TokenStream,
}
impl Parse for ParenthesizedTokens {
fn parse(input: ParseStream) -> syn::Result<Self> {
let content;
let paren = parenthesized!(content in input);
Ok(Self { paren, tokens: content.parse()? })
}
}
/// parses a comma-delimeted list of `T`s with optional trailing comma
pub struct SimpleCommaDelimeted<T>(pub Vec<T>);
impl<T: Parse> Parse for SimpleCommaDelimeted<T> {
fn parse(input: ParseStream) -> syn::Result<Self> {
let punct = Punctuated::<T, Token![,]>::parse_terminated(input)?;
Ok(Self(punct.into_iter().collect()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn attr_inner_single_string() {
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
("a string literal")
});
assert!(res.is_ok());
assert_eq!(res.ok().unwrap().content.value(), "a string literal");
let res = syn::parse2::<AttrInnerSingleString>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
#[test]
fn key_eq_string() {
let res = syn::parse2::<KeyEqString>(quote::quote! {
key="value"
});
assert!(res.is_ok());
let res = res.ok().unwrap();
assert_eq!(res.key, "key");
assert_eq!(res.lit.value(), "value");
let res = syn::parse2::<KeyEqString>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
#[test]
fn attr_inner_key_string_map() {
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
(key="value", key2="value2")
});
assert!(res.is_ok());
let res = res.ok().unwrap();
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
assert_eq!(item.key, k);
assert_eq!(item.lit.value(), v);
}
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
(key="value", key2="value2",)
});
assert!(res.is_ok());
let res = res.ok().unwrap();
for (item, (k, v)) in res.parts.into_iter().zip(vec![("key", "value"), ("key2", "value2")]) {
assert_eq!(item.key, k);
assert_eq!(item.lit.value(), v);
}
let res = syn::parse2::<AttrInnerKeyStringMap>(quote::quote! {
wrong, "stuff"
});
assert!(res.is_err());
}
}

View File

@@ -0,0 +1,67 @@
use proc_macro2::{Ident, Span};
use syn::punctuated::Punctuated;
use syn::{Path, PathArguments, PathSegment, Token};
/// Returns `Ok(Vec<T>)` if all items are `Ok(T)`, else returns a combination of every error encountered (not just the first one)
pub fn fold_error_iter<T>(iter: impl Iterator<Item = syn::Result<T>>) -> syn::Result<Vec<T>> {
iter.fold(Ok(vec![]), |acc, x| match acc {
Ok(mut v) => x.map(|x| {
v.push(x);
v
}),
Err(mut e) => match x {
Ok(_) => Err(e),
Err(e2) => {
e.combine(e2);
Err(e)
}
},
})
}
/// Creates an ident at the call site
pub fn call_site_ident<S: AsRef<str>>(s: S) -> Ident {
Ident::new(s.as_ref(), Span::call_site())
}
/// Creates the path `left::right` from the idents `left` and `right`
pub fn two_segment_path(left_ident: Ident, right_ident: Ident) -> Path {
let mut segments: Punctuated<PathSegment, Token![::]> = Punctuated::new();
segments.push(PathSegment {
ident: left_ident,
arguments: PathArguments::None,
});
segments.push(PathSegment {
ident: right_ident,
arguments: PathArguments::None,
});
Path { leading_colon: None, segments }
}
#[cfg(test)]
mod tests {
use super::*;
use quote::ToTokens;
use syn::spanned::Spanned;
#[test]
fn test_fold_error_iter() {
let res = fold_error_iter(vec![Ok(()), Ok(())].into_iter());
assert!(res.is_ok());
let _span = quote::quote! { "" }.span();
let res = fold_error_iter(vec![Ok(()), Err(syn::Error::new(_span, "err1")), Err(syn::Error::new(_span, "err2"))].into_iter());
assert!(res.is_err());
let err = res.unwrap_err();
let mut check_err = syn::Error::new(_span, "err1");
check_err.combine(syn::Error::new(_span, "err2"));
assert_eq!(err.to_compile_error().to_string(), check_err.to_compile_error().to_string());
}
#[test]
fn test_two_path() {
let _span = quote::quote! { "" }.span();
assert_eq!(two_segment_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b");
}
}

85
proc-macros/src/hint.rs Normal file
View File

@@ -0,0 +1,85 @@
use crate::helper_structs::AttrInnerKeyStringMap;
use crate::helpers::{fold_error_iter, two_segment_path};
use proc_macro2::{Span, TokenStream as TokenStream2};
use syn::{Attribute, Data, DeriveInput, LitStr, Variant};
fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec<LitStr>, Vec<LitStr>)> {
fold_error_iter(
attrs
.iter()
.filter(|a| a.path.get_ident().map_or(false, |i| i == "hint"))
.map(|attr| syn::parse2::<AttrInnerKeyStringMap>(attr.tokens.clone())),
)
.and_then(|v: Vec<AttrInnerKeyStringMap>| {
fold_error_iter(AttrInnerKeyStringMap::multi_into_iter(v).map(|(k, mut v)| match v.len() {
0 => panic!("internal error: a key without values was somehow inserted into the hashmap"),
1 => {
let single_val = v.pop().unwrap();
Ok((LitStr::new(&k.to_string(), Span::call_site()), single_val))
}
_ => {
// the first value is ok, the other ones should error
let after_first = v.into_iter().skip(1);
// this call to fold_error_iter will always return Err with a combined error
fold_error_iter(after_first.map(|lit| Err(syn::Error::new(lit.span(), format!("value for key {} was already given", k))))).map(|_: Vec<()>| unreachable!())
}
}))
})
.map(|v| v.into_iter().unzip())
}
pub fn derive_hint_impl(input_item: TokenStream2) -> syn::Result<TokenStream2> {
let input = syn::parse2::<DeriveInput>(input_item)?;
let ident = input.ident;
match input.data {
Data::Enum(data) => {
let variants = data.variants.iter().map(|var: &Variant| two_segment_path(ident.clone(), var.ident.clone())).collect::<Vec<_>>();
let hint_result = fold_error_iter(data.variants.into_iter().map(|var: Variant| parse_hint_helper_attrs(&var.attrs)));
hint_result.map(|hints: Vec<(Vec<LitStr>, Vec<LitStr>)>| {
let (keys, values): (Vec<Vec<LitStr>>, Vec<Vec<LitStr>>) = hints.into_iter().unzip();
let cap: Vec<usize> = keys.iter().map(|v| v.len()).collect();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
#(
#variants { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
)*
}
}
}
}
})
}
Data::Struct(_) | Data::Union(_) => {
let hint_result = parse_hint_helper_attrs(&input.attrs);
hint_result.map(|(keys, values)| {
let cap = keys.len();
quote::quote! {
impl Hint for #ident {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(#cap);
#(
hm.insert(#keys.to_string(), #values.to_string());
)*
hm
}
}
}
})
}
}
}

389
proc-macros/src/lib.rs Normal file
View File

@@ -0,0 +1,389 @@
mod as_message;
mod combined_message_attrs;
mod discriminant;
mod helper_structs;
mod helpers;
mod hint;
mod transitive_child;
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::helper_structs::AttrInnerSingleString;
use crate::hint::derive_hint_impl;
use crate::transitive_child::derive_transitive_child_impl;
use proc_macro::TokenStream;
use syn::parse_macro_input;
/// Derive the `ToDiscriminant` trait and create a `<Type Name>Discriminant` enum
///
/// This derive macro is enum-only.
///
/// The discriminant enum is a copy of the input enum with all fields of every variant removed.\
/// *) The exception to that rule is the `#[child]` attribute
///
/// # Helper attributes
/// - `#[sub_discriminant]`: only usable on variants with a single field; instead of no fields, the discriminant of the single field will be included in the discriminant,
/// acting as a sub-discriminant.
/// - `#[discriminant_attr(…)]`: usable on the enum itself or on any variant; applies `#[…]` in its place on the discriminant.
///
/// # Attributes on the Discriminant
/// All attributes on variants and the type itself are cleared when constructing the discriminant.
/// If the discriminant is supposed to also have an attribute, you must double it with `#[discriminant_attr(…)]`
///
/// # Example
/// ```
/// # use graphite_proc_macros::ToDiscriminant;
/// # use editor::misc::derivable_custom_traits::ToDiscriminant;
/// # use std::ffi::OsString;
///
/// #[derive(ToDiscriminant)]
/// #[discriminant_attr(derive(Debug, Eq, PartialEq))]
/// pub enum EnumA {
/// A(u8),
/// #[sub_discriminant]
/// B(EnumB)
/// }
///
/// #[derive(ToDiscriminant)]
/// #[discriminant_attr(derive(Debug, Eq, PartialEq))]
/// #[discriminant_attr(repr(u8))]
/// pub enum EnumB {
/// Foo(u8),
/// Bar(String),
/// #[cfg(feature = "some-feature")]
/// #[discriminant_attr(cfg(feature = "some-feature"))]
/// WindowsBar(OsString)
/// }
///
/// let a = EnumA::A(1);
/// assert_eq!(a.to_discriminant(), EnumADiscriminant::A);
/// let b = EnumA::B(EnumB::Bar("bar".to_string()));
/// assert_eq!(b.to_discriminant(), EnumADiscriminant::B(EnumBDiscriminant::Bar));
/// ```
#[proc_macro_derive(ToDiscriminant, attributes(sub_discriminant, discriminant_attr))]
pub fn derive_discriminant(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_discriminant_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// Derive the `TransitiveChild` trait and generate `From` impls to convert into the parent, as well as the top parent type
///
/// This macro cannot be invoked on the top parent (which has no parent but itself). Instead, implement `TransitiveChild` manually
/// like in the example.
///
/// # Helper Attributes
/// - `#[parent(<Type>, <Expr>)]` (**required**): declare the parent type (`<Type>`)
/// and a function (`<Expr>`, has to evaluate to a single arg function) for converting a value of this type to the parent type
/// - `#[parent_is_top]`: Denote that the parent type has no further parent type (this is required because otherwise the `From` impls for parent and top parent would overlap)
///
/// # Example
/// ```
/// # use graphite_proc_macros::TransitiveChild;
/// # use editor::misc::derivable_custom_traits::TransitiveChild;
///
/// #[derive(Debug, Eq, PartialEq)]
/// struct A { u: u8, b: B };
///
/// impl A {
/// pub fn from_b(b: B) -> Self {
/// Self { u: 7, b }
/// }
/// }
///
/// impl TransitiveChild for A {
/// type Parent = Self;
/// type TopParent = Self;
/// }
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(A, A::from_b)]
/// #[parent_is_top]
/// enum B {
/// Foo,
/// Bar,
/// Child(C)
/// }
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(B, B::Child)]
/// struct C(D);
///
/// #[derive(TransitiveChild, Debug, Eq, PartialEq)]
/// #[parent(C, C)]
/// struct D;
///
/// let d = D;
/// assert_eq!(A::from(d), A { u: 7, b: B::Child(C(D)) });
/// ```
#[proc_macro_derive(TransitiveChild, attributes(parent, parent_is_top))]
pub fn derive_transitive_child(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_transitive_child_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// Derive the `AsMessage` trait
///
/// # Helper Attributes
/// - `#[child]`: only on tuple variants with a single field; Denote that the message path should continue inside the variant
///
/// # Example
/// See also [`TransitiveChild`]
/// ```
/// # use graphite_proc_macros::{TransitiveChild, AsMessage};
/// # use editor::misc::derivable_custom_traits::TransitiveChild;
/// # use editor::message_prelude::*;
///
/// #[derive(AsMessage)]
/// pub enum TopMessage {
/// A(u8),
/// B(u16),
/// #[child]
/// C(MessageC),
/// #[child]
/// D(MessageD)
/// }
///
/// impl TransitiveChild for TopMessage {
/// type Parent = Self;
/// type TopParent = Self;
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(TopMessage, TopMessage::C)]
/// #[parent_is_top]
/// pub enum MessageC {
/// X1,
/// X2
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(TopMessage, TopMessage::D)]
/// #[parent_is_top]
/// pub enum MessageD {
/// Y1,
/// #[child]
/// Y2(MessageE)
/// }
///
/// #[derive(TransitiveChild, AsMessage, Copy, Clone)]
/// #[parent(MessageD, MessageD::Y2)]
/// pub enum MessageE {
/// Alpha,
/// Beta
/// }
///
/// let c = MessageC::X1;
/// assert_eq!(c.local_name(), "X1");
/// assert_eq!(c.global_name(), "C.X1");
/// let d = MessageD::Y2(MessageE::Alpha);
/// assert_eq!(d.local_name(), "Y2.Alpha");
/// assert_eq!(d.global_name(), "D.Y2.Alpha");
/// let e = MessageE::Beta;
/// assert_eq!(e.local_name(), "Beta");
/// assert_eq!(e.global_name(), "D.Y2.Beta");
/// ```
#[proc_macro_derive(AsMessage, attributes(child))]
pub fn derive_message(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_as_message_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// This macro is basically an abbreviation for the usual [`ToDiscriminant`], [`TransitiveChild`] and [`AsMessage`] invokations
///
/// This macro is enum-only.
///
/// Also note that all three of those derives have to be in scope.
///
/// # Usage
/// There are three possible argument syntaxes you can use:
/// 1. no arguments: this is for the top-level message enum. It derives `ToDiscriminant`, `AsMessage` on the discriminant, and implements `TransitiveChild` on both
/// (the parent and top parent being the respective types themselves).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 2. two arguments: this is for message enums whose direct parent is the top level message enum. The syntax is `#[impl_message(<Type>, <Ident>)]`,
/// where `<Type>` is the parent message type and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both (adding `#[parent_is_top]` to both).
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// 3. three arguments: this is for all other message enums that are transitive children of the top level message enum. The syntax is
/// `#[impl_message(<Type>, <Type>, <Ident>)]`, where the first `<Type>` is the top parent message type, the secont `<Type>` is the parent message type
/// and `<Ident>` is the identifier of the variant used to construct this child.
/// It derives `ToDiscriminant`, `AsMessage` on the discriminant, and `TransitiveChild` on both.
/// It also derives the following `std` traits on the discriminant: `Debug, Copy, Clone, PartialEq, Eq, Hash`.
/// **This third option will likely change in the future**
#[proc_macro_attribute]
pub fn impl_message(attr: TokenStream, input_item: TokenStream) -> TokenStream {
TokenStream::from(combined_message_attrs_impl(attr.into(), input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// Derive the `Hint` trait
///
/// # Example
/// ```
/// # use graphite_proc_macros::Hint;
/// # use editor::misc::derivable_custom_traits::Hint;
///
/// #[derive(Hint)]
/// pub enum StateMachine {
/// #[hint(rmb = "foo", lmb = "bar")]
/// Ready,
/// #[hint(alt = "baz")]
/// RMBDown,
/// // no hint (also ok)
/// LMBDown
/// }
/// ```
#[proc_macro_derive(Hint, attributes(hint))]
pub fn derive_hint(input_item: TokenStream) -> TokenStream {
TokenStream::from(derive_hint_impl(input_item.into()).unwrap_or_else(|err| err.to_compile_error()))
}
/// The `edge` proc macro does nothing, it is intended for use with an external tool
///
/// # Example
/// ```ignore
/// match (example_tool_state, event) {
/// (ToolState::Ready, Event::MouseDown(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Down")]
/// ToolState::Pending
/// }
/// (SelectToolState::Pending, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Up: Select Object")]
/// SelectToolState::Ready
/// }
/// (SelectToolState::Pending, Event::MouseMove(x,y)) => {
/// #[edge("Mouse Move")]
/// SelectToolState::TransformSelected
/// }
/// (SelectToolState::TransformSelected, Event::MouseMove(x,y)) => {
/// #[egde("Mouse Move")]
/// SelectToolState::TransformSelected
/// }
/// (SelectToolState::TransformSelected, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
/// #[edge("LMB Up")]
/// SelectToolState::Ready
/// }
/// (state, _) => {
/// // Do nothing
/// state
/// }
/// }
/// ```
#[proc_macro_attribute]
pub fn edge(attr: TokenStream, item: TokenStream) -> TokenStream {
// to make sure that only `#[edge("string")]` is allowed
let _verify = parse_macro_input!(attr as AttrInnerSingleString);
item
}
#[cfg(test)]
mod tests {
use super::*;
use proc_macro2::TokenStream as TokenStream2;
fn ts_assert_eq(l: TokenStream2, r: TokenStream2) {
// not sure if this is the best way of doing things but if two TokenStreams are equal, their `to_string` is also equal
// so there are at least no false negatives
assert_eq!(l.to_string(), r.to_string());
}
#[test]
fn test_derive_hint() {
let res = derive_hint_impl(quote::quote! {
#[hint(key1="val1",key2="val2",)]
struct S { a: u8, b: String, c: bool }
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for S {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("key1".to_string(), "val1".to_string());
hm.insert("key2".to_string(), "val2".to_string());
hm
}
}
},
);
let res = derive_hint_impl(quote::quote! {
enum E {
#[hint(key1="val1",key2="val2",)]
S { a: u8, b: String, c: bool },
#[hint(key3="val3")]
X,
Y
}
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for E {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
match self {
E::S { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("key1".to_string(), "val1".to_string());
hm.insert("key2".to_string(), "val2".to_string());
hm
}
E::X { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(1usize);
hm.insert("key3".to_string(), "val3".to_string());
hm
}
E::Y { .. } => {
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
hm
}
}
}
}
},
);
let res = derive_hint_impl(quote::quote! {
union NoHint {}
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for NoHint {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(0usize);
hm
}
}
},
);
let res = derive_hint_impl(quote::quote! {
#[hint(a="1", a="2")]
struct S;
});
assert!(res.is_err());
let res = derive_hint_impl(quote::quote! {
#[hint(a="1")]
#[hint(b="2")]
struct S;
});
assert!(res.is_ok());
ts_assert_eq(
res.unwrap(),
quote::quote! {
impl Hint for S {
fn hints(&self) -> ::std::collections::HashMap<String, String> {
let mut hm = ::std::collections::HashMap::with_capacity(2usize);
hm.insert("a".to_string(), "1".to_string());
hm.insert("b".to_string(), "2".to_string());
hm
}
}
},
)
}
// note: edge needs no testing since AttrInnerSingleString has testing and that's all you'd need to test with edge
}

View File

@@ -0,0 +1,54 @@
use crate::helper_structs::Pair;
use proc_macro2::{Span, TokenStream};
use syn::{Attribute, DeriveInput, Expr, Type};
pub fn derive_transitive_child_impl(input_item: TokenStream) -> syn::Result<TokenStream> {
let input = syn::parse2::<DeriveInput>(input_item).unwrap();
let Attribute { tokens, .. } = input
.attrs
.iter()
.find(|a| a.path.is_ident("parent"))
.ok_or_else(|| syn::Error::new(Span::call_site(), format!("tried to derive TransitiveChild without a #[parent] attribute (on {})", input.ident)))?;
let parent_is_top = input.attrs.iter().any(|a| a.path.is_ident("parent_is_top"));
let Pair {
first: parent_type,
second: to_parent,
..
} = syn::parse2::<Pair<Type, Expr>>(tokens.clone())?;
let top_parent_type: Type = syn::parse_quote! { <#parent_type as TransitiveChild>::TopParent };
let input_type = &input.ident;
let trait_impl = quote::quote! {
impl TransitiveChild for #input_type {
type Parent = #parent_type;
type TopParent = #top_parent_type;
}
};
let from_for_parent = quote::quote! {
impl From<#input_type> for #parent_type {
fn from(x: #input_type) -> #parent_type {
(#to_parent)(x)
}
}
};
let from_for_top = quote::quote! {
impl From<#input_type> for #top_parent_type {
fn from(x: #input_type) -> #top_parent_type {
#top_parent_type::from((#to_parent)(x))
}
}
};
Ok(if parent_is_top {
quote::quote! { #trait_impl #from_for_parent }
} else {
quote::quote! { #trait_impl #from_for_parent #from_for_top }
})
}