diff --git a/Cargo.lock b/Cargo.lock index bfa786723..c33ee8e7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -58,9 +58,20 @@ version = "0.1.0" dependencies = [ "bitflags", "graphite-document-core", + "graphite-proc-macros", "log", ] +[[package]] +name = "graphite-proc-macros" +version = "0.1.0" +dependencies = [ + "graphite-editor-core", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "graphite-renderer-core" version = "0.1.0" @@ -78,18 +89,18 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +checksum = "2d99f9e3e84b8f67f846ef5b4cbbc3b1c29f6c759fcbce6f01aa0e73d932a24c" dependencies = [ "wasm-bindgen", ] [[package]] name = "kurbo" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb348d766edbac91ba1eb83020d96f4f8867924d194393083c15a51f185e6a82" +checksum = "e30b1df631d23875f230ed3ddd1a88c231f269a04b2044eb6ca87e763b5f4c42" dependencies = [ "arrayvec", ] @@ -111,9 +122,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.24" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71" +checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec" dependencies = [ "unicode-xid", ] @@ -135,9 +146,9 @@ checksum = "ea6a9290e3c9cf0f18145ef7ffa62d68ee0bf5fcd651017e586dc7fd5da448c2" [[package]] name = "syn" -version = "1.0.64" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fd9d1e9976102a03c542daa2eff1b43f9d72306342f3f8b3ed5fb8908195d6f" +checksum = "3ce15dd3ed8aa2f8eeac4716d6ef5ab58b6b9256db41d7e1a0224c2788e8fd87" dependencies = [ "proc-macro2", "quote", @@ -152,9 +163,9 @@ checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" [[package]] name = "wasm-bindgen" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +checksum = "83240549659d187488f91f33c0f8547cbfef0b2088bc470c116d1d260ef623d9" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -162,9 +173,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +checksum = "ae70622411ca953215ca6d06d3ebeb1e915f0f6613e3b495122878d7ebec7dae" dependencies = [ "bumpalo", "lazy_static", @@ -177,9 +188,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73157efb9af26fb564bb59a009afd1c7c334a44db171d280690d0c3faaec3468" +checksum = "81b8b767af23de6ac18bf2168b690bed2902743ddf0fb39252e36f9e2bfc63ea" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -189,9 +200,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +checksum = "3e734d91443f177bfdb41969de821e15c516931c3c3db3d318fa1b68975d0f6f" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -199,9 +210,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +checksum = "d53739ff08c8a68b0fdbcd54c372b8ab800b1449ab3c9d706503bc7dd1621b2c" dependencies = [ "proc-macro2", "quote", @@ -212,15 +223,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.72" +version = "0.2.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" +checksum = "d9a543ae66aa233d14bb765ed9af4a33e81b8b58d1584cf1b47ff8cd0b9e4489" [[package]] name = "wasm-bindgen-test" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f002ea97b5abdb19aafd48cbb5a0a7f6931cf36ea05a0a46ccc95d9f4c2cf43" +checksum = "e972e914de63aa53bd84865e54f5c761bd274d48e5be3a6329a662c0386aa67a" dependencies = [ "console_error_panic_hook", "js-sys", @@ -232,9 +243,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.22" +version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a6c0bd3933daf64c78fc25a7452530f79fa7e21f77fa03d608d1e988a66735" +checksum = "ea6153a8f9bf24588e9f25c87223414fff124049f68d3a442a0f0eab4768a8b6" dependencies = [ "proc-macro2", "quote", @@ -242,9 +253,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.49" +version = "0.3.50" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" +checksum = "a905d57e488fec8861446d3393670fb50d27a262344013181c2cdf9fff5481be" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/core/editor/Cargo.toml b/core/editor/Cargo.toml index 2d7412904..babe6572e 100644 --- a/core/editor/Cargo.toml +++ b/core/editor/Cargo.toml @@ -15,3 +15,7 @@ bitflags = "1.2.1" [dependencies.document-core] path = "../document" package = "graphite-document-core" + +[dependencies.proc-macros] +path = "../proc-macro" +package = "graphite-proc-macros" diff --git a/core/editor/src/hint.rs b/core/editor/src/hint.rs new file mode 100644 index 000000000..eb87eca90 --- /dev/null +++ b/core/editor/src/hint.rs @@ -0,0 +1,5 @@ +use std::collections::HashMap; + +pub trait Hint { + fn hints(&self) -> HashMap; +} diff --git a/core/editor/src/lib.rs b/core/editor/src/lib.rs index 876c32444..9bf330bdb 100644 --- a/core/editor/src/lib.rs +++ b/core/editor/src/lib.rs @@ -1,8 +1,10 @@ +#[macro_use] +mod macros; + mod color; mod dispatcher; mod error; -#[macro_use] -mod macros; +pub mod hint; pub mod tools; pub mod workspace; diff --git a/core/proc-macro/Cargo.toml b/core/proc-macro/Cargo.toml new file mode 100644 index 000000000..b5d6b3165 --- /dev/null +++ b/core/proc-macro/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "graphite-proc-macros" +version = "0.1.0" +authors = ["Graphite Authors "] +edition = "2018" +publish = false + +[lib] +path = "src/lib.rs" +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.26" +syn = "1.0.68" +quote = "1.0.9" + +[dev-dependencies.editor-core] +path = "../editor" +package = "graphite-editor-core" diff --git a/core/proc-macro/src/helpers.rs b/core/proc-macro/src/helpers.rs new file mode 100644 index 000000000..d64d3358d --- /dev/null +++ b/core/proc-macro/src/helpers.rs @@ -0,0 +1,62 @@ +use proc_macro2::Ident; +use syn::punctuated::Punctuated; +use syn::{Path, PathArguments, PathSegment, Token}; + +/// Returns `Ok(Vec)` if all items are `Ok(T)`, else returns a combination of every error encountered (not just the first one) +pub fn fold_error_iter(iter: impl Iterator>) -> syn::Result> { + 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 the path `left::right` from the idents `left` and `right` +pub fn two_path(left_ident: Ident, right_ident: Ident) -> Path { + let mut segments: Punctuated = 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_path(Ident::new("a", _span), Ident::new("b", _span)).to_token_stream().to_string(), "a :: b"); + } +} diff --git a/core/proc-macro/src/lib.rs b/core/proc-macro/src/lib.rs new file mode 100644 index 000000000..fd2de8d9b --- /dev/null +++ b/core/proc-macro/src/lib.rs @@ -0,0 +1,264 @@ +mod helpers; +mod structs; + +use crate::helpers::{fold_error_iter, two_path}; +use crate::structs::{AttrInnerKeyStringMap, AttrInnerSingleString}; +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use syn::{parse_macro_input, Attribute, Data, DeriveInput, LitStr, Variant}; + +fn parse_hint_helper_attrs(attrs: &[Attribute]) -> syn::Result<(Vec, Vec)> { + fold_error_iter( + attrs + .iter() + .filter(|a| a.path.get_ident().map_or(false, |i| i == "hint")) + .map(|attr| syn::parse2::(attr.tokens.clone())), + ) + .and_then(|v: Vec| { + 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()) +} + +fn derive_hint_impl(input_item: TokenStream2) -> syn::Result { + let input = syn::parse2::(input_item)?; + + let ident = input.ident; + + match input.data { + Data::Enum(data) => { + let variants = data.variants.iter().map(|var: &Variant| two_path(ident.clone(), var.ident.clone())).collect::>(); + + 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, Vec)>| { + let (keys, values): (Vec>, Vec>) = hints.into_iter().unzip(); + let cap: Vec = keys.iter().map(|v| v.len()).collect(); + + quote::quote! { + impl Hint for #ident { + fn hints(&self) -> ::std::collections::HashMap { + 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 { + let mut hm = ::std::collections::HashMap::with_capacity(#cap); + #( + hm.insert(#keys.to_string(), #values.to_string()); + )* + hm + } + } + } + }) + } + } +} + +/// Derive the `Hint` trait +/// +/// # Example +/// ``` +/// # use graphite_proc_macros::Hint; +/// # use editor_core::hint::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::*; + + 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 { + 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 { + 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 { + 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 { + 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 +} diff --git a/core/proc-macro/src/structs.rs b/core/proc-macro/src/structs.rs new file mode 100644 index 000000000..c87e3cf98 --- /dev/null +++ b/core/proc-macro/src/structs.rs @@ -0,0 +1,144 @@ +use proc_macro2::Ident; +use std::collections::HashMap; +use syn::parse::{Parse, ParseStream}; +use syn::punctuated::Punctuated; +use syn::token::Paren; +use syn::{parenthesized, LitStr, Token}; + +/// Parses `("some text")` +pub struct AttrInnerSingleString { + _paren_token: Paren, + pub content: LitStr, +} + +impl Parse for AttrInnerSingleString { + fn parse(input: ParseStream) -> syn::Result { + 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 { + 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, +} + +impl Parse for AttrInnerKeyStringMap { + fn parse(input: ParseStream) -> syn::Result { + 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) -> impl Iterator)> { + use std::collections::hash_map::Entry; + + let mut res = Vec::<(Ident, Vec)>::new(); + let mut idx = HashMap::::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() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn attr_inner_single_string() { + let res = syn::parse2::(quote::quote! { + ("a string literal") + }); + assert!(res.is_ok()); + assert_eq!(res.ok().unwrap().content.value(), "a string literal"); + + let res = syn::parse2::(quote::quote! { + wrong, "stuff" + }); + assert!(res.is_err()); + } + + #[test] + fn key_eq_string() { + let res = syn::parse2::(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::(quote::quote! { + wrong, "stuff" + }); + assert!(res.is_err()); + } + + #[test] + fn attr_inner_key_string_map() { + let res = syn::parse2::(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::(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::(quote::quote! { + wrong, "stuff" + }); + assert!(res.is_err()); + } +}