From 794f43ffee84c2b838d9f895912c00ebe52fa957 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 24 Apr 2022 02:35:52 -0400 Subject: [PATCH 01/95] concept: move rsx out to its own crate --- Cargo.toml | 10 +- packages/core-macro/Cargo.toml | 1 + packages/core-macro/src/lib.rs | 4 +- packages/rsx/Cargo.toml | 12 ++ packages/rsx/src/component.rs | 233 ++++++++++++++++++++++++++++++ packages/rsx/src/element.rs | 253 +++++++++++++++++++++++++++++++++ packages/rsx/src/errors.rs | 15 ++ packages/rsx/src/lib.rs | 100 +++++++++++++ packages/rsx/src/node.rs | 84 +++++++++++ packages/rsx/src/pretty.rs | 1 + 10 files changed, 706 insertions(+), 7 deletions(-) create mode 100644 packages/rsx/Cargo.toml create mode 100644 packages/rsx/src/component.rs create mode 100644 packages/rsx/src/element.rs create mode 100644 packages/rsx/src/errors.rs create mode 100644 packages/rsx/src/lib.rs create mode 100644 packages/rsx/src/node.rs create mode 100644 packages/rsx/src/pretty.rs diff --git a/Cargo.toml b/Cargo.toml index 8291a290c..70c1daa7f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,15 +27,12 @@ dioxus-interpreter-js = { path = "./packages/interpreter", version = "^0.2.0", o dioxus-tui = { path = "./packages/tui", version = "^0.2.0", optional = true } dioxus-liveview = { path = "./packages/liveview", optional = true } - -# dioxus-mobile = { path = "./packages/mobile", version = "^0.2.0", optional = true } -# dioxus-rsx = { path = "./packages/rsx", optional = true } -# macro = ["dioxus-core-macro", "dioxus-rsx"] +dioxus-rsx = { path = "./packages/rsx", optional = true } [features] default = ["macro", "hooks", "html"] -macro = ["dioxus-core-macro"] +macro = ["dioxus-core-macro", "dioxus-rsx"] hooks = ["dioxus-hooks"] html = ["dioxus-html"] ssr = ["dioxus-ssr"] @@ -61,6 +58,7 @@ members = [ "packages/fermi", "packages/tui", "packages/liveview", + "packages/rsx", ] [dev-dependencies] @@ -91,4 +89,4 @@ harness = false [[bench]] name = "tui_update" -harness = false \ No newline at end of file +harness = false diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 5922ffad1..7aef8f49f 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -19,6 +19,7 @@ proc-macro-error = "1" proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } +dioxus-rsx = { path = "../rsx" } # testing [dev-dependencies] diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 931933386..d68cb2148 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -5,7 +5,9 @@ use syn::parse_macro_input; mod ifmt; mod inlineprops; mod props; -mod rsx; +use dioxus_rsx as rsx; + +// mod rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { diff --git a/packages/rsx/Cargo.toml b/packages/rsx/Cargo.toml new file mode 100644 index 000000000..5b492c9fe --- /dev/null +++ b/packages/rsx/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-rsx" +version = "0.0.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = { version = "1.0.6" } +proc-macro-error = "1" +quote = "1.0" +syn = { version = "1.0.11", features = ["full", "extra-traits"] } diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs new file mode 100644 index 000000000..dfdda74b8 --- /dev/null +++ b/packages/rsx/src/component.rs @@ -0,0 +1,233 @@ +//! Parse components into the VComponent VNode +//! ========================================== +//! +//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format. +//! We can be reasonably sure that whatever enters this parsing path is in the right format. +//! This feature must support +//! - [x] Namespaced components +//! - [x] Fields +//! - [x] Componentbuilder synax +//! - [x] Optional commas +//! - [ ] Children +//! - [ ] Keys +//! - [ ] Properties spreading with with `..` syntax + +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + ext::IdentExt, + parse::{Parse, ParseBuffer, ParseStream}, + token, Expr, Ident, LitStr, Result, Token, +}; + +pub struct Component { + pub name: syn::Path, + pub body: Vec, + pub children: Vec, + pub manual_props: Option, +} + +impl Parse for Component { + fn parse(stream: ParseStream) -> Result { + let name = syn::Path::parse_mod_style(stream)?; + + let content: ParseBuffer; + + // if we see a `{` then we have a block + // else parse as a function-like call + if stream.peek(token::Brace) { + syn::braced!(content in stream); + } else { + syn::parenthesized!(content in stream); + } + + let mut body = Vec::new(); + let mut children = Vec::new(); + let mut manual_props = None; + + while !content.is_empty() { + // if we splat into a component then we're merging properties + if content.peek(Token![..]) { + content.parse::()?; + manual_props = Some(content.parse::()?); + } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + body.push(content.parse::()?); + } else { + children.push(content.parse::()?); + } + + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + name, + body, + children, + manual_props, + }) + } +} + +impl ToTokens for Component { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + + let mut has_key = None; + + let builder = match &self.manual_props { + Some(manual_props) => { + let mut toks = quote! { + let mut __manual_props = #manual_props; + }; + for field in &self.body { + if field.name == "key" { + has_key = Some(field); + } else { + let name = &field.name; + let val = &field.content; + toks.append_all(quote! { + __manual_props.#name = #val; + }); + } + } + toks.append_all(quote! { + __manual_props + }); + quote! {{ + #toks + }} + } + None => { + let mut toks = quote! { fc_to_builder(#name) }; + for field in &self.body { + match field.name.to_string().as_str() { + "key" => { + // + has_key = Some(field); + } + _ => toks.append_all(quote! {#field}), + } + } + + if !self.children.is_empty() { + let childs = &self.children; + toks.append_all(quote! { + .children(__cx.create_children([ #( #childs ),* ])) + }); + } + + toks.append_all(quote! { + .build() + }); + toks + } + }; + + let key_token = match has_key { + Some(field) => { + let inners = &field.content; + quote! { Some(format_args_f!(#inners)) } + } + None => quote! { None }, + }; + + let fn_name = self.name.segments.last().unwrap().ident.to_string(); + + tokens.append_all(quote! { + __cx.component( + #name, + #builder, + #key_token, + #fn_name + ) + }) + } +} + +// the struct's fields info +pub struct ComponentField { + name: Ident, + content: ContentField, +} + +enum ContentField { + ManExpr(Expr), + Formatted(LitStr), + OnHandlerRaw(Expr), +} + +impl ToTokens for ContentField { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match self { + ContentField::ManExpr(e) => e.to_tokens(tokens), + ContentField::Formatted(s) => tokens.append_all(quote! { + __cx.raw_text(format_args_f!(#s)).0 + }), + ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { + __cx.event_handler(#e) + }), + } + } +} + +impl Parse for ComponentField { + fn parse(input: ParseStream) -> Result { + let name = Ident::parse_any(input)?; + input.parse::()?; + + if name.to_string().starts_with("on") { + let content = ContentField::OnHandlerRaw(input.parse()?); + return Ok(Self { name, content }); + } + + if name == "key" { + let content = ContentField::ManExpr(input.parse()?); + return Ok(Self { name, content }); + } + + if input.peek(LitStr) && input.peek2(Token![,]) { + let t: LitStr = input.fork().parse()?; + + if is_literal_foramtted(&t) { + let content = ContentField::Formatted(input.parse()?); + return Ok(Self { name, content }); + } + } + + if input.peek(LitStr) && input.peek2(LitStr) { + missing_trailing_comma!(input.span()); + } + + let content = ContentField::ManExpr(input.parse()?); + Ok(Self { name, content }) + } +} + +impl ToTokens for ComponentField { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ComponentField { name, content, .. } = self; + tokens.append_all(quote! { + .#name(#content) + }) + } +} + +fn is_literal_foramtted(lit: &LitStr) -> bool { + let s = lit.value(); + let mut chars = s.chars(); + + while let Some(next) = chars.next() { + if next == '{' { + let nen = chars.next(); + if nen != Some('{') { + return true; + } + } + } + + false +} diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs new file mode 100644 index 000000000..96c7a2efc --- /dev/null +++ b/packages/rsx/src/element.rs @@ -0,0 +1,253 @@ +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseBuffer, ParseStream}, + Expr, Ident, LitStr, Result, Token, +}; + +// ======================================= +// Parse the VNode::Element type +// ======================================= +pub struct Element { + pub name: Ident, + pub key: Option, + pub attributes: Vec, + pub children: Vec, + pub _is_static: bool, +} + +impl Parse for Element { + fn parse(stream: ParseStream) -> Result { + let el_name = Ident::parse(stream)?; + + // parse the guts + let content: ParseBuffer; + syn::braced!(content in stream); + + let mut attributes: Vec = vec![]; + let mut children: Vec = vec![]; + let mut key = None; + let mut _el_ref = None; + + // parse fields with commas + // break when we don't get this pattern anymore + // start parsing bodynodes + // "def": 456, + // abc: 123, + loop { + // Parse the raw literal fields + if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + let name = content.parse::()?; + let ident = name.clone(); + + content.parse::()?; + + if content.peek(LitStr) && content.peek2(Token![,]) { + let value = content.parse::()?; + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrText { name, value }, + }); + } else { + let value = content.parse::()?; + + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::CustomAttrExpression { name, value }, + }); + } + + if content.is_empty() { + break; + } + + if content.parse::().is_err() { + missing_trailing_comma!(ident); + } + continue; + } + + if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { + let name = content.parse::()?; + let ident = name.clone(); + + let name_str = name.to_string(); + content.parse::()?; + + if name_str.starts_with("on") { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::EventTokens { + name, + tokens: content.parse()?, + }, + }); + } else { + match name_str.as_str() { + "key" => { + key = Some(content.parse()?); + } + "classes" => todo!("custom class list not supported yet"), + // "namespace" => todo!("custom namespace not supported yet"), + "node_ref" => { + _el_ref = Some(content.parse::()?); + } + _ => { + if content.peek(LitStr) { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrText { + name, + value: content.parse()?, + }, + }); + } else { + attributes.push(ElementAttrNamed { + el_name: el_name.clone(), + attr: ElementAttr::AttrExpression { + name, + value: content.parse()?, + }, + }); + } + } + } + } + + if content.is_empty() { + break; + } + + // todo: add a message saying you need to include commas between fields + if content.parse::().is_err() { + missing_trailing_comma!(ident); + } + continue; + } + + break; + } + + while !content.is_empty() { + if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + attr_after_element!(content.span()); + } + + if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { + attr_after_element!(content.span()); + } + + children.push(content.parse::()?); + // consume comma if it exists + // we don't actually care if there *are* commas after elements/text + if content.peek(Token![,]) { + let _ = content.parse::(); + } + } + + Ok(Self { + key, + name: el_name, + attributes, + children, + _is_static: false, + }) + } +} + +impl ToTokens for Element { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let name = &self.name; + let children = &self.children; + + let key = match &self.key { + Some(ty) => quote! { Some(format_args_f!(#ty)) }, + None => quote! { None }, + }; + + let listeners = self + .attributes + .iter() + .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); + + let attr = self + .attributes + .iter() + .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); + + tokens.append_all(quote! { + __cx.element( + dioxus_elements::#name, + __cx.bump().alloc([ #(#listeners),* ]), + __cx.bump().alloc([ #(#attr),* ]), + __cx.bump().alloc([ #(#children),* ]), + #key, + ) + }); + } +} + +pub enum ElementAttr { + /// attribute: "valuee {}" + AttrText { name: Ident, value: LitStr }, + + /// attribute: true, + AttrExpression { name: Ident, value: Expr }, + + /// "attribute": "value {}" + CustomAttrText { name: LitStr, value: LitStr }, + + /// "attribute": true, + CustomAttrExpression { name: LitStr, value: Expr }, + + // /// onclick: move |_| {} + // EventClosure { name: Ident, closure: ExprClosure }, + /// onclick: {} + EventTokens { name: Ident, tokens: Expr }, +} + +pub struct ElementAttrNamed { + pub el_name: Ident, + pub attr: ElementAttr, +} + +impl ToTokens for ElementAttrNamed { + fn to_tokens(&self, tokens: &mut TokenStream2) { + let ElementAttrNamed { el_name, attr } = self; + + tokens.append_all(match attr { + ElementAttr::AttrText { name, value } => { + quote! { + dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) + } + } + ElementAttr::AttrExpression { name, value } => { + quote! { + dioxus_elements::#el_name.#name(__cx, #value) + } + } + ElementAttr::CustomAttrText { name, value } => { + quote! { + __cx.attr( #name, format_args_f!(#value), None, false ) + } + } + ElementAttr::CustomAttrExpression { name, value } => { + quote! { + __cx.attr( #name, format_args_f!(#value), None, false ) + } + } + // ElementAttr::EventClosure { name, closure } => { + // quote! { + // dioxus_elements::on::#name(__cx, #closure) + // } + // } + ElementAttr::EventTokens { name, tokens } => { + quote! { + dioxus_elements::on::#name(__cx, #tokens) + } + } + }); + } +} diff --git a/packages/rsx/src/errors.rs b/packages/rsx/src/errors.rs new file mode 100644 index 000000000..27e2c7f98 --- /dev/null +++ b/packages/rsx/src/errors.rs @@ -0,0 +1,15 @@ +macro_rules! missing_trailing_comma { + ($span:expr) => { + proc_macro_error::emit_error!($span, "missing trailing comma") + }; +} + +macro_rules! attr_after_element { + ($span:expr) => { + proc_macro_error::emit_error!( + $span, + "expected element"; + help = "move the attribute above all the children and text elements" + ) + }; +} diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs new file mode 100644 index 000000000..6b9dfc3dc --- /dev/null +++ b/packages/rsx/src/lib.rs @@ -0,0 +1,100 @@ +//! Parse the root tokens in the rsx!{} macro +//! ========================================= +//! +//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. +//! This feature must support: +//! - [x] Optionally rendering if the `in XYZ` pattern is present +//! - [x] Fragments as top-level element (through ambiguous) +//! - [x] Components as top-level element (through ambiguous) +//! - [x] Tags as top-level elements (through ambiguous) +//! - [x] Good errors if parsing fails +//! +//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. + +#[macro_use] +mod errors; + +mod component; +mod element; +mod node; + +pub mod pretty; + +// Re-export the namespaces into each other +pub use component::*; +pub use element::*; +pub use node::*; + +// imports +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + Ident, Result, Token, +}; + +pub struct CallBody { + pub custom_context: Option, + pub roots: Vec, +} + +impl Parse for CallBody { + fn parse(input: ParseStream) -> Result { + let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { + let name = input.parse::()?; + input.parse::()?; + + Some(name) + } else { + None + }; + + let mut roots = Vec::new(); + + while !input.is_empty() { + let node = input.parse::()?; + + if input.peek(Token![,]) { + let _ = input.parse::(); + } + + roots.push(node); + } + + Ok(Self { + custom_context, + roots, + }) + } +} + +/// Serialize the same way, regardless of flavor +impl ToTokens for CallBody { + fn to_tokens(&self, out_tokens: &mut TokenStream2) { + let inner = if self.roots.len() == 1 { + let inner = &self.roots[0]; + quote! { #inner } + } else { + let childs = &self.roots; + quote! { __cx.fragment_root([ #(#childs),* ]) } + }; + + match &self.custom_context { + // The `in cx` pattern allows directly rendering + Some(ident) => out_tokens.append_all(quote! { + #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + })) + }), + + // Otherwise we just build the LazyNode wrapper + None => out_tokens.append_all(quote! { + LazyNodes::new(move |__cx: NodeFactory| -> VNode { + use dioxus_elements::{GlobalAttributes, SvgAttributes}; + #inner + }) + }), + }; + } +} diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs new file mode 100644 index 000000000..d5d701767 --- /dev/null +++ b/packages/rsx/src/node.rs @@ -0,0 +1,84 @@ +use super::*; + +use proc_macro2::TokenStream as TokenStream2; +use quote::{quote, ToTokens, TokenStreamExt}; +use syn::{ + parse::{Parse, ParseStream}, + token, Expr, LitStr, Result, Token, +}; + +/* +Parse +-> div {} +-> Component {} +-> component() +-> "text {with_args}" +-> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr +*/ +pub enum BodyNode { + Element(Element), + Component(Component), + Text(LitStr), + RawExpr(Expr), +} + +impl Parse for BodyNode { + fn parse(stream: ParseStream) -> Result { + if stream.peek(LitStr) { + return Ok(BodyNode::Text(stream.parse()?)); + } + + // div {} -> el + // Div {} -> comp + if stream.peek(syn::Ident) && stream.peek2(token::Brace) { + if stream + .fork() + .parse::()? + .to_string() + .chars() + .next() + .unwrap() + .is_ascii_uppercase() + { + return Ok(BodyNode::Component(stream.parse()?)); + } else { + return Ok(BodyNode::Element(stream.parse::()?)); + } + } + + // component() -> comp + // ::component {} -> comp + // ::component () -> comp + if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) + || (stream.peek(Token![::])) + || (stream.peek(Token![:]) && stream.peek2(Token![:])) + { + return Ok(BodyNode::Component(stream.parse::()?)); + } + + // crate::component{} -> comp + // crate::component() -> comp + if let Ok(pat) = stream.fork().parse::() { + if pat.segments.len() > 1 { + return Ok(BodyNode::Component(stream.parse::()?)); + } + } + + Ok(BodyNode::RawExpr(stream.parse::()?)) + } +} + +impl ToTokens for BodyNode { + fn to_tokens(&self, tokens: &mut TokenStream2) { + match &self { + BodyNode::Element(el) => el.to_tokens(tokens), + BodyNode::Component(comp) => comp.to_tokens(tokens), + BodyNode::Text(txt) => tokens.append_all(quote! { + __cx.text(format_args_f!(#txt)) + }), + BodyNode::RawExpr(exp) => tokens.append_all(quote! { + __cx.fragment_from_iter(#exp) + }), + } + } +} diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs new file mode 100644 index 000000000..26ad54006 --- /dev/null +++ b/packages/rsx/src/pretty.rs @@ -0,0 +1 @@ +//! pretty printer for rsx! From 2a1970b3a910473eb7e0f47e27a18968ebac183e Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 24 Apr 2022 02:55:20 -0400 Subject: [PATCH 02/95] feat: add prettyplease vendored and get basic test working --- Cargo.toml | 3 +- packages/autofmt/Cargo.toml | 12 + packages/autofmt/README.md | 5 + packages/autofmt/src/lib.rs | 117 ++ .../autofmt/src/prettyplease/algorithm.rs | 376 ++++++ packages/autofmt/src/prettyplease/attr.rs | 208 ++++ .../autofmt/src/prettyplease/convenience.rs | 98 ++ packages/autofmt/src/prettyplease/data.rs | 95 ++ packages/autofmt/src/prettyplease/expr.rs | 1028 +++++++++++++++++ packages/autofmt/src/prettyplease/file.rs | 17 + packages/autofmt/src/prettyplease/generics.rs | 280 +++++ packages/autofmt/src/prettyplease/item.rs | 757 ++++++++++++ packages/autofmt/src/prettyplease/iter.rs | 46 + packages/autofmt/src/prettyplease/lifetime.rs | 9 + packages/autofmt/src/prettyplease/lit.rs | 50 + packages/autofmt/src/prettyplease/mac.rs | 220 ++++ packages/autofmt/src/prettyplease/mod.rs | 381 ++++++ packages/autofmt/src/prettyplease/pat.rs | 210 ++++ packages/autofmt/src/prettyplease/path.rs | 181 +++ packages/autofmt/src/prettyplease/ring.rs | 81 ++ packages/autofmt/src/prettyplease/stmt.rs | 85 ++ packages/autofmt/src/prettyplease/token.rs | 80 ++ packages/autofmt/src/prettyplease/ty.rs | 202 ++++ packages/autofmt/tests/sink.rs | 61 + packages/core-macro/src/lib.rs | 2 +- packages/rsx/src/element.rs | 31 +- packages/rsx/src/lib.rs | 2 - packages/rsx/src/node.rs | 2 + packages/rsx/src/pretty.rs | 1 - 29 files changed, 4630 insertions(+), 10 deletions(-) create mode 100644 packages/autofmt/Cargo.toml create mode 100644 packages/autofmt/README.md create mode 100644 packages/autofmt/src/lib.rs create mode 100644 packages/autofmt/src/prettyplease/algorithm.rs create mode 100644 packages/autofmt/src/prettyplease/attr.rs create mode 100644 packages/autofmt/src/prettyplease/convenience.rs create mode 100644 packages/autofmt/src/prettyplease/data.rs create mode 100644 packages/autofmt/src/prettyplease/expr.rs create mode 100644 packages/autofmt/src/prettyplease/file.rs create mode 100644 packages/autofmt/src/prettyplease/generics.rs create mode 100644 packages/autofmt/src/prettyplease/item.rs create mode 100644 packages/autofmt/src/prettyplease/iter.rs create mode 100644 packages/autofmt/src/prettyplease/lifetime.rs create mode 100644 packages/autofmt/src/prettyplease/lit.rs create mode 100644 packages/autofmt/src/prettyplease/mac.rs create mode 100644 packages/autofmt/src/prettyplease/mod.rs create mode 100644 packages/autofmt/src/prettyplease/pat.rs create mode 100644 packages/autofmt/src/prettyplease/path.rs create mode 100644 packages/autofmt/src/prettyplease/ring.rs create mode 100644 packages/autofmt/src/prettyplease/stmt.rs create mode 100644 packages/autofmt/src/prettyplease/token.rs create mode 100644 packages/autofmt/src/prettyplease/ty.rs create mode 100644 packages/autofmt/tests/sink.rs delete mode 100644 packages/rsx/src/pretty.rs diff --git a/Cargo.toml b/Cargo.toml index 70c1daa7f..6e651cf5a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ dioxus-tui = { path = "./packages/tui", version = "^0.2.0", optional = true } dioxus-liveview = { path = "./packages/liveview", optional = true } dioxus-rsx = { path = "./packages/rsx", optional = true } +dioxus-autofmt = { path = "./packages/autofmt", optional = true } [features] default = ["macro", "hooks", "html"] @@ -42,7 +43,7 @@ ayatana = ["dioxus-desktop/ayatana"] router = ["dioxus-router"] tui = ["dioxus-tui"] liveview = ["dioxus-liveview"] - +autofmt = ["dioxus-autofmt"] [workspace] members = [ diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml new file mode 100644 index 000000000..6411f5add --- /dev/null +++ b/packages/autofmt/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-autofmt" +version = "0.0.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +proc-macro2 = { version = "1.0.6" } +quote = "1.0" +syn = { version = "1.0.11", features = ["full", "extra-traits"] } +dioxus-rsx = { path = "../rsx" } diff --git a/packages/autofmt/README.md b/packages/autofmt/README.md new file mode 100644 index 000000000..9fded3260 --- /dev/null +++ b/packages/autofmt/README.md @@ -0,0 +1,5 @@ +# This crate autofmts blocks of rsx! + +This crate formats rsx! by parsing call bodies and pretty-printing them back out. + +It also incorporates a fork of prettyplease to allow formatting arbitrary rust code too. Prettyplease rejected a suggestion to allow arbitrary expression formatting - something our fork lets us do. diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs new file mode 100644 index 000000000..a7a0bbbdf --- /dev/null +++ b/packages/autofmt/src/lib.rs @@ -0,0 +1,117 @@ +//! pretty printer for rsx! +use dioxus_rsx::*; +use std::fmt::{self, Write}; +mod prettyplease; + +pub fn fmt_block(block: &str) -> Option { + let parsed: CallBody = syn::parse_str(block).ok()?; + + let mut buf = String::new(); + + for node in parsed.roots.iter() { + write_ident(&mut buf, node, 0).ok()?; + } + + Some(buf) +} + +pub fn write_ident(buf: &mut dyn Write, node: &BodyNode, indent: usize) -> fmt::Result { + match node { + BodyNode::Element(el) => { + let Element { + name, + key, + attributes, + children, + _is_static, + } = el; + + write_tabs(buf, indent)?; + writeln!(buf, "{name} {{")?; + + if let Some(key) = key { + let key = key.value(); + write_tabs(buf, indent + 1)?; + write!(buf, "key: \"{key}\"")?; + if !attributes.is_empty() { + writeln!(buf, ",")?; + } + } + + for attr in attributes { + write_tabs(buf, indent + 1)?; + match &attr.attr { + ElementAttr::AttrText { name, value } => { + writeln!(buf, "{name}: \"{value}\",", value = value.value())?; + } + ElementAttr::AttrExpression { name, value } => { + let out = prettyplease::unparse_expr(value); + writeln!(buf, "{}: {},", name, out)?; + } + + ElementAttr::CustomAttrText { name, value } => todo!(), + ElementAttr::CustomAttrExpression { name, value } => todo!(), + + ElementAttr::EventTokens { name, tokens } => { + let out = prettyplease::unparse_expr(tokens); + + let mut lines = out.split('\n').peekable(); + let first = lines.next().unwrap(); + writeln!(buf, "{}: {}", name, first)?; + + while let Some(line) = lines.next() { + write_tabs(buf, indent + 1)?; + write!(buf, "{}", line)?; + // writeln!(buf, "{}", line)?; + if lines.peek().is_none() { + writeln!(buf, ",")?; + } else { + writeln!(buf)?; + } + } + } + ElementAttr::Meta(_) => {} + } + } + + for child in children { + write_ident(buf, child, indent + 1)?; + } + + write_tabs(buf, indent)?; + writeln!(buf, "}}")?; + } + BodyNode::Component(_) => { + // + // write!(buf, "{}", " ".repeat(ident)) + } + BodyNode::Text(t) => { + // + // write!(buf, "{}", " ".repeat(ident)) + write_tabs(buf, indent)?; + writeln!(buf, "\"{}\"", t.value())?; + } + BodyNode::RawExpr(_) => { + // + // write!(buf, "{}", " ".repeat(ident)) + } + BodyNode::Meta(att) => { + // + // if att.path.segments.last().unwrap().ident == "doc" { + let val = att.to_string(); + write_tabs(buf, indent)?; + writeln!(buf, "{}", val)?; + // } + // match att {} + } + } + + Ok(()) +} + +pub fn write_tabs(f: &mut dyn Write, num: usize) -> std::fmt::Result { + for _ in 0..num { + write!(f, " ")? + } + Ok(()) +} diff --git a/packages/autofmt/src/prettyplease/algorithm.rs b/packages/autofmt/src/prettyplease/algorithm.rs new file mode 100644 index 000000000..b70bd57ea --- /dev/null +++ b/packages/autofmt/src/prettyplease/algorithm.rs @@ -0,0 +1,376 @@ +// Adapted from https://github.com/rust-lang/rust/blob/1.57.0/compiler/rustc_ast_pretty/src/pp.rs. +// See "Algorithm notes" in the crate-level rustdoc. + +use super::ring::RingBuffer; +use super::{MARGIN, MIN_SPACE}; +use std::borrow::Cow; +use std::cmp; +use std::collections::VecDeque; +use std::iter; + +#[derive(Clone, Copy, PartialEq)] +pub enum Breaks { + Consistent, + Inconsistent, +} + +#[derive(Clone, Copy, Default)] +pub struct BreakToken { + pub offset: isize, + pub blank_space: usize, + pub pre_break: Option, + pub post_break: Option, + pub no_break: Option, + pub if_nonempty: bool, + pub never_break: bool, +} + +#[derive(Clone, Copy)] +pub struct BeginToken { + pub offset: isize, + pub breaks: Breaks, +} + +#[derive(Clone)] +pub enum Token { + String(Cow<'static, str>), + Break(BreakToken), + Begin(BeginToken), + End, +} + +#[derive(Copy, Clone)] +enum PrintFrame { + Fits(Breaks), + Broken(usize, Breaks), +} + +pub const SIZE_INFINITY: isize = 0xffff; + +pub struct Printer { + out: String, + // Number of spaces left on line + space: isize, + // Ring-buffer of tokens and calculated sizes + buf: RingBuffer, + // Total size of tokens already printed + left_total: isize, + // Total size of tokens enqueued, including printed and not yet printed + right_total: isize, + // Holds the ring-buffer index of the Begin that started the current block, + // possibly with the most recent Break after that Begin (if there is any) on + // top of it. Values are pushed and popped on the back of the queue using it + // like stack, and elsewhere old values are popped from the front of the + // queue as they become irrelevant due to the primary ring-buffer advancing. + scan_stack: VecDeque, + // Stack of blocks-in-progress being flushed by print + print_stack: Vec, + // Level of indentation of current line + indent: usize, + // Buffered indentation to avoid writing trailing whitespace + pending_indentation: usize, +} + +#[derive(Clone)] +struct BufEntry { + token: Token, + size: isize, +} + +impl Printer { + pub fn new() -> Self { + Printer { + out: String::new(), + space: MARGIN, + buf: RingBuffer::new(), + left_total: 0, + right_total: 0, + scan_stack: VecDeque::new(), + print_stack: Vec::new(), + indent: 0, + pending_indentation: 0, + } + } + + pub fn eof(mut self) -> String { + if !self.scan_stack.is_empty() { + self.check_stack(0); + self.advance_left(); + } + self.out + } + + pub fn scan_begin(&mut self, token: BeginToken) { + if self.scan_stack.is_empty() { + self.left_total = 1; + self.right_total = 1; + self.buf.clear(); + } + let right = self.buf.push(BufEntry { + token: Token::Begin(token), + size: -self.right_total, + }); + self.scan_stack.push_back(right); + } + + pub fn scan_end(&mut self) { + if self.scan_stack.is_empty() { + self.print_end(); + } else { + if !self.buf.is_empty() { + if let Token::Break(break_token) = self.buf.last().token { + if self.buf.len() >= 2 { + if let Token::Begin(_) = self.buf.second_last().token { + self.buf.pop_last(); + self.buf.pop_last(); + self.scan_stack.pop_back(); + self.scan_stack.pop_back(); + self.right_total -= break_token.blank_space as isize; + return; + } + } + if break_token.if_nonempty { + self.buf.pop_last(); + self.scan_stack.pop_back(); + self.right_total -= break_token.blank_space as isize; + } + } + } + let right = self.buf.push(BufEntry { + token: Token::End, + size: -1, + }); + self.scan_stack.push_back(right); + } + } + + pub fn scan_break(&mut self, token: BreakToken) { + if self.scan_stack.is_empty() { + self.left_total = 1; + self.right_total = 1; + self.buf.clear(); + } else { + self.check_stack(0); + } + let right = self.buf.push(BufEntry { + token: Token::Break(token), + size: -self.right_total, + }); + self.scan_stack.push_back(right); + self.right_total += token.blank_space as isize; + } + + pub fn scan_string(&mut self, string: Cow<'static, str>) { + if self.scan_stack.is_empty() { + self.print_string(string); + } else { + let len = string.len() as isize; + self.buf.push(BufEntry { + token: Token::String(string), + size: len, + }); + self.right_total += len; + self.check_stream(); + } + } + + pub fn offset(&mut self, offset: isize) { + match &mut self.buf.last_mut().token { + Token::Break(token) => token.offset += offset, + Token::Begin(_) => {} + Token::String(_) | Token::End => unreachable!(), + } + } + + pub fn end_with_max_width(&mut self, max: isize) { + let mut depth = 1; + for &index in self.scan_stack.iter().rev() { + let entry = &self.buf[index]; + match entry.token { + Token::Begin(_) => { + depth -= 1; + if depth == 0 { + if entry.size < 0 { + let actual_width = entry.size + self.right_total; + if actual_width > max { + self.buf.push(BufEntry { + token: Token::String(Cow::Borrowed("")), + size: SIZE_INFINITY, + }); + self.right_total += SIZE_INFINITY; + } + } + break; + } + } + Token::End => depth += 1, + Token::Break(_) => {} + Token::String(_) => unreachable!(), + } + } + self.scan_end(); + } + + fn check_stream(&mut self) { + while self.right_total - self.left_total > self.space { + if *self.scan_stack.front().unwrap() == self.buf.index_of_first() { + self.scan_stack.pop_front().unwrap(); + self.buf.first_mut().size = SIZE_INFINITY; + } + + self.advance_left(); + + if self.buf.is_empty() { + break; + } + } + } + + fn advance_left(&mut self) { + while self.buf.first().size >= 0 { + let left = self.buf.pop_first(); + + match left.token { + Token::String(string) => { + self.left_total += left.size; + self.print_string(string); + } + Token::Break(token) => { + self.left_total += token.blank_space as isize; + self.print_break(token, left.size); + } + Token::Begin(token) => self.print_begin(token, left.size), + Token::End => self.print_end(), + } + + if self.buf.is_empty() { + break; + } + } + } + + fn check_stack(&mut self, mut depth: usize) { + while let Some(&index) = self.scan_stack.back() { + let mut entry = &mut self.buf[index]; + match entry.token { + Token::Begin(_) => { + if depth == 0 { + break; + } + self.scan_stack.pop_back().unwrap(); + entry.size += self.right_total; + depth -= 1; + } + Token::End => { + self.scan_stack.pop_back().unwrap(); + entry.size = 1; + depth += 1; + } + Token::Break(_) => { + self.scan_stack.pop_back().unwrap(); + entry.size += self.right_total; + if depth == 0 { + break; + } + } + Token::String(_) => unreachable!(), + } + } + } + + fn get_top(&self) -> PrintFrame { + const OUTER: PrintFrame = PrintFrame::Broken(0, Breaks::Inconsistent); + self.print_stack.last().map_or(OUTER, PrintFrame::clone) + } + + fn print_begin(&mut self, token: BeginToken, size: isize) { + if cfg!(prettyplease_debug) { + self.out.push(match token.breaks { + Breaks::Consistent => '«', + Breaks::Inconsistent => '‹', + }); + if cfg!(prettyplease_debug_indent) { + self.out + .extend(token.offset.to_string().chars().map(|ch| match ch { + '0'..='9' => ['₀', '₁', '₂', '₃', '₄', '₅', '₆', '₇', '₈', '₉'] + [(ch as u8 - b'0') as usize] + as char, + '-' => '₋', + _ => unreachable!(), + })); + } + } + if size > self.space { + self.print_stack + .push(PrintFrame::Broken(self.indent, token.breaks)); + self.indent = usize::try_from(self.indent as isize + token.offset).unwrap(); + } else { + self.print_stack.push(PrintFrame::Fits(token.breaks)); + } + } + + fn print_end(&mut self) { + let breaks = match self.print_stack.pop().unwrap() { + PrintFrame::Broken(indent, breaks) => { + self.indent = indent; + breaks + } + PrintFrame::Fits(breaks) => breaks, + }; + if cfg!(prettyplease_debug) { + self.out.push(match breaks { + Breaks::Consistent => '»', + Breaks::Inconsistent => '›', + }); + } + } + + fn print_break(&mut self, token: BreakToken, size: isize) { + let fits = token.never_break + || match self.get_top() { + PrintFrame::Fits(..) => true, + PrintFrame::Broken(.., Breaks::Consistent) => false, + PrintFrame::Broken(.., Breaks::Inconsistent) => size <= self.space, + }; + if fits { + self.pending_indentation += token.blank_space; + self.space -= token.blank_space as isize; + if let Some(no_break) = token.no_break { + self.out.push(no_break); + self.space -= no_break.len_utf8() as isize; + } + if cfg!(prettyplease_debug) { + self.out.push('·'); + } + } else { + if let Some(pre_break) = token.pre_break { + self.out.push(pre_break); + } + if cfg!(prettyplease_debug) { + self.out.push('·'); + } + self.out.push('\n'); + let indent = self.indent as isize + token.offset; + self.pending_indentation = usize::try_from(indent).unwrap(); + self.space = cmp::max(MARGIN - indent, MIN_SPACE); + if let Some(post_break) = token.post_break { + self.print_indent(); + self.out.push(post_break); + self.space -= post_break.len_utf8() as isize; + } + } + } + + fn print_string(&mut self, string: Cow<'static, str>) { + self.print_indent(); + self.out.push_str(&string); + self.space -= string.len() as isize; + } + + fn print_indent(&mut self) { + self.out.reserve(self.pending_indentation); + self.out + .extend(iter::repeat(' ').take(self.pending_indentation)); + self.pending_indentation = 0; + } +} diff --git a/packages/autofmt/src/prettyplease/attr.rs b/packages/autofmt/src/prettyplease/attr.rs new file mode 100644 index 000000000..d934a1c1f --- /dev/null +++ b/packages/autofmt/src/prettyplease/attr.rs @@ -0,0 +1,208 @@ +use super::algorithm::Printer; +use super::INDENT; +use proc_macro2::{Delimiter, TokenStream, TokenTree}; +use syn::{AttrStyle, Attribute, Lit, PathArguments}; + +impl Printer { + pub fn outer_attrs(&mut self, attrs: &[Attribute]) { + for attr in attrs { + if let AttrStyle::Outer = attr.style { + self.attr(attr); + } + } + } + + pub fn inner_attrs(&mut self, attrs: &[Attribute]) { + for attr in attrs { + if let AttrStyle::Inner(_) = attr.style { + self.attr(attr); + } + } + } + + fn attr(&mut self, attr: &Attribute) { + if let Some(doc) = value_of_attribute("doc", attr) { + if doc.contains('\n') { + self.word(match attr.style { + AttrStyle::Outer => "/**", + AttrStyle::Inner(_) => "/*!", + }); + self.word(doc); + self.word("*/"); + } else { + self.word(match attr.style { + AttrStyle::Outer => "///", + AttrStyle::Inner(_) => "//!", + }); + self.word(doc); + } + self.hardbreak(); + } else if let Some(comment) = value_of_attribute("comment", attr) { + if comment.contains('\n') { + self.word("/*"); + self.word(comment); + self.word("*/"); + } else { + self.word("//"); + self.word(comment); + } + self.hardbreak(); + } else { + self.word(match attr.style { + AttrStyle::Outer => "#", + AttrStyle::Inner(_) => "#!", + }); + self.word("["); + self.path(&attr.path); + self.attr_tokens(attr.tokens.clone()); + self.word("]"); + self.space(); + } + } + + fn attr_tokens(&mut self, tokens: TokenStream) { + let mut stack = Vec::new(); + stack.push((tokens.into_iter().peekable(), Delimiter::None)); + let mut space = Self::nbsp as fn(&mut Self); + + #[derive(PartialEq)] + enum State { + Word, + Punct, + TrailingComma, + } + + use State::*; + let mut state = Word; + + while let Some((tokens, delimiter)) = stack.last_mut() { + match tokens.next() { + Some(TokenTree::Ident(ident)) => { + if let Word = state { + space(self); + } + self.ident(&ident); + state = Word; + } + Some(TokenTree::Punct(punct)) => { + let ch = punct.as_char(); + if let (Word, '=') = (state, ch) { + self.nbsp(); + } + if ch == ',' && tokens.peek().is_none() { + self.trailing_comma(true); + state = TrailingComma; + } else { + self.token_punct(ch); + if ch == '=' { + self.nbsp(); + } else if ch == ',' { + space(self); + } + state = Punct; + } + } + Some(TokenTree::Literal(literal)) => { + if let Word = state { + space(self); + } + self.token_literal(&literal); + state = Word; + } + Some(TokenTree::Group(group)) => { + let delimiter = group.delimiter(); + let stream = group.stream(); + match delimiter { + Delimiter::Parenthesis => { + self.word("("); + self.cbox(INDENT); + self.zerobreak(); + state = Punct; + } + Delimiter::Brace => { + self.word("{"); + state = Punct; + } + Delimiter::Bracket => { + self.word("["); + state = Punct; + } + Delimiter::None => {} + } + stack.push((stream.into_iter().peekable(), delimiter)); + space = Self::space; + } + None => { + match delimiter { + Delimiter::Parenthesis => { + if state != TrailingComma { + self.zerobreak(); + } + self.offset(-INDENT); + self.end(); + self.word(")"); + state = Punct; + } + Delimiter::Brace => { + self.word("}"); + state = Punct; + } + Delimiter::Bracket => { + self.word("]"); + state = Punct; + } + Delimiter::None => {} + } + stack.pop(); + if stack.is_empty() { + space = Self::nbsp; + } + } + } + } + } +} + +fn value_of_attribute(requested: &str, attr: &Attribute) -> Option { + let is_doc = attr.path.leading_colon.is_none() + && attr.path.segments.len() == 1 + && matches!(attr.path.segments[0].arguments, PathArguments::None) + && attr.path.segments[0].ident == requested; + if !is_doc { + return None; + } + let mut tokens = attr.tokens.clone().into_iter(); + match tokens.next() { + Some(TokenTree::Punct(punct)) if punct.as_char() == '=' => {} + _ => return None, + } + let literal = match tokens.next() { + Some(TokenTree::Literal(literal)) => literal, + _ => return None, + }; + if tokens.next().is_some() { + return None; + } + match Lit::new(literal) { + Lit::Str(string) => Some(string.value()), + _ => None, + } +} + +pub fn has_outer(attrs: &[Attribute]) -> bool { + for attr in attrs { + if let AttrStyle::Outer = attr.style { + return true; + } + } + false +} + +pub fn has_inner(attrs: &[Attribute]) -> bool { + for attr in attrs { + if let AttrStyle::Inner(_) = attr.style { + return true; + } + } + false +} diff --git a/packages/autofmt/src/prettyplease/convenience.rs b/packages/autofmt/src/prettyplease/convenience.rs new file mode 100644 index 000000000..f87206320 --- /dev/null +++ b/packages/autofmt/src/prettyplease/convenience.rs @@ -0,0 +1,98 @@ +use super::algorithm::{self, BeginToken, BreakToken, Breaks, Printer}; +use std::borrow::Cow; + +impl Printer { + pub fn ibox(&mut self, indent: isize) { + self.scan_begin(BeginToken { + offset: indent, + breaks: Breaks::Inconsistent, + }); + } + + pub fn cbox(&mut self, indent: isize) { + self.scan_begin(BeginToken { + offset: indent, + breaks: Breaks::Consistent, + }); + } + + pub fn end(&mut self) { + self.scan_end(); + } + + pub fn word>>(&mut self, wrd: S) { + let s = wrd.into(); + self.scan_string(s); + } + + fn spaces(&mut self, n: usize) { + self.scan_break(BreakToken { + blank_space: n, + ..BreakToken::default() + }); + } + + pub fn zerobreak(&mut self) { + self.spaces(0); + } + + pub fn space(&mut self) { + self.spaces(1); + } + + pub fn nbsp(&mut self) { + self.word(" "); + } + + pub fn hardbreak(&mut self) { + self.spaces(algorithm::SIZE_INFINITY as usize); + } + + pub fn space_if_nonempty(&mut self) { + self.scan_break(BreakToken { + blank_space: 1, + if_nonempty: true, + ..BreakToken::default() + }); + } + + pub fn hardbreak_if_nonempty(&mut self) { + self.scan_break(BreakToken { + blank_space: algorithm::SIZE_INFINITY as usize, + if_nonempty: true, + ..BreakToken::default() + }); + } + + pub fn trailing_comma(&mut self, is_last: bool) { + if is_last { + self.scan_break(BreakToken { + pre_break: Some(','), + ..BreakToken::default() + }); + } else { + self.word(","); + self.space(); + } + } + + pub fn trailing_comma_or_space(&mut self, is_last: bool) { + if is_last { + self.scan_break(BreakToken { + blank_space: 1, + pre_break: Some(','), + ..BreakToken::default() + }); + } else { + self.word(","); + self.space(); + } + } + + pub fn neverbreak(&mut self) { + self.scan_break(BreakToken { + never_break: true, + ..BreakToken::default() + }); + } +} diff --git a/packages/autofmt/src/prettyplease/data.rs b/packages/autofmt/src/prettyplease/data.rs new file mode 100644 index 000000000..be20eae05 --- /dev/null +++ b/packages/autofmt/src/prettyplease/data.rs @@ -0,0 +1,95 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use syn::{ + Field, Fields, FieldsUnnamed, PathArguments, Variant, VisCrate, VisPublic, VisRestricted, + Visibility, +}; + +impl Printer { + pub fn variant(&mut self, variant: &Variant) { + self.outer_attrs(&variant.attrs); + self.ident(&variant.ident); + match &variant.fields { + Fields::Named(fields) => { + self.nbsp(); + self.word("{"); + self.cbox(INDENT); + self.space(); + for field in fields.named.iter().delimited() { + self.field(&field); + self.trailing_comma_or_space(field.is_last); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + Fields::Unnamed(fields) => { + self.cbox(INDENT); + self.fields_unnamed(fields); + self.end(); + } + Fields::Unit => {} + } + if let Some((_eq_token, discriminant)) = &variant.discriminant { + self.word(" = "); + self.expr(discriminant); + } + } + + pub fn fields_unnamed(&mut self, fields: &FieldsUnnamed) { + self.word("("); + self.zerobreak(); + for field in fields.unnamed.iter().delimited() { + self.field(&field); + self.trailing_comma(field.is_last); + } + self.offset(-INDENT); + self.word(")"); + } + + pub fn field(&mut self, field: &Field) { + self.outer_attrs(&field.attrs); + self.visibility(&field.vis); + if let Some(ident) = &field.ident { + self.ident(ident); + self.word(": "); + } + self.ty(&field.ty); + } + + pub fn visibility(&mut self, vis: &Visibility) { + match vis { + Visibility::Public(vis) => self.vis_public(vis), + Visibility::Crate(vis) => self.vis_crate(vis), + Visibility::Restricted(vis) => self.vis_restricted(vis), + Visibility::Inherited => {} + } + } + + fn vis_public(&mut self, vis: &VisPublic) { + let _ = vis; + self.word("pub "); + } + + fn vis_crate(&mut self, vis: &VisCrate) { + let _ = vis; + self.word("crate "); + } + + fn vis_restricted(&mut self, vis: &VisRestricted) { + self.word("pub("); + let omit_in = vis.path.leading_colon.is_none() + && vis.path.segments.len() == 1 + && matches!(vis.path.segments[0].arguments, PathArguments::None) + && matches!( + vis.path.segments[0].ident.to_string().as_str(), + "self" | "super" | "crate", + ); + if !omit_in { + self.word("in "); + } + self.path(&vis.path); + self.word(") "); + } +} diff --git a/packages/autofmt/src/prettyplease/expr.rs b/packages/autofmt/src/prettyplease/expr.rs new file mode 100644 index 000000000..e659118a4 --- /dev/null +++ b/packages/autofmt/src/prettyplease/expr.rs @@ -0,0 +1,1028 @@ +use super::algorithm::{BreakToken, Printer}; +use super::attr; +use super::iter::IterDelimited; +use super::stmt; +use super::INDENT; +use proc_macro2::TokenStream; +use syn::punctuated::Punctuated; +use syn::{ + token, Arm, Attribute, BinOp, Block, Expr, ExprArray, ExprAssign, ExprAssignOp, ExprAsync, + ExprAwait, ExprBinary, ExprBlock, ExprBox, ExprBreak, ExprCall, ExprCast, ExprClosure, + ExprContinue, ExprField, ExprForLoop, ExprGroup, ExprIf, ExprIndex, ExprLet, ExprLit, ExprLoop, + ExprMacro, ExprMatch, ExprMethodCall, ExprParen, ExprPath, ExprRange, ExprReference, + ExprRepeat, ExprReturn, ExprStruct, ExprTry, ExprTryBlock, ExprTuple, ExprType, ExprUnary, + ExprUnsafe, ExprWhile, ExprYield, FieldValue, GenericMethodArgument, Index, Label, Member, + MethodTurbofish, PathArguments, RangeLimits, ReturnType, Stmt, Token, UnOp, +}; + +impl Printer { + pub fn expr(&mut self, expr: &Expr) { + match expr { + Expr::Array(expr) => self.expr_array(expr), + Expr::Assign(expr) => self.expr_assign(expr), + Expr::AssignOp(expr) => self.expr_assign_op(expr), + Expr::Async(expr) => self.expr_async(expr), + Expr::Await(expr) => self.expr_await(expr, false), + Expr::Binary(expr) => self.expr_binary(expr), + Expr::Block(expr) => self.expr_block(expr), + Expr::Box(expr) => self.expr_box(expr), + Expr::Break(expr) => self.expr_break(expr), + Expr::Call(expr) => self.expr_call(expr, false), + Expr::Cast(expr) => self.expr_cast(expr), + Expr::Closure(expr) => self.expr_closure(expr), + Expr::Continue(expr) => self.expr_continue(expr), + Expr::Field(expr) => self.expr_field(expr, false), + Expr::ForLoop(expr) => self.expr_for_loop(expr), + Expr::Group(expr) => self.expr_group(expr), + Expr::If(expr) => self.expr_if(expr), + Expr::Index(expr) => self.expr_index(expr, false), + Expr::Let(expr) => self.expr_let(expr), + Expr::Lit(expr) => self.expr_lit(expr), + Expr::Loop(expr) => self.expr_loop(expr), + Expr::Macro(expr) => self.expr_macro(expr), + Expr::Match(expr) => self.expr_match(expr), + Expr::MethodCall(expr) => self.expr_method_call(expr, false), + Expr::Paren(expr) => self.expr_paren(expr), + Expr::Path(expr) => self.expr_path(expr), + Expr::Range(expr) => self.expr_range(expr), + Expr::Reference(expr) => self.expr_reference(expr), + Expr::Repeat(expr) => self.expr_repeat(expr), + Expr::Return(expr) => self.expr_return(expr), + Expr::Struct(expr) => self.expr_struct(expr), + Expr::Try(expr) => self.expr_try(expr, false), + Expr::TryBlock(expr) => self.expr_try_block(expr), + Expr::Tuple(expr) => self.expr_tuple(expr), + Expr::Type(expr) => self.expr_type(expr), + Expr::Unary(expr) => self.expr_unary(expr), + Expr::Unsafe(expr) => self.expr_unsafe(expr), + Expr::Verbatim(expr) => self.expr_verbatim(expr), + Expr::While(expr) => self.expr_while(expr), + Expr::Yield(expr) => self.expr_yield(expr), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown Expr"), + } + } + + pub fn expr_beginning_of_line(&mut self, expr: &Expr, beginning_of_line: bool) { + match expr { + Expr::Await(expr) => self.expr_await(expr, beginning_of_line), + Expr::Field(expr) => self.expr_field(expr, beginning_of_line), + Expr::Index(expr) => self.expr_index(expr, beginning_of_line), + Expr::MethodCall(expr) => self.expr_method_call(expr, beginning_of_line), + Expr::Try(expr) => self.expr_try(expr, beginning_of_line), + _ => self.expr(expr), + } + } + + fn subexpr(&mut self, expr: &Expr, beginning_of_line: bool) { + match expr { + Expr::Await(expr) => self.subexpr_await(expr, beginning_of_line), + Expr::Call(expr) => self.subexpr_call(expr), + Expr::Field(expr) => self.subexpr_field(expr, beginning_of_line), + Expr::Index(expr) => self.subexpr_index(expr, beginning_of_line), + Expr::MethodCall(expr) => self.subexpr_method_call(expr, beginning_of_line, false), + Expr::Try(expr) => self.subexpr_try(expr, beginning_of_line), + _ => { + self.cbox(-INDENT); + self.expr(expr); + self.end(); + } + } + } + + // If the given expression is a bare `ExprStruct`, wraps it in parenthesis + // before appending it to `TokenStream`. + fn wrap_exterior_struct(&mut self, expr: &Expr) { + let needs_paren = contains_exterior_struct_lit(expr); + if needs_paren { + self.word("("); + } + self.cbox(0); + self.expr(expr); + if needs_paren { + self.word(")"); + } + if needs_newline_if_wrap(expr) { + self.space(); + } else { + self.nbsp(); + } + self.end(); + } + + fn expr_array(&mut self, expr: &ExprArray) { + self.outer_attrs(&expr.attrs); + self.word("["); + self.cbox(INDENT); + self.zerobreak(); + for element in expr.elems.iter().delimited() { + self.expr(&element); + self.trailing_comma(element.is_last); + } + self.offset(-INDENT); + self.end(); + self.word("]"); + } + + fn expr_assign(&mut self, expr: &ExprAssign) { + self.outer_attrs(&expr.attrs); + self.ibox(0); + self.expr(&expr.left); + self.word(" = "); + self.expr(&expr.right); + self.end(); + } + + fn expr_assign_op(&mut self, expr: &ExprAssignOp) { + self.outer_attrs(&expr.attrs); + self.ibox(INDENT); + self.ibox(-INDENT); + self.expr(&expr.left); + self.end(); + self.space(); + self.binary_operator(&expr.op); + self.nbsp(); + self.expr(&expr.right); + self.end(); + } + + fn expr_async(&mut self, expr: &ExprAsync) { + self.outer_attrs(&expr.attrs); + self.word("async "); + if expr.capture.is_some() { + self.word("move "); + } + self.cbox(INDENT); + self.small_block(&expr.block, &expr.attrs); + self.end(); + } + + fn expr_await(&mut self, expr: &ExprAwait, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.cbox(INDENT); + self.subexpr_await(expr, beginning_of_line); + self.end(); + } + + fn subexpr_await(&mut self, expr: &ExprAwait, beginning_of_line: bool) { + self.subexpr(&expr.base, beginning_of_line); + self.zerobreak_unless_short_ident(beginning_of_line, &expr.base); + self.word(".await"); + } + + fn expr_binary(&mut self, expr: &ExprBinary) { + self.outer_attrs(&expr.attrs); + self.ibox(INDENT); + self.ibox(-INDENT); + self.expr(&expr.left); + self.end(); + self.space(); + self.binary_operator(&expr.op); + self.nbsp(); + self.expr(&expr.right); + self.end(); + } + + pub fn expr_block(&mut self, expr: &ExprBlock) { + self.outer_attrs(&expr.attrs); + if let Some(label) = &expr.label { + self.label(label); + } + self.cbox(INDENT); + self.small_block(&expr.block, &expr.attrs); + self.end(); + } + + fn expr_box(&mut self, expr: &ExprBox) { + self.outer_attrs(&expr.attrs); + self.word("box "); + self.expr(&expr.expr); + } + + fn expr_break(&mut self, expr: &ExprBreak) { + self.outer_attrs(&expr.attrs); + self.word("break"); + if let Some(lifetime) = &expr.label { + self.nbsp(); + self.lifetime(lifetime); + } + if let Some(value) = &expr.expr { + self.nbsp(); + self.expr(value); + } + } + + fn expr_call(&mut self, expr: &ExprCall, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.expr_beginning_of_line(&expr.func, beginning_of_line); + self.call_args(&expr.args); + } + + fn subexpr_call(&mut self, expr: &ExprCall) { + self.subexpr(&expr.func, false); + self.call_args(&expr.args); + } + + fn expr_cast(&mut self, expr: &ExprCast) { + self.outer_attrs(&expr.attrs); + self.ibox(INDENT); + self.ibox(-INDENT); + self.expr(&expr.expr); + self.end(); + self.space(); + self.word("as "); + self.ty(&expr.ty); + self.end(); + } + + fn expr_closure(&mut self, expr: &ExprClosure) { + self.outer_attrs(&expr.attrs); + self.ibox(0); + if expr.asyncness.is_some() { + self.word("async "); + } + if expr.movability.is_some() { + self.word("static "); + } + if expr.capture.is_some() { + self.word("move "); + } + self.cbox(INDENT); + self.word("|"); + for pat in expr.inputs.iter().delimited() { + if pat.is_first { + self.zerobreak(); + } + self.pat(&pat); + if !pat.is_last { + self.word(","); + self.space(); + } + } + match &expr.output { + ReturnType::Default => { + self.word("|"); + self.space(); + self.offset(-INDENT); + self.end(); + } + ReturnType::Type(_arrow, ty) => { + if !expr.inputs.is_empty() { + self.trailing_comma(true); + self.offset(-INDENT); + } + self.word("|"); + self.end(); + self.word(" -> "); + self.ty(ty); + self.nbsp(); + } + } + self.neverbreak(); + self.expr(&expr.body); + self.end(); + } + + fn expr_continue(&mut self, expr: &ExprContinue) { + self.outer_attrs(&expr.attrs); + self.word("continue"); + if let Some(lifetime) = &expr.label { + self.nbsp(); + self.lifetime(lifetime); + } + } + + fn expr_field(&mut self, expr: &ExprField, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.cbox(INDENT); + self.subexpr_field(expr, beginning_of_line); + self.end(); + } + + fn subexpr_field(&mut self, expr: &ExprField, beginning_of_line: bool) { + self.subexpr(&expr.base, beginning_of_line); + self.zerobreak_unless_short_ident(beginning_of_line, &expr.base); + self.word("."); + self.member(&expr.member); + } + + fn expr_for_loop(&mut self, expr: &ExprForLoop) { + self.outer_attrs(&expr.attrs); + self.ibox(0); + if let Some(label) = &expr.label { + self.label(label); + } + self.word("for "); + self.pat(&expr.pat); + self.word(" in "); + self.neverbreak(); + self.wrap_exterior_struct(&expr.expr); + self.word("{"); + self.neverbreak(); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + self.inner_attrs(&expr.attrs); + for stmt in &expr.body.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.end(); + } + + fn expr_group(&mut self, expr: &ExprGroup) { + self.outer_attrs(&expr.attrs); + self.expr(&expr.expr); + } + + fn expr_if(&mut self, expr: &ExprIf) { + self.outer_attrs(&expr.attrs); + self.cbox(INDENT); + self.word("if "); + self.cbox(-INDENT); + self.wrap_exterior_struct(&expr.cond); + self.end(); + if let Some((_else_token, else_branch)) = &expr.else_branch { + let mut else_branch = &**else_branch; + self.small_block(&expr.then_branch, &[]); + loop { + self.word(" else "); + match else_branch { + Expr::If(expr) => { + self.word("if "); + self.wrap_exterior_struct(&expr.cond); + self.small_block(&expr.then_branch, &[]); + if let Some((_else_token, next)) = &expr.else_branch { + else_branch = next; + continue; + } + } + Expr::Block(expr) => { + self.small_block(&expr.block, &[]); + } + // If not one of the valid expressions to exist in an else + // clause, wrap in a block. + other => { + self.word("{"); + self.space(); + self.ibox(INDENT); + self.expr(other); + self.end(); + self.space(); + self.offset(-INDENT); + self.word("}"); + } + } + break; + } + } else if expr.then_branch.stmts.is_empty() { + self.word("{}"); + } else { + self.word("{"); + self.hardbreak(); + for stmt in &expr.then_branch.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.word("}"); + } + self.end(); + } + + fn expr_index(&mut self, expr: &ExprIndex, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.expr_beginning_of_line(&expr.expr, beginning_of_line); + self.word("["); + self.expr(&expr.index); + self.word("]"); + } + + fn subexpr_index(&mut self, expr: &ExprIndex, beginning_of_line: bool) { + self.subexpr(&expr.expr, beginning_of_line); + self.word("["); + self.expr(&expr.index); + self.word("]"); + } + + fn expr_let(&mut self, expr: &ExprLet) { + self.outer_attrs(&expr.attrs); + self.ibox(INDENT); + self.word("let "); + self.ibox(-INDENT); + self.pat(&expr.pat); + self.end(); + self.space(); + self.word("= "); + let needs_paren = contains_exterior_struct_lit(&expr.expr); + if needs_paren { + self.word("("); + } + self.expr(&expr.expr); + if needs_paren { + self.word(")"); + } + self.end(); + } + + pub fn expr_lit(&mut self, expr: &ExprLit) { + self.outer_attrs(&expr.attrs); + self.lit(&expr.lit); + } + + fn expr_loop(&mut self, expr: &ExprLoop) { + self.outer_attrs(&expr.attrs); + if let Some(label) = &expr.label { + self.label(label); + } + self.word("loop {"); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + self.inner_attrs(&expr.attrs); + for stmt in &expr.body.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + + fn expr_macro(&mut self, expr: &ExprMacro) { + self.outer_attrs(&expr.attrs); + self.mac(&expr.mac, None); + } + + fn expr_match(&mut self, expr: &ExprMatch) { + self.outer_attrs(&expr.attrs); + self.ibox(0); + self.word("match "); + self.wrap_exterior_struct(&expr.expr); + self.word("{"); + self.neverbreak(); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + self.inner_attrs(&expr.attrs); + for arm in &expr.arms { + self.arm(arm); + self.hardbreak(); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.end(); + } + + fn expr_method_call(&mut self, expr: &ExprMethodCall, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.cbox(INDENT); + let unindent_call_args = beginning_of_line && is_short_ident(&expr.receiver); + self.subexpr_method_call(expr, beginning_of_line, unindent_call_args); + self.end(); + } + + fn subexpr_method_call( + &mut self, + expr: &ExprMethodCall, + beginning_of_line: bool, + unindent_call_args: bool, + ) { + self.subexpr(&expr.receiver, beginning_of_line); + self.zerobreak_unless_short_ident(beginning_of_line, &expr.receiver); + self.word("."); + self.ident(&expr.method); + if let Some(turbofish) = &expr.turbofish { + self.method_turbofish(turbofish); + } + self.cbox(if unindent_call_args { -INDENT } else { 0 }); + self.call_args(&expr.args); + self.end(); + } + + fn expr_paren(&mut self, expr: &ExprParen) { + self.outer_attrs(&expr.attrs); + self.word("("); + self.expr(&expr.expr); + self.word(")"); + } + + fn expr_path(&mut self, expr: &ExprPath) { + self.outer_attrs(&expr.attrs); + self.qpath(&expr.qself, &expr.path); + } + + fn expr_range(&mut self, expr: &ExprRange) { + self.outer_attrs(&expr.attrs); + if let Some(from) = &expr.from { + self.expr(from); + } + self.word(match expr.limits { + RangeLimits::HalfOpen(_) => "..", + RangeLimits::Closed(_) => "..=", + }); + if let Some(to) = &expr.to { + self.expr(to); + } + } + + fn expr_reference(&mut self, expr: &ExprReference) { + self.outer_attrs(&expr.attrs); + self.word("&"); + if expr.mutability.is_some() { + self.word("mut "); + } + self.expr(&expr.expr); + } + + fn expr_repeat(&mut self, expr: &ExprRepeat) { + self.outer_attrs(&expr.attrs); + self.word("["); + self.expr(&expr.expr); + self.word("; "); + self.expr(&expr.len); + self.word("]"); + } + + fn expr_return(&mut self, expr: &ExprReturn) { + self.outer_attrs(&expr.attrs); + self.word("return"); + if let Some(value) = &expr.expr { + self.nbsp(); + self.expr(value); + } + } + + fn expr_struct(&mut self, expr: &ExprStruct) { + self.outer_attrs(&expr.attrs); + self.cbox(INDENT); + self.ibox(-INDENT); + self.path(&expr.path); + self.end(); + self.word(" {"); + self.space_if_nonempty(); + for field_value in expr.fields.iter().delimited() { + self.field_value(&field_value); + self.trailing_comma_or_space(field_value.is_last && expr.rest.is_none()); + } + if let Some(rest) = &expr.rest { + self.word(".."); + self.expr(rest); + self.space(); + } + self.offset(-INDENT); + self.end_with_max_width(34); + self.word("}"); + } + + fn expr_try(&mut self, expr: &ExprTry, beginning_of_line: bool) { + self.outer_attrs(&expr.attrs); + self.expr_beginning_of_line(&expr.expr, beginning_of_line); + self.word("?"); + } + + fn subexpr_try(&mut self, expr: &ExprTry, beginning_of_line: bool) { + self.subexpr(&expr.expr, beginning_of_line); + self.word("?"); + } + + fn expr_try_block(&mut self, expr: &ExprTryBlock) { + self.outer_attrs(&expr.attrs); + self.word("try "); + self.cbox(INDENT); + self.small_block(&expr.block, &expr.attrs); + self.end(); + } + + fn expr_tuple(&mut self, expr: &ExprTuple) { + self.outer_attrs(&expr.attrs); + self.word("("); + self.cbox(INDENT); + self.zerobreak(); + for elem in expr.elems.iter().delimited() { + self.expr(&elem); + if expr.elems.len() == 1 { + self.word(","); + self.zerobreak(); + } else { + self.trailing_comma(elem.is_last); + } + } + self.offset(-INDENT); + self.end(); + self.word(")"); + } + + fn expr_type(&mut self, expr: &ExprType) { + self.outer_attrs(&expr.attrs); + self.ibox(INDENT); + self.ibox(-INDENT); + self.expr(&expr.expr); + self.end(); + self.space(); + self.word(": "); + self.ty(&expr.ty); + self.end(); + } + + fn expr_unary(&mut self, expr: &ExprUnary) { + self.outer_attrs(&expr.attrs); + self.unary_operator(&expr.op); + self.expr(&expr.expr); + } + + fn expr_unsafe(&mut self, expr: &ExprUnsafe) { + self.outer_attrs(&expr.attrs); + self.word("unsafe {"); + self.cbox(INDENT); + self.space_if_nonempty(); + self.inner_attrs(&expr.attrs); + for stmt in expr.block.stmts.iter().delimited() { + if stmt.is_first && stmt.is_last { + if let Stmt::Expr(expr) = &*stmt { + self.expr(expr); + self.space(); + continue; + } + } + self.stmt(&stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + + fn expr_verbatim(&mut self, expr: &TokenStream) { + if !expr.is_empty() { + unimplemented!("Expr::Verbatim `{}`", expr); + } + } + + fn expr_while(&mut self, expr: &ExprWhile) { + self.outer_attrs(&expr.attrs); + if let Some(label) = &expr.label { + self.label(label); + } + self.word("while "); + self.wrap_exterior_struct(&expr.cond); + self.word("{"); + self.neverbreak(); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + self.inner_attrs(&expr.attrs); + for stmt in &expr.body.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + + fn expr_yield(&mut self, expr: &ExprYield) { + self.outer_attrs(&expr.attrs); + self.word("yield"); + if let Some(value) = &expr.expr { + self.nbsp(); + self.expr(value); + } + } + + fn label(&mut self, label: &Label) { + self.lifetime(&label.name); + self.word(": "); + } + + fn field_value(&mut self, field_value: &FieldValue) { + self.outer_attrs(&field_value.attrs); + self.member(&field_value.member); + if field_value.colon_token.is_some() { + self.word(": "); + self.ibox(0); + self.expr(&field_value.expr); + self.end(); + } + } + + fn arm(&mut self, arm: &Arm) { + self.outer_attrs(&arm.attrs); + self.ibox(0); + self.pat(&arm.pat); + if let Some((_if_token, guard)) = &arm.guard { + self.word(" if "); + self.expr(guard); + } + self.word(" =>"); + let empty_block; + let mut body = &*arm.body; + while let Expr::Block(expr) = body { + if expr.attrs.is_empty() && expr.label.is_none() { + let mut stmts = expr.block.stmts.iter(); + if let (Some(Stmt::Expr(inner)), None) = (stmts.next(), stmts.next()) { + body = inner; + continue; + } + } + break; + } + if let Expr::Tuple(expr) = body { + if expr.elems.is_empty() && expr.attrs.is_empty() { + empty_block = Expr::Block(ExprBlock { + attrs: Vec::new(), + label: None, + block: Block { + brace_token: token::Brace::default(), + stmts: Vec::new(), + }, + }); + body = &empty_block; + } + } + if let Expr::Block(body) = body { + self.nbsp(); + if let Some(label) = &body.label { + self.label(label); + } + self.word("{"); + self.neverbreak(); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + self.inner_attrs(&body.attrs); + for stmt in &body.block.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.end(); + } else { + self.nbsp(); + self.neverbreak(); + self.cbox(INDENT); + self.scan_break(BreakToken { + pre_break: Some('{'), + ..BreakToken::default() + }); + self.expr(body); + self.scan_break(BreakToken { + offset: -INDENT, + pre_break: stmt::add_semi(body).then(|| ';'), + post_break: Some('}'), + no_break: requires_terminator(body).then(|| ','), + ..BreakToken::default() + }); + self.end(); + self.end(); + } + } + + fn method_turbofish(&mut self, turbofish: &MethodTurbofish) { + self.word("::<"); + self.cbox(0); + self.zerobreak(); + for arg in turbofish.args.iter().delimited() { + self.generic_method_argument(&arg); + self.trailing_comma(arg.is_last); + } + self.offset(-INDENT); + self.end(); + self.word(">"); + } + + fn generic_method_argument(&mut self, generic: &GenericMethodArgument) { + match generic { + GenericMethodArgument::Type(arg) => self.ty(arg), + GenericMethodArgument::Const(arg) => self.expr(arg), + } + } + + fn call_args(&mut self, args: &Punctuated) { + self.word("("); + let mut iter = args.iter(); + match (iter.next(), iter.next()) { + ( + Some( + expr @ (Expr::Array(ExprArray { attrs, .. }) + | Expr::Async(ExprAsync { attrs, .. }) + | Expr::Block(ExprBlock { attrs, .. }) + | Expr::Closure(ExprClosure { attrs, .. }) + | Expr::Struct(ExprStruct { attrs, .. }) + | Expr::TryBlock(ExprTryBlock { attrs, .. }) + | Expr::Tuple(ExprTuple { attrs, .. }) + | Expr::Unsafe(ExprUnsafe { attrs, .. })), + ), + None, + ) if !attr::has_outer(attrs) => { + self.expr(expr); + } + _ => { + self.cbox(INDENT); + self.zerobreak(); + for arg in args.iter().delimited() { + self.expr(&arg); + self.trailing_comma(arg.is_last); + } + self.offset(-INDENT); + self.end(); + } + } + self.word(")"); + } + + fn small_block(&mut self, block: &Block, attrs: &[Attribute]) { + self.word("{"); + if attr::has_inner(attrs) || !block.stmts.is_empty() { + self.space(); + self.inner_attrs(attrs); + match (block.stmts.get(0), block.stmts.get(1)) { + (Some(Stmt::Expr(expr)), None) if stmt::break_after(expr) => { + self.ibox(0); + self.expr_beginning_of_line(expr, true); + self.end(); + self.space(); + } + _ => { + for stmt in &block.stmts { + self.stmt(stmt); + } + } + } + self.offset(-INDENT); + } + self.word("}"); + } + + pub fn member(&mut self, member: &Member) { + match member { + Member::Named(ident) => self.ident(ident), + Member::Unnamed(index) => self.index(index), + } + } + + fn index(&mut self, member: &Index) { + self.word(member.index.to_string()); + } + + fn binary_operator(&mut self, op: &BinOp) { + self.word(match op { + BinOp::Add(_) => "+", + BinOp::Sub(_) => "-", + BinOp::Mul(_) => "*", + BinOp::Div(_) => "/", + BinOp::Rem(_) => "%", + BinOp::And(_) => "&&", + BinOp::Or(_) => "||", + BinOp::BitXor(_) => "^", + BinOp::BitAnd(_) => "&", + BinOp::BitOr(_) => "|", + BinOp::Shl(_) => "<<", + BinOp::Shr(_) => ">>", + BinOp::Eq(_) => "==", + BinOp::Lt(_) => "<", + BinOp::Le(_) => "<=", + BinOp::Ne(_) => "!=", + BinOp::Ge(_) => ">=", + BinOp::Gt(_) => ">", + BinOp::AddEq(_) => "+=", + BinOp::SubEq(_) => "-=", + BinOp::MulEq(_) => "*=", + BinOp::DivEq(_) => "/=", + BinOp::RemEq(_) => "%=", + BinOp::BitXorEq(_) => "^=", + BinOp::BitAndEq(_) => "&=", + BinOp::BitOrEq(_) => "|=", + BinOp::ShlEq(_) => "<<=", + BinOp::ShrEq(_) => ">>=", + }); + } + + fn unary_operator(&mut self, op: &UnOp) { + self.word(match op { + UnOp::Deref(_) => "*", + UnOp::Not(_) => "!", + UnOp::Neg(_) => "-", + }); + } + + fn zerobreak_unless_short_ident(&mut self, beginning_of_line: bool, expr: &Expr) { + if beginning_of_line && is_short_ident(expr) { + return; + } + self.zerobreak(); + } +} + +pub fn requires_terminator(expr: &Expr) -> bool { + // see https://github.com/rust-lang/rust/blob/2679c38fc/src/librustc_ast/util/classify.rs#L7-L25 + match expr { + Expr::Unsafe(_) + | Expr::Block(_) + | Expr::If(_) + | Expr::Match(_) + | Expr::While(_) + | Expr::Loop(_) + | Expr::ForLoop(_) + | Expr::Async(_) + | Expr::TryBlock(_) => false, + _ => true, + } +} + +// Expressions that syntactically contain an "exterior" struct literal i.e. not +// surrounded by any parens or other delimiters. For example `X { y: 1 }`, `X { +// y: 1 }.method()`, `foo == X { y: 1 }` and `X { y: 1 } == foo` all do, but `(X +// { y: 1 }) == foo` does not. +fn contains_exterior_struct_lit(expr: &Expr) -> bool { + match expr { + Expr::Struct(_) => true, + + Expr::Assign(ExprAssign { left, right, .. }) + | Expr::AssignOp(ExprAssignOp { left, right, .. }) + | Expr::Binary(ExprBinary { left, right, .. }) => { + // X { y: 1 } + X { y: 2 } + contains_exterior_struct_lit(left) || contains_exterior_struct_lit(right) + } + + Expr::Await(ExprAwait { base: e, .. }) + | Expr::Box(ExprBox { expr: e, .. }) + | Expr::Cast(ExprCast { expr: e, .. }) + | Expr::Field(ExprField { base: e, .. }) + | Expr::Index(ExprIndex { expr: e, .. }) + | Expr::MethodCall(ExprMethodCall { receiver: e, .. }) + | Expr::Reference(ExprReference { expr: e, .. }) + | Expr::Type(ExprType { expr: e, .. }) + | Expr::Unary(ExprUnary { expr: e, .. }) => { + // &X { y: 1 }, X { y: 1 }.y + contains_exterior_struct_lit(e) + } + + _ => false, + } +} + +fn needs_newline_if_wrap(expr: &Expr) -> bool { + match expr { + Expr::Array(_) + | Expr::Async(_) + | Expr::Block(_) + | Expr::Break(ExprBreak { expr: None, .. }) + | Expr::Closure(_) + | Expr::Continue(_) + | Expr::ForLoop(_) + | Expr::If(_) + | Expr::Lit(_) + | Expr::Loop(_) + | Expr::Macro(_) + | Expr::Match(_) + | Expr::Path(_) + | Expr::Range(ExprRange { to: None, .. }) + | Expr::Repeat(_) + | Expr::Return(ExprReturn { expr: None, .. }) + | Expr::Struct(_) + | Expr::TryBlock(_) + | Expr::Tuple(_) + | Expr::Unsafe(_) + | Expr::Verbatim(_) + | Expr::While(_) + | Expr::Yield(ExprYield { expr: None, .. }) => false, + + Expr::Assign(_) + | Expr::AssignOp(_) + | Expr::Await(_) + | Expr::Binary(_) + | Expr::Cast(_) + | Expr::Field(_) + | Expr::Index(_) + | Expr::MethodCall(_) + | Expr::Type(_) => true, + + Expr::Box(ExprBox { expr: e, .. }) + | Expr::Break(ExprBreak { expr: Some(e), .. }) + | Expr::Call(ExprCall { func: e, .. }) + | Expr::Group(ExprGroup { expr: e, .. }) + | Expr::Let(ExprLet { expr: e, .. }) + | Expr::Paren(ExprParen { expr: e, .. }) + | Expr::Range(ExprRange { to: Some(e), .. }) + | Expr::Reference(ExprReference { expr: e, .. }) + | Expr::Return(ExprReturn { expr: Some(e), .. }) + | Expr::Try(ExprTry { expr: e, .. }) + | Expr::Unary(ExprUnary { expr: e, .. }) + | Expr::Yield(ExprYield { expr: Some(e), .. }) => needs_newline_if_wrap(e), + + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => false, + } +} + +fn is_short_ident(expr: &Expr) -> bool { + if let Expr::Path(expr) = expr { + if expr.attrs.is_empty() + && expr.qself.is_none() + && expr.path.leading_colon.is_none() + && expr.path.segments.len() == 1 + && expr.path.segments[0].ident.to_string().len() as isize <= INDENT + { + if let PathArguments::None = expr.path.segments[0].arguments { + return true; + } + } + } + false +} diff --git a/packages/autofmt/src/prettyplease/file.rs b/packages/autofmt/src/prettyplease/file.rs new file mode 100644 index 000000000..51eece54c --- /dev/null +++ b/packages/autofmt/src/prettyplease/file.rs @@ -0,0 +1,17 @@ +use super::algorithm::Printer; +use syn::File; + +impl Printer { + pub fn file(&mut self, file: &File) { + self.cbox(0); + if let Some(shebang) = &file.shebang { + self.word(shebang.clone()); + self.hardbreak(); + } + self.inner_attrs(&file.attrs); + for item in &file.items { + self.item(item); + } + self.end(); + } +} diff --git a/packages/autofmt/src/prettyplease/generics.rs b/packages/autofmt/src/prettyplease/generics.rs new file mode 100644 index 000000000..ded973b08 --- /dev/null +++ b/packages/autofmt/src/prettyplease/generics.rs @@ -0,0 +1,280 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use syn::{ + BoundLifetimes, ConstParam, GenericParam, Generics, LifetimeDef, PredicateEq, + PredicateLifetime, PredicateType, TraitBound, TraitBoundModifier, TypeParam, TypeParamBound, + WhereClause, WherePredicate, +}; + +impl Printer { + pub fn generics(&mut self, generics: &Generics) { + if generics.params.is_empty() { + return; + } + + self.word("<"); + self.cbox(0); + self.zerobreak(); + + // Print lifetimes before types and consts, regardless of their + // order in self.params. + // + // TODO: ordering rules for const parameters vs type parameters have + // not been settled yet. https://github.com/rust-lang/rust/issues/44580 + for param in generics.params.iter().delimited() { + if let GenericParam::Lifetime(_) = *param { + self.generic_param(¶m); + self.trailing_comma(param.is_last); + } + } + for param in generics.params.iter().delimited() { + match *param { + GenericParam::Type(_) | GenericParam::Const(_) => { + self.generic_param(¶m); + self.trailing_comma(param.is_last); + } + GenericParam::Lifetime(_) => {} + } + } + + self.offset(-INDENT); + self.end(); + self.word(">"); + } + + fn generic_param(&mut self, generic_param: &GenericParam) { + match generic_param { + GenericParam::Type(type_param) => self.type_param(type_param), + GenericParam::Lifetime(lifetime_def) => self.lifetime_def(lifetime_def), + GenericParam::Const(const_param) => self.const_param(const_param), + } + } + + pub fn bound_lifetimes(&mut self, bound_lifetimes: &BoundLifetimes) { + self.word("for<"); + for lifetime_def in bound_lifetimes.lifetimes.iter().delimited() { + self.lifetime_def(&lifetime_def); + if !lifetime_def.is_last { + self.word(", "); + } + } + self.word("> "); + } + + fn lifetime_def(&mut self, lifetime_def: &LifetimeDef) { + self.outer_attrs(&lifetime_def.attrs); + self.lifetime(&lifetime_def.lifetime); + for lifetime in lifetime_def.bounds.iter().delimited() { + if lifetime.is_first { + self.word(": "); + } else { + self.word(" + "); + } + self.lifetime(&lifetime); + } + } + + fn type_param(&mut self, type_param: &TypeParam) { + self.outer_attrs(&type_param.attrs); + self.ident(&type_param.ident); + self.ibox(INDENT); + for type_param_bound in type_param.bounds.iter().delimited() { + if type_param_bound.is_first { + self.word(": "); + } else { + self.space(); + self.word("+ "); + } + self.type_param_bound(&type_param_bound); + } + if let Some(default) = &type_param.default { + self.space(); + self.word("= "); + self.ty(default); + } + self.end(); + } + + pub fn type_param_bound(&mut self, type_param_bound: &TypeParamBound) { + match type_param_bound { + TypeParamBound::Trait(trait_bound) => self.trait_bound(trait_bound), + TypeParamBound::Lifetime(lifetime) => self.lifetime(lifetime), + } + } + + fn trait_bound(&mut self, trait_bound: &TraitBound) { + if trait_bound.paren_token.is_some() { + self.word("("); + } + let skip = match trait_bound.path.segments.first() { + Some(segment) if segment.ident == "const" => { + self.word("~const "); + 1 + } + _ => 0, + }; + self.trait_bound_modifier(&trait_bound.modifier); + if let Some(bound_lifetimes) = &trait_bound.lifetimes { + self.bound_lifetimes(bound_lifetimes); + } + for segment in trait_bound.path.segments.iter().skip(skip).delimited() { + if !segment.is_first || trait_bound.path.leading_colon.is_some() { + self.word("::"); + } + self.path_segment(&segment); + } + if trait_bound.paren_token.is_some() { + self.word(")"); + } + } + + fn trait_bound_modifier(&mut self, trait_bound_modifier: &TraitBoundModifier) { + match trait_bound_modifier { + TraitBoundModifier::None => {} + TraitBoundModifier::Maybe(_question_mark) => self.word("?"), + } + } + + fn const_param(&mut self, const_param: &ConstParam) { + self.outer_attrs(&const_param.attrs); + self.word("const "); + self.ident(&const_param.ident); + self.word(": "); + self.ty(&const_param.ty); + if let Some(default) = &const_param.default { + self.word(" = "); + self.expr(default); + } + } + + pub fn where_clause_for_body(&mut self, where_clause: &Option) { + let hardbreaks = true; + let semi = false; + self.where_clause_impl(where_clause, hardbreaks, semi); + } + + pub fn where_clause_semi(&mut self, where_clause: &Option) { + let hardbreaks = true; + let semi = true; + self.where_clause_impl(where_clause, hardbreaks, semi); + } + + pub fn where_clause_oneline(&mut self, where_clause: &Option) { + let hardbreaks = false; + let semi = false; + self.where_clause_impl(where_clause, hardbreaks, semi); + } + + pub fn where_clause_oneline_semi(&mut self, where_clause: &Option) { + let hardbreaks = false; + let semi = true; + self.where_clause_impl(where_clause, hardbreaks, semi); + } + + fn where_clause_impl( + &mut self, + where_clause: &Option, + hardbreaks: bool, + semi: bool, + ) { + let where_clause = match where_clause { + Some(where_clause) if !where_clause.predicates.is_empty() => where_clause, + _ => { + if semi { + self.word(";"); + } else { + self.nbsp(); + } + return; + } + }; + if hardbreaks { + self.hardbreak(); + self.offset(-INDENT); + self.word("where"); + self.hardbreak(); + for predicate in where_clause.predicates.iter().delimited() { + self.where_predicate(&predicate); + if predicate.is_last && semi { + self.word(";"); + } else { + self.word(","); + self.hardbreak(); + } + } + if !semi { + self.offset(-INDENT); + } + } else { + self.space(); + self.offset(-INDENT); + self.word("where"); + self.space(); + for predicate in where_clause.predicates.iter().delimited() { + self.where_predicate(&predicate); + if predicate.is_last && semi { + self.word(";"); + } else { + self.trailing_comma_or_space(predicate.is_last); + } + } + if !semi { + self.offset(-INDENT); + } + } + } + + fn where_predicate(&mut self, predicate: &WherePredicate) { + match predicate { + WherePredicate::Type(predicate) => self.predicate_type(predicate), + WherePredicate::Lifetime(predicate) => self.predicate_lifetime(predicate), + WherePredicate::Eq(predicate) => self.predicate_eq(predicate), + } + } + + fn predicate_type(&mut self, predicate: &PredicateType) { + if let Some(bound_lifetimes) = &predicate.lifetimes { + self.bound_lifetimes(bound_lifetimes); + } + self.ty(&predicate.bounded_ty); + self.word(":"); + if predicate.bounds.len() == 1 { + self.ibox(0); + } else { + self.ibox(INDENT); + } + for type_param_bound in predicate.bounds.iter().delimited() { + if type_param_bound.is_first { + self.nbsp(); + } else { + self.space(); + self.word("+ "); + } + self.type_param_bound(&type_param_bound); + } + self.end(); + } + + fn predicate_lifetime(&mut self, predicate: &PredicateLifetime) { + self.lifetime(&predicate.lifetime); + self.word(":"); + self.ibox(INDENT); + for lifetime in predicate.bounds.iter().delimited() { + if lifetime.is_first { + self.nbsp(); + } else { + self.space(); + self.word("+ "); + } + self.lifetime(&lifetime); + } + self.end(); + } + + fn predicate_eq(&mut self, predicate: &PredicateEq) { + self.ty(&predicate.lhs_ty); + self.word(" = "); + self.ty(&predicate.rhs_ty); + } +} diff --git a/packages/autofmt/src/prettyplease/item.rs b/packages/autofmt/src/prettyplease/item.rs new file mode 100644 index 000000000..5512137d9 --- /dev/null +++ b/packages/autofmt/src/prettyplease/item.rs @@ -0,0 +1,757 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use proc_macro2::TokenStream; +use syn::{ + Fields, FnArg, ForeignItem, ForeignItemFn, ForeignItemMacro, ForeignItemStatic, + ForeignItemType, ImplItem, ImplItemConst, ImplItemMacro, ImplItemMethod, ImplItemType, Item, + ItemConst, ItemEnum, ItemExternCrate, ItemFn, ItemForeignMod, ItemImpl, ItemMacro, ItemMacro2, + ItemMod, ItemStatic, ItemStruct, ItemTrait, ItemTraitAlias, ItemType, ItemUnion, ItemUse, Pat, + Receiver, Signature, Stmt, TraitItem, TraitItemConst, TraitItemMacro, TraitItemMethod, + TraitItemType, Type, UseGlob, UseGroup, UseName, UsePath, UseRename, UseTree, +}; + +impl Printer { + pub fn item(&mut self, item: &Item) { + match item { + Item::Const(item) => self.item_const(item), + Item::Enum(item) => self.item_enum(item), + Item::ExternCrate(item) => self.item_extern_crate(item), + Item::Fn(item) => self.item_fn(item), + Item::ForeignMod(item) => self.item_foreign_mod(item), + Item::Impl(item) => self.item_impl(item), + Item::Macro(item) => self.item_macro(item), + Item::Macro2(item) => self.item_macro2(item), + Item::Mod(item) => self.item_mod(item), + Item::Static(item) => self.item_static(item), + Item::Struct(item) => self.item_struct(item), + Item::Trait(item) => self.item_trait(item), + Item::TraitAlias(item) => self.item_trait_alias(item), + Item::Type(item) => self.item_type(item), + Item::Union(item) => self.item_union(item), + Item::Use(item) => self.item_use(item), + Item::Verbatim(item) => self.item_verbatim(item), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown Item"), + } + } + + fn item_const(&mut self, item: &ItemConst) { + self.outer_attrs(&item.attrs); + self.cbox(0); + self.visibility(&item.vis); + self.word("const "); + self.ident(&item.ident); + self.word(": "); + self.ty(&item.ty); + self.word(" = "); + self.neverbreak(); + self.expr(&item.expr); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn item_enum(&mut self, item: &ItemEnum) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("enum "); + self.ident(&item.ident); + self.generics(&item.generics); + self.where_clause_for_body(&item.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + for variant in &item.variants { + self.variant(variant); + self.word(","); + self.hardbreak(); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_extern_crate(&mut self, item: &ItemExternCrate) { + self.outer_attrs(&item.attrs); + self.visibility(&item.vis); + self.word("extern crate "); + self.ident(&item.ident); + if let Some((_as_token, rename)) = &item.rename { + self.word(" as "); + self.ident(rename); + } + self.word(";"); + self.hardbreak(); + } + + fn item_fn(&mut self, item: &ItemFn) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.signature(&item.sig); + self.where_clause_for_body(&item.sig.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&item.attrs); + for stmt in &item.block.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_foreign_mod(&mut self, item: &ItemForeignMod) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.abi(&item.abi); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&item.attrs); + for foreign_item in &item.items { + self.foreign_item(foreign_item); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_impl(&mut self, item: &ItemImpl) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.ibox(-INDENT); + self.cbox(INDENT); + if item.defaultness.is_some() { + self.word("default "); + } + if item.unsafety.is_some() { + self.word("unsafe "); + } + self.word("impl"); + self.generics(&item.generics); + self.end(); + self.nbsp(); + if let Some((negative_polarity, path, _for_token)) = &item.trait_ { + if negative_polarity.is_some() { + self.word("!"); + } + self.path(path); + self.space(); + self.word("for "); + } + self.ty(&item.self_ty); + self.end(); + self.where_clause_for_body(&item.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&item.attrs); + for impl_item in &item.items { + self.impl_item(impl_item); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_macro(&mut self, item: &ItemMacro) { + self.outer_attrs(&item.attrs); + self.mac(&item.mac, item.ident.as_ref()); + self.mac_semi_if_needed(&item.mac.delimiter); + self.hardbreak(); + } + + fn item_macro2(&mut self, item: &ItemMacro2) { + unimplemented!("Item::Macro2 `macro {} {}`", item.ident, item.rules); + } + + fn item_mod(&mut self, item: &ItemMod) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("mod "); + self.ident(&item.ident); + if let Some((_brace, items)) = &item.content { + self.word(" {"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&item.attrs); + for item in items { + self.item(item); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } else { + self.word(";"); + self.end(); + } + self.hardbreak(); + } + + fn item_static(&mut self, item: &ItemStatic) { + self.outer_attrs(&item.attrs); + self.cbox(0); + self.visibility(&item.vis); + self.word("static "); + if item.mutability.is_some() { + self.word("mut "); + } + self.ident(&item.ident); + self.word(": "); + self.ty(&item.ty); + self.word(" = "); + self.neverbreak(); + self.expr(&item.expr); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn item_struct(&mut self, item: &ItemStruct) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("struct "); + self.ident(&item.ident); + self.generics(&item.generics); + match &item.fields { + Fields::Named(fields) => { + self.where_clause_for_body(&item.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + for field in &fields.named { + self.field(field); + self.word(","); + self.hardbreak(); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + Fields::Unnamed(fields) => { + self.fields_unnamed(fields); + self.where_clause_semi(&item.generics.where_clause); + self.end(); + } + Fields::Unit => { + self.where_clause_semi(&item.generics.where_clause); + self.end(); + } + } + self.hardbreak(); + } + + fn item_trait(&mut self, item: &ItemTrait) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + if item.unsafety.is_some() { + self.word("unsafe "); + } + if item.auto_token.is_some() { + self.word("auto "); + } + self.word("trait "); + self.ident(&item.ident); + self.generics(&item.generics); + for supertrait in item.supertraits.iter().delimited() { + if supertrait.is_first { + self.word(": "); + } else { + self.word(" + "); + } + self.type_param_bound(&supertrait); + } + self.where_clause_for_body(&item.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&item.attrs); + for trait_item in &item.items { + self.trait_item(trait_item); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_trait_alias(&mut self, item: &ItemTraitAlias) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("trait "); + self.ident(&item.ident); + self.generics(&item.generics); + self.word(" = "); + self.neverbreak(); + for bound in item.bounds.iter().delimited() { + if !bound.is_first { + self.space(); + self.word("+ "); + } + self.type_param_bound(&bound); + } + self.where_clause_semi(&item.generics.where_clause); + self.end(); + self.hardbreak(); + } + + fn item_type(&mut self, item: &ItemType) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("type "); + self.ident(&item.ident); + self.generics(&item.generics); + self.where_clause_oneline(&item.generics.where_clause); + self.word("= "); + self.neverbreak(); + self.ibox(-INDENT); + self.ty(&item.ty); + self.end(); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn item_union(&mut self, item: &ItemUnion) { + self.outer_attrs(&item.attrs); + self.cbox(INDENT); + self.visibility(&item.vis); + self.word("union "); + self.ident(&item.ident); + self.generics(&item.generics); + self.where_clause_for_body(&item.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + for field in &item.fields.named { + self.field(field); + self.word(","); + self.hardbreak(); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn item_use(&mut self, item: &ItemUse) { + self.outer_attrs(&item.attrs); + self.visibility(&item.vis); + self.word("use "); + if item.leading_colon.is_some() { + self.word("::"); + } + self.use_tree(&item.tree); + self.word(";"); + self.hardbreak(); + } + + fn item_verbatim(&mut self, item: &TokenStream) { + if !item.is_empty() { + unimplemented!("Item::Verbatim `{}`", item); + } + self.hardbreak(); + } + + fn use_tree(&mut self, use_tree: &UseTree) { + match use_tree { + UseTree::Path(use_path) => self.use_path(use_path), + UseTree::Name(use_name) => self.use_name(use_name), + UseTree::Rename(use_rename) => self.use_rename(use_rename), + UseTree::Glob(use_glob) => self.use_glob(use_glob), + UseTree::Group(use_group) => self.use_group(use_group), + } + } + + fn use_path(&mut self, use_path: &UsePath) { + self.ident(&use_path.ident); + self.word("::"); + self.use_tree(&use_path.tree); + } + + fn use_name(&mut self, use_name: &UseName) { + self.ident(&use_name.ident); + } + + fn use_rename(&mut self, use_rename: &UseRename) { + self.ident(&use_rename.ident); + self.word(" as "); + self.ident(&use_rename.rename); + } + + fn use_glob(&mut self, use_glob: &UseGlob) { + let _ = use_glob; + self.word("*"); + } + + fn use_group(&mut self, use_group: &UseGroup) { + if use_group.items.is_empty() { + self.word("{}"); + } else if use_group.items.len() == 1 { + self.use_tree(&use_group.items[0]); + } else { + self.cbox(INDENT); + self.word("{"); + self.zerobreak(); + self.ibox(0); + for use_tree in use_group.items.iter().delimited() { + self.use_tree(&use_tree); + if !use_tree.is_last { + self.word(","); + let mut use_tree = *use_tree; + while let UseTree::Path(use_path) = use_tree { + use_tree = &use_path.tree; + } + if let UseTree::Group(_) = use_tree { + self.hardbreak(); + } else { + self.space(); + } + } + } + self.end(); + self.trailing_comma(true); + self.offset(-INDENT); + self.word("}"); + self.end(); + } + } + + fn foreign_item(&mut self, foreign_item: &ForeignItem) { + match foreign_item { + ForeignItem::Fn(item) => self.foreign_item_fn(item), + ForeignItem::Static(item) => self.foreign_item_static(item), + ForeignItem::Type(item) => self.foreign_item_type(item), + ForeignItem::Macro(item) => self.foreign_item_macro(item), + ForeignItem::Verbatim(item) => self.foreign_item_verbatim(item), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown ForeignItem"), + } + } + + fn foreign_item_fn(&mut self, foreign_item: &ForeignItemFn) { + self.outer_attrs(&foreign_item.attrs); + self.cbox(INDENT); + self.visibility(&foreign_item.vis); + self.signature(&foreign_item.sig); + self.where_clause_semi(&foreign_item.sig.generics.where_clause); + self.end(); + self.hardbreak(); + } + + fn foreign_item_static(&mut self, foreign_item: &ForeignItemStatic) { + self.outer_attrs(&foreign_item.attrs); + self.cbox(0); + self.visibility(&foreign_item.vis); + self.word("static "); + if foreign_item.mutability.is_some() { + self.word("mut "); + } + self.ident(&foreign_item.ident); + self.word(": "); + self.ty(&foreign_item.ty); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn foreign_item_type(&mut self, foreign_item: &ForeignItemType) { + self.outer_attrs(&foreign_item.attrs); + self.cbox(0); + self.visibility(&foreign_item.vis); + self.word("type "); + self.ident(&foreign_item.ident); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn foreign_item_macro(&mut self, foreign_item: &ForeignItemMacro) { + self.outer_attrs(&foreign_item.attrs); + self.mac(&foreign_item.mac, None); + self.mac_semi_if_needed(&foreign_item.mac.delimiter); + self.hardbreak(); + } + + fn foreign_item_verbatim(&mut self, foreign_item: &TokenStream) { + if !foreign_item.is_empty() { + unimplemented!("ForeignItem::Verbatim `{}`", foreign_item); + } + self.hardbreak(); + } + + fn trait_item(&mut self, trait_item: &TraitItem) { + match trait_item { + TraitItem::Const(item) => self.trait_item_const(item), + TraitItem::Method(item) => self.trait_item_method(item), + TraitItem::Type(item) => self.trait_item_type(item), + TraitItem::Macro(item) => self.trait_item_macro(item), + TraitItem::Verbatim(item) => self.trait_item_verbatim(item), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown TraitItem"), + } + } + + fn trait_item_const(&mut self, trait_item: &TraitItemConst) { + self.outer_attrs(&trait_item.attrs); + self.cbox(0); + self.word("const "); + self.ident(&trait_item.ident); + self.word(": "); + self.ty(&trait_item.ty); + if let Some((_eq_token, default)) = &trait_item.default { + self.word(" = "); + self.neverbreak(); + self.expr(default); + } + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn trait_item_method(&mut self, trait_item: &TraitItemMethod) { + self.outer_attrs(&trait_item.attrs); + self.cbox(INDENT); + self.signature(&trait_item.sig); + if let Some(block) = &trait_item.default { + self.where_clause_for_body(&trait_item.sig.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&trait_item.attrs); + for stmt in &block.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } else { + self.where_clause_semi(&trait_item.sig.generics.where_clause); + self.end(); + } + self.hardbreak(); + } + + fn trait_item_type(&mut self, trait_item: &TraitItemType) { + self.outer_attrs(&trait_item.attrs); + self.cbox(INDENT); + self.word("type "); + self.ident(&trait_item.ident); + self.generics(&trait_item.generics); + for bound in trait_item.bounds.iter().delimited() { + if bound.is_first { + self.word(": "); + } else { + self.space(); + self.word("+ "); + } + self.type_param_bound(&bound); + } + if let Some((_eq_token, default)) = &trait_item.default { + self.where_clause_oneline(&trait_item.generics.where_clause); + self.word("= "); + self.neverbreak(); + self.ty(default); + } else { + self.where_clause_oneline_semi(&trait_item.generics.where_clause); + } + self.end(); + self.hardbreak(); + } + + fn trait_item_macro(&mut self, trait_item: &TraitItemMacro) { + self.outer_attrs(&trait_item.attrs); + self.mac(&trait_item.mac, None); + self.mac_semi_if_needed(&trait_item.mac.delimiter); + self.hardbreak(); + } + + fn trait_item_verbatim(&mut self, trait_item: &TokenStream) { + if !trait_item.is_empty() { + unimplemented!("TraitItem::Verbatim `{}`", trait_item); + } + self.hardbreak(); + } + + fn impl_item(&mut self, impl_item: &ImplItem) { + match impl_item { + ImplItem::Const(item) => self.impl_item_const(item), + ImplItem::Method(item) => self.impl_item_method(item), + ImplItem::Type(item) => self.impl_item_type(item), + ImplItem::Macro(item) => self.impl_item_macro(item), + ImplItem::Verbatim(item) => self.impl_item_verbatim(item), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown ImplItem"), + } + } + + fn impl_item_const(&mut self, impl_item: &ImplItemConst) { + self.outer_attrs(&impl_item.attrs); + self.cbox(0); + self.visibility(&impl_item.vis); + if impl_item.defaultness.is_some() { + self.word("default "); + } + self.word("const "); + self.ident(&impl_item.ident); + self.word(": "); + self.ty(&impl_item.ty); + self.word(" = "); + self.neverbreak(); + self.expr(&impl_item.expr); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn impl_item_method(&mut self, impl_item: &ImplItemMethod) { + self.outer_attrs(&impl_item.attrs); + self.cbox(INDENT); + self.visibility(&impl_item.vis); + if impl_item.defaultness.is_some() { + self.word("default "); + } + self.signature(&impl_item.sig); + if impl_item.block.stmts.len() == 1 { + if let Stmt::Item(Item::Verbatim(verbatim)) = &impl_item.block.stmts[0] { + if verbatim.to_string() == ";" { + self.where_clause_semi(&impl_item.sig.generics.where_clause); + self.end(); + self.hardbreak(); + return; + } + } + } + self.where_clause_for_body(&impl_item.sig.generics.where_clause); + self.word("{"); + self.hardbreak_if_nonempty(); + self.inner_attrs(&impl_item.attrs); + for stmt in &impl_item.block.stmts { + self.stmt(stmt); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + self.hardbreak(); + } + + fn impl_item_type(&mut self, impl_item: &ImplItemType) { + self.outer_attrs(&impl_item.attrs); + self.cbox(INDENT); + self.visibility(&impl_item.vis); + if impl_item.defaultness.is_some() { + self.word("default "); + } + self.word("type "); + self.ident(&impl_item.ident); + self.generics(&impl_item.generics); + self.where_clause_oneline(&impl_item.generics.where_clause); + self.word("= "); + self.neverbreak(); + self.ibox(-INDENT); + self.ty(&impl_item.ty); + self.end(); + self.word(";"); + self.end(); + self.hardbreak(); + } + + fn impl_item_macro(&mut self, impl_item: &ImplItemMacro) { + self.outer_attrs(&impl_item.attrs); + self.mac(&impl_item.mac, None); + self.mac_semi_if_needed(&impl_item.mac.delimiter); + self.hardbreak(); + } + + fn impl_item_verbatim(&mut self, impl_item: &TokenStream) { + if !impl_item.is_empty() { + unimplemented!("ImplItem::Verbatim `{}`", impl_item); + } + self.hardbreak(); + } + + fn maybe_variadic(&mut self, arg: &FnArg) -> bool { + let pat_type = match arg { + FnArg::Typed(pat_type) => pat_type, + FnArg::Receiver(receiver) => { + self.receiver(receiver); + return false; + } + }; + + match pat_type.ty.as_ref() { + Type::Verbatim(ty) if ty.to_string() == "..." => { + match pat_type.pat.as_ref() { + Pat::Verbatim(pat) if pat.to_string() == "..." => { + self.outer_attrs(&pat_type.attrs); + self.word("..."); + } + _ => self.pat_type(pat_type), + } + true + } + _ => { + self.pat_type(pat_type); + false + } + } + } + + fn signature(&mut self, signature: &Signature) { + if signature.constness.is_some() { + self.word("const "); + } + if signature.asyncness.is_some() { + self.word("async "); + } + if signature.unsafety.is_some() { + self.word("unsafe "); + } + if let Some(abi) = &signature.abi { + self.abi(abi); + } + self.word("fn "); + self.ident(&signature.ident); + self.generics(&signature.generics); + self.word("("); + self.neverbreak(); + self.cbox(0); + self.zerobreak(); + let mut last_is_variadic = false; + for input in signature.inputs.iter().delimited() { + last_is_variadic = self.maybe_variadic(&input); + if last_is_variadic { + self.zerobreak(); + } else { + self.trailing_comma(input.is_last); + } + } + if signature.variadic.is_some() && !last_is_variadic { + self.word("..."); + self.zerobreak(); + } + self.offset(-INDENT); + self.end(); + self.word(")"); + self.return_type(&signature.output); + } + + fn receiver(&mut self, receiver: &Receiver) { + self.outer_attrs(&receiver.attrs); + if let Some((_ampersand, lifetime)) = &receiver.reference { + self.word("&"); + if let Some(lifetime) = lifetime { + self.lifetime(lifetime); + self.nbsp(); + } + } + if receiver.mutability.is_some() { + self.word("mut "); + } + self.word("self"); + } +} diff --git a/packages/autofmt/src/prettyplease/iter.rs b/packages/autofmt/src/prettyplease/iter.rs new file mode 100644 index 000000000..702c653f5 --- /dev/null +++ b/packages/autofmt/src/prettyplease/iter.rs @@ -0,0 +1,46 @@ +use std::iter::Peekable; +use std::ops::Deref; + +pub struct Delimited { + is_first: bool, + iter: Peekable, +} + +pub trait IterDelimited: Iterator + Sized { + fn delimited(self) -> Delimited { + Delimited { + is_first: true, + iter: self.peekable(), + } + } +} + +impl IterDelimited for I {} + +pub struct IteratorItem { + value: T, + pub is_first: bool, + pub is_last: bool, +} + +impl Iterator for Delimited { + type Item = IteratorItem; + + fn next(&mut self) -> Option { + let item = IteratorItem { + value: self.iter.next()?, + is_first: self.is_first, + is_last: self.iter.peek().is_none(), + }; + self.is_first = false; + Some(item) + } +} + +impl Deref for IteratorItem { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.value + } +} diff --git a/packages/autofmt/src/prettyplease/lifetime.rs b/packages/autofmt/src/prettyplease/lifetime.rs new file mode 100644 index 000000000..760d5776d --- /dev/null +++ b/packages/autofmt/src/prettyplease/lifetime.rs @@ -0,0 +1,9 @@ +use super::algorithm::Printer; +use syn::Lifetime; + +impl Printer { + pub fn lifetime(&mut self, lifetime: &Lifetime) { + self.word("'"); + self.ident(&lifetime.ident); + } +} diff --git a/packages/autofmt/src/prettyplease/lit.rs b/packages/autofmt/src/prettyplease/lit.rs new file mode 100644 index 000000000..37f2160ad --- /dev/null +++ b/packages/autofmt/src/prettyplease/lit.rs @@ -0,0 +1,50 @@ +use super::algorithm::Printer; +use proc_macro2::Literal; +use syn::{Lit, LitBool, LitByte, LitByteStr, LitChar, LitFloat, LitInt, LitStr}; + +impl Printer { + pub fn lit(&mut self, lit: &Lit) { + match lit { + Lit::Str(lit) => self.lit_str(lit), + Lit::ByteStr(lit) => self.lit_byte_str(lit), + Lit::Byte(lit) => self.lit_byte(lit), + Lit::Char(lit) => self.lit_char(lit), + Lit::Int(lit) => self.lit_int(lit), + Lit::Float(lit) => self.lit_float(lit), + Lit::Bool(lit) => self.lit_bool(lit), + Lit::Verbatim(lit) => self.lit_verbatim(lit), + } + } + + pub fn lit_str(&mut self, lit: &LitStr) { + self.word(lit.token().to_string()); + } + + fn lit_byte_str(&mut self, lit: &LitByteStr) { + self.word(lit.token().to_string()); + } + + fn lit_byte(&mut self, lit: &LitByte) { + self.word(lit.token().to_string()); + } + + fn lit_char(&mut self, lit: &LitChar) { + self.word(lit.token().to_string()); + } + + fn lit_int(&mut self, lit: &LitInt) { + self.word(lit.token().to_string()); + } + + fn lit_float(&mut self, lit: &LitFloat) { + self.word(lit.token().to_string()); + } + + fn lit_bool(&mut self, lit: &LitBool) { + self.word(if lit.value { "true" } else { "false" }); + } + + fn lit_verbatim(&mut self, token: &Literal) { + self.word(token.to_string()); + } +} diff --git a/packages/autofmt/src/prettyplease/mac.rs b/packages/autofmt/src/prettyplease/mac.rs new file mode 100644 index 000000000..4608bc303 --- /dev/null +++ b/packages/autofmt/src/prettyplease/mac.rs @@ -0,0 +1,220 @@ +use super::algorithm::Printer; +use super::token::Token; +use super::INDENT; +use proc_macro2::{Delimiter, Spacing, TokenStream}; +use syn::{Ident, Macro, MacroDelimiter, PathArguments}; + +impl Printer { + pub fn mac(&mut self, mac: &Macro, ident: Option<&Ident>) { + let is_macro_rules = mac.path.leading_colon.is_none() + && mac.path.segments.len() == 1 + && matches!(mac.path.segments[0].arguments, PathArguments::None) + && mac.path.segments[0].ident == "macro_rules"; + if is_macro_rules { + if let Some(ident) = ident { + self.macro_rules(ident, &mac.tokens); + return; + } + } + self.path(&mac.path); + self.word("!"); + if let Some(ident) = ident { + self.nbsp(); + self.ident(ident); + } + let (open, close, delimiter_break) = match mac.delimiter { + MacroDelimiter::Paren(_) => ("(", ")", Self::zerobreak as fn(&mut Self)), + MacroDelimiter::Brace(_) => (" {", "}", Self::hardbreak as fn(&mut Self)), + MacroDelimiter::Bracket(_) => ("[", "]", Self::zerobreak as fn(&mut Self)), + }; + self.word(open); + self.cbox(INDENT); + delimiter_break(self); + self.ibox(0); + self.macro_rules_tokens(mac.tokens.clone(), false); + self.end(); + delimiter_break(self); + self.offset(-INDENT); + self.end(); + self.word(close); + } + + pub fn mac_semi_if_needed(&mut self, delimiter: &MacroDelimiter) { + match delimiter { + MacroDelimiter::Paren(_) | MacroDelimiter::Bracket(_) => self.word(";"), + MacroDelimiter::Brace(_) => {} + } + } + + fn macro_rules(&mut self, name: &Ident, rules: &TokenStream) { + enum State { + Start, + Matcher, + Equal, + Greater, + Expander, + } + + use State::*; + + self.word("macro_rules! "); + self.ident(name); + self.word(" {"); + self.cbox(INDENT); + self.hardbreak_if_nonempty(); + let mut state = State::Start; + for tt in rules.clone() { + let token = Token::from(tt); + match (state, token) { + (Start, Token::Group(delimiter, stream)) => { + self.delimiter_open(delimiter); + if !stream.is_empty() { + self.cbox(INDENT); + self.zerobreak(); + self.ibox(0); + self.macro_rules_tokens(stream, true); + self.end(); + self.zerobreak(); + self.offset(-INDENT); + self.end(); + } + self.delimiter_close(delimiter); + state = Matcher; + } + (Matcher, Token::Punct('=', Spacing::Joint)) => { + self.word(" ="); + state = Equal; + } + (Equal, Token::Punct('>', Spacing::Alone)) => { + self.word(">"); + state = Greater; + } + (Greater, Token::Group(_delimiter, stream)) => { + self.word(" {"); + self.neverbreak(); + if !stream.is_empty() { + self.cbox(INDENT); + self.hardbreak(); + self.ibox(0); + self.macro_rules_tokens(stream, false); + self.end(); + self.hardbreak(); + self.offset(-INDENT); + self.end(); + } + self.word("}"); + state = Expander; + } + (Expander, Token::Punct(';', Spacing::Alone)) => { + self.word(";"); + self.hardbreak(); + state = Start; + } + _ => unimplemented!("bad macro_rules syntax"), + } + } + match state { + Start => {} + Expander => { + self.word(";"); + self.hardbreak(); + } + _ => self.hardbreak(), + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + + fn macro_rules_tokens(&mut self, stream: TokenStream, matcher: bool) { + #[derive(PartialEq)] + enum State { + Start, + Dollar, + DollarIdent, + DollarIdentColon, + DollarParen, + DollarParenSep, + Pound, + PoundBang, + Dot, + Colon, + Colon2, + Ident, + IdentBang, + Delim, + Other, + } + + use State::*; + + let mut state = Start; + let mut previous_is_joint = true; + for tt in stream { + let token = Token::from(tt); + let (needs_space, next_state) = match (&state, &token) { + (Dollar, Token::Ident(_)) => (false, if matcher { DollarIdent } else { Other }), + (DollarIdent, Token::Punct(':', Spacing::Alone)) => (false, DollarIdentColon), + (DollarIdentColon, Token::Ident(_)) => (false, Other), + (DollarParen, Token::Punct('+' | '*' | '?', Spacing::Alone)) => (false, Other), + (DollarParen, Token::Ident(_) | Token::Literal(_)) => (false, DollarParenSep), + (DollarParen, Token::Punct(_, Spacing::Joint)) => (false, DollarParen), + (DollarParen, Token::Punct(_, Spacing::Alone)) => (false, DollarParenSep), + (DollarParenSep, Token::Punct('+' | '*', _)) => (false, Other), + (Pound, Token::Punct('!', _)) => (false, PoundBang), + (Dollar, Token::Group(Delimiter::Parenthesis, _)) => (false, DollarParen), + (Pound | PoundBang, Token::Group(Delimiter::Bracket, _)) => (false, Other), + (Ident, Token::Group(Delimiter::Parenthesis | Delimiter::Bracket, _)) => { + (false, Delim) + } + (Ident, Token::Punct('!', Spacing::Alone)) => (false, IdentBang), + (IdentBang, Token::Group(Delimiter::Parenthesis | Delimiter::Bracket, _)) => { + (false, Other) + } + (Colon, Token::Punct(':', _)) => (false, Colon2), + (_, Token::Group(Delimiter::Parenthesis | Delimiter::Bracket, _)) => (true, Delim), + (_, Token::Group(Delimiter::Brace | Delimiter::None, _)) => (true, Other), + (_, Token::Ident(ident)) if !is_keyword(ident) => { + (state != Dot && state != Colon2, Ident) + } + (_, Token::Literal(_)) => (state != Dot, Ident), + (_, Token::Punct(',' | ';', _)) => (false, Other), + (_, Token::Punct('.', _)) if !matcher => (state != Ident && state != Delim, Dot), + (_, Token::Punct(':', Spacing::Joint)) => (state != Ident, Colon), + (_, Token::Punct('$', _)) => (true, Dollar), + (_, Token::Punct('#', _)) => (true, Pound), + (_, _) => (true, Other), + }; + if !previous_is_joint { + if needs_space { + self.space(); + } else if let Token::Punct('.', _) = token { + self.zerobreak(); + } + } + previous_is_joint = match token { + Token::Punct(_, Spacing::Joint) | Token::Punct('$', _) => true, + _ => false, + }; + self.single_token( + token, + if matcher { + |printer, stream| printer.macro_rules_tokens(stream, true) + } else { + |printer, stream| printer.macro_rules_tokens(stream, false) + }, + ); + state = next_state; + } + } +} + +fn is_keyword(ident: &Ident) -> bool { + match ident.to_string().as_str() { + "as" | "box" | "break" | "const" | "continue" | "crate" | "else" | "enum" | "extern" + | "fn" | "for" | "if" | "impl" | "in" | "let" | "loop" | "macro" | "match" | "mod" + | "move" | "mut" | "pub" | "ref" | "return" | "static" | "struct" | "trait" | "type" + | "unsafe" | "use" | "where" | "while" | "yield" => true, + _ => false, + } +} diff --git a/packages/autofmt/src/prettyplease/mod.rs b/packages/autofmt/src/prettyplease/mod.rs new file mode 100644 index 000000000..68d8e1303 --- /dev/null +++ b/packages/autofmt/src/prettyplease/mod.rs @@ -0,0 +1,381 @@ +//! [![github]](https://github.com/dtolnay/prettyplease) [![crates-io]](https://crates.io/crates/prettyplease) [![docs-rs]](https://docs.rs/prettyplease) +//! +//! [github]: https://img.shields.io/badge/github-8da0cb?style=for-the-badge&labelColor=555555&logo=github +//! [crates-io]: https://img.shields.io/badge/crates.io-fc8d62?style=for-the-badge&labelColor=555555&logo=rust +//! [docs-rs]: https://img.shields.io/badge/docs.rs-66c2a5?style=for-the-badge&labelColor=555555&logoColor=white&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K +//! +//!
+//! +//! **prettyplease::unparse** — a minimal `syn` syntax tree pretty-printer +//! +//!
+//! +//! # Overview +//! +//! This is a pretty-printer to turn a `syn` syntax tree into a `String` of +//! well-formatted source code. In contrast to rustfmt, this library is intended +//! to be suitable for arbitrary generated code. +//! +//! Rustfmt prioritizes high-quality output that is impeccable enough that you'd +//! be comfortable spending your career staring at its output — but that +//! means some heavyweight algorithms, and it has a tendency to bail out on code +//! that is hard to format (for example [rustfmt#3697], and there are dozens +//! more issues like it). That's not necessarily a big deal for human-generated +//! code because when code gets highly nested, the human will naturally be +//! inclined to refactor into more easily formattable code. But for generated +//! code, having the formatter just give up leaves it totally unreadable. +//! +//! [rustfmt#3697]: https://github.com/rust-lang/rustfmt/issues/3697 +//! +//! This library is designed using the simplest possible algorithm and data +//! structures that can deliver about 95% of the quality of rustfmt-formatted +//! output. In my experience testing real-world code, approximately 97-98% of +//! output lines come out identical between rustfmt's formatting and this +//! crate's. The rest have slightly different linebreak decisions, but still +//! clearly follow the dominant modern Rust style. +//! +//! The tradeoffs made by this crate are a good fit for generated code that you +//! will *not* spend your career staring at. For example, the output of +//! `bindgen`, or the output of `cargo-expand`. In those cases it's more +//! important that the whole thing be formattable without the formatter giving +//! up, than that it be flawless. +//! +//!
+//! +//! # Feature matrix +//! +//! Here are a few superficial comparisons of this crate against the AST +//! pretty-printer built into rustc, and rustfmt. The sections below go into +//! more detail comparing the output of each of these libraries. +//! +//! | | prettyplease | rustc | rustfmt | +//! | --- | --- | --- | --- | +//! | non-pathological behavior on big or generated code | ✅ | ❌ | ❌ | +//! | idiomatic modern formatting ("locally indistinguishable from rustfmt") | ✅ | ❌ | ✅ | +//! | throughput | 60 MB/s | 39 MB/s | 2.8 MB/s | +//! | number of dependencies | 3 | 72 | 66 | +//! | compile time including dependencies | 2.4 sec | 23.1 sec | 29.8 sec | +//! | buildable using a stable Rust compiler | ✅ | ❌ | ❌ | +//! | published to crates.io | ✅ | ❌ | ❌ | +//! | extensively configurable output | ❌ | ❌ | ✅ | +//! | intended to accommodate hand-maintained source code | ❌ | ❌ | ✅ | +//! +//!
+//! +//! # Comparison to rustfmt +//! +//! - [input.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/input.rs) +//! - [output.prettyplease.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/output.prettyplease.rs) +//! - [output.rustfmt.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/output.rustfmt.rs) +//! +//! If you weren't told which output file is which, it would be practically +//! impossible to tell — **except** for line 435 in the rustfmt output, +//! which is more than 1000 characters long because rustfmt just gave up +//! formatting that part of the file: +//! +//! ``` +//! # const _: &str = stringify! {{{ +//! match segments[5] { +//! 0 => write!(f, "::{}", ipv4), +//! 0xffff => write!(f, "::ffff:{}", ipv4), +//! _ => unreachable!(), +//! } +//! } else { # [derive (Copy , Clone , Default)] struct Span { start : usize , len : usize , } let zeroes = { let mut longest = Span :: default () ; let mut current = Span :: default () ; for (i , & segment) in segments . iter () . enumerate () { if segment == 0 { if current . len == 0 { current . start = i ; } current . len += 1 ; if current . len > longest . len { longest = current ; } } else { current = Span :: default () ; } } longest } ; # [doc = " Write a colon-separated part of the address"] # [inline] fn fmt_subslice (f : & mut fmt :: Formatter < '_ > , chunk : & [u16]) -> fmt :: Result { if let Some ((first , tail)) = chunk . split_first () { write ! (f , "{:x}" , first) ? ; for segment in tail { f . write_char (':') ? ; write ! (f , "{:x}" , segment) ? ; } } Ok (()) } if zeroes . len > 1 { fmt_subslice (f , & segments [.. zeroes . start]) ? ; f . write_str ("::") ? ; fmt_subslice (f , & segments [zeroes . start + zeroes . len ..]) } else { fmt_subslice (f , & segments) } } +//! } else { +//! const IPV6_BUF_LEN: usize = (4 * 8) + 7; +//! let mut buf = [0u8; IPV6_BUF_LEN]; +//! let mut buf_slice = &mut buf[..]; +//! # }}; +//! ``` +//! +//! This is a pretty typical manifestation of rustfmt bailing out in generated +//! code — a chunk of the input ends up on one line. The other +//! manifestation is that you're working on some code, running rustfmt on save +//! like a conscientious developer, but after a while notice it isn't doing +//! anything. You introduce an intentional formatting issue, like a stray indent +//! or semicolon, and run rustfmt to check your suspicion. Nope, it doesn't get +//! cleaned up — rustfmt is just not formatting the part of the file you +//! are working on. +//! +//! The prettyplease library is designed to have no pathological cases that +//! force a bail out; the entire input you give it will get formatted in some +//! "good enough" form. +//! +//! Separately, rustfmt can be problematic to integrate into projects. It's +//! written using rustc's internal syntax tree, so it can't be built by a stable +//! compiler. Its releases are not regularly published to crates.io, so in Cargo +//! builds you'd need to depend on it as a git dependency, which precludes +//! publishing your crate to crates.io also. You can shell out to a `rustfmt` +//! binary, but that'll be whatever rustfmt version is installed on each +//! developer's system (if any), which can lead to spurious diffs in checked-in +//! generated code formatted by different versions. In contrast prettyplease is +//! designed to be easy to pull in as a library, and compiles fast. +//! +//!
+//! +//! # Comparison to rustc_ast_pretty +//! +//! - [input.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/input.rs) +//! - [output.prettyplease.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/output.prettyplease.rs) +//! - [output.rustc.rs](https://github.com/dtolnay/prettyplease/blob/0.1.0/examples/output.rustc.rs) +//! +//! This is the pretty-printer that gets used when rustc prints source code, +//! such as `rustc -Zunpretty=expanded`. It's used also by the standard +//! library's `stringify!` when stringifying an interpolated macro_rules AST +//! fragment, like an $:expr, and transitively by `dbg!` and many macros in the +//! ecosystem. +//! +//! Rustc's formatting is mostly okay, but does not hew closely to the dominant +//! contemporary style of Rust formatting. Some things wouldn't ever be written +//! on one line, like this `match` expression, and certainly not with a comma in +//! front of the closing brace: +//! +//! ``` +//! # const _: &str = stringify! { +//! fn eq(&self, other: &IpAddr) -> bool { +//! match other { IpAddr::V4(v4) => self == v4, IpAddr::V6(_) => false, } +//! } +//! # }; +//! ``` +//! +//! Some places use non-multiple-of-4 indentation, which is definitely not the +//! norm: +//! +//! ``` +//! # const _: &str = stringify! { +//! pub const fn to_ipv6_mapped(&self) -> Ipv6Addr { +//! let [a, b, c, d] = self.octets(); +//! Ipv6Addr{inner: +//! c::in6_addr{s6_addr: +//! [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, +//! 0xFF, a, b, c, d],},} +//! } +//! # }; +//! ``` +//! +//! And although there isn't an egregious example of it in the link because the +//! input code is pretty tame, in general rustc_ast_pretty has pathological +//! behavior on generated code. It has a tendency to use excessive horizontal +//! indentation and rapidly run out of width: +//! +//! ``` +//! # const _: &str = stringify! { +//! ::std::io::_print(::core::fmt::Arguments::new_v1(&[""], +//! &match (&msg,) { +//! _args => +//! [::core::fmt::ArgumentV1::new(_args.0, +//! ::core::fmt::Display::fmt)], +//! })); +//! # }; +//! ``` +//! +//! The snippets above are clearly different from modern rustfmt style. In +//! contrast, prettyplease is designed to have output that is practically +//! indistinguishable from rustfmt-formatted code. +//! +//!
+//! +//! # Example +//! +//! ``` +//! // [dependencies] +//! // prettyplease = "0.1" +//! // syn = { version = "1", default-features = false, features = ["full", "parsing"] } +//! +//! const INPUT: &str = stringify! { +//! use super::{ +//! lazy::{Lazy, SyncLazy, SyncOnceCell}, panic, +//! sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, +//! mpsc::channel, Mutex, }, +//! thread, +//! }; +//! impl Into for T where U: From { +//! fn into(self) -> U { U::from(self) } +//! } +//! }; +//! +//! fn main() { +//! let syntax_tree = syn::parse_file(INPUT).unwrap(); +//! let formatted = prettyplease::unparse(&syntax_tree); +//! print!("{}", formatted); +//! } +//! ``` +//! +//!
+//! +//! # Algorithm notes +//! +//! The approach and terminology used in the implementation are derived from +//! [*Derek C. Oppen, "Pretty Printing" (1979)*][paper], on which +//! rustc_ast_pretty is also based, and from rustc_ast_pretty's implementation +//! written by Graydon Hoare in 2011 (and modernized over the years by dozens of +//! volunteer maintainers). +//! +//! [paper]: http://i.stanford.edu/pub/cstr/reports/cs/tr/79/770/CS-TR-79-770.pdf +//! +//! The paper describes two language-agnostic interacting procedures `Scan()` +//! and `Print()`. Language-specific code decomposes an input data structure +//! into a stream of `string` and `break` tokens, and `begin` and `end` tokens +//! for grouping. Each `begin`–`end` range may be identified as either +//! "consistent breaking" or "inconsistent breaking". If a group is consistently +//! breaking, then if the whole contents do not fit on the line, *every* `break` +//! token in the group will receive a linebreak. This is appropriate, for +//! example, for Rust struct literals, or arguments of a function call. If a +//! group is inconsistently breaking, then the `string` tokens in the group are +//! greedily placed on the line until out of space, and linebroken only at those +//! `break` tokens for which the next string would not fit. For example, this is +//! appropriate for the contents of a braced `use` statement in Rust. +//! +//! Scan's job is to efficiently accumulate sizing information about groups and +//! breaks. For every `begin` token we compute the distance to the matched `end` +//! token, and for every `break` we compute the distance to the next `break`. +//! The algorithm uses a ringbuffer to hold tokens whose size is not yet +//! ascertained. The maximum size of the ringbuffer is bounded by the target +//! line length and does not grow indefinitely, regardless of deep nesting in +//! the input stream. That's because once a group is sufficiently big, the +//! precise size can no longer make a difference to linebreak decisions and we +//! can effectively treat it as "infinity". +//! +//! Print's job is to use the sizing information to efficiently assign a +//! "broken" or "not broken" status to every `begin` token. At that point the +//! output is easily constructed by concatenating `string` tokens and breaking +//! at `break` tokens contained within a broken group. +//! +//! Leveraging these primitives (i.e. cleverly placing the all-or-nothing +//! consistent breaks and greedy inconsistent breaks) to yield +//! rustfmt-compatible formatting for all of Rust's syntax tree nodes is a fun +//! challenge. +//! +//! Here is a visualization of some Rust tokens fed into the pretty printing +//! algorithm. Consistently breaking `begin`—`end` pairs are represented +//! by `«`⁠`»`, inconsistently breaking by `‹`⁠`›`, `break` by `·`, +//! and the rest of the non-whitespace are `string`. +//! +//! ```text +//! use super::«{· +//! ‹ lazy::«{·‹Lazy,· SyncLazy,· SyncOnceCell›·}»,· +//! panic,· +//! sync::«{· +//! ‹ atomic::«{·‹AtomicUsize,· Ordering::SeqCst›·}»,· +//! mpsc::channel,· Mutex›,· +//! }»,· +//! thread›,· +//! }»;· +//! «‹«impl<«·T‹›,· U‹›·»>» Into<«·U·»>· for T›· +//! where· +//! U:‹ From<«·T·»>›,· +//! {· +//! « fn into(·«·self·») -> U {· +//! ‹ U::from(«·self·»)›· +//! » }· +//! »}· +//! ``` +//! +//! The algorithm described in the paper is not quite sufficient for producing +//! well-formatted Rust code that is locally indistinguishable from rustfmt's +//! style. The reason is that in the paper, the complete non-whitespace contents +//! are assumed to be independent of linebreak decisions, with Scan and Print +//! being only in control of the whitespace (spaces and line breaks). In Rust as +//! idiomatically formattted by rustfmt, that is not the case. Trailing commas +//! are one example; the punctuation is only known *after* the broken vs +//! non-broken status of the surrounding group is known: +//! +//! ``` +//! # struct Struct { x: u64, y: bool } +//! # let xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx = 0; +//! # let yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy = true; +//! # +//! let _ = Struct { x: 0, y: true }; +//! +//! let _ = Struct { +//! x: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx, +//! y: yyyyyyyyyyyyyyyyyyyyyyyyyyyyyy, //<- trailing comma if the expression wrapped +//! }; +//! ``` +//! +//! The formatting of `match` expressions is another case; we want small arms on +//! the same line as the pattern, and big arms wrapped in a brace. The presence +//! of the brace punctuation, comma, and semicolon are all dependent on whether +//! the arm fits on the line: +//! +//! ``` +//! # struct Entry { nanos: u32 } +//! # let total_nanos = 0u64; +//! # let mut total_secs = 0u64; +//! # let tmp; +//! # let entry = Entry { nanos: 0 }; +//! # const NANOS_PER_SEC: u32 = 1_000_000_000; +//! # +//! match total_nanos.checked_add(entry.nanos as u64) { +//! Some(n) => tmp = n, //<- small arm, inline with comma +//! None => { +//! total_secs = total_secs +//! .checked_add(total_nanos / NANOS_PER_SEC as u64) +//! .expect("overflow in iter::sum over durations"); +//! } //<- big arm, needs brace added, and also semicolon^ +//! } +//! ``` +//! +//! The printing algorithm implementation in this crate accommodates all of +//! these situations with conditional punctuation tokens whose selection can be +//! deferred and populated after it's known that the group is or is not broken. + +#![allow( + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + clippy::doc_markdown, + clippy::enum_glob_use, + clippy::items_after_statements, + clippy::match_like_matches_macro, + clippy::match_same_arms, + clippy::module_name_repetitions, + clippy::must_use_candidate, + clippy::needless_pass_by_value, + clippy::similar_names, + clippy::unused_self, + clippy::vec_init_then_push +)] +#![cfg_attr(all(test, exhaustive), feature(non_exhaustive_omitted_patterns_lint))] + +mod algorithm; +mod attr; +mod convenience; +mod data; +mod expr; +mod file; +mod generics; +mod item; +mod iter; +mod lifetime; +mod lit; +mod mac; +mod pat; +mod path; +mod ring; +mod stmt; +mod token; +mod ty; + +use algorithm::Printer; +use syn::{Expr, File}; + +// Target line width. +const MARGIN: isize = 89; + +// Number of spaces increment at each level of block indentation. +const INDENT: isize = 4; + +// Every line is allowed at least this much space, even if highly indented. +const MIN_SPACE: isize = 60; + +pub fn unparse(file: &File) -> String { + let mut p = Printer::new(); + p.file(file); + p.eof() +} + +pub fn unparse_expr(file: &Expr) -> String { + let mut p = Printer::new(); + p.expr(file); + p.eof() +} diff --git a/packages/autofmt/src/prettyplease/pat.rs b/packages/autofmt/src/prettyplease/pat.rs new file mode 100644 index 000000000..0ca86ef06 --- /dev/null +++ b/packages/autofmt/src/prettyplease/pat.rs @@ -0,0 +1,210 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use proc_macro2::TokenStream; +use syn::{ + FieldPat, Pat, PatBox, PatIdent, PatLit, PatMacro, PatOr, PatPath, PatRange, PatReference, + PatRest, PatSlice, PatStruct, PatTuple, PatTupleStruct, PatType, PatWild, RangeLimits, +}; + +impl Printer { + pub fn pat(&mut self, pat: &Pat) { + match pat { + Pat::Box(pat) => self.pat_box(pat), + Pat::Ident(pat) => self.pat_ident(pat), + Pat::Lit(pat) => self.pat_lit(pat), + Pat::Macro(pat) => self.pat_macro(pat), + Pat::Or(pat) => self.pat_or(pat), + Pat::Path(pat) => self.pat_path(pat), + Pat::Range(pat) => self.pat_range(pat), + Pat::Reference(pat) => self.pat_reference(pat), + Pat::Rest(pat) => self.pat_rest(pat), + Pat::Slice(pat) => self.pat_slice(pat), + Pat::Struct(pat) => self.pat_struct(pat), + Pat::Tuple(pat) => self.pat_tuple(pat), + Pat::TupleStruct(pat) => self.pat_tuple_struct(pat), + Pat::Type(pat) => self.pat_type(pat), + Pat::Verbatim(pat) => self.pat_verbatim(pat), + Pat::Wild(pat) => self.pat_wild(pat), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown Pat"), + } + } + + fn pat_box(&mut self, pat: &PatBox) { + self.outer_attrs(&pat.attrs); + self.word("box "); + self.pat(&pat.pat); + } + + fn pat_ident(&mut self, pat: &PatIdent) { + self.outer_attrs(&pat.attrs); + if pat.by_ref.is_some() { + self.word("ref "); + } + if pat.mutability.is_some() { + self.word("mut "); + } + self.ident(&pat.ident); + if let Some((_at_token, subpat)) = &pat.subpat { + self.word(" @ "); + self.pat(subpat); + } + } + + fn pat_lit(&mut self, pat: &PatLit) { + self.outer_attrs(&pat.attrs); + self.expr(&pat.expr); + } + + fn pat_macro(&mut self, pat: &PatMacro) { + self.outer_attrs(&pat.attrs); + self.mac(&pat.mac, None); + } + + fn pat_or(&mut self, pat: &PatOr) { + self.outer_attrs(&pat.attrs); + let mut consistent_break = false; + for case in &pat.cases { + match case { + Pat::Lit(_) | Pat::Wild(_) => {} + _ => { + consistent_break = true; + break; + } + } + } + if consistent_break { + self.cbox(0); + } else { + self.ibox(0); + } + for case in pat.cases.iter().delimited() { + if !case.is_first { + self.space(); + self.word("| "); + } + self.pat(&case); + } + self.end(); + } + + fn pat_path(&mut self, pat: &PatPath) { + self.outer_attrs(&pat.attrs); + self.qpath(&pat.qself, &pat.path); + } + + fn pat_range(&mut self, pat: &PatRange) { + self.outer_attrs(&pat.attrs); + self.expr(&pat.lo); + match &pat.limits { + RangeLimits::HalfOpen(_) => self.word(".."), + RangeLimits::Closed(_) => self.word("..="), + } + self.expr(&pat.hi); + } + + fn pat_reference(&mut self, pat: &PatReference) { + self.outer_attrs(&pat.attrs); + self.word("&"); + if pat.mutability.is_some() { + self.word("mut "); + } + self.pat(&pat.pat); + } + + fn pat_rest(&mut self, pat: &PatRest) { + self.outer_attrs(&pat.attrs); + self.word(".."); + } + + fn pat_slice(&mut self, pat: &PatSlice) { + self.outer_attrs(&pat.attrs); + self.word("["); + for elem in pat.elems.iter().delimited() { + self.pat(&elem); + self.trailing_comma(elem.is_last); + } + self.word("]"); + } + + fn pat_struct(&mut self, pat: &PatStruct) { + self.outer_attrs(&pat.attrs); + self.cbox(INDENT); + self.path(&pat.path); + self.word(" {"); + self.space_if_nonempty(); + for field in pat.fields.iter().delimited() { + self.field_pat(&field); + self.trailing_comma_or_space(field.is_last && pat.dot2_token.is_none()); + } + if pat.dot2_token.is_some() { + self.word(".."); + self.space(); + } + self.offset(-INDENT); + self.end(); + self.word("}"); + } + + fn pat_tuple(&mut self, pat: &PatTuple) { + self.outer_attrs(&pat.attrs); + self.word("("); + self.cbox(INDENT); + self.zerobreak(); + for elem in pat.elems.iter().delimited() { + self.pat(&elem); + if pat.elems.len() == 1 { + if pat.elems.trailing_punct() { + self.word(","); + } + self.zerobreak(); + } else { + self.trailing_comma(elem.is_last); + } + } + self.offset(-INDENT); + self.end(); + self.word(")"); + } + + fn pat_tuple_struct(&mut self, pat: &PatTupleStruct) { + self.outer_attrs(&pat.attrs); + self.path(&pat.path); + self.word("("); + self.cbox(INDENT); + self.zerobreak(); + for elem in pat.pat.elems.iter().delimited() { + self.pat(&elem); + self.trailing_comma(elem.is_last); + } + self.offset(-INDENT); + self.end(); + self.word(")"); + } + + pub fn pat_type(&mut self, pat: &PatType) { + self.outer_attrs(&pat.attrs); + self.pat(&pat.pat); + self.word(": "); + self.ty(&pat.ty); + } + + fn pat_verbatim(&mut self, pat: &TokenStream) { + unimplemented!("Pat::Verbatim `{}`", pat); + } + + fn pat_wild(&mut self, pat: &PatWild) { + self.outer_attrs(&pat.attrs); + self.word("_"); + } + + fn field_pat(&mut self, field_pat: &FieldPat) { + self.outer_attrs(&field_pat.attrs); + if field_pat.colon_token.is_some() { + self.member(&field_pat.member); + self.word(": "); + } + self.pat(&field_pat.pat); + } +} diff --git a/packages/autofmt/src/prettyplease/path.rs b/packages/autofmt/src/prettyplease/path.rs new file mode 100644 index 000000000..460fe5cb7 --- /dev/null +++ b/packages/autofmt/src/prettyplease/path.rs @@ -0,0 +1,181 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use syn::{ + AngleBracketedGenericArguments, Binding, Constraint, Expr, GenericArgument, + ParenthesizedGenericArguments, Path, PathArguments, PathSegment, QSelf, +}; + +impl Printer { + pub fn path(&mut self, path: &Path) { + assert!(!path.segments.is_empty()); + for segment in path.segments.iter().delimited() { + if !segment.is_first || path.leading_colon.is_some() { + self.word("::"); + } + self.path_segment(&segment); + } + } + + pub fn path_segment(&mut self, segment: &PathSegment) { + self.ident(&segment.ident); + self.path_arguments(&segment.arguments); + } + + fn path_arguments(&mut self, arguments: &PathArguments) { + match arguments { + PathArguments::None => {} + PathArguments::AngleBracketed(arguments) => { + self.angle_bracketed_generic_arguments(arguments); + } + PathArguments::Parenthesized(arguments) => { + self.parenthesized_generic_arguments(arguments); + } + } + } + + fn generic_argument(&mut self, arg: &GenericArgument) { + match arg { + GenericArgument::Lifetime(lifetime) => self.lifetime(lifetime), + GenericArgument::Type(ty) => self.ty(ty), + GenericArgument::Binding(binding) => self.binding(binding), + GenericArgument::Constraint(constraint) => self.constraint(constraint), + GenericArgument::Const(expr) => { + match expr { + Expr::Lit(expr) => self.expr_lit(expr), + Expr::Block(expr) => self.expr_block(expr), + // ERROR CORRECTION: Add braces to make sure that the + // generated code is valid. + _ => { + self.word("{"); + self.expr(expr); + self.word("}"); + } + } + } + } + } + + fn angle_bracketed_generic_arguments(&mut self, generic: &AngleBracketedGenericArguments) { + if generic.colon2_token.is_some() { + self.word("::"); + } + self.word("<"); + self.cbox(INDENT); + self.zerobreak(); + + // Print lifetimes before types and consts, all before bindings, + // regardless of their order in self.args. + // + // TODO: ordering rules for const arguments vs type arguments have + // not been settled yet. https://github.com/rust-lang/rust/issues/44580 + for arg in generic.args.iter().delimited() { + match *arg { + GenericArgument::Lifetime(_) => { + self.generic_argument(&arg); + self.trailing_comma(arg.is_last); + } + GenericArgument::Type(_) + | GenericArgument::Binding(_) + | GenericArgument::Constraint(_) + | GenericArgument::Const(_) => {} + } + } + for arg in generic.args.iter().delimited() { + match *arg { + GenericArgument::Type(_) | GenericArgument::Const(_) => { + self.generic_argument(&arg); + self.trailing_comma(arg.is_last); + } + GenericArgument::Lifetime(_) + | GenericArgument::Binding(_) + | GenericArgument::Constraint(_) => {} + } + } + for arg in generic.args.iter().delimited() { + match *arg { + GenericArgument::Binding(_) | GenericArgument::Constraint(_) => { + self.generic_argument(&arg); + self.trailing_comma(arg.is_last); + } + GenericArgument::Lifetime(_) + | GenericArgument::Type(_) + | GenericArgument::Const(_) => {} + } + } + + self.offset(-INDENT); + self.end(); + self.word(">"); + } + + fn binding(&mut self, binding: &Binding) { + self.ident(&binding.ident); + self.word(" = "); + self.ty(&binding.ty); + } + + fn constraint(&mut self, constraint: &Constraint) { + self.ident(&constraint.ident); + self.ibox(INDENT); + for bound in constraint.bounds.iter().delimited() { + if bound.is_first { + self.word(": "); + } else { + self.space(); + self.word("+ "); + } + self.type_param_bound(&bound); + } + self.end(); + } + + fn parenthesized_generic_arguments(&mut self, arguments: &ParenthesizedGenericArguments) { + self.cbox(INDENT); + self.word("("); + self.zerobreak(); + for ty in arguments.inputs.iter().delimited() { + self.ty(&ty); + self.trailing_comma(ty.is_last); + } + self.offset(-INDENT); + self.word(")"); + self.return_type(&arguments.output); + self.end(); + } + + pub fn qpath(&mut self, qself: &Option, path: &Path) { + let qself = match qself { + Some(qself) => qself, + None => { + self.path(path); + return; + } + }; + + assert!(qself.position < path.segments.len()); + + self.word("<"); + self.ty(&qself.ty); + + let mut segments = path.segments.iter(); + if qself.position > 0 { + self.word(" as "); + for segment in segments.by_ref().take(qself.position).delimited() { + if !segment.is_first || path.leading_colon.is_some() { + self.word("::"); + } + self.path_segment(&segment); + if segment.is_last { + self.word(">"); + } + } + } else { + self.word(">"); + } + for segment in segments { + self.word("::"); + self.path_segment(segment); + } + } +} diff --git a/packages/autofmt/src/prettyplease/ring.rs b/packages/autofmt/src/prettyplease/ring.rs new file mode 100644 index 000000000..aff92589a --- /dev/null +++ b/packages/autofmt/src/prettyplease/ring.rs @@ -0,0 +1,81 @@ +use std::collections::VecDeque; +use std::ops::{Index, IndexMut}; + +pub struct RingBuffer { + data: VecDeque, + // Abstract index of data[0] in infinitely sized queue + offset: usize, +} + +impl RingBuffer { + pub fn new() -> Self { + RingBuffer { + data: VecDeque::new(), + offset: 0, + } + } + + pub fn is_empty(&self) -> bool { + self.data.is_empty() + } + + pub fn len(&self) -> usize { + self.data.len() + } + + pub fn push(&mut self, value: T) -> usize { + let index = self.offset + self.data.len(); + self.data.push_back(value); + index + } + + pub fn clear(&mut self) { + self.data.clear(); + } + + pub fn index_of_first(&self) -> usize { + self.offset + } + + pub fn first(&self) -> &T { + &self.data[0] + } + + pub fn first_mut(&mut self) -> &mut T { + &mut self.data[0] + } + + pub fn pop_first(&mut self) -> T { + self.offset += 1; + self.data.pop_front().unwrap() + } + + pub fn last(&self) -> &T { + self.data.back().unwrap() + } + + pub fn last_mut(&mut self) -> &mut T { + self.data.back_mut().unwrap() + } + + pub fn second_last(&self) -> &T { + &self.data[self.data.len() - 2] + } + + pub fn pop_last(&mut self) { + self.data.pop_back().unwrap(); + } +} + +impl Index for RingBuffer { + type Output = T; + fn index(&self, index: usize) -> &Self::Output { + &self.data[index.checked_sub(self.offset).unwrap()] + } +} + +impl IndexMut for RingBuffer { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + &mut self.data[index.checked_sub(self.offset).unwrap()] + } +} diff --git a/packages/autofmt/src/prettyplease/stmt.rs b/packages/autofmt/src/prettyplease/stmt.rs new file mode 100644 index 000000000..68295fa70 --- /dev/null +++ b/packages/autofmt/src/prettyplease/stmt.rs @@ -0,0 +1,85 @@ +use super::algorithm::Printer; +use syn::{Expr, Stmt}; + +impl Printer { + pub fn stmt(&mut self, stmt: &Stmt) { + match stmt { + Stmt::Local(local) => { + self.outer_attrs(&local.attrs); + self.ibox(0); + self.word("let "); + self.pat(&local.pat); + if let Some((_eq, init)) = &local.init { + self.word(" = "); + self.neverbreak(); + self.expr(init); + } + self.word(";"); + self.end(); + self.hardbreak(); + } + Stmt::Item(item) => self.item(item), + Stmt::Expr(expr) => { + if break_after(expr) { + self.ibox(0); + self.expr_beginning_of_line(expr, true); + if add_semi(expr) { + self.word(";"); + } + self.end(); + self.hardbreak(); + } else { + self.expr_beginning_of_line(expr, true); + } + } + Stmt::Semi(expr, _semi) => { + if let Expr::Verbatim(tokens) = expr { + if tokens.is_empty() { + return; + } + } + self.ibox(0); + self.expr_beginning_of_line(expr, true); + if !remove_semi(expr) { + self.word(";"); + } + self.end(); + self.hardbreak(); + } + } + } +} + +pub fn add_semi(expr: &Expr) -> bool { + match expr { + Expr::Assign(_) + | Expr::AssignOp(_) + | Expr::Break(_) + | Expr::Continue(_) + | Expr::Return(_) + | Expr::Yield(_) => true, + Expr::Group(group) => add_semi(&group.expr), + _ => false, + } +} + +pub fn break_after(expr: &Expr) -> bool { + if let Expr::Group(group) = expr { + if let Expr::Verbatim(verbatim) = group.expr.as_ref() { + return !verbatim.is_empty(); + } + } + true +} + +fn remove_semi(expr: &Expr) -> bool { + match expr { + Expr::ForLoop(_) | Expr::While(_) => true, + Expr::Group(group) => remove_semi(&group.expr), + Expr::If(expr) => match &expr.else_branch { + Some((_else_token, else_branch)) => remove_semi(else_branch), + None => true, + }, + _ => false, + } +} diff --git a/packages/autofmt/src/prettyplease/token.rs b/packages/autofmt/src/prettyplease/token.rs new file mode 100644 index 000000000..f25f0d3c8 --- /dev/null +++ b/packages/autofmt/src/prettyplease/token.rs @@ -0,0 +1,80 @@ +use super::algorithm::Printer; +use proc_macro2::{Delimiter, Ident, Literal, Spacing, TokenStream, TokenTree}; + +impl Printer { + pub fn single_token(&mut self, token: Token, group_contents: fn(&mut Self, TokenStream)) { + match token { + Token::Group(delimiter, stream) => self.token_group(delimiter, stream, group_contents), + Token::Ident(ident) => self.ident(&ident), + Token::Punct(ch, _spacing) => self.token_punct(ch), + Token::Literal(literal) => self.token_literal(&literal), + } + } + + fn token_group( + &mut self, + delimiter: Delimiter, + stream: TokenStream, + group_contents: fn(&mut Self, TokenStream), + ) { + self.delimiter_open(delimiter); + if !stream.is_empty() { + if delimiter == Delimiter::Brace { + self.space(); + } + group_contents(self, stream); + if delimiter == Delimiter::Brace { + self.space(); + } + } + self.delimiter_close(delimiter); + } + + pub fn ident(&mut self, ident: &Ident) { + self.word(ident.to_string()); + } + + pub fn token_punct(&mut self, ch: char) { + self.word(ch.to_string()); + } + + pub fn token_literal(&mut self, literal: &Literal) { + self.word(literal.to_string()); + } + + pub fn delimiter_open(&mut self, delimiter: Delimiter) { + self.word(match delimiter { + Delimiter::Parenthesis => "(", + Delimiter::Brace => "{", + Delimiter::Bracket => "[", + Delimiter::None => return, + }); + } + + pub fn delimiter_close(&mut self, delimiter: Delimiter) { + self.word(match delimiter { + Delimiter::Parenthesis => ")", + Delimiter::Brace => "}", + Delimiter::Bracket => "]", + Delimiter::None => return, + }); + } +} + +pub enum Token { + Group(Delimiter, TokenStream), + Ident(Ident), + Punct(char, Spacing), + Literal(Literal), +} + +impl From for Token { + fn from(tt: TokenTree) -> Self { + match tt { + TokenTree::Group(group) => Token::Group(group.delimiter(), group.stream()), + TokenTree::Ident(ident) => Token::Ident(ident), + TokenTree::Punct(punct) => Token::Punct(punct.as_char(), punct.spacing()), + TokenTree::Literal(literal) => Token::Literal(literal), + } + } +} diff --git a/packages/autofmt/src/prettyplease/ty.rs b/packages/autofmt/src/prettyplease/ty.rs new file mode 100644 index 000000000..93cb48c77 --- /dev/null +++ b/packages/autofmt/src/prettyplease/ty.rs @@ -0,0 +1,202 @@ +use super::algorithm::Printer; +use super::iter::IterDelimited; +use super::INDENT; +use proc_macro2::TokenStream; +use syn::{ + Abi, BareFnArg, ReturnType, Type, TypeArray, TypeBareFn, TypeGroup, TypeImplTrait, TypeInfer, + TypeMacro, TypeNever, TypeParen, TypePath, TypePtr, TypeReference, TypeSlice, TypeTraitObject, + TypeTuple, Variadic, +}; + +impl Printer { + pub fn ty(&mut self, ty: &Type) { + match ty { + Type::Array(ty) => self.type_array(ty), + Type::BareFn(ty) => self.type_bare_fn(ty), + Type::Group(ty) => self.type_group(ty), + Type::ImplTrait(ty) => self.type_impl_trait(ty), + Type::Infer(ty) => self.type_infer(ty), + Type::Macro(ty) => self.type_macro(ty), + Type::Never(ty) => self.type_never(ty), + Type::Paren(ty) => self.type_paren(ty), + Type::Path(ty) => self.type_path(ty), + Type::Ptr(ty) => self.type_ptr(ty), + Type::Reference(ty) => self.type_reference(ty), + Type::Slice(ty) => self.type_slice(ty), + Type::TraitObject(ty) => self.type_trait_object(ty), + Type::Tuple(ty) => self.type_tuple(ty), + Type::Verbatim(ty) => self.type_verbatim(ty), + #[cfg_attr(all(test, exhaustive), deny(non_exhaustive_omitted_patterns))] + _ => unimplemented!("unknown Type"), + } + } + + fn type_array(&mut self, ty: &TypeArray) { + self.word("["); + self.ty(&ty.elem); + self.word("; "); + self.expr(&ty.len); + self.word("]"); + } + + fn type_bare_fn(&mut self, ty: &TypeBareFn) { + if let Some(bound_lifetimes) = &ty.lifetimes { + self.bound_lifetimes(bound_lifetimes); + } + if ty.unsafety.is_some() { + self.word("unsafe "); + } + if let Some(abi) = &ty.abi { + self.abi(abi); + } + self.word("fn("); + self.cbox(INDENT); + self.zerobreak(); + for bare_fn_arg in ty.inputs.iter().delimited() { + self.bare_fn_arg(&bare_fn_arg); + self.trailing_comma(bare_fn_arg.is_last && ty.variadic.is_none()); + } + if let Some(variadic) = &ty.variadic { + self.variadic(variadic); + self.zerobreak(); + } + self.offset(-INDENT); + self.end(); + self.word(")"); + self.return_type(&ty.output); + } + + fn type_group(&mut self, ty: &TypeGroup) { + self.ty(&ty.elem); + } + + fn type_impl_trait(&mut self, ty: &TypeImplTrait) { + self.word("impl "); + for type_param_bound in ty.bounds.iter().delimited() { + if !type_param_bound.is_first { + self.word(" + "); + } + self.type_param_bound(&type_param_bound); + } + } + + fn type_infer(&mut self, ty: &TypeInfer) { + let _ = ty; + self.word("_"); + } + + fn type_macro(&mut self, ty: &TypeMacro) { + self.mac(&ty.mac, None); + } + + fn type_never(&mut self, ty: &TypeNever) { + let _ = ty; + self.word("!"); + } + + fn type_paren(&mut self, ty: &TypeParen) { + self.word("("); + self.ty(&ty.elem); + self.word(")"); + } + + fn type_path(&mut self, ty: &TypePath) { + self.qpath(&ty.qself, &ty.path); + } + + fn type_ptr(&mut self, ty: &TypePtr) { + self.word("*"); + if ty.mutability.is_some() { + self.word("mut "); + } else { + self.word("const "); + } + self.ty(&ty.elem); + } + + fn type_reference(&mut self, ty: &TypeReference) { + self.word("&"); + if let Some(lifetime) = &ty.lifetime { + self.lifetime(lifetime); + self.nbsp(); + } + if ty.mutability.is_some() { + self.word("mut "); + } + self.ty(&ty.elem); + } + + fn type_slice(&mut self, ty: &TypeSlice) { + self.word("["); + self.ty(&ty.elem); + self.word("]"); + } + + fn type_trait_object(&mut self, ty: &TypeTraitObject) { + self.word("dyn "); + for type_param_bound in ty.bounds.iter().delimited() { + if !type_param_bound.is_first { + self.word(" + "); + } + self.type_param_bound(&type_param_bound); + } + } + + fn type_tuple(&mut self, ty: &TypeTuple) { + self.word("("); + self.cbox(INDENT); + self.zerobreak(); + for elem in ty.elems.iter().delimited() { + self.ty(&elem); + if ty.elems.len() == 1 { + self.word(","); + self.zerobreak(); + } else { + self.trailing_comma(elem.is_last); + } + } + self.offset(-INDENT); + self.end(); + self.word(")"); + } + + fn type_verbatim(&mut self, ty: &TokenStream) { + if ty.to_string() == "..." { + self.word("..."); + } else { + unimplemented!("Type::Verbatim `{}`", ty); + } + } + + pub fn return_type(&mut self, ty: &ReturnType) { + match ty { + ReturnType::Default => {} + ReturnType::Type(_arrow, ty) => { + self.word(" -> "); + self.ty(ty); + } + } + } + + fn bare_fn_arg(&mut self, bare_fn_arg: &BareFnArg) { + self.outer_attrs(&bare_fn_arg.attrs); + if let Some((name, _colon)) = &bare_fn_arg.name { + self.ident(name); + self.word(": "); + } + self.ty(&bare_fn_arg.ty); + } + + fn variadic(&mut self, variadic: &Variadic) { + self.outer_attrs(&variadic.attrs); + self.word("..."); + } + + pub fn abi(&mut self, abi: &Abi) { + self.word("extern "); + if let Some(name) = &abi.name { + self.lit_str(name); + self.nbsp(); + } + } +} diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs new file mode 100644 index 000000000..c35f996b7 --- /dev/null +++ b/packages/autofmt/tests/sink.rs @@ -0,0 +1,61 @@ +use dioxus_autofmt::*; +use proc_macro2::TokenStream as TokenStream2; +use syn::{Attribute, Meta}; + +#[test] +fn formats_block() { + let block = r#" + div { + div { + class: "asd", + class: "asd",class: "asd",class: "asd",class: "asd",class: "asd", + key: "ddd", + onclick: move |_| { + let blah = 120; + true + }, + blah: 123, + onclick: move |_| { + let blah = 120; + true + }, + onclick: move |_| { + let blah = 120; + true + }, + onclick: move |_| { + let blah = 120; + true + }, + + div { + div { + "hi" + } + h2 { + class: "asd", + } + } + } + } + "#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn parse_comment() { + let block = r#" + div { + adsasd: "asd", // this is a comment + } + "#; + + // let parsed: CallBody = syn::parse_str(block).ok()?; + + let parsed: TokenStream2 = syn::parse_str(block).unwrap(); + + dbg!(parsed); +} diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index d68cb2148..06aea9570 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -5,9 +5,9 @@ use syn::parse_macro_input; mod ifmt; mod inlineprops; mod props; -use dioxus_rsx as rsx; // mod rsx; +use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { diff --git a/packages/rsx/src/element.rs b/packages/rsx/src/element.rs index 96c7a2efc..d1188ae55 100644 --- a/packages/rsx/src/element.rs +++ b/packages/rsx/src/element.rs @@ -191,21 +191,38 @@ impl ToTokens for Element { pub enum ElementAttr { /// attribute: "valuee {}" - AttrText { name: Ident, value: LitStr }, + AttrText { + name: Ident, + value: LitStr, + }, /// attribute: true, - AttrExpression { name: Ident, value: Expr }, + AttrExpression { + name: Ident, + value: Expr, + }, /// "attribute": "value {}" - CustomAttrText { name: LitStr, value: LitStr }, + CustomAttrText { + name: LitStr, + value: LitStr, + }, /// "attribute": true, - CustomAttrExpression { name: LitStr, value: Expr }, + CustomAttrExpression { + name: LitStr, + value: Expr, + }, // /// onclick: move |_| {} // EventClosure { name: Ident, closure: ExprClosure }, /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, + EventTokens { + name: Ident, + tokens: Expr, + }, + + Meta(String), } pub struct ElementAttrNamed { @@ -248,6 +265,10 @@ impl ToTokens for ElementAttrNamed { dioxus_elements::on::#name(__cx, #tokens) } } + + ElementAttr::Meta(_) => { + todo!("meta attributes not supported yet"); + } }); } } diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 6b9dfc3dc..5f01d9306 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -18,8 +18,6 @@ mod component; mod element; mod node; -pub mod pretty; - // Re-export the namespaces into each other pub use component::*; pub use element::*; diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index d5d701767..5eb195716 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -20,6 +20,7 @@ pub enum BodyNode { Component(Component), Text(LitStr), RawExpr(Expr), + Meta(String), } impl Parse for BodyNode { @@ -79,6 +80,7 @@ impl ToTokens for BodyNode { BodyNode::RawExpr(exp) => tokens.append_all(quote! { __cx.fragment_from_iter(#exp) }), + BodyNode::Meta(_) => {} } } } diff --git a/packages/rsx/src/pretty.rs b/packages/rsx/src/pretty.rs deleted file mode 100644 index 26ad54006..000000000 --- a/packages/rsx/src/pretty.rs +++ /dev/null @@ -1 +0,0 @@ -//! pretty printer for rsx! From f9d9f859240c6f7e78b0303ea40e46622d42bc3c Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 24 Apr 2022 13:12:20 -0400 Subject: [PATCH 03/95] feat: format components properly --- packages/autofmt/src/lib.rs | 64 ++++++++++++++++++++++++++++++++-- packages/autofmt/tests/sink.rs | 38 ++++++++++++++++++-- packages/rsx/src/component.rs | 6 ++-- 3 files changed, 101 insertions(+), 7 deletions(-) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index a7a0bbbdf..9eeb82e7c 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,5 +1,6 @@ //! pretty printer for rsx! use dioxus_rsx::*; +use quote::ToTokens; use std::fmt::{self, Write}; mod prettyplease; @@ -15,7 +16,7 @@ pub fn fmt_block(block: &str) -> Option { Some(buf) } -pub fn write_ident(buf: &mut dyn Write, node: &BodyNode, indent: usize) -> fmt::Result { +pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Result { match node { BodyNode::Element(el) => { let Element { @@ -81,7 +82,66 @@ pub fn write_ident(buf: &mut dyn Write, node: &BodyNode, indent: usize) -> fmt:: write_tabs(buf, indent)?; writeln!(buf, "}}")?; } - BodyNode::Component(_) => { + BodyNode::Component(component) => { + let Component { + name, + body, + children, + manual_props, + } = component; + + let name = name.to_token_stream().to_string(); + + write_tabs(buf, indent)?; + writeln!(buf, "{name} {{")?; + + for field in body { + write_tabs(buf, indent + 1)?; + let name = &field.name; + match &field.content { + ContentField::ManExpr(exp) => { + let out = prettyplease::unparse_expr(exp); + writeln!(buf, "{}: {},", name, out)?; + } + ContentField::Formatted(s) => { + writeln!(buf, "{}: {},", name, s.value())?; + } + ContentField::OnHandlerRaw(exp) => { + let out = prettyplease::unparse_expr(exp); + let mut lines = out.split('\n').peekable(); + let first = lines.next().unwrap(); + write!(buf, "{}: {}", name, first)?; + for line in lines { + writeln!(buf)?; + write_tabs(buf, indent + 1)?; + write!(buf, "{}", line)?; + } + writeln!(buf, ",")?; + } + } + } + + if let Some(exp) = manual_props { + write_tabs(buf, indent + 1)?; + let out = prettyplease::unparse_expr(exp); + let mut lines = out.split('\n').peekable(); + let first = lines.next().unwrap(); + write!(buf, "..{}", first)?; + for line in lines { + writeln!(buf)?; + write_tabs(buf, indent + 1)?; + write!(buf, "{}", line)?; + } + writeln!(buf)?; + } + + for child in children { + write_ident(buf, child, indent + 1)?; + } + + write_tabs(buf, indent)?; + writeln!(buf, "}}")?; + // // write!(buf, "{}", " ".repeat(ident)) } diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index c35f996b7..62feeef59 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -53,9 +53,43 @@ fn parse_comment() { } "#; - // let parsed: CallBody = syn::parse_str(block).ok()?; - let parsed: TokenStream2 = syn::parse_str(block).unwrap(); dbg!(parsed); } + +#[test] +fn formats_component() { + let block = r#" + Component { + adsasd: "asd", // this is a comment + onclick: move |_| { + let blah = 120; + let blah = 120; + }, + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_component_man_props() { + let block = r#" + Component { + ..MyProps { + val: 123 + }, + adsasd: "asd", // this is a comment + onclick: move |_| { + let blah = 120; + }, + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} diff --git a/packages/rsx/src/component.rs b/packages/rsx/src/component.rs index dfdda74b8..158660baa 100644 --- a/packages/rsx/src/component.rs +++ b/packages/rsx/src/component.rs @@ -150,11 +150,11 @@ impl ToTokens for Component { // the struct's fields info pub struct ComponentField { - name: Ident, - content: ContentField, + pub name: Ident, + pub content: ContentField, } -enum ContentField { +pub enum ContentField { ManExpr(Expr), Formatted(LitStr), OnHandlerRaw(Expr), From 4fa909e8647a944dc7816de36d55e5970d95232b Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 24 Apr 2022 19:38:28 -0400 Subject: [PATCH 04/95] feat: ensure parsing works with right indicies --- Cargo.toml | 1 + packages/autofmt/Cargo.toml | 2 + packages/autofmt/src/lib.rs | 88 +++++++++++++++++++++++++++++++++- packages/autofmt/tests/sink.rs | 38 +++++++++++++++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 6e651cf5a..b3f31e516 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "packages/tui", "packages/liveview", "packages/rsx", + "packages/autofmt", ] [dev-dependencies] diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index 6411f5add..2c527ed98 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -10,3 +10,5 @@ proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } dioxus-rsx = { path = "../rsx" } +triple_accel = "0.4.0" +serde = { version = "1.0.136", features = ["derive"] } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 9eeb82e7c..20d13afb6 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -1,9 +1,74 @@ //! pretty printer for rsx! use dioxus_rsx::*; +use proc_macro2::TokenStream as TokenStream2; use quote::ToTokens; -use std::fmt::{self, Write}; +use std::{ + fmt::{self, Write}, + ptr::NonNull, +}; +use syn::{ + buffer::TokenBuffer, + parse::{ParseBuffer, ParseStream}, +}; +use triple_accel::{levenshtein_search, Match}; + mod prettyplease; +#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Hash)] +pub struct ForamttedBlock { + pub formatted: String, + pub start: usize, + pub end: usize, +} + +/* +TODO: nested rsx! calls + +*/ +pub fn formmat_document(contents: &str) -> Vec { + let mut matches = levenshtein_search(b"rsx! {", contents.as_bytes()).peekable(); + + let mut cur_match: Option = None; + + let mut formatted_blocks = Vec::new(); + + while let Some(item) = matches.next() { + let Match { start, end, k } = item; + + match cur_match { + Some(ref this_match) => { + // abort nested matches - these get handled automatically + if start < this_match.end { + continue; + } else { + cur_match = Some(item); + } + } + None => { + cur_match = Some(item); + } + } + + let remaining = &contents[end - 1..]; + + if let Some(bracket_end) = find_bracket_end(remaining) { + let sub_string = &contents[end..bracket_end + end - 1]; + + if let Some(new) = fmt_block(sub_string) { + if !new.is_empty() { + formatted_blocks.push(ForamttedBlock { + formatted: new, + start: end, + end: end + bracket_end - 1, + }); + } + } + } + } + + formatted_blocks +} + pub fn fmt_block(block: &str) -> Option { let parsed: CallBody = syn::parse_str(block).ok()?; @@ -175,3 +240,24 @@ pub fn write_tabs(f: &mut dyn Write, num: usize) -> std::fmt::Result { } Ok(()) } + +fn find_bracket_end(contents: &str) -> Option { + let mut depth = 0; + let mut i = 0; + + for c in contents.chars() { + if c == '{' { + depth += 1; + } else if c == '}' { + depth -= 1; + } + + if depth == 0 { + return Some(i); + } + + i += 1; + } + + None +} diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index 62feeef59..8a1983b07 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -93,3 +93,41 @@ fn formats_component_man_props() { print!("{formatted}"); } + +#[test] +fn formats_document() { + let block = r#" +rsx!{ + Component { + adsasd: "asd", // this is a comment + onclick: move |_| { + let blah = 120; + }, + } +} + + +"#; + + let formatted = formmat_document(block); + + print!("{formatted:?}"); +} + +#[test] +fn formats_valid_rust_src() { + let src = r#" +// +rsx! { + div {} + div { + h3 {"asd" + } + } +} +"#; + + let formatted = formmat_document(src); + + println!("{formatted:?}"); +} From 36ba49fe27cd7f6d3790401483f7f34a132d81aa Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Sun, 24 Apr 2022 20:44:38 -0400 Subject: [PATCH 05/95] wip: fixing tabs --- packages/autofmt/src/lib.rs | 24 ++++++++++++----- packages/autofmt/tests/sink.rs | 49 ++++++++++++++++++++++++++++++++-- 2 files changed, 65 insertions(+), 8 deletions(-) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 20d13afb6..02dc00a3e 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -15,24 +15,24 @@ use triple_accel::{levenshtein_search, Match}; mod prettyplease; #[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Hash)] -pub struct ForamttedBlock { +pub struct FormattedBlock { pub formatted: String, pub start: usize, pub end: usize, } /* -TODO: nested rsx! calls + */ -pub fn formmat_document(contents: &str) -> Vec { +pub fn get_format_blocks(contents: &str) -> Vec { let mut matches = levenshtein_search(b"rsx! {", contents.as_bytes()).peekable(); let mut cur_match: Option = None; let mut formatted_blocks = Vec::new(); - while let Some(item) = matches.next() { + for item in matches { let Match { start, end, k } = item; match cur_match { @@ -54,10 +54,22 @@ pub fn formmat_document(contents: &str) -> Vec { if let Some(bracket_end) = find_bracket_end(remaining) { let sub_string = &contents[end..bracket_end + end - 1]; + // with the edge brackets + // println!("{}", &contents[end - 1..bracket_end + end]); + if let Some(new) = fmt_block(sub_string) { if !new.is_empty() { - formatted_blocks.push(ForamttedBlock { - formatted: new, + // if we have code to push, we want the code to end up on the right lines with the right indentation + + let mut output = String::new(); + writeln!(output).unwrap(); + + for line in new.lines() { + writeln!(output, "\t{}", line).ok(); + } + + formatted_blocks.push(FormattedBlock { + formatted: output, start: end, end: end + bracket_end - 1, }); diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index 8a1983b07..d345b267d 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -109,7 +109,7 @@ rsx!{ "#; - let formatted = formmat_document(block); + let formatted = get_format_blocks(block); print!("{formatted:?}"); } @@ -127,7 +127,52 @@ rsx! { } "#; - let formatted = formmat_document(src); + let formatted = get_format_blocks(src); println!("{formatted:?}"); } + +#[test] +fn formats_valid_rust_src_with_indents() { + let mut src = r#" +#[inline_props] +fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element { + const ICON_SIZE: u32 = 36; + + rsx! { + div { + + h1 {"thing"} + + + } + + + + + } +} +"# + .to_string(); + + let formatted = get_format_blocks(&src); + + let block = formatted.into_iter().next().unwrap(); + + // println!("{}", &src[block.start..block.end]); + // println!("{}", &src[block.start..block.end - 1]); + + src.replace_range( + block.start - 1..block.end + 1, + &format!("{{ {} }}", &block.formatted), + // &format!("{{ {} }}", &block.formatted), + ); + + println!("{src}"); + + println!("\tthing"); + + // for block in formatted { + // println!("{}", block.formatted); + // } +} From 804d9af1c70af010645319c4ec493931cc8d9a48 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Tue, 26 Apr 2022 18:45:08 -0400 Subject: [PATCH 06/95] feat: improve autoformatting algorithm - tons of new tests for various cases to be consistent --- packages/autofmt/src/lib.rs | 146 ++++++++++++++++++----- packages/autofmt/tests/sink.rs | 210 +++++++++++++++++++++++++++++++-- packages/rsx/src/node.rs | 9 ++ 3 files changed, 321 insertions(+), 44 deletions(-) diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 02dc00a3e..8d4a0cedf 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -32,28 +32,36 @@ pub fn get_format_blocks(contents: &str) -> Vec { let mut formatted_blocks = Vec::new(); + let mut last_bracket_end = 0; + for item in matches { let Match { start, end, k } = item; - match cur_match { - Some(ref this_match) => { - // abort nested matches - these get handled automatically - if start < this_match.end { - continue; - } else { - cur_match = Some(item); - } - } - None => { - cur_match = Some(item); - } + if start < last_bracket_end { + continue; } + // match cur_match { + // Some(ref this_match) => { + // // abort nested matches - these get handled automatically + // if start < this_match.end { + // continue; + // } else { + // cur_match = Some(item); + // } + // } + // None => { + // cur_match = Some(item); + // } + // } + let remaining = &contents[end - 1..]; if let Some(bracket_end) = find_bracket_end(remaining) { let sub_string = &contents[end..bracket_end + end - 1]; + last_bracket_end = bracket_end + end - 1; + // with the edge brackets // println!("{}", &contents[end - 1..bracket_end + end]); @@ -65,7 +73,7 @@ pub fn get_format_blocks(contents: &str) -> Vec { writeln!(output).unwrap(); for line in new.lines() { - writeln!(output, "\t{}", line).ok(); + writeln!(output, " {}", line).ok(); } formatted_blocks.push(FormattedBlock { @@ -74,7 +82,11 @@ pub fn get_format_blocks(contents: &str) -> Vec { end: end + bracket_end - 1, }); } + } else { + panic!("failed to format block: {}", sub_string); } + } else { + panic!("failed to find end of block: {}", remaining); } } @@ -105,58 +117,120 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res } = el; write_tabs(buf, indent)?; - writeln!(buf, "{name} {{")?; + write!(buf, "{name} {{")?; + + let total_attr_len = attributes + .iter() + .map(|attr| match &attr.attr { + ElementAttr::AttrText { name, value } => value.value().len(), + ElementAttr::AttrExpression { name, value } => 10, + ElementAttr::CustomAttrText { name, value } => value.value().len(), + ElementAttr::CustomAttrExpression { name, value } => 10, + ElementAttr::EventTokens { name, tokens } => 1000000, + ElementAttr::Meta(_) => todo!(), + }) + .sum::(); + + let is_long_attr_list = total_attr_len > 80; if let Some(key) = key { let key = key.value(); - write_tabs(buf, indent + 1)?; + if is_long_attr_list { + write_tabs(buf, indent + 1)?; + } else { + write!(buf, " ")?; + } write!(buf, "key: \"{key}\"")?; + if !attributes.is_empty() { writeln!(buf, ",")?; } } - for attr in attributes { - write_tabs(buf, indent + 1)?; + let mut attr_iter = attributes.iter().peekable(); + while let Some(attr) = attr_iter.next() { + if is_long_attr_list { + writeln!(buf)?; + write_tabs(buf, indent + 1)?; + } else { + write!(buf, " ")?; + } + match &attr.attr { ElementAttr::AttrText { name, value } => { - writeln!(buf, "{name}: \"{value}\",", value = value.value())?; + write!(buf, "{name}: \"{value}\"", value = value.value())?; } ElementAttr::AttrExpression { name, value } => { let out = prettyplease::unparse_expr(value); - writeln!(buf, "{}: {},", name, out)?; + write!(buf, "{}: {}", name, out)?; } - ElementAttr::CustomAttrText { name, value } => todo!(), - ElementAttr::CustomAttrExpression { name, value } => todo!(), + ElementAttr::CustomAttrText { name, value } => { + write!( + buf, + "{name}: \"{value}\"", + name = name.value(), + value = value.value() + )?; + } + + ElementAttr::CustomAttrExpression { name, value } => { + let out = prettyplease::unparse_expr(value); + write!(buf, "{}: {}", name.value(), out)?; + } ElementAttr::EventTokens { name, tokens } => { let out = prettyplease::unparse_expr(tokens); let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); - writeln!(buf, "{}: {}", name, first)?; + write!(buf, "{}: {}", name, first)?; while let Some(line) = lines.next() { write_tabs(buf, indent + 1)?; write!(buf, "{}", line)?; - // writeln!(buf, "{}", line)?; + // write!(buf, "{}", line)?; if lines.peek().is_none() { - writeln!(buf, ",")?; - } else { - writeln!(buf)?; + write!(buf, "")?; } } } ElementAttr::Meta(_) => {} } + + if attr_iter.peek().is_some() || !children.is_empty() { + write!(buf, ",")?; + } } - for child in children { - write_ident(buf, child, indent + 1)?; + if children.len() == 1 && children[0].is_litstr() && !is_long_attr_list { + if let BodyNode::Text(text) = &children[0] { + let text_val = text.value(); + if total_attr_len + text_val.len() > 80 { + writeln!(buf)?; + write_tabs(buf, indent + 1)?; + writeln!(buf, "\"{}\"", text.value())?; + write_tabs(buf, indent)?; + } else { + write!(buf, " \"{}\" ", text.value())?; + } + } + } else { + if is_long_attr_list || !children.is_empty() { + writeln!(buf)?; + } + + for child in children { + write_ident(buf, child, indent + 1)?; + } + + if is_long_attr_list || !children.is_empty() { + write_tabs(buf, indent)?; + } else { + write!(buf, " ")?; + } } - write_tabs(buf, indent)?; writeln!(buf, "}}")?; } BodyNode::Component(component) => { @@ -167,10 +241,15 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res manual_props, } = component; - let name = name.to_token_stream().to_string(); + let mut name = name.to_token_stream().to_string(); + name.retain(|c| !c.is_whitespace()); write_tabs(buf, indent)?; - writeln!(buf, "{name} {{")?; + write!(buf, "{name} {{")?; + + if !body.is_empty() || !children.is_empty() { + writeln!(buf)?; + } for field in body { write_tabs(buf, indent + 1)?; @@ -216,7 +295,10 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res write_ident(buf, child, indent + 1)?; } - write_tabs(buf, indent)?; + if !body.is_empty() || !children.is_empty() { + write_tabs(buf, indent)?; + } + writeln!(buf, "}}")?; // diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index d345b267d..ced3af07b 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -75,6 +75,103 @@ fn formats_component() { print!("{formatted}"); } +#[test] +fn formats_element() { + let block = r#" + div { + a: "1234567891012345678910123456789101234567891012345678910123456789101234567891012345678910123456789101234567891012345678910", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_element_short() { + let block = r#" + div { + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + a: "123", + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_element_nested() { + let block = r#" + h3 { + class: "mb-2 text-xl font-bold", + "Invite Member" + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_element_nested_no_trailing_tabs() { + let block = r#" + img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "", } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_element_with_correct_indent() { + let block = r###" + div { + + a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", + "Send invitation" + } + } + +"###; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn small_elements_and_text_are_small() { + let block = r###" + a { class: " text-white", + "Send invitation" + } +"###; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + #[test] fn formats_component_man_props() { let block = r#" @@ -94,6 +191,18 @@ fn formats_component_man_props() { print!("{formatted}"); } +#[test] +fn formats_component_tiny() { + let block = r#" + Component { a: 123 + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + #[test] fn formats_document() { let block = r#" @@ -114,6 +223,24 @@ rsx!{ print!("{formatted:?}"); } +#[test] +fn component_path_mod_style() { + let block = r#" +rsx!{ + my::thing::Component { + adsasd: "asd", // this is a comment + onclick: move |_| { + let blah = 120; + }, + } +} +"#; + + let formatted = get_format_blocks(block); + + print!("{formatted:?}"); +} + #[test] fn formats_valid_rust_src() { let src = r#" @@ -159,20 +286,79 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) let block = formatted.into_iter().next().unwrap(); - // println!("{}", &src[block.start..block.end]); - // println!("{}", &src[block.start..block.end - 1]); + src.replace_range( + block.start - 1..block.end + 1, + &format!("{{ {} }}", &block.formatted), + ); +} + +#[test] +fn formats_multiple_blocks() { + let mut src = r#" +#[inline_props] +fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element { + const ICON_SIZE: u32 = 36; + + rsx! { + div { + + h1 {"thing"} + + + } + + + } + + rsx! { + div { + + Ball { + a: rsx!{ + "asdasd" + } + } + } + } +} + +#[inline_props] +fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element { + const ICON_SIZE: u32 = 36; + + rsx! { + div { + + h1 {"thing"} + + + } + + + } + + rsx! { + div { + + Ball { + a: rsx!{ + "asdasd" + } + } + } + } +} +"# + .to_string(); + + let formatted = get_format_blocks(&src); + + dbg!(&formatted); + + let block = formatted.into_iter().next().unwrap(); src.replace_range( block.start - 1..block.end + 1, - &format!("{{ {} }}", &block.formatted), - // &format!("{{ {} }}", &block.formatted), + &format!("{{ {} }}", &block.formatted), ); - - println!("{src}"); - - println!("\tthing"); - - // for block in formatted { - // println!("{}", block.formatted); - // } } diff --git a/packages/rsx/src/node.rs b/packages/rsx/src/node.rs index 5eb195716..a41bd7098 100644 --- a/packages/rsx/src/node.rs +++ b/packages/rsx/src/node.rs @@ -23,6 +23,15 @@ pub enum BodyNode { Meta(String), } +impl BodyNode { + pub fn is_litstr(&self) -> bool { + match self { + BodyNode::Text(_) => true, + _ => false, + } + } +} + impl Parse for BodyNode { fn parse(stream: ParseStream) -> Result { if stream.peek(LitStr) { From 284860f470181158e453c43d5fe65f5f273dd814 Mon Sep 17 00:00:00 2001 From: Jonathan Kelley Date: Thu, 28 Apr 2022 14:16:21 -0400 Subject: [PATCH 07/95] feat: more tests and spans --- packages/autofmt/Cargo.toml | 2 +- packages/autofmt/src/lib.rs | 114 ++++++++++++++++++++++++--------- packages/autofmt/tests/sink.rs | 109 ++++++++++++++++++++++++++++++- 3 files changed, 194 insertions(+), 31 deletions(-) diff --git a/packages/autofmt/Cargo.toml b/packages/autofmt/Cargo.toml index 2c527ed98..8fb7a128f 100644 --- a/packages/autofmt/Cargo.toml +++ b/packages/autofmt/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -proc-macro2 = { version = "1.0.6" } +proc-macro2 = { version = "1.0.6", features = ["span-locations"] } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } dioxus-rsx = { path = "../rsx" } diff --git a/packages/autofmt/src/lib.rs b/packages/autofmt/src/lib.rs index 8d4a0cedf..4f467e139 100644 --- a/packages/autofmt/src/lib.rs +++ b/packages/autofmt/src/lib.rs @@ -41,20 +41,6 @@ pub fn get_format_blocks(contents: &str) -> Vec { continue; } - // match cur_match { - // Some(ref this_match) => { - // // abort nested matches - these get handled automatically - // if start < this_match.end { - // continue; - // } else { - // cur_match = Some(item); - // } - // } - // None => { - // cur_match = Some(item); - // } - // } - let remaining = &contents[end - 1..]; if let Some(bracket_end) = find_bracket_end(remaining) { @@ -67,6 +53,14 @@ pub fn get_format_blocks(contents: &str) -> Vec { if let Some(new) = fmt_block(sub_string) { if !new.is_empty() { + println!("{}", &contents[end + 1..bracket_end + end - 1]); + println!("{}", new); + + let stripped = &contents[end + 1..bracket_end + end - 1]; + if stripped == new { + println!("no changes necessary"); + } + // if we have code to push, we want the code to end up on the right lines with the right indentation let mut output = String::new(); @@ -94,18 +88,25 @@ pub fn get_format_blocks(contents: &str) -> Vec { } pub fn fmt_block(block: &str) -> Option { + let mut raw_lines = block.split('\n').collect::>(); + let parsed: CallBody = syn::parse_str(block).ok()?; let mut buf = String::new(); for node in parsed.roots.iter() { - write_ident(&mut buf, node, 0).ok()?; + write_ident(&mut buf, &raw_lines, node, 0).ok()?; } Some(buf) } -pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Result { +pub fn write_ident( + buf: &mut String, + lines: &[&str], + node: &BodyNode, + indent: usize, +) -> fmt::Result { match node { BodyNode::Element(el) => { let Element { @@ -168,7 +169,7 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res ElementAttr::CustomAttrText { name, value } => { write!( buf, - "{name}: \"{value}\"", + "\"{name}\": \"{value}\"", name = name.value(), value = value.value() )?; @@ -176,22 +177,32 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res ElementAttr::CustomAttrExpression { name, value } => { let out = prettyplease::unparse_expr(value); - write!(buf, "{}: {}", name.value(), out)?; + write!(buf, "\"{}\": {}", name.value(), out)?; } ElementAttr::EventTokens { name, tokens } => { let out = prettyplease::unparse_expr(tokens); + dbg!(&out); + let mut lines = out.split('\n').peekable(); let first = lines.next().unwrap(); - write!(buf, "{}: {}", name, first)?; - while let Some(line) = lines.next() { - write_tabs(buf, indent + 1)?; - write!(buf, "{}", line)?; - // write!(buf, "{}", line)?; - if lines.peek().is_none() { - write!(buf, "")?; + // a one-liner for whatever reason + // Does not need a new line + if lines.peek().is_none() { + write!(buf, "{}: {}", name, first)?; + } else { + writeln!(buf, "{}: {}", name, first)?; + + while let Some(line) = lines.next() { + write_tabs(buf, indent + 1)?; + write!(buf, "{}", line)?; + if lines.peek().is_none() { + write!(buf, "")?; + } else { + writeln!(buf)?; + } } } } @@ -221,7 +232,7 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res } for child in children { - write_ident(buf, child, indent + 1)?; + write_ident(buf, lines, child, indent + 1)?; } if is_long_attr_list || !children.is_empty() { @@ -260,7 +271,7 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res writeln!(buf, "{}: {},", name, out)?; } ContentField::Formatted(s) => { - writeln!(buf, "{}: {},", name, s.value())?; + writeln!(buf, "{}: \"{}\",", name, s.value())?; } ContentField::OnHandlerRaw(exp) => { let out = prettyplease::unparse_expr(exp); @@ -292,7 +303,7 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res } for child in children { - write_ident(buf, child, indent + 1)?; + write_ident(buf, lines, child, indent + 1)?; } if !body.is_empty() || !children.is_empty() { @@ -310,7 +321,52 @@ pub fn write_ident(buf: &mut String, node: &BodyNode, indent: usize) -> fmt::Res write_tabs(buf, indent)?; writeln!(buf, "\"{}\"", t.value())?; } - BodyNode::RawExpr(_) => { + BodyNode::RawExpr(exp) => { + use syn::spanned::Spanned; + + let placement = exp.span(); + + let start = placement.start(); + let end = placement.end(); + + let num_spaces_desired = (indent * 4) as isize; + let first = lines[start.line - 1]; + let num_spaces_real = first.chars().take_while(|c| c.is_whitespace()).count() as isize; + let offset = num_spaces_real - num_spaces_desired; + + for line_id in start.line - 1..end.line { + let line = lines[line_id]; + + // trim the leading whitespace + + if offset < 0 { + for _ in 0..-offset { + write!(buf, " ")?; + } + + writeln!(buf, "{}", line)?; + } else { + let offset = offset as usize; + + let right = &line[offset..]; + writeln!(buf, "{}", right)?; + } + } + + // let toks = exp.to_token_stream(); + + // let out = prettyplease::unparse_expr(exp); + // let mut lines = out.split('\n').peekable(); + // for line in lines { + // write_tabs(buf, indent)?; + // writeln!(buf, "{}", line)?; + // // writeln!(buf)?; + // } + // write_tabs(buf, indent)?; + // let first = lines.next().unwrap(); + // write!(buf, "{}", name, first)?; + // writeln!(buf)?; + // // write!(buf, "{}", " ".repeat(ident)) } diff --git a/packages/autofmt/tests/sink.rs b/packages/autofmt/tests/sink.rs index ced3af07b..736abc7dc 100644 --- a/packages/autofmt/tests/sink.rs +++ b/packages/autofmt/tests/sink.rs @@ -203,6 +203,98 @@ fn formats_component_tiny() { print!("{formatted}"); } +#[test] +fn formats_exprs() { + let block = r#" + ul { + (0..10).map(|f| rsx!{ + li { + "hi" + } + }) + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_exprs_neg_indent() { + let block = r#" + ul { + (0..10).map(|f| rsx!{ + li { + "hi" + } + }) + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_exprs_handlers() { + let block = r#" + button { + class: "flex items-center pl-3 py-3 pr-2 text-gray-500 hover:bg-indigo-50 rounded", + onclick: move |evt| { + show_user_menu.set(!show_user_menu.get()); evt.cancel_bubble(); }, + + onclick: move |evt| + + show_user_menu.set(!show_user_menu.get()), + span { class: "inline-block mr-4", + icons::icon_14 {} + } + span { "Settings" } + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + +#[test] +fn formats_complex() { + let block = r#" + li { + Link { + class: "flex items-center pl-3 py-3 pr-4 {active_class} rounded", + to: "{to}", + span { class: "inline-block mr-3", + icons::icon_0 {} + } + span { "{name}" } + children.is_some().then(|| rsx!{ + span { + class: "inline-block ml-auto hover:bg-gray-500", + onclick: move |evt| { + // open.set(!open.get()); + evt.cancel_bubble(); + }, + icons::icon_8 {} + } + }) + } + div { + class: "px-4", + is_current.then(|| rsx!{ children }) + // open.then(|| rsx!{ children }) + } + } +"#; + + let formatted = fmt_block(block).unwrap(); + + print!("{formatted}"); +} + #[test] fn formats_document() { let block = r#" @@ -321,7 +413,6 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) } } } - #[inline_props] fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) -> Element { const ICON_SIZE: u32 = 36; @@ -362,3 +453,19 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape) &format!("{{ {} }}", &block.formatted), ); } + +#[test] +fn empty_blocks() { + let mut src = r###" +pub fn Alert(cx: Scope) -> Element { + cx.render(rsx! { + div { } + }) +} +"### + .to_string(); + + let formatted = get_format_blocks(&src); + + dbg!(&formatted); +} From 7a176834474630aeb4805ac32290377c734bd6a5 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 11:01:49 -0500 Subject: [PATCH 08/95] add persistant iterator to native core --- .../tests/peristant_iterator.rs | 342 ++++++++++++++++++ packages/native-core/src/lib.rs | 1 + packages/native-core/src/utils.rs | 224 ++++++++++++ 3 files changed, 567 insertions(+) create mode 100644 packages/native-core-macro/tests/peristant_iterator.rs create mode 100644 packages/native-core/src/utils.rs diff --git a/packages/native-core-macro/tests/peristant_iterator.rs b/packages/native-core-macro/tests/peristant_iterator.rs new file mode 100644 index 000000000..29df3afe5 --- /dev/null +++ b/packages/native-core-macro/tests/peristant_iterator.rs @@ -0,0 +1,342 @@ +use dioxus_native_core::{ + real_dom::{NodeType, RealDom}, + state::State, + utils::PersistantElementIter, +}; +use dioxus_native_core_macro::State; + +#[derive(State, Default, Clone)] +struct Empty {} + +#[test] +#[allow(unused_variables)] +fn traverse() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let mutations = vdom.create_vnodes(rsx! { + div{ + div{ + "hello" + p{ + "world" + } + "hello world" + } + } + }); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![mutations]); + + let mut iter = PersistantElementIter::new(); + let div_tag = "div".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + let text1 = "hello".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + let p_tag = "p".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text2 = "world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + let text3 = "hello world".to_string(); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.next(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text2, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: p_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text1, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Element { tag: div_tag, .. } + )); + assert!(matches!( + &rdom[iter.prev(&rdom).id()].node_type, + NodeType::Text { text: text3, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_removes() { + use dioxus_core::VNode; + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + // this will end on the node that is removed + let mut iter1 = PersistantElementIter::new(); + // this will end on the after node that is removed + let mut iter2 = PersistantElementIter::new(); + // div + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "hello" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // "world" + iter1.next(&rdom).id(); + iter2.next(&rdom).id(); + // p + iter2.next(&rdom).id(); + // "hello world" + iter2.next(&rdom).id(); + + iter1.prune(&update, &rdom); + iter2.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter1.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); + let div_tag = "div".to_string(); + let idx = iter2.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: div_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_before() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "3", + "hello world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "div".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); +} + +#[test] +#[allow(unused_variables)] +fn persist_instertions_after() { + use dioxus_core::*; + use dioxus_core_macro::*; + use dioxus_html as dioxus_elements; + + #[allow(non_snake_case)] + fn Base(cx: Scope) -> Element { + rsx!(cx, div {}) + } + let vdom = VirtualDom::new(Base); + let (build, update) = vdom.diff_lazynodes( + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + } + }, + rsx! { + div{ + p{ + key: "1", + "hello" + } + p{ + key: "2", + "world" + } + p{ + key: "3", + "hello world" + } + } + }, + ); + + let mut rdom: RealDom = RealDom::new(); + + let _to_update = rdom.apply_mutations(vec![build]); + + let mut iter = PersistantElementIter::new(); + // div + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "hello" + iter.next(&rdom).id(); + // p + iter.next(&rdom).id(); + // "world" + iter.next(&rdom).id(); + + iter.prune(&update, &rdom); + let _to_update = rdom.apply_mutations(vec![update]); + + let p_tag = "p".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!( + &rdom[idx].node_type, + NodeType::Element { tag: p_tag, .. } + )); + let text = "hello world".to_string(); + let idx = iter.next(&rdom).id(); + assert!(matches!(&rdom[idx].node_type, NodeType::Text { text, .. })); +} diff --git a/packages/native-core/src/lib.rs b/packages/native-core/src/lib.rs index a6e011fa9..2319ca4ac 100644 --- a/packages/native-core/src/lib.rs +++ b/packages/native-core/src/lib.rs @@ -2,3 +2,4 @@ pub mod layout_attributes; pub mod node_ref; pub mod real_dom; pub mod state; +pub mod utils; diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs new file mode 100644 index 000000000..41f79a04b --- /dev/null +++ b/packages/native-core/src/utils.rs @@ -0,0 +1,224 @@ +use crate::{ + real_dom::{NodeType, RealDom}, + state::State, +}; +use dioxus_core::{DomEdit, ElementId, Mutations}; + +pub enum ElementProduced { + Progressed(ElementId), + Looped(ElementId), +} +impl ElementProduced { + pub fn id(&self) -> ElementId { + match self { + ElementProduced::Progressed(id) => *id, + ElementProduced::Looped(id) => *id, + } + } +} + +#[derive(Debug)] +enum NodePosition { + AtNode, + InChild(usize), +} + +impl NodePosition { + fn map(&self, mut f: impl FnMut(usize) -> usize) -> Self { + match self { + Self::AtNode => Self::AtNode, + Self::InChild(i) => Self::InChild(f(*i)), + } + } + + fn get_or_insert(&mut self, child_idx: usize) -> usize { + match self { + Self::AtNode => { + *self = Self::InChild(child_idx); + child_idx + } + Self::InChild(i) => *i, + } + } +} + +/// The focus system needs a iterator that can persist through changes in the [VirtualDom]. +/// Iterate through it with [ElementIter::next] [ElementIter::prev], and update it with [ElementIter::update] (with data from [`VirtualDom::work_with_deadline`]). +/// The iterator loops around when it reaches the end or the beginning. +pub struct PersistantElementIter { + // stack of elements and fragments + stack: smallvec::SmallVec<[(ElementId, NodePosition); 5]>, +} + +impl Default for PersistantElementIter { + fn default() -> Self { + PersistantElementIter { + stack: smallvec::smallvec![(ElementId(0), NodePosition::AtNode)], + } + } +} + +impl PersistantElementIter { + pub fn new() -> Self { + Self::default() + } + + /// remove stale element refreneces + pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) { + let ids_removed: Vec<_> = mutations + .edits + .iter() + .filter_map(|e| { + if let DomEdit::Remove { root } = e { + Some(*root) + } else { + None + } + }) + .collect(); + if let Some(r) = self + .stack + .iter() + .position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id)) + { + self.stack.truncate(r); + } + // if a child is removed or inserted before or at the current element, update the child index + for (el_id, child_idx) in self.stack.iter_mut() { + if let NodePosition::InChild(child_idx) = child_idx { + if let NodeType::Element { children, .. } = &rdom[*el_id].node_type { + for m in &mutations.edits { + match m { + DomEdit::Remove { root } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx -= 1; + } + } + DomEdit::InsertBefore { root, n } => { + if children + .iter() + .take(*child_idx + 1) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + DomEdit::InsertAfter { root, n } => { + if children + .iter() + .take(*child_idx) + .any(|c| c.as_u64() == *root) + { + *child_idx += *n as usize; + } + } + _ => (), + } + } + } + } + } + } + + /// get the next element + pub fn next(&mut self, rdom: &RealDom) -> ElementProduced { + if self.stack.is_empty() { + let id = ElementId(0); + let new = (id, NodePosition::AtNode); + self.stack.push(new); + ElementProduced::Looped(id) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + *o_child_idx = o_child_idx.map(|i| i + 1); + // if we have children, go to the next child + let child_idx = o_child_idx.get_or_insert(0); + if child_idx >= children.len() { + self.pop(); + self.next(rdom) + } else { + let id = children[child_idx]; + if let NodeType::Element { .. } = &rdom[id].node_type { + self.stack.push((id, NodePosition::AtNode)); + } + ElementProduced::Progressed(id) + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + /// get the previous element + pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + fn push_back( + stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>, + new_node: ElementId, + rdom: &RealDom, + ) -> ElementId { + match &rdom[new_node].node_type { + NodeType::Element { children, .. } => { + if children.is_empty() { + new_node + } else { + stack.push((new_node, NodePosition::InChild(children.len() - 1))); + push_back(stack, *children.last().unwrap(), rdom) + } + } + _ => new_node, + } + } + if self.stack.is_empty() { + let new_node = ElementId(0); + ElementProduced::Looped(push_back(&mut self.stack, new_node, rdom)) + } else { + let (last, o_child_idx) = self.stack.last_mut().unwrap(); + let node = &rdom[*last]; + match &node.node_type { + NodeType::Element { children, .. } => { + // if we have children, go to the next child + if let NodePosition::InChild(0) = o_child_idx { + ElementProduced::Progressed(self.pop()) + } else { + *o_child_idx = o_child_idx.map(|i| i - 1); + if let NodePosition::InChild(child_idx) = o_child_idx { + if *child_idx >= children.len() || children.len() == 0 { + self.pop(); + self.prev(rdom) + } else { + let new_node = children[*child_idx]; + ElementProduced::Progressed(push_back( + &mut self.stack, + new_node, + rdom, + )) + } + } else { + self.pop(); + self.prev(rdom) + } + } + } + + NodeType::Text { .. } | NodeType::Placeholder { .. } => { + // we are at a leaf, so we are done + ElementProduced::Progressed(self.pop()) + } + } + } + } + + fn pop(&mut self) -> ElementId { + self.stack.pop().unwrap().0 + } +} From 35ee243d0d0663f112cc8ae82306d475901d95f8 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 11:02:35 -0500 Subject: [PATCH 09/95] intigrate focus system with tui --- examples/tui_buttons.rs | 59 ++++++++++++++++++++++++++++++++++++ packages/tui/src/hooks.rs | 17 ++++++----- packages/tui/src/lib.rs | 61 +++++++++++++++++++++++++++++++++----- packages/tui/src/render.rs | 10 +++++-- packages/tui/src/widget.rs | 5 ++-- 5 files changed, 132 insertions(+), 20 deletions(-) create mode 100644 examples/tui_buttons.rs diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs new file mode 100644 index 000000000..3581a0d7c --- /dev/null +++ b/examples/tui_buttons.rs @@ -0,0 +1,59 @@ +#![allow(non_snake_case)] + +use dioxus::prelude::*; + +fn main() { + dioxus::tui::launch(app); +} + +fn Button(cx: Scope) -> Element { + let state = use_state(&cx, || false); + let color = if *state.get() { "red" } else { "blue" }; + let text = if *state.get() { "☐" } else { "☒" }; + + cx.render(rsx! { + div { + border_width: "1px", + width: "50%", + height: "100%", + background_color: "{color}", + justify_content: "center", + align_items: "center", + onkeydown: |_| state.modify(|s| !s), + + "{text}" + } + }) +} + +fn app(cx: Scope) -> Element { + cx.render(rsx! { + div { + width: "100%", + height: "100%", + flex_direction: "column", + + div { + width: "100%", + height: "50%", + flex_direction: "row", + + Button{}, + Button{}, + Button{}, + Button{}, + } + + div { + width: "100%", + height: "50%", + flex_direction: "row", + + Button{}, + Button{}, + Button{}, + Button{}, + } + } + }) +} diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 8da04fc5e..d36682750 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -550,7 +550,6 @@ impl RinkInputHandler { }) .map(|evt| (evt.0, evt.1.into_any())); - // todo: currently resolves events in all nodes, but once the focus system is added it should filter by focus let mut hm: FxHashMap<&'static str, Vec>> = FxHashMap::default(); for (event, data) in events { if let Some(v) = hm.get_mut(event) { @@ -562,13 +561,15 @@ impl RinkInputHandler { for (event, datas) in hm { for node in dom.get_listening_sorted(event) { for data in &datas { - resolved_events.push(UserEvent { - scope_id: None, - priority: EventPriority::Medium, - name: event, - element: Some(node.id), - data: data.clone(), - }); + if node.state.focused { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: event, + element: Some(node.id), + data: data.clone(), + }); + } } } } diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 9f6bd78ed..033eb1d8d 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,7 +7,9 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use dioxus_native_core::{real_dom::RealDom, state::*}; +use dioxus_native_core::real_dom::{NodeType, RealDom}; +use dioxus_native_core::state::*; +use dioxus_native_core::utils::PersistantElementIter; use dioxus_native_core_macro::State; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, @@ -42,6 +44,7 @@ struct NodeState { // depends on attributes, the C component of it's parent and a u8 context #[parent_dep_state(style)] style: StyleModifier, + focused: bool, } #[derive(Clone)] @@ -130,7 +133,10 @@ fn render_vdom( } let to_rerender: fxhash::FxHashSet = vec![0].into_iter().collect(); - let mut resized = true; + let mut updated = true; + + let mut focus_iter = PersistantElementIter::new(); + let mut focus_id = ElementId(0); loop { /* @@ -144,8 +150,8 @@ fn render_vdom( todo: lazy re-rendering */ - if !to_rerender.is_empty() || resized { - resized = false; + if !to_rerender.is_empty() || updated { + updated = false; fn resize(dims: Rect, stretch: &mut Stretch, rdom: &Dom) { let width = dims.width; let height = dims.height; @@ -192,6 +198,8 @@ fn render_vdom( // } Either::Right((evt, _o)) => { + let mut evt_intersepted = false; + match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -201,15 +209,51 @@ fn render_vdom( { break; } + if let KeyCode::BackTab = key.code { + let mut new_focused_id; + loop { + new_focused_id = focus_iter.prev(&rdom).id(); + if let NodeType::Element { .. } = + &rdom[new_focused_id].node_type + { + break; + } + } + rdom[focus_id].state.focused = false; + focus_id = new_focused_id; + rdom[focus_id].state.focused = true; + evt_intersepted = true; + updated = true; + // println!("{:?}", focus_id); + } + if let KeyCode::Tab = key.code { + let mut new_focused_id; + loop { + new_focused_id = focus_iter.next(&rdom).id(); + if let NodeType::Element { .. } = + &rdom[new_focused_id].node_type + { + break; + } + } + rdom[focus_id].state.focused = false; + focus_id = new_focused_id; + rdom[focus_id].state.focused = true; + evt_intersepted = true; + updated = true; + // println!("{:?}", focus_id); + } } - TermEvent::Resize(_, _) => resized = true, + TermEvent::Resize(_, _) => updated = true, TermEvent::Mouse(_) => {} }, InputEvent::Close => break, }; - if let InputEvent::UserInput(evt) = evt.unwrap() { - register_event(evt); + if !evt_intersepted { + if let InputEvent::UserInput(evt) = evt.unwrap() { + register_event(evt); + } } } } @@ -221,6 +265,9 @@ fn render_vdom( vdom.handle_message(SchedulerMsg::Event(e)); } let mutations = vdom.work_with_deadline(|| false); + for m in &mutations { + focus_iter.prune(m, &rdom); + } // updates the dom's nodes let to_update = rdom.apply_mutations(mutations); // update the style and layout diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs index 3e75f3a5e..7fa09094e 100644 --- a/packages/tui/src/render.rs +++ b/packages/tui/src/render.rs @@ -5,7 +5,7 @@ use stretch2::{ prelude::{Layout, Size}, Stretch, }; -use tui::{backend::CrosstermBackend, layout::Rect}; +use tui::{backend::CrosstermBackend, layout::Rect, style::Color}; use crate::{ style::{RinkColor, RinkStyle}, @@ -43,7 +43,7 @@ pub(crate) fn render_vnode( } impl<'a> RinkWidget for Label<'a> { - fn render(self, area: Rect, mut buf: RinkBuffer) { + fn render(self, area: Rect, buf: &mut RinkBuffer) { for (i, c) in self.text.char_indices() { let mut new_cell = RinkCell::default(); new_cell.set_style(self.style); @@ -81,7 +81,7 @@ pub(crate) fn render_vnode( } impl RinkWidget for &Node { - fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { + fn render(self, area: Rect, mut buf: &mut RinkBuffer<'_>) { use tui::symbols::line::*; enum Direction { @@ -267,6 +267,10 @@ impl RinkWidget for &Node { if let Some(c) = self.state.style.core.bg { new_cell.bg = c; } + if self.state.focused { + new_cell.bg.alpha = 100; + new_cell.bg.color = new_cell.bg.blend(Color::White); + } buf.set(x, y, new_cell); } } diff --git a/packages/tui/src/widget.rs b/packages/tui/src/widget.rs index b0c96d607..1e4af52fc 100644 --- a/packages/tui/src/widget.rs +++ b/packages/tui/src/widget.rs @@ -41,7 +41,7 @@ impl<'a> RinkBuffer<'a> { } pub trait RinkWidget { - fn render(self, area: Rect, buf: RinkBuffer); + fn render(self, area: Rect, buf: &mut RinkBuffer); } pub struct WidgetWithContext { @@ -57,7 +57,8 @@ impl WidgetWithContext { impl Widget for WidgetWithContext { fn render(self, area: Rect, buf: &mut Buffer) { - self.widget.render(area, RinkBuffer::new(buf, self.config)) + let mut rbuf = RinkBuffer::new(buf, self.config); + self.widget.render(area, &mut rbuf); } } From ce5ade9fa6a584178256a4b4d05e33adf85c229e Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 15:06:07 -0500 Subject: [PATCH 10/95] add focus state --- packages/tui/src/focus.rs | 268 ++++++++++++++++++++++++++++++++++++++ packages/tui/src/lib.rs | 46 ++----- 2 files changed, 279 insertions(+), 35 deletions(-) create mode 100644 packages/tui/src/focus.rs diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs new file mode 100644 index 000000000..cc89e9660 --- /dev/null +++ b/packages/tui/src/focus.rs @@ -0,0 +1,268 @@ +use crate::Dom; + +use dioxus_core::ElementId; +use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; + +use std::num::NonZeroU16; + +use dioxus_native_core::{ + node_ref::{AttributeMask, NodeMask, NodeView}, + real_dom::NodeType, + state::NodeDepState, +}; + +#[derive(Clone, Copy, PartialEq, Eq, Debug, Ord)] +pub(crate) enum FocusLevel { + Unfocusable, + Focusable, + Ordered(std::num::NonZeroU16), +} + +impl FocusLevel { + pub fn focusable(&self) -> bool { + match self { + FocusLevel::Unfocusable => false, + FocusLevel::Focusable => true, + FocusLevel::Ordered(_) => true, + } + } +} + +impl PartialOrd for FocusLevel { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (FocusLevel::Unfocusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Equal), + (FocusLevel::Unfocusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Less), + (FocusLevel::Unfocusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Less), + (FocusLevel::Focusable, FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Focusable, FocusLevel::Focusable) => Some(std::cmp::Ordering::Equal), + (FocusLevel::Focusable, FocusLevel::Ordered(_)) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Ordered(_), FocusLevel::Unfocusable) => Some(std::cmp::Ordering::Greater), + (FocusLevel::Ordered(_), FocusLevel::Focusable) => Some(std::cmp::Ordering::Less), + (FocusLevel::Ordered(a), FocusLevel::Ordered(b)) => a.partial_cmp(b), + } + } +} + +impl Default for FocusLevel { + fn default() -> Self { + FocusLevel::Unfocusable + } +} + +#[derive(Clone, PartialEq, Debug, Default)] +pub(crate) struct Focus { + pub pass_focus: bool, + pub level: FocusLevel, +} + +impl NodeDepState for Focus { + type Ctx = (); + type DepState = (); + const NODE_MASK: NodeMask = + NodeMask::new_with_attrs(AttributeMask::Static(FOCUS_ATTRIBUTES)).with_listeners(); + + fn reduce(&mut self, node: NodeView<'_>, _sibling: &Self::DepState, _: &Self::Ctx) -> bool { + let new = Focus { + pass_focus: !node + .attributes() + .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "true"), + level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { + if let Ok(index) = a.value.parse::() { + if index < 0 { + FocusLevel::Unfocusable + } else if index == 0 { + FocusLevel::Focusable + } else { + FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap()) + } + } else { + FocusLevel::Unfocusable + } + } else { + if node + .listeners() + .iter() + .any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok()) + { + FocusLevel::Focusable + } else { + FocusLevel::Unfocusable + } + }, + }; + if *self != new { + *self = new; + true + } else { + false + } + } +} + +// must be sorted +const FOCUS_EVENTS: &[&str] = &["keydown", "keypress", "keyup"]; +const FOCUS_ATTRIBUTES: &[&str] = &["dioxus-prevent-default", "tabindex"]; + +#[derive(Default)] +pub(crate) struct FocusState { + pub(crate) focus_iter: PersistantElementIter, + pub(crate) last_focused_id: Option, + pub(crate) focus_level: FocusLevel, +} + +impl FocusState { + // returns true if the focus has changed + pub fn progress(&mut self, rdom: &mut Dom, forward: bool) -> bool { + if let Some(last) = self.last_focused_id { + if !rdom[last].state.focus.pass_focus { + return false; + } + } + let mut loop_marker_id = self.last_focused_id; + let focus_level = &mut self.focus_level; + let mut next_focus = None; + let starting_focus_level = *focus_level; + + loop { + let new = if forward { + self.focus_iter.next(&rdom) + } else { + self.focus_iter.prev(&rdom) + }; + let new_id = new.id(); + let current_level = rdom[new_id].state.focus.level; + if let ElementProduced::Looped(_) = new { + let mut closest_level = None; + + if forward { + // find the closest focusable element after the current level + rdom.traverse_depth_first(|n| { + let current_level = n.state.focus.level; + if current_level != *focus_level { + if current_level > *focus_level { + if let Some(level) = &mut closest_level { + if current_level < *level { + *level = current_level; + } + } else { + closest_level = Some(current_level); + } + } + } + }); + } else { + // find the closest focusable element before the current level + rdom.traverse_depth_first(|n| { + let current_level = n.state.focus.level; + if current_level != *focus_level { + if current_level < *focus_level { + if let Some(level) = &mut closest_level { + if current_level > *level { + *level = current_level; + } + } else { + closest_level = Some(current_level); + } + } + } + }); + } + + // extend the loop_marker_id to allow for another pass + loop_marker_id = None; + + if let Some(level) = closest_level { + *focus_level = level; + } else { + if forward { + *focus_level = FocusLevel::Unfocusable; + } else { + *focus_level = FocusLevel::Focusable; + } + } + + // if the focus level looped, we are done + if *focus_level == starting_focus_level { + break; + } + } + + // once we have looked at all the elements exit the loop + if let Some(last) = loop_marker_id { + if new_id == last { + break; + } + } else { + loop_marker_id = Some(new_id); + } + + let after_previous_focused = if forward { + current_level >= *focus_level + } else { + current_level <= *focus_level + }; + if after_previous_focused && current_level.focusable() { + if current_level == *focus_level { + next_focus = Some((new_id, current_level)); + break; + } + } + } + + if let Some((id, order)) = next_focus { + if order.focusable() { + rdom[id].state.focused = true; + if let Some(old) = self.last_focused_id.replace(id) { + rdom[old].state.focused = false; + } + // reset the position to the currently focused element + while if forward { + self.focus_iter.next(&rdom).id() + } else { + self.focus_iter.prev(&rdom).id() + } != id + {} + return true; + } + } + + false + } + + pub(crate) fn prune(&mut self, mutations: &dioxus_core::Mutations, rdom: &Dom) { + fn remove_children( + to_prune: &mut [&mut Option], + rdom: &Dom, + removed: ElementId, + ) { + for opt in to_prune.iter_mut() { + if let Some(id) = opt { + if *id == removed { + **opt = None; + } + } + } + if let NodeType::Element { children, .. } = &rdom[removed].node_type { + for child in children { + remove_children(to_prune, rdom, *child); + } + } + } + for m in &mutations.edits { + match m { + dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children( + &mut [&mut self.last_focused_id], + rdom, + ElementId(*root as usize), + ), + dioxus_core::DomEdit::Remove { root } => remove_children( + &mut [&mut self.last_focused_id], + rdom, + ElementId(*root as usize), + ), + _ => (), + } + } + } +} diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index 033eb1d8d..d8dec50e9 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -7,10 +7,11 @@ use crossterm::{ }; use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; -use dioxus_native_core::real_dom::{NodeType, RealDom}; +use dioxus_native_core::real_dom::RealDom; use dioxus_native_core::state::*; -use dioxus_native_core::utils::PersistantElementIter; use dioxus_native_core_macro::State; +use focus::Focus; +use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, pin_mut, StreamExt, @@ -24,6 +25,7 @@ use style_attributes::StyleModifier; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; +mod focus; mod hooks; mod layout; mod render; @@ -44,6 +46,8 @@ struct NodeState { // depends on attributes, the C component of it's parent and a u8 context #[parent_dep_state(style)] style: StyleModifier, + #[node_dep_state()] + focus: Focus, focused: bool, } @@ -135,8 +139,7 @@ fn render_vdom( let to_rerender: fxhash::FxHashSet = vec![0].into_iter().collect(); let mut updated = true; - let mut focus_iter = PersistantElementIter::new(); - let mut focus_id = ElementId(0); + let mut focus_state = FocusState::default(); loop { /* @@ -210,38 +213,11 @@ fn render_vdom( break; } if let KeyCode::BackTab = key.code { - let mut new_focused_id; - loop { - new_focused_id = focus_iter.prev(&rdom).id(); - if let NodeType::Element { .. } = - &rdom[new_focused_id].node_type - { - break; - } - } - rdom[focus_id].state.focused = false; - focus_id = new_focused_id; - rdom[focus_id].state.focused = true; - evt_intersepted = true; - updated = true; - // println!("{:?}", focus_id); + evt_intersepted = + focus_state.progress(&mut rdom, false); } if let KeyCode::Tab = key.code { - let mut new_focused_id; - loop { - new_focused_id = focus_iter.next(&rdom).id(); - if let NodeType::Element { .. } = - &rdom[new_focused_id].node_type - { - break; - } - } - rdom[focus_id].state.focused = false; - focus_id = new_focused_id; - rdom[focus_id].state.focused = true; - evt_intersepted = true; - updated = true; - // println!("{:?}", focus_id); + evt_intersepted = focus_state.progress(&mut rdom, true); } } TermEvent::Resize(_, _) => updated = true, @@ -266,7 +242,7 @@ fn render_vdom( } let mutations = vdom.work_with_deadline(|| false); for m in &mutations { - focus_iter.prune(m, &rdom); + focus_state.prune(m, &rdom); } // updates the dom's nodes let to_update = rdom.apply_mutations(mutations); From 23376fef8e2d452394cc3dc113a88e36570f2f32 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 16:44:53 -0500 Subject: [PATCH 11/95] move focus into event system --- examples/tui_buttons.rs | 1 + packages/tui/src/focus.rs | 38 ++++++------ packages/tui/src/hooks.rs | 122 +++++++++++++++++++++++--------------- packages/tui/src/lib.rs | 22 ++----- 4 files changed, 100 insertions(+), 83 deletions(-) diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs index 3581a0d7c..39c458919 100644 --- a/examples/tui_buttons.rs +++ b/examples/tui_buttons.rs @@ -20,6 +20,7 @@ fn Button(cx: Scope) -> Element { justify_content: "center", align_items: "center", onkeydown: |_| state.modify(|s| !s), + onclick: |_| state.modify(|s| !s), "{text}" } diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index cc89e9660..3590075dd 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -2,6 +2,7 @@ use crate::Dom; use dioxus_core::ElementId; use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; +use dioxus_native_core_macro::sorted_str_slice; use std::num::NonZeroU16; @@ -100,9 +101,8 @@ impl NodeDepState for Focus { } } -// must be sorted -const FOCUS_EVENTS: &[&str] = &["keydown", "keypress", "keyup"]; -const FOCUS_ATTRIBUTES: &[&str] = &["dioxus-prevent-default", "tabindex"]; +const FOCUS_EVENTS: &[&str] = &sorted_str_slice!(["keydown", "keypress", "keyup"]); +const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["dioxus-prevent-default", "tabindex"]); #[derive(Default)] pub(crate) struct FocusState { @@ -123,6 +123,7 @@ impl FocusState { let focus_level = &mut self.focus_level; let mut next_focus = None; let starting_focus_level = *focus_level; + let mut focus_level_changed = false; loop { let new = if forward { @@ -131,22 +132,21 @@ impl FocusState { self.focus_iter.prev(&rdom) }; let new_id = new.id(); - let current_level = rdom[new_id].state.focus.level; if let ElementProduced::Looped(_) = new { let mut closest_level = None; if forward { // find the closest focusable element after the current level rdom.traverse_depth_first(|n| { - let current_level = n.state.focus.level; - if current_level != *focus_level { - if current_level > *focus_level { + let node_level = n.state.focus.level; + if node_level != *focus_level && node_level.focusable() { + if node_level > *focus_level { if let Some(level) = &mut closest_level { - if current_level < *level { - *level = current_level; + if node_level < *level { + *level = node_level; } } else { - closest_level = Some(current_level); + closest_level = Some(node_level); } } } @@ -154,15 +154,15 @@ impl FocusState { } else { // find the closest focusable element before the current level rdom.traverse_depth_first(|n| { - let current_level = n.state.focus.level; - if current_level != *focus_level { - if current_level < *focus_level { + let node_level = n.state.focus.level; + if node_level != *focus_level && node_level.focusable() { + if node_level < *focus_level { if let Some(level) = &mut closest_level { - if current_level > *level { - *level = current_level; + if node_level > *level { + *level = node_level; } } else { - closest_level = Some(current_level); + closest_level = Some(node_level); } } } @@ -174,6 +174,9 @@ impl FocusState { if let Some(level) = closest_level { *focus_level = level; + if *focus_level != starting_focus_level { + focus_level_changed = true; + } } else { if forward { *focus_level = FocusLevel::Unfocusable; @@ -183,7 +186,7 @@ impl FocusState { } // if the focus level looped, we are done - if *focus_level == starting_focus_level { + if *focus_level == starting_focus_level && focus_level_changed { break; } } @@ -197,6 +200,7 @@ impl FocusState { loop_marker_id = Some(new_id); } + let current_level = rdom[new_id].state.focus.level; let after_previous_focused = if forward { current_level >= *focus_level } else { diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index d36682750..e045a8f25 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -14,6 +14,7 @@ use std::{ }; use stretch2::{prelude::Layout, Stretch}; +use crate::FocusState; use crate::{Dom, Node}; // a wrapper around the input state for easier access @@ -78,6 +79,7 @@ pub struct InnerInputState { wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, + focus_state: FocusState, // subscribers: Vec>, } @@ -89,6 +91,7 @@ impl InnerInputState { last_key_pressed: None, screen: None, // subscribers: Vec::new(), + focus_state: FocusState::default(), } } @@ -96,54 +99,56 @@ impl InnerInputState { fn apply_event(&mut self, evt: &mut EventCore) { match evt.1 { // limitations: only two buttons may be held at once - EventData::Mouse(ref mut m) => match &mut self.mouse { - Some(state) => { - let mut buttons = state.0.buttons; - state.0 = m.clone(); - match evt.0 { - // this code only runs when there are no buttons down - "mouseup" => { - buttons = 0; - state.1 = Vec::new(); - } - "mousedown" => { - if state.1.contains(&m.buttons) { - // if we already pressed a button and there is another button released the button crossterm sends is the button remaining - if state.1.len() > 1 { - evt.0 = "mouseup"; - state.1 = vec![m.buttons]; - } - // otherwise some other button was pressed. In testing it was consistantly this mapping - else { - match m.buttons { - 0x01 => state.1.push(0x02), - 0x02 => state.1.push(0x01), - 0x04 => state.1.push(0x01), - _ => (), - } - } - } else { - state.1.push(m.buttons); + EventData::Mouse(ref mut m) => { + match &mut self.mouse { + Some(state) => { + let mut buttons = state.0.buttons; + state.0 = m.clone(); + match evt.0 { + // this code only runs when there are no buttons down + "mouseup" => { + buttons = 0; + state.1 = Vec::new(); } + "mousedown" => { + if state.1.contains(&m.buttons) { + // if we already pressed a button and there is another button released the button crossterm sends is the button remaining + if state.1.len() > 1 { + evt.0 = "mouseup"; + state.1 = vec![m.buttons]; + } + // otherwise some other button was pressed. In testing it was consistantly this mapping + else { + match m.buttons { + 0x01 => state.1.push(0x02), + 0x02 => state.1.push(0x01), + 0x04 => state.1.push(0x01), + _ => (), + } + } + } else { + state.1.push(m.buttons); + } - buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap(); + buttons = state.1.iter().copied().reduce(|a, b| a | b).unwrap(); + } + _ => (), } - _ => (), + state.0.buttons = buttons; + m.buttons = buttons; + } + None => { + self.mouse = Some(( + m.clone(), + if m.buttons == 0 { + Vec::new() + } else { + vec![m.buttons] + }, + )); } - state.0.buttons = buttons; - m.buttons = buttons; } - None => { - self.mouse = Some(( - m.clone(), - if m.buttons == 0 { - Vec::new() - } else { - vec![m.buttons] - }, - )); - } - }, + } EventData::Wheel(ref w) => self.wheel = Some(w.clone()), EventData::Screen(ref s) => self.screen = Some(*s), EventData::Keyboard(ref mut k) => { @@ -161,14 +166,27 @@ impl InnerInputState { fn update( &mut self, - evts: &mut [EventCore], + evts: &mut Vec, resolved_events: &mut Vec, layout: &Stretch, dom: &mut Dom, - ) { + ) -> bool { let previous_mouse = self.mouse.as_ref().map(|m| (m.0.clone(), m.1.clone())); self.wheel = None; + let mut force_redraw = false; + + evts.retain(|e| match &e.1 { + EventData::Keyboard(k) => match k.key_code { + KeyCode::Tab => { + let focus_event = self.focus_state.progress(dom, !k.shift_key); + force_redraw |= focus_event; + !focus_event + } + _ => true, + }, + _ => true, + }); for e in evts.iter_mut() { self.apply_event(e); @@ -179,6 +197,7 @@ impl InnerInputState { // for s in &self.subscribers { // s(); // } + force_redraw } fn resolve_mouse_events( @@ -517,10 +536,15 @@ impl RinkInputHandler { ) } - pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> Vec { + pub(crate) fn prune(&self, mutations: &dioxus_core::Mutations, rdom: &Dom) { + self.state.borrow_mut().focus_state.prune(mutations, rdom); + } + + // returns a list of events and if a event will force a rerender + pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> (Vec, bool) { let mut resolved_events = Vec::new(); - (*self.state).borrow_mut().update( + let rerender = (*self.state).borrow_mut().update( &mut (*self.queued_events).borrow_mut(), &mut resolved_events, layout, @@ -574,7 +598,7 @@ impl RinkInputHandler { } } - resolved_events + (resolved_events, rerender) } } @@ -759,7 +783,7 @@ fn translate_key_event(event: crossterm::event::KeyEvent) -> Option { 12 => KeyCode::F12, _ => return None, }, - TermKeyCode::BackTab => return None, + TermKeyCode::BackTab => KeyCode::Tab, TermKeyCode::Null => return None, _ => return None, }; diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index d8dec50e9..cb1628206 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -139,8 +139,6 @@ fn render_vdom( let to_rerender: fxhash::FxHashSet = vec![0].into_iter().collect(); let mut updated = true; - let mut focus_state = FocusState::default(); - loop { /* -> render the nodes in the right place with tui/crossterm @@ -201,8 +199,6 @@ fn render_vdom( // } Either::Right((evt, _o)) => { - let mut evt_intersepted = false; - match evt.as_ref().unwrap() { InputEvent::UserInput(event) => match event { TermEvent::Key(key) => { @@ -212,13 +208,6 @@ fn render_vdom( { break; } - if let KeyCode::BackTab = key.code { - evt_intersepted = - focus_state.progress(&mut rdom, false); - } - if let KeyCode::Tab = key.code { - evt_intersepted = focus_state.progress(&mut rdom, true); - } } TermEvent::Resize(_, _) => updated = true, TermEvent::Mouse(_) => {} @@ -226,23 +215,22 @@ fn render_vdom( InputEvent::Close => break, }; - if !evt_intersepted { - if let InputEvent::UserInput(evt) = evt.unwrap() { - register_event(evt); - } + if let InputEvent::UserInput(evt) = evt.unwrap() { + register_event(evt); } } } } { - let evts = handler.get_events(&stretch.borrow(), &mut rdom); + let (evts, rerender) = handler.get_events(&stretch.borrow(), &mut rdom); + updated |= rerender; for e in evts { vdom.handle_message(SchedulerMsg::Event(e)); } let mutations = vdom.work_with_deadline(|| false); for m in &mutations { - focus_state.prune(m, &rdom); + handler.prune(m, &rdom); } // updates the dom's nodes let to_update = rdom.apply_mutations(mutations); From 9ed722ae4560e321582785c4c45f2dfb913c27e1 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 17:19:16 -0500 Subject: [PATCH 12/95] change focus when an element is clicked --- packages/native-core/src/utils.rs | 6 ++- packages/tui/src/focus.rs | 25 ++++++++- packages/tui/src/hooks.rs | 84 ++++++++++++++++++------------- packages/tui/src/lib.rs | 6 ++- 4 files changed, 83 insertions(+), 38 deletions(-) diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs index 41f79a04b..efd2a1d3b 100644 --- a/packages/native-core/src/utils.rs +++ b/packages/native-core/src/utils.rs @@ -64,7 +64,9 @@ impl PersistantElementIter { } /// remove stale element refreneces - pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) { + /// returns true if the focused element is removed + pub fn prune(&mut self, mutations: &Mutations, rdom: &RealDom) -> bool { + let mut changed = false; let ids_removed: Vec<_> = mutations .edits .iter() @@ -82,6 +84,7 @@ impl PersistantElementIter { .position(|(el_id, _)| ids_removed.iter().any(|id| el_id.as_u64() == *id)) { self.stack.truncate(r); + changed = true; } // if a child is removed or inserted before or at the current element, update the child index for (el_id, child_idx) in self.stack.iter_mut() { @@ -122,6 +125,7 @@ impl PersistantElementIter { } } } + changed } /// get the next element diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index 3590075dd..cb578a8f1 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -109,10 +109,11 @@ pub(crate) struct FocusState { pub(crate) focus_iter: PersistantElementIter, pub(crate) last_focused_id: Option, pub(crate) focus_level: FocusLevel, + pub(crate) dirty: bool, } impl FocusState { - // returns true if the focus has changed + /// Returns true if the focus has changed. pub fn progress(&mut self, rdom: &mut Dom, forward: bool) -> bool { if let Some(last) = self.last_focused_id { if !rdom[last].state.focus.pass_focus { @@ -227,6 +228,7 @@ impl FocusState { self.focus_iter.prev(&rdom).id() } != id {} + self.dirty = true; return true; } } @@ -253,6 +255,9 @@ impl FocusState { } } } + if self.focus_iter.prune(mutations, rdom) { + self.dirty = true; + } for m in &mutations.edits { match m { dioxus_core::DomEdit::ReplaceWith { root, .. } => remove_children( @@ -269,4 +274,22 @@ impl FocusState { } } } + + pub(crate) fn set_focus(&mut self, rdom: &mut Dom, id: ElementId) { + if let Some(old) = self.last_focused_id.replace(id) { + rdom[old].state.focused = false; + } + let state = &mut rdom[id].state; + state.focused = true; + self.focus_level = state.focus.level; + // reset the position to the currently focused element + while self.focus_iter.next(&rdom).id() != id {} + self.dirty = true; + } + + pub(crate) fn clean(&mut self) -> bool { + let old = self.dirty; + self.dirty = false; + old + } } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index e045a8f25..6dd3e88bb 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -7,7 +7,7 @@ use fxhash::{FxHashMap, FxHashSet}; use dioxus_html::{on::*, KeyCode}; use std::{ any::Any, - cell::RefCell, + cell::{RefCell, RefMut}, rc::Rc, sync::Arc, time::{Duration, Instant}, @@ -79,7 +79,7 @@ pub struct InnerInputState { wheel: Option, last_key_pressed: Option<(KeyboardData, Instant)>, screen: Option<(u16, u16)>, - focus_state: FocusState, + pub(crate) focus_state: FocusState, // subscribers: Vec>, } @@ -170,19 +170,14 @@ impl InnerInputState { resolved_events: &mut Vec, layout: &Stretch, dom: &mut Dom, - ) -> bool { + ) { let previous_mouse = self.mouse.as_ref().map(|m| (m.0.clone(), m.1.clone())); self.wheel = None; - let mut force_redraw = false; evts.retain(|e| match &e.1 { EventData::Keyboard(k) => match k.key_code { - KeyCode::Tab => { - let focus_event = self.focus_state.progress(dom, !k.shift_key); - force_redraw |= focus_event; - !focus_event - } + KeyCode::Tab => !self.focus_state.progress(dom, !k.shift_key), _ => true, }, _ => true, @@ -197,11 +192,10 @@ impl InnerInputState { // for s in &self.subscribers { // s(); // } - force_redraw } fn resolve_mouse_events( - &self, + &mut self, previous_mouse: Option<(MouseData, Vec)>, resolved_events: &mut Vec, layout: &Stretch, @@ -343,14 +337,14 @@ impl InnerInputState { } } - { - // mousedown + // mousedown + if data.clicked { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mousedown") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, data.new_pos); - if currently_contains && data.clicked { + if currently_contains { try_create_event( "mousedown", Arc::new(data.mouse_data.clone()), @@ -363,14 +357,14 @@ impl InnerInputState { } } - { - // mouseup + // mouseup + if data.released { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("mouseup") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, data.new_pos); - if currently_contains && data.released { + if currently_contains { try_create_event( "mouseup", Arc::new(data.mouse_data.clone()), @@ -383,14 +377,14 @@ impl InnerInputState { } } - { - // click + // click + if data.released && data.mouse_data.button == 0 { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("click") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, data.new_pos); - if currently_contains && data.released && data.mouse_data.button == 0 { + if currently_contains { try_create_event( "click", Arc::new(data.mouse_data.clone()), @@ -403,14 +397,14 @@ impl InnerInputState { } } - { - // contextmenu + // contextmenu + if data.released && data.mouse_data.button == 2 { let mut will_bubble = FxHashSet::default(); for node in dom.get_listening_sorted("contextmenu") { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, data.new_pos); - if currently_contains && data.released && data.mouse_data.button == 2 { + if currently_contains { try_create_event( "contextmenu", Arc::new(data.mouse_data.clone()), @@ -423,15 +417,15 @@ impl InnerInputState { } } - { - // wheel - let mut will_bubble = FxHashSet::default(); - for node in dom.get_listening_sorted("wheel") { - let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); - let currently_contains = layout_contains_point(node_layout, data.new_pos); + // wheel + if let Some(w) = data.wheel_data { + if data.wheel_delta != 0.0 { + let mut will_bubble = FxHashSet::default(); + for node in dom.get_listening_sorted("wheel") { + let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let currently_contains = layout_contains_point(node_layout, data.new_pos); - if let Some(w) = data.wheel_data { - if currently_contains && data.wheel_delta != 0.0 { + if currently_contains { try_create_event( "wheel", Arc::new(w.clone()), @@ -492,6 +486,24 @@ impl InnerInputState { } } } + + // update focus + if data.released { + let mut focus_id = None; + dom.traverse_depth_first(|node| { + let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); + let currently_contains = layout_contains_point(node_layout, data.new_pos); + + if currently_contains { + if node.state.focus.level.focusable() { + focus_id = Some(node.id); + } + } + }); + if let Some(id) = focus_id { + self.focus_state.set_focus(dom, id); + } + } } } @@ -541,10 +553,10 @@ impl RinkInputHandler { } // returns a list of events and if a event will force a rerender - pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> (Vec, bool) { + pub(crate) fn get_events(&self, layout: &Stretch, dom: &mut Dom) -> Vec { let mut resolved_events = Vec::new(); - let rerender = (*self.state).borrow_mut().update( + (*self.state).borrow_mut().update( &mut (*self.queued_events).borrow_mut(), &mut resolved_events, layout, @@ -598,7 +610,11 @@ impl RinkInputHandler { } } - (resolved_events, rerender) + resolved_events + } + + pub(crate) fn state(&self) -> RefMut { + self.state.borrow_mut() } } diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index cb1628206..f8ee2ec80 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -223,8 +223,10 @@ fn render_vdom( } { - let (evts, rerender) = handler.get_events(&stretch.borrow(), &mut rdom); - updated |= rerender; + let evts = handler.get_events(&stretch.borrow(), &mut rdom); + { + updated |= handler.state().focus_state.clean(); + } for e in evts { vdom.handle_message(SchedulerMsg::Event(e)); } From 592f6ca225bcf0b6c6ea214e6d3ad905ed9e8a11 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 17:50:04 -0500 Subject: [PATCH 13/95] change button example to showcase tabindex --- examples/tui_buttons.rs | 103 ++++++++++++++++++++++++++-------------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/examples/tui_buttons.rs b/examples/tui_buttons.rs index 39c458919..31d2e1ce0 100644 --- a/examples/tui_buttons.rs +++ b/examples/tui_buttons.rs @@ -1,28 +1,51 @@ -#![allow(non_snake_case)] - -use dioxus::prelude::*; +use dioxus::{events::KeyCode, prelude::*}; fn main() { dioxus::tui::launch(app); } -fn Button(cx: Scope) -> Element { - let state = use_state(&cx, || false); - let color = if *state.get() { "red" } else { "blue" }; - let text = if *state.get() { "☐" } else { "☒" }; +#[derive(PartialEq, Props)] +struct ButtonProps { + color_offset: u32, + layer: u16, +} + +#[allow(non_snake_case)] +fn Button(cx: Scope) -> Element { + let toggle = use_state(&cx, || false); + let hovered = use_state(&cx, || false); + + let hue = cx.props.color_offset % 255; + let saturation = if *toggle.get() { 50 } else { 25 } + if *hovered.get() { 50 } else { 25 }; + let brightness = saturation / 2; + let color = format!("hsl({hue}, {saturation}, {brightness})"); cx.render(rsx! { - div { - border_width: "1px", - width: "50%", + div{ + width: "100%", height: "100%", background_color: "{color}", + tabindex: "{cx.props.layer}", + onkeydown: |e| { + if let KeyCode::Space = e.data.key_code{ + toggle.modify(|f| !f); + } + }, + onclick: |_| { + toggle.modify(|f| !f); + }, + onmouseenter: |_|{ + hovered.set(true); + }, + onmouseleave: |_|{ + hovered.set(false); + }, justify_content: "center", align_items: "center", - onkeydown: |_| state.modify(|s| !s), - onclick: |_| state.modify(|s| !s), + display: "flex", + flex_direction: "column", - "{text}" + p{"tabindex: {cx.props.layer}"} } }) } @@ -30,31 +53,41 @@ fn Button(cx: Scope) -> Element { fn app(cx: Scope) -> Element { cx.render(rsx! { div { + display: "flex", + flex_direction: "column", width: "100%", height: "100%", - flex_direction: "column", - div { - width: "100%", - height: "50%", - flex_direction: "row", - - Button{}, - Button{}, - Button{}, - Button{}, - } - - div { - width: "100%", - height: "50%", - flex_direction: "row", - - Button{}, - Button{}, - Button{}, - Button{}, - } + (1..8).map(|y| + cx.render(rsx!{ + div{ + display: "flex", + flex_direction: "row", + width: "100%", + height: "100%", + (1..8).map(|x|{ + if (x + y) % 2 == 0{ + cx.render(rsx!{ + div{ + width: "100%", + height: "100%", + background_color: "rgb(100, 100, 100)", + } + }) + } + else{ + let layer = (x + y) % 3; + cx.render(rsx!{ + Button{ + color_offset: x * y, + layer: layer as u16, + } + }) + } + }) + } + }) + ) } }) } From 2176da4d75db3dfeeac849ad6716f1cf7b1806c2 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Tue, 3 May 2022 18:43:28 -0500 Subject: [PATCH 14/95] update readme --- packages/tui/README.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/tui/README.md b/packages/tui/README.md index 20adaeb6c..ccce60ea3 100644 --- a/packages/tui/README.md +++ b/packages/tui/README.md @@ -85,11 +85,10 @@ Rink features: - [x] Flexbox based layout system - [ ] CSS selectors - [x] inline CSS support -- [ ] Built-in focusing system +- [x] Built-in focusing system - [ ] high-quality keyboard support -- [ ] Support for events, hooks, and callbacks -* [ ] Html tags1 - -1 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. - +* [ ] Support for events, hooks, and callbacks1 +* [ ] Html tags2 +1 Basic keyboard and mouse events are implemented. +2 Currently, HTML tags don't translate into any meaning inside of rink. So an `input` won't really mean anything nor does it have any additional functionality. From 4aaf790ae9d4d37ff200055f39aaac399c1c0484 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 May 2022 13:35:30 -0500 Subject: [PATCH 15/95] revert widget trait --- packages/tui/src/render.rs | 4 ++-- packages/tui/src/widget.rs | 5 ++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/tui/src/render.rs b/packages/tui/src/render.rs index 7fa09094e..ab25d6dc6 100644 --- a/packages/tui/src/render.rs +++ b/packages/tui/src/render.rs @@ -43,7 +43,7 @@ pub(crate) fn render_vnode( } impl<'a> RinkWidget for Label<'a> { - fn render(self, area: Rect, buf: &mut RinkBuffer) { + fn render(self, area: Rect, mut buf: RinkBuffer) { for (i, c) in self.text.char_indices() { let mut new_cell = RinkCell::default(); new_cell.set_style(self.style); @@ -81,7 +81,7 @@ pub(crate) fn render_vnode( } impl RinkWidget for &Node { - fn render(self, area: Rect, mut buf: &mut RinkBuffer<'_>) { + fn render(self, area: Rect, mut buf: RinkBuffer<'_>) { use tui::symbols::line::*; enum Direction { diff --git a/packages/tui/src/widget.rs b/packages/tui/src/widget.rs index 1e4af52fc..416538462 100644 --- a/packages/tui/src/widget.rs +++ b/packages/tui/src/widget.rs @@ -41,7 +41,7 @@ impl<'a> RinkBuffer<'a> { } pub trait RinkWidget { - fn render(self, area: Rect, buf: &mut RinkBuffer); + fn render(self, area: Rect, buf: RinkBuffer); } pub struct WidgetWithContext { @@ -57,8 +57,7 @@ impl WidgetWithContext { impl Widget for WidgetWithContext { fn render(self, area: Rect, buf: &mut Buffer) { - let mut rbuf = RinkBuffer::new(buf, self.config); - self.widget.render(area, &mut rbuf); + self.widget.render(area, RinkBuffer::new(buf, self.config)); } } From 749150e22d222575d46c837f5199fbb38fdaac05 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 May 2022 14:15:07 -0500 Subject: [PATCH 16/95] change prevent default to listen to tab index --- packages/tui/src/focus.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index cb578a8f1..2ea83a897 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -67,7 +67,7 @@ impl NodeDepState for Focus { let new = Focus { pass_focus: !node .attributes() - .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "true"), + .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "keydown"), level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { if let Ok(index) = a.value.parse::() { if index < 0 { From dd7784d20582b422257a95f0c0b1eae3d518cff6 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 May 2022 14:28:02 -0500 Subject: [PATCH 17/95] clean up and document focus system --- packages/native-core/src/utils.rs | 2 ++ packages/tui/src/focus.rs | 36 +++++++++---------------------- 2 files changed, 12 insertions(+), 26 deletions(-) diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs index efd2a1d3b..cd18df01e 100644 --- a/packages/native-core/src/utils.rs +++ b/packages/native-core/src/utils.rs @@ -78,6 +78,7 @@ impl PersistantElementIter { } }) .collect(); + // if any element is removed in the chain, remove it and its children from the stack if let Some(r) = self .stack .iter() @@ -165,6 +166,7 @@ impl PersistantElementIter { /// get the previous element pub fn prev(&mut self, rdom: &RealDom) -> ElementProduced { + // recursively add the last child element to the stack fn push_back( stack: &mut smallvec::SmallVec<[(ElementId, NodePosition); 5]>, new_node: ElementId, diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index 2ea83a897..fa2e1dd97 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -120,11 +120,10 @@ impl FocusState { return false; } } + // the id that started focused to track when a loop has happened let mut loop_marker_id = self.last_focused_id; let focus_level = &mut self.focus_level; let mut next_focus = None; - let starting_focus_level = *focus_level; - let mut focus_level_changed = false; loop { let new = if forward { @@ -175,9 +174,6 @@ impl FocusState { if let Some(level) = closest_level { *focus_level = level; - if *focus_level != starting_focus_level { - focus_level_changed = true; - } } else { if forward { *focus_level = FocusLevel::Unfocusable; @@ -185,11 +181,6 @@ impl FocusState { *focus_level = FocusLevel::Focusable; } } - - // if the focus level looped, we are done - if *focus_level == starting_focus_level && focus_level_changed { - break; - } } // once we have looked at all the elements exit the loop @@ -209,28 +200,21 @@ impl FocusState { }; if after_previous_focused && current_level.focusable() { if current_level == *focus_level { - next_focus = Some((new_id, current_level)); + next_focus = Some(new_id); break; } } } - if let Some((id, order)) = next_focus { - if order.focusable() { - rdom[id].state.focused = true; - if let Some(old) = self.last_focused_id.replace(id) { - rdom[old].state.focused = false; - } - // reset the position to the currently focused element - while if forward { - self.focus_iter.next(&rdom).id() - } else { - self.focus_iter.prev(&rdom).id() - } != id - {} - self.dirty = true; - return true; + if let Some(id) = next_focus { + rdom[id].state.focused = true; + if let Some(old) = self.last_focused_id.replace(id) { + rdom[old].state.focused = false; } + // reset the position to the currently focused element + while self.focus_iter.next(&rdom).id() != id {} + self.dirty = true; + return true; } false From 5d5e7f55fdae6f928df85b76f5efdae036b96896 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 4 May 2022 14:37:30 -0500 Subject: [PATCH 18/95] pass clippy --- packages/native-core/src/utils.rs | 2 +- packages/tui/src/focus.rs | 97 ++++++++++++++++--------------- packages/tui/src/hooks.rs | 6 +- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/packages/native-core/src/utils.rs b/packages/native-core/src/utils.rs index cd18df01e..143149521 100644 --- a/packages/native-core/src/utils.rs +++ b/packages/native-core/src/utils.rs @@ -198,7 +198,7 @@ impl PersistantElementIter { } else { *o_child_idx = o_child_idx.map(|i| i - 1); if let NodePosition::InChild(child_idx) = o_child_idx { - if *child_idx >= children.len() || children.len() == 0 { + if *child_idx >= children.len() || children.is_empty() { self.pop(); self.prev(rdom) } else { diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index fa2e1dd97..623bc255c 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -4,7 +4,7 @@ use dioxus_core::ElementId; use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; use dioxus_native_core_macro::sorted_str_slice; -use std::num::NonZeroU16; +use std::{cmp::Ordering, num::NonZeroU16}; use dioxus_native_core::{ node_ref::{AttributeMask, NodeMask, NodeView}, @@ -12,7 +12,7 @@ use dioxus_native_core::{ state::NodeDepState, }; -#[derive(Clone, Copy, PartialEq, Eq, Debug, Ord)] +#[derive(Clone, Copy, PartialEq, Eq, Debug)] pub(crate) enum FocusLevel { Unfocusable, Focusable, @@ -45,6 +45,12 @@ impl PartialOrd for FocusLevel { } } +impl Ord for FocusLevel { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap() + } +} + impl Default for FocusLevel { fn default() -> Self { FocusLevel::Unfocusable @@ -70,26 +76,24 @@ impl NodeDepState for Focus { .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "keydown"), level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { if let Ok(index) = a.value.parse::() { - if index < 0 { - FocusLevel::Unfocusable - } else if index == 0 { - FocusLevel::Focusable - } else { - FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap()) + match index.cmp(&0) { + Ordering::Less => FocusLevel::Unfocusable, + Ordering::Equal => FocusLevel::Focusable, + Ordering::Greater => { + FocusLevel::Ordered(NonZeroU16::new(index as u16).unwrap()) + } } } else { FocusLevel::Unfocusable } + } else if node + .listeners() + .iter() + .any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok()) + { + FocusLevel::Focusable } else { - if node - .listeners() - .iter() - .any(|l| FOCUS_EVENTS.binary_search(&l.event).is_ok()) - { - FocusLevel::Focusable - } else { - FocusLevel::Unfocusable - } + FocusLevel::Unfocusable }, }; if *self != new { @@ -127,9 +131,9 @@ impl FocusState { loop { let new = if forward { - self.focus_iter.next(&rdom) + self.focus_iter.next(rdom) } else { - self.focus_iter.prev(&rdom) + self.focus_iter.prev(rdom) }; let new_id = new.id(); if let ElementProduced::Looped(_) = new { @@ -139,15 +143,16 @@ impl FocusState { // find the closest focusable element after the current level rdom.traverse_depth_first(|n| { let node_level = n.state.focus.level; - if node_level != *focus_level && node_level.focusable() { - if node_level > *focus_level { - if let Some(level) = &mut closest_level { - if node_level < *level { - *level = node_level; - } - } else { - closest_level = Some(node_level); + if node_level != *focus_level + && node_level.focusable() + && node_level > *focus_level + { + if let Some(level) = &mut closest_level { + if node_level < *level { + *level = node_level; } + } else { + closest_level = Some(node_level); } } }); @@ -155,15 +160,16 @@ impl FocusState { // find the closest focusable element before the current level rdom.traverse_depth_first(|n| { let node_level = n.state.focus.level; - if node_level != *focus_level && node_level.focusable() { - if node_level < *focus_level { - if let Some(level) = &mut closest_level { - if node_level > *level { - *level = node_level; - } - } else { - closest_level = Some(node_level); + if node_level != *focus_level + && node_level.focusable() + && node_level < *focus_level + { + if let Some(level) = &mut closest_level { + if node_level > *level { + *level = node_level; } + } else { + closest_level = Some(node_level); } } }); @@ -174,12 +180,10 @@ impl FocusState { if let Some(level) = closest_level { *focus_level = level; + } else if forward { + *focus_level = FocusLevel::Unfocusable; } else { - if forward { - *focus_level = FocusLevel::Unfocusable; - } else { - *focus_level = FocusLevel::Focusable; - } + *focus_level = FocusLevel::Focusable; } } @@ -198,11 +202,10 @@ impl FocusState { } else { current_level <= *focus_level }; - if after_previous_focused && current_level.focusable() { - if current_level == *focus_level { - next_focus = Some(new_id); - break; - } + if after_previous_focused && current_level.focusable() && current_level == *focus_level + { + next_focus = Some(new_id); + break; } } @@ -212,7 +215,7 @@ impl FocusState { rdom[old].state.focused = false; } // reset the position to the currently focused element - while self.focus_iter.next(&rdom).id() != id {} + while self.focus_iter.next(rdom).id() != id {} self.dirty = true; return true; } @@ -267,7 +270,7 @@ impl FocusState { state.focused = true; self.focus_level = state.focus.level; // reset the position to the currently focused element - while self.focus_iter.next(&rdom).id() != id {} + while self.focus_iter.next(rdom).id() != id {} self.dirty = true; } diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 6dd3e88bb..79a32c5e1 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -494,10 +494,8 @@ impl InnerInputState { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); let currently_contains = layout_contains_point(node_layout, data.new_pos); - if currently_contains { - if node.state.focus.level.focusable() { - focus_id = Some(node.id); - } + if currently_contains && node.state.focus.level.focusable() { + focus_id = Some(node.id); } }); if let Some(id) = focus_id { From a87edd1a76b8efb964bb97c666a314b78e45fc67 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 May 2022 07:08:51 -0500 Subject: [PATCH 19/95] add focus event --- packages/tui/src/hooks.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 79a32c5e1..bb3ffb784 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -500,6 +500,13 @@ impl InnerInputState { }); if let Some(id) = focus_id { self.focus_state.set_focus(dom, id); + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focus", + element: Some(id), + data: Arc::new(FocusData {}), + }) } } } From acfde71d7e254cf46375b1686e7b66c5e4945999 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 May 2022 07:09:18 -0500 Subject: [PATCH 20/95] refactor prevent-default --- packages/tui/src/focus.rs | 10 ++--- packages/tui/src/lib.rs | 22 +--------- packages/tui/src/node.rs | 91 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 27 deletions(-) create mode 100644 packages/tui/src/node.rs diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index 623bc255c..c3fffd119 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -1,4 +1,4 @@ -use crate::Dom; +use crate::{node::PreventDefault, Dom}; use dioxus_core::ElementId; use dioxus_native_core::utils::{ElementProduced, PersistantElementIter}; @@ -59,7 +59,6 @@ impl Default for FocusLevel { #[derive(Clone, PartialEq, Debug, Default)] pub(crate) struct Focus { - pub pass_focus: bool, pub level: FocusLevel, } @@ -71,9 +70,6 @@ impl NodeDepState for Focus { fn reduce(&mut self, node: NodeView<'_>, _sibling: &Self::DepState, _: &Self::Ctx) -> bool { let new = Focus { - pass_focus: !node - .attributes() - .any(|a| a.name == "dioxus-prevent-default" && a.value.trim() == "keydown"), level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { if let Ok(index) = a.value.parse::() { match index.cmp(&0) { @@ -106,7 +102,7 @@ impl NodeDepState for Focus { } const FOCUS_EVENTS: &[&str] = &sorted_str_slice!(["keydown", "keypress", "keyup"]); -const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["dioxus-prevent-default", "tabindex"]); +const FOCUS_ATTRIBUTES: &[&str] = &sorted_str_slice!(["tabindex"]); #[derive(Default)] pub(crate) struct FocusState { @@ -120,7 +116,7 @@ impl FocusState { /// Returns true if the focus has changed. pub fn progress(&mut self, rdom: &mut Dom, forward: bool) -> bool { if let Some(last) = self.last_focused_id { - if !rdom[last].state.focus.pass_focus { + if rdom[last].state.prevent_default == PreventDefault::KeyDown { return false; } } diff --git a/packages/tui/src/lib.rs b/packages/tui/src/lib.rs index f8ee2ec80..b310b1186 100644 --- a/packages/tui/src/lib.rs +++ b/packages/tui/src/lib.rs @@ -8,26 +8,22 @@ use crossterm::{ use dioxus_core::exports::futures_channel::mpsc::unbounded; use dioxus_core::*; use dioxus_native_core::real_dom::RealDom; -use dioxus_native_core::state::*; -use dioxus_native_core_macro::State; -use focus::Focus; use focus::FocusState; use futures::{ channel::mpsc::{UnboundedReceiver, UnboundedSender}, pin_mut, StreamExt, }; -use layout::StretchLayout; use std::cell::RefCell; use std::rc::Rc; use std::{io, time::Duration}; use stretch2::{prelude::Size, Stretch}; -use style_attributes::StyleModifier; use tui::{backend::CrosstermBackend, layout::Rect, Terminal}; mod config; mod focus; mod hooks; mod layout; +mod node; mod render; mod style; mod style_attributes; @@ -35,21 +31,7 @@ mod widget; pub use config::*; pub use hooks::*; - -type Dom = RealDom; -type Node = dioxus_native_core::real_dom::Node; - -#[derive(Debug, Clone, State, Default)] -struct NodeState { - #[child_dep_state(layout, RefCell)] - layout: StretchLayout, - // depends on attributes, the C component of it's parent and a u8 context - #[parent_dep_state(style)] - style: StyleModifier, - #[node_dep_state()] - focus: Focus, - focused: bool, -} +pub(crate) use node::*; #[derive(Clone)] pub struct TuiContext { diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs new file mode 100644 index 000000000..d16c9cc19 --- /dev/null +++ b/packages/tui/src/node.rs @@ -0,0 +1,91 @@ +use crate::focus::Focus; +use crate::layout::StretchLayout; +use crate::style_attributes::StyleModifier; +use dioxus_native_core::{real_dom::RealDom, state::*}; +use dioxus_native_core_macro::State; + +pub(crate) type Dom = RealDom; +pub(crate) type Node = dioxus_native_core::real_dom::Node; + +#[derive(Debug, Clone, State, Default)] +pub(crate) struct NodeState { + #[child_dep_state(layout, RefCell)] + pub layout: StretchLayout, + // depends on attributes, the C component of it's parent and a u8 context + #[parent_dep_state(style)] + pub style: StyleModifier, + #[node_dep_state()] + pub prevent_default: PreventDefault, + #[node_dep_state()] + pub focus: Focus, + pub focused: bool, +} + +#[derive(PartialEq, Debug, Clone)] +pub(crate) enum PreventDefault { + Focus, + KeyPress, + KeyRelease, + KeyDown, + KeyUp, + MouseDown, + Click, + MouseEnter, + MouseLeave, + MouseOut, + Unknown, + MouseOver, + ContextMenu, + Wheel, + MouseUp, +} + +impl Default for PreventDefault { + fn default() -> Self { + PreventDefault::Unknown + } +} + +impl NodeDepState for PreventDefault { + type Ctx = (); + + type DepState = (); + + const NODE_MASK: dioxus_native_core::node_ref::NodeMask = + dioxus_native_core::node_ref::NodeMask::NONE; + + fn reduce( + &mut self, + node: dioxus_native_core::node_ref::NodeView, + _sibling: &Self::DepState, + _ctx: &Self::Ctx, + ) -> bool { + let new = match node + .attributes() + .find(|a| a.name == "dioxus-prevent-default") + .map(|a| a.value) + { + Some("focus") => PreventDefault::Focus, + Some("keypress") => PreventDefault::KeyPress, + Some("keyrelease") => PreventDefault::KeyRelease, + Some("keydown") => PreventDefault::KeyDown, + Some("keyup") => PreventDefault::KeyUp, + Some("click") => PreventDefault::Click, + Some("mousedown") => PreventDefault::MouseDown, + Some("mouseup") => PreventDefault::MouseUp, + Some("mouseenter") => PreventDefault::MouseEnter, + Some("mouseover") => PreventDefault::MouseOver, + Some("mouseleave") => PreventDefault::MouseLeave, + Some("mouseout") => PreventDefault::MouseOut, + Some("wheel") => PreventDefault::Wheel, + Some("contextmenu") => PreventDefault::ContextMenu, + _ => return false, + }; + if new == *self { + false + } else { + *self = new; + true + } + } +} From 07c1964ebf98dc6fc5eec88813d144c1b73b9e9f Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 May 2022 07:17:33 -0500 Subject: [PATCH 21/95] fix focus event --- packages/tui/src/hooks.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index bb3ffb784..f787a9da9 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -175,6 +175,8 @@ impl InnerInputState { self.wheel = None; + let old_focus = self.focus_state.last_focused_id; + evts.retain(|e| match &e.1 { EventData::Keyboard(k) => match k.key_code { KeyCode::Tab => !self.focus_state.progress(dom, !k.shift_key), @@ -189,6 +191,18 @@ impl InnerInputState { self.resolve_mouse_events(previous_mouse, resolved_events, layout, dom); + if old_focus != self.focus_state.last_focused_id { + if let Some(id) = self.focus_state.last_focused_id { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focus", + element: Some(id), + data: Arc::new(FocusData {}), + }); + } + } + // for s in &self.subscribers { // s(); // } @@ -500,13 +514,6 @@ impl InnerInputState { }); if let Some(id) = focus_id { self.focus_state.set_focus(dom, id); - resolved_events.push(UserEvent { - scope_id: None, - priority: EventPriority::Medium, - name: "focus", - element: Some(id), - data: Arc::new(FocusData {}), - }) } } } From bbe9c1bbf03bae674036e89de60c7eb01a809677 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 May 2022 07:21:36 -0500 Subject: [PATCH 22/95] change prevent default mask --- packages/tui/src/node.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs index d16c9cc19..8e19e2632 100644 --- a/packages/tui/src/node.rs +++ b/packages/tui/src/node.rs @@ -2,7 +2,7 @@ use crate::focus::Focus; use crate::layout::StretchLayout; use crate::style_attributes::StyleModifier; use dioxus_native_core::{real_dom::RealDom, state::*}; -use dioxus_native_core_macro::State; +use dioxus_native_core_macro::{sorted_str_slice, State}; pub(crate) type Dom = RealDom; pub(crate) type Node = dioxus_native_core::real_dom::Node; @@ -52,7 +52,11 @@ impl NodeDepState for PreventDefault { type DepState = (); const NODE_MASK: dioxus_native_core::node_ref::NodeMask = - dioxus_native_core::node_ref::NodeMask::NONE; + dioxus_native_core::node_ref::NodeMask::new_with_attrs( + dioxus_native_core::node_ref::AttributeMask::Static(&sorted_str_slice!([ + "dioxus-prevent-default" + ])), + ); fn reduce( &mut self, From 53d5ea2f4292fd42c25ebf406ebb30e075545270 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 5 May 2022 11:37:11 -0500 Subject: [PATCH 23/95] change prevent default to detect on{event} rather than {event} --- packages/tui/src/node.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs index 8e19e2632..80d5ef094 100644 --- a/packages/tui/src/node.rs +++ b/packages/tui/src/node.rs @@ -69,20 +69,20 @@ impl NodeDepState for PreventDefault { .find(|a| a.name == "dioxus-prevent-default") .map(|a| a.value) { - Some("focus") => PreventDefault::Focus, - Some("keypress") => PreventDefault::KeyPress, - Some("keyrelease") => PreventDefault::KeyRelease, - Some("keydown") => PreventDefault::KeyDown, - Some("keyup") => PreventDefault::KeyUp, - Some("click") => PreventDefault::Click, - Some("mousedown") => PreventDefault::MouseDown, - Some("mouseup") => PreventDefault::MouseUp, - Some("mouseenter") => PreventDefault::MouseEnter, - Some("mouseover") => PreventDefault::MouseOver, - Some("mouseleave") => PreventDefault::MouseLeave, - Some("mouseout") => PreventDefault::MouseOut, - Some("wheel") => PreventDefault::Wheel, - Some("contextmenu") => PreventDefault::ContextMenu, + Some("onfocus") => PreventDefault::Focus, + Some("onkeypress") => PreventDefault::KeyPress, + Some("onkeyrelease") => PreventDefault::KeyRelease, + Some("onkeydown") => PreventDefault::KeyDown, + Some("onkeyup") => PreventDefault::KeyUp, + Some("onclick") => PreventDefault::Click, + Some("onmousedown") => PreventDefault::MouseDown, + Some("onmouseup") => PreventDefault::MouseUp, + Some("onmouseenter") => PreventDefault::MouseEnter, + Some("onmouseover") => PreventDefault::MouseOver, + Some("onmouseleave") => PreventDefault::MouseLeave, + Some("onmouseout") => PreventDefault::MouseOut, + Some("onwheel") => PreventDefault::Wheel, + Some("oncontextmenu") => PreventDefault::ContextMenu, _ => return false, }; if new == *self { From 3cba79d835c7f559bd2fd80ed87e3b7ff68c13d4 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Sat, 7 May 2022 08:39:55 -0500 Subject: [PATCH 24/95] add focus in and focus out events --- packages/tui/src/hooks.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 706e8c341..8c91ddcef 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -200,6 +200,22 @@ impl InnerInputState { element: Some(id), data: Arc::new(FocusData {}), }); + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focusin", + element: Some(id), + data: Arc::new(FocusData {}), + }); + } + if let Some(id) = old_focus { + resolved_events.push(UserEvent { + scope_id: None, + priority: EventPriority::Medium, + name: "focusout", + element: Some(id), + data: Arc::new(FocusData {}), + }); } } From 31c7907f257a35c10317088e687cb961736410ff Mon Sep 17 00:00:00 2001 From: demonthos Date: Sat, 7 May 2022 13:56:11 -0500 Subject: [PATCH 25/95] update with AttritubeValue --- packages/tui/src/focus.rs | 6 +++++- packages/tui/src/node.rs | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/focus.rs b/packages/tui/src/focus.rs index c3fffd119..18e2fbd3f 100644 --- a/packages/tui/src/focus.rs +++ b/packages/tui/src/focus.rs @@ -71,7 +71,11 @@ impl NodeDepState for Focus { fn reduce(&mut self, node: NodeView<'_>, _sibling: &Self::DepState, _: &Self::Ctx) -> bool { let new = Focus { level: if let Some(a) = node.attributes().find(|a| a.name == "tabindex") { - if let Ok(index) = a.value.parse::() { + if let Some(index) = a + .value + .as_int32() + .or(a.value.as_text().and_then(|v| v.parse::().ok())) + { match index.cmp(&0) { Ordering::Less => FocusLevel::Unfocusable, Ordering::Equal => FocusLevel::Focusable, diff --git a/packages/tui/src/node.rs b/packages/tui/src/node.rs index 80d5ef094..58b80a80f 100644 --- a/packages/tui/src/node.rs +++ b/packages/tui/src/node.rs @@ -67,7 +67,7 @@ impl NodeDepState for PreventDefault { let new = match node .attributes() .find(|a| a.name == "dioxus-prevent-default") - .map(|a| a.value) + .and_then(|a| a.value.as_text()) { Some("onfocus") => PreventDefault::Focus, Some("onkeypress") => PreventDefault::KeyPress, From bdbe452b336fbb12bf3e5ff34e647fdf7c51dfe3 Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Thu, 12 May 2022 20:52:06 -0500 Subject: [PATCH 26/95] fix merge --- packages/tui/src/hooks.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/tui/src/hooks.rs b/packages/tui/src/hooks.rs index 4070f865a..921b72ed0 100644 --- a/packages/tui/src/hooks.rs +++ b/packages/tui/src/hooks.rs @@ -213,7 +213,7 @@ impl InnerInputState { } fn resolve_mouse_events( - &self, + &mut self, previous_mouse: Option, resolved_events: &mut Vec, layout: &Stretch, @@ -259,8 +259,13 @@ impl InnerInputState { let new_client_coordinates = (mouse_data.client_coordinates() - node_origin) .to_point() .cast_unit(); + + let coordinates = Coordinates::new( + mouse_data.screen_coordinates(), + mouse_data.client_coordinates(), new_client_coordinates, mouse_data.page_coordinates(), + ); MouseData::new( coordinates, @@ -517,7 +522,7 @@ impl InnerInputState { } // update focus - if released { + if was_released { let mut focus_id = None; dom.traverse_depth_first(|node| { let node_layout = layout.layout(node.state.layout.node.unwrap()).unwrap(); From c7c84da8ab4fbab66b1fa9a259da312c3a3417be Mon Sep 17 00:00:00 2001 From: Evan Almloff Date: Wed, 25 May 2022 08:58:59 -0500 Subject: [PATCH 27/95] WIP hot reload --- Cargo.toml | 6 +- packages/core-macro/Cargo.toml | 4 + packages/core-macro/src/lib.rs | 17 +- packages/core-macro/src/rsx/component.rs | 233 --- packages/core-macro/src/rsx/element.rs | 253 --- packages/core-macro/src/rsx/errors.rs | 15 - packages/core-macro/src/rsx/mod.rs | 100 -- packages/core-macro/src/rsx/node.rs | 84 - packages/core-macro/src/rsx/pretty.rs | 1 - packages/core/src/lib.rs | 2 + packages/{core-macro => rsx}/src/ifmt.rs | 6 +- packages/rsx/src/lib.rs | 2 + packages/rsx_interperter/Cargo.toml | 12 + packages/rsx_interperter/src/attributes.rs | 869 +++++++++++ .../rsx_interperter/src/captuered_context.rs | 137 ++ packages/rsx_interperter/src/elements.rs | 1380 +++++++++++++++++ packages/rsx_interperter/src/interperter.rs | 88 ++ packages/rsx_interperter/src/lib.rs | 13 + src/lib.rs | 2 + 19 files changed, 2531 insertions(+), 693 deletions(-) delete mode 100644 packages/core-macro/src/rsx/component.rs delete mode 100644 packages/core-macro/src/rsx/element.rs delete mode 100644 packages/core-macro/src/rsx/errors.rs delete mode 100644 packages/core-macro/src/rsx/mod.rs delete mode 100644 packages/core-macro/src/rsx/node.rs delete mode 100644 packages/core-macro/src/rsx/pretty.rs rename packages/{core-macro => rsx}/src/ifmt.rs (98%) create mode 100644 packages/rsx_interperter/Cargo.toml create mode 100644 packages/rsx_interperter/src/attributes.rs create mode 100644 packages/rsx_interperter/src/captuered_context.rs create mode 100644 packages/rsx_interperter/src/elements.rs create mode 100644 packages/rsx_interperter/src/interperter.rs create mode 100644 packages/rsx_interperter/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index b3f31e516..91955b533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ dioxus-tui = { path = "./packages/tui", version = "^0.2.0", optional = true } dioxus-liveview = { path = "./packages/liveview", optional = true } dioxus-rsx = { path = "./packages/rsx", optional = true } +dioxus-rsx-interperter = { path = "./packages/rsx_interperter", optional = true } dioxus-autofmt = { path = "./packages/autofmt", optional = true } [features] @@ -44,6 +45,7 @@ router = ["dioxus-router"] tui = ["dioxus-tui"] liveview = ["dioxus-liveview"] autofmt = ["dioxus-autofmt"] +hot_reload = ["dioxus-core-macro/hot_reload"] [workspace] members = [ @@ -61,6 +63,7 @@ members = [ "packages/liveview", "packages/rsx", "packages/autofmt", + "packages/rsx_interperter", ] [dev-dependencies] @@ -75,7 +78,8 @@ serde_json = "1.0.79" rand = { version = "0.8.4", features = ["small_rng"] } tokio = { version = "1.16.1", features = ["full"] } reqwest = { version = "0.11.9", features = ["json"] } -dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi", "tui"] } +# dioxus = { path = ".", features = ["desktop", "ssr", "router", "fermi", "tui"] } +dioxus = { path = ".", features = ["desktop"] } fern = { version = "0.6.0", features = ["colored"] } criterion = "0.3.5" thiserror = "1.0.30" diff --git a/packages/core-macro/Cargo.toml b/packages/core-macro/Cargo.toml index 7aef8f49f..14e1baf3a 100644 --- a/packages/core-macro/Cargo.toml +++ b/packages/core-macro/Cargo.toml @@ -20,8 +20,12 @@ proc-macro2 = { version = "1.0.6" } quote = "1.0" syn = { version = "1.0.11", features = ["full", "extra-traits"] } dioxus-rsx = { path = "../rsx" } +dioxus-rsx-interperter = { path = "../rsx_interperter", optional = true } # testing [dev-dependencies] rustversion = "1.0" trybuild = "1.0" + +[features] +hot_reload = ["dioxus-rsx-interperter"] \ No newline at end of file diff --git a/packages/core-macro/src/lib.rs b/packages/core-macro/src/lib.rs index 06aea9570..f1dd28a60 100644 --- a/packages/core-macro/src/lib.rs +++ b/packages/core-macro/src/lib.rs @@ -1,8 +1,9 @@ use proc_macro::TokenStream; use quote::ToTokens; +use rsx::{BodyNode, Component, ContentField, ElementAttr, IfmtInput}; +use std::collections::HashMap; use syn::parse_macro_input; -mod ifmt; mod inlineprops; mod props; @@ -11,7 +12,7 @@ use dioxus_rsx as rsx; #[proc_macro] pub fn format_args_f(input: TokenStream) -> TokenStream { - use ifmt::*; + use rsx::*; let item = parse_macro_input!(input as IfmtInput); format_args_f_impl(item) .unwrap_or_else(|err| err.to_compile_error()) @@ -183,7 +184,17 @@ pub fn derive_typed_builder(input: proc_macro::TokenStream) -> proc_macro::Token pub fn rsx(s: TokenStream) -> TokenStream { match syn::parse::(s) { Err(err) => err.to_compile_error().into(), - Ok(stream) => stream.to_token_stream().into(), + Ok(body) => { + #[cfg(feature = "hot_reload")] + { + use dioxus_rsx_interperter::captuered_context::CapturedContextBuilder; + + let captured = CapturedContextBuilder::from_call_body(body); + captured.to_token_stream().into() + } + #[cfg(not(feature = "hot_reload"))] + body.to_token_stream().into() + } } } diff --git a/packages/core-macro/src/rsx/component.rs b/packages/core-macro/src/rsx/component.rs deleted file mode 100644 index dfdda74b8..000000000 --- a/packages/core-macro/src/rsx/component.rs +++ /dev/null @@ -1,233 +0,0 @@ -//! Parse components into the VComponent VNode -//! ========================================== -//! -//! This parsing path emerges from [`AmbiguousElement`] which supports validation of the vcomponent format. -//! We can be reasonably sure that whatever enters this parsing path is in the right format. -//! This feature must support -//! - [x] Namespaced components -//! - [x] Fields -//! - [x] Componentbuilder synax -//! - [x] Optional commas -//! - [ ] Children -//! - [ ] Keys -//! - [ ] Properties spreading with with `..` syntax - -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - ext::IdentExt, - parse::{Parse, ParseBuffer, ParseStream}, - token, Expr, Ident, LitStr, Result, Token, -}; - -pub struct Component { - pub name: syn::Path, - pub body: Vec, - pub children: Vec, - pub manual_props: Option, -} - -impl Parse for Component { - fn parse(stream: ParseStream) -> Result { - let name = syn::Path::parse_mod_style(stream)?; - - let content: ParseBuffer; - - // if we see a `{` then we have a block - // else parse as a function-like call - if stream.peek(token::Brace) { - syn::braced!(content in stream); - } else { - syn::parenthesized!(content in stream); - } - - let mut body = Vec::new(); - let mut children = Vec::new(); - let mut manual_props = None; - - while !content.is_empty() { - // if we splat into a component then we're merging properties - if content.peek(Token![..]) { - content.parse::()?; - manual_props = Some(content.parse::()?); - } else if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - body.push(content.parse::()?); - } else { - children.push(content.parse::()?); - } - - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - - Ok(Self { - name, - body, - children, - manual_props, - }) - } -} - -impl ToTokens for Component { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - - let mut has_key = None; - - let builder = match &self.manual_props { - Some(manual_props) => { - let mut toks = quote! { - let mut __manual_props = #manual_props; - }; - for field in &self.body { - if field.name == "key" { - has_key = Some(field); - } else { - let name = &field.name; - let val = &field.content; - toks.append_all(quote! { - __manual_props.#name = #val; - }); - } - } - toks.append_all(quote! { - __manual_props - }); - quote! {{ - #toks - }} - } - None => { - let mut toks = quote! { fc_to_builder(#name) }; - for field in &self.body { - match field.name.to_string().as_str() { - "key" => { - // - has_key = Some(field); - } - _ => toks.append_all(quote! {#field}), - } - } - - if !self.children.is_empty() { - let childs = &self.children; - toks.append_all(quote! { - .children(__cx.create_children([ #( #childs ),* ])) - }); - } - - toks.append_all(quote! { - .build() - }); - toks - } - }; - - let key_token = match has_key { - Some(field) => { - let inners = &field.content; - quote! { Some(format_args_f!(#inners)) } - } - None => quote! { None }, - }; - - let fn_name = self.name.segments.last().unwrap().ident.to_string(); - - tokens.append_all(quote! { - __cx.component( - #name, - #builder, - #key_token, - #fn_name - ) - }) - } -} - -// the struct's fields info -pub struct ComponentField { - name: Ident, - content: ContentField, -} - -enum ContentField { - ManExpr(Expr), - Formatted(LitStr), - OnHandlerRaw(Expr), -} - -impl ToTokens for ContentField { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match self { - ContentField::ManExpr(e) => e.to_tokens(tokens), - ContentField::Formatted(s) => tokens.append_all(quote! { - __cx.raw_text(format_args_f!(#s)).0 - }), - ContentField::OnHandlerRaw(e) => tokens.append_all(quote! { - __cx.event_handler(#e) - }), - } - } -} - -impl Parse for ComponentField { - fn parse(input: ParseStream) -> Result { - let name = Ident::parse_any(input)?; - input.parse::()?; - - if name.to_string().starts_with("on") { - let content = ContentField::OnHandlerRaw(input.parse()?); - return Ok(Self { name, content }); - } - - if name == "key" { - let content = ContentField::ManExpr(input.parse()?); - return Ok(Self { name, content }); - } - - if input.peek(LitStr) && input.peek2(Token![,]) { - let t: LitStr = input.fork().parse()?; - - if is_literal_foramtted(&t) { - let content = ContentField::Formatted(input.parse()?); - return Ok(Self { name, content }); - } - } - - if input.peek(LitStr) && input.peek2(LitStr) { - missing_trailing_comma!(input.span()); - } - - let content = ContentField::ManExpr(input.parse()?); - Ok(Self { name, content }) - } -} - -impl ToTokens for ComponentField { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ComponentField { name, content, .. } = self; - tokens.append_all(quote! { - .#name(#content) - }) - } -} - -fn is_literal_foramtted(lit: &LitStr) -> bool { - let s = lit.value(); - let mut chars = s.chars(); - - while let Some(next) = chars.next() { - if next == '{' { - let nen = chars.next(); - if nen != Some('{') { - return true; - } - } - } - - false -} diff --git a/packages/core-macro/src/rsx/element.rs b/packages/core-macro/src/rsx/element.rs deleted file mode 100644 index 96c7a2efc..000000000 --- a/packages/core-macro/src/rsx/element.rs +++ /dev/null @@ -1,253 +0,0 @@ -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseBuffer, ParseStream}, - Expr, Ident, LitStr, Result, Token, -}; - -// ======================================= -// Parse the VNode::Element type -// ======================================= -pub struct Element { - pub name: Ident, - pub key: Option, - pub attributes: Vec, - pub children: Vec, - pub _is_static: bool, -} - -impl Parse for Element { - fn parse(stream: ParseStream) -> Result { - let el_name = Ident::parse(stream)?; - - // parse the guts - let content: ParseBuffer; - syn::braced!(content in stream); - - let mut attributes: Vec = vec![]; - let mut children: Vec = vec![]; - let mut key = None; - let mut _el_ref = None; - - // parse fields with commas - // break when we don't get this pattern anymore - // start parsing bodynodes - // "def": 456, - // abc: 123, - loop { - // Parse the raw literal fields - if content.peek(LitStr) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - let name = content.parse::()?; - let ident = name.clone(); - - content.parse::()?; - - if content.peek(LitStr) && content.peek2(Token![,]) { - let value = content.parse::()?; - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrText { name, value }, - }); - } else { - let value = content.parse::()?; - - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::CustomAttrExpression { name, value }, - }); - } - - if content.is_empty() { - break; - } - - if content.parse::().is_err() { - missing_trailing_comma!(ident); - } - continue; - } - - if content.peek(Ident) && content.peek2(Token![:]) && !content.peek3(Token![:]) { - let name = content.parse::()?; - let ident = name.clone(); - - let name_str = name.to_string(); - content.parse::()?; - - if name_str.starts_with("on") { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::EventTokens { - name, - tokens: content.parse()?, - }, - }); - } else { - match name_str.as_str() { - "key" => { - key = Some(content.parse()?); - } - "classes" => todo!("custom class list not supported yet"), - // "namespace" => todo!("custom namespace not supported yet"), - "node_ref" => { - _el_ref = Some(content.parse::()?); - } - _ => { - if content.peek(LitStr) { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrText { - name, - value: content.parse()?, - }, - }); - } else { - attributes.push(ElementAttrNamed { - el_name: el_name.clone(), - attr: ElementAttr::AttrExpression { - name, - value: content.parse()?, - }, - }); - } - } - } - } - - if content.is_empty() { - break; - } - - // todo: add a message saying you need to include commas between fields - if content.parse::().is_err() { - missing_trailing_comma!(ident); - } - continue; - } - - break; - } - - while !content.is_empty() { - if (content.peek(LitStr) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - attr_after_element!(content.span()); - } - - if (content.peek(Ident) && content.peek2(Token![:])) && !content.peek3(Token![:]) { - attr_after_element!(content.span()); - } - - children.push(content.parse::()?); - // consume comma if it exists - // we don't actually care if there *are* commas after elements/text - if content.peek(Token![,]) { - let _ = content.parse::(); - } - } - - Ok(Self { - key, - name: el_name, - attributes, - children, - _is_static: false, - }) - } -} - -impl ToTokens for Element { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let name = &self.name; - let children = &self.children; - - let key = match &self.key { - Some(ty) => quote! { Some(format_args_f!(#ty)) }, - None => quote! { None }, - }; - - let listeners = self - .attributes - .iter() - .filter(|f| matches!(f.attr, ElementAttr::EventTokens { .. })); - - let attr = self - .attributes - .iter() - .filter(|f| !matches!(f.attr, ElementAttr::EventTokens { .. })); - - tokens.append_all(quote! { - __cx.element( - dioxus_elements::#name, - __cx.bump().alloc([ #(#listeners),* ]), - __cx.bump().alloc([ #(#attr),* ]), - __cx.bump().alloc([ #(#children),* ]), - #key, - ) - }); - } -} - -pub enum ElementAttr { - /// attribute: "valuee {}" - AttrText { name: Ident, value: LitStr }, - - /// attribute: true, - AttrExpression { name: Ident, value: Expr }, - - /// "attribute": "value {}" - CustomAttrText { name: LitStr, value: LitStr }, - - /// "attribute": true, - CustomAttrExpression { name: LitStr, value: Expr }, - - // /// onclick: move |_| {} - // EventClosure { name: Ident, closure: ExprClosure }, - /// onclick: {} - EventTokens { name: Ident, tokens: Expr }, -} - -pub struct ElementAttrNamed { - pub el_name: Ident, - pub attr: ElementAttr, -} - -impl ToTokens for ElementAttrNamed { - fn to_tokens(&self, tokens: &mut TokenStream2) { - let ElementAttrNamed { el_name, attr } = self; - - tokens.append_all(match attr { - ElementAttr::AttrText { name, value } => { - quote! { - dioxus_elements::#el_name.#name(__cx, format_args_f!(#value)) - } - } - ElementAttr::AttrExpression { name, value } => { - quote! { - dioxus_elements::#el_name.#name(__cx, #value) - } - } - ElementAttr::CustomAttrText { name, value } => { - quote! { - __cx.attr( #name, format_args_f!(#value), None, false ) - } - } - ElementAttr::CustomAttrExpression { name, value } => { - quote! { - __cx.attr( #name, format_args_f!(#value), None, false ) - } - } - // ElementAttr::EventClosure { name, closure } => { - // quote! { - // dioxus_elements::on::#name(__cx, #closure) - // } - // } - ElementAttr::EventTokens { name, tokens } => { - quote! { - dioxus_elements::on::#name(__cx, #tokens) - } - } - }); - } -} diff --git a/packages/core-macro/src/rsx/errors.rs b/packages/core-macro/src/rsx/errors.rs deleted file mode 100644 index 27e2c7f98..000000000 --- a/packages/core-macro/src/rsx/errors.rs +++ /dev/null @@ -1,15 +0,0 @@ -macro_rules! missing_trailing_comma { - ($span:expr) => { - proc_macro_error::emit_error!($span, "missing trailing comma") - }; -} - -macro_rules! attr_after_element { - ($span:expr) => { - proc_macro_error::emit_error!( - $span, - "expected element"; - help = "move the attribute above all the children and text elements" - ) - }; -} diff --git a/packages/core-macro/src/rsx/mod.rs b/packages/core-macro/src/rsx/mod.rs deleted file mode 100644 index 6b9dfc3dc..000000000 --- a/packages/core-macro/src/rsx/mod.rs +++ /dev/null @@ -1,100 +0,0 @@ -//! Parse the root tokens in the rsx!{} macro -//! ========================================= -//! -//! This parsing path emerges directly from the macro call, with `RsxRender` being the primary entrance into parsing. -//! This feature must support: -//! - [x] Optionally rendering if the `in XYZ` pattern is present -//! - [x] Fragments as top-level element (through ambiguous) -//! - [x] Components as top-level element (through ambiguous) -//! - [x] Tags as top-level elements (through ambiguous) -//! - [x] Good errors if parsing fails -//! -//! Any errors in using rsx! will likely occur when people start using it, so the first errors must be really helpful. - -#[macro_use] -mod errors; - -mod component; -mod element; -mod node; - -pub mod pretty; - -// Re-export the namespaces into each other -pub use component::*; -pub use element::*; -pub use node::*; - -// imports -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - Ident, Result, Token, -}; - -pub struct CallBody { - pub custom_context: Option, - pub roots: Vec, -} - -impl Parse for CallBody { - fn parse(input: ParseStream) -> Result { - let custom_context = if input.peek(Ident) && input.peek2(Token![,]) { - let name = input.parse::()?; - input.parse::()?; - - Some(name) - } else { - None - }; - - let mut roots = Vec::new(); - - while !input.is_empty() { - let node = input.parse::()?; - - if input.peek(Token![,]) { - let _ = input.parse::(); - } - - roots.push(node); - } - - Ok(Self { - custom_context, - roots, - }) - } -} - -/// Serialize the same way, regardless of flavor -impl ToTokens for CallBody { - fn to_tokens(&self, out_tokens: &mut TokenStream2) { - let inner = if self.roots.len() == 1 { - let inner = &self.roots[0]; - quote! { #inner } - } else { - let childs = &self.roots; - quote! { __cx.fragment_root([ #(#childs),* ]) } - }; - - match &self.custom_context { - // The `in cx` pattern allows directly rendering - Some(ident) => out_tokens.append_all(quote! { - #ident.render(LazyNodes::new(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - })) - }), - - // Otherwise we just build the LazyNode wrapper - None => out_tokens.append_all(quote! { - LazyNodes::new(move |__cx: NodeFactory| -> VNode { - use dioxus_elements::{GlobalAttributes, SvgAttributes}; - #inner - }) - }), - }; - } -} diff --git a/packages/core-macro/src/rsx/node.rs b/packages/core-macro/src/rsx/node.rs deleted file mode 100644 index d5d701767..000000000 --- a/packages/core-macro/src/rsx/node.rs +++ /dev/null @@ -1,84 +0,0 @@ -use super::*; - -use proc_macro2::TokenStream as TokenStream2; -use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{ - parse::{Parse, ParseStream}, - token, Expr, LitStr, Result, Token, -}; - -/* -Parse --> div {} --> Component {} --> component() --> "text {with_args}" --> (0..10).map(|f| rsx!("asd")), // <--- notice the comma - must be a complete expr -*/ -pub enum BodyNode { - Element(Element), - Component(Component), - Text(LitStr), - RawExpr(Expr), -} - -impl Parse for BodyNode { - fn parse(stream: ParseStream) -> Result { - if stream.peek(LitStr) { - return Ok(BodyNode::Text(stream.parse()?)); - } - - // div {} -> el - // Div {} -> comp - if stream.peek(syn::Ident) && stream.peek2(token::Brace) { - if stream - .fork() - .parse::()? - .to_string() - .chars() - .next() - .unwrap() - .is_ascii_uppercase() - { - return Ok(BodyNode::Component(stream.parse()?)); - } else { - return Ok(BodyNode::Element(stream.parse::()?)); - } - } - - // component() -> comp - // ::component {} -> comp - // ::component () -> comp - if (stream.peek(syn::Ident) && stream.peek2(token::Paren)) - || (stream.peek(Token![::])) - || (stream.peek(Token![:]) && stream.peek2(Token![:])) - { - return Ok(BodyNode::Component(stream.parse::()?)); - } - - // crate::component{} -> comp - // crate::component() -> comp - if let Ok(pat) = stream.fork().parse::() { - if pat.segments.len() > 1 { - return Ok(BodyNode::Component(stream.parse::()?)); - } - } - - Ok(BodyNode::RawExpr(stream.parse::()?)) - } -} - -impl ToTokens for BodyNode { - fn to_tokens(&self, tokens: &mut TokenStream2) { - match &self { - BodyNode::Element(el) => el.to_tokens(tokens), - BodyNode::Component(comp) => comp.to_tokens(tokens), - BodyNode::Text(txt) => tokens.append_all(quote! { - __cx.text(format_args_f!(#txt)) - }), - BodyNode::RawExpr(exp) => tokens.append_all(quote! { - __cx.fragment_from_iter(#exp) - }), - } - } -} diff --git a/packages/core-macro/src/rsx/pretty.rs b/packages/core-macro/src/rsx/pretty.rs deleted file mode 100644 index 26ad54006..000000000 --- a/packages/core-macro/src/rsx/pretty.rs +++ /dev/null @@ -1 +0,0 @@ -//! pretty printer for rsx! diff --git a/packages/core/src/lib.rs b/packages/core/src/lib.rs index f31e2e328..eabec8983 100644 --- a/packages/core/src/lib.rs +++ b/packages/core/src/lib.rs @@ -85,6 +85,8 @@ pub mod prelude { fc_to_builder, Attributes, Component, DioxusElement, Element, EventHandler, Fragment, LazyNodes, NodeFactory, Properties, Scope, ScopeId, ScopeState, VNode, VirtualDom, }; + #[cfg(feature = "hot_reload")] + pub use dioxus_rsx_interperter::captuered_context::{CapturedContext, IfmtArgs}; } pub mod exports { diff --git a/packages/core-macro/src/ifmt.rs b/packages/rsx/src/ifmt.rs similarity index 98% rename from packages/core-macro/src/ifmt.rs rename to packages/rsx/src/ifmt.rs index 912d78f7e..097725043 100644 --- a/packages/core-macro/src/ifmt.rs +++ b/packages/rsx/src/ifmt.rs @@ -140,9 +140,9 @@ pub fn format_args_f_impl(input: IfmtInput) -> Result { #[allow(dead_code)] // dumb compiler does not see the struct being used... pub struct IfmtInput { - format_literal: LitStr, - positional_args: Vec, - named_args: Vec<(Ident, Expr)>, + pub format_literal: LitStr, + pub positional_args: Vec, + pub named_args: Vec<(Ident, Expr)>, } impl Parse for IfmtInput { diff --git a/packages/rsx/src/lib.rs b/packages/rsx/src/lib.rs index 5f01d9306..a5b8143f2 100644 --- a/packages/rsx/src/lib.rs +++ b/packages/rsx/src/lib.rs @@ -16,11 +16,13 @@ mod errors; mod component; mod element; +mod ifmt; mod node; // Re-export the namespaces into each other pub use component::*; pub use element::*; +pub use ifmt::*; pub use node::*; // imports diff --git a/packages/rsx_interperter/Cargo.toml b/packages/rsx_interperter/Cargo.toml new file mode 100644 index 000000000..054e1f563 --- /dev/null +++ b/packages/rsx_interperter/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "dioxus-rsx-interperter" +version = "0.1.0" +edition = "2021" + +[dependencies] +syn = "1.0" +quote = "1.0" +dioxus-rsx = { path = "../rsx" } +dioxus-ssr = { path = "../ssr" } +dioxus-core = { path = "../core" } +dioxus-html = { path = "../html" } diff --git a/packages/rsx_interperter/src/attributes.rs b/packages/rsx_interperter/src/attributes.rs new file mode 100644 index 000000000..c0b2c9980 --- /dev/null +++ b/packages/rsx_interperter/src/attributes.rs @@ -0,0 +1,869 @@ +use crate::elements::*; + +// find the mapped attribute name +pub fn attrbute_to_static_str(attr: &str) -> Option<(&'static str, Option<&'static str>)> { + NO_NAMESPACE_ATTRIBUTES + .iter() + .find(|&a| *a == attr) + .map(|a| (*a, None)) + .or_else(|| { + STYLE_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, Some("style"))) + }) + .or_else(|| { + MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + svg::MAPPED_ATTRIBUTES + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + .or_else(|| { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find_map(|(_, attrs)| { + attrs + .iter() + .find(|(a, _)| *a == attr) + .map(|(_, b)| (*b, None)) + }) + }) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find_map(|(_, _, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find_map(|(_, attrs)| attrs.iter().find(|a| **a == attr).map(|a| (*a, None))) + }) +} + +macro_rules! no_namespace_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident; + )* + ) => { + pub const NO_NAMESPACE_ATTRIBUTES: &'static [&'static str] = &[ + $( + stringify!($name), + )* + ]; + }; +} + +macro_rules! style_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const STYLE_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ]; + }; +} + +macro_rules! mapped_trait_methods { + ( + $( + $(#[$attr:meta])* + $name:ident: $lit:literal, + )* + ) => { + pub const MAPPED_ATTRIBUTES: &'static [(&'static str, &'static str)] = &[ + $( + (stringify!($name), $lit), + )* + ("prevent_default", "dioxus-prevent-default"), + ]; + }; +} + +no_namespace_trait_methods! { + accesskey; + + /// The HTML class attribute is used to specify a class for an HTML element. + /// + /// ## Details + /// Multiple HTML elements can share the same class. + /// + /// The class global attribute is a space-separated list of the case-sensitive classes of the element. + /// Classes allow CSS and Javascript to select and access specific elements via the class selectors or + /// functions like the DOM method document.getElementsByClassName. + /// + /// ## Example + /// + /// ### HTML: + /// ```html + ///

Above point sounds a bit obvious. Remove/rewrite?

+ /// ``` + /// + /// ### CSS: + /// ```css + /// .note { + /// font-style: italic; + /// font-weight: bold; + /// } + /// + /// .editorial { + /// background: rgb(255, 0, 0, .25); + /// padding: 10px; + /// } + /// ``` + class; + contenteditable; + data; + dir; + draggable; + hidden; + id; + lang; + spellcheck; + style; + tabindex; + title; + translate; + + role; + + /// dangerous_inner_html is Dioxus's replacement for using innerHTML in the browser DOM. In general, setting + /// HTML from code is risky because it’s easy to inadvertently expose your users to a cross-site scripting (XSS) + /// attack. So, you can set HTML directly from Dioxus, but you have to type out dangerous_inner_html to remind + /// yourself that it’s dangerous + dangerous_inner_html; +} + +// This macro creates an explicit method call for each of the style attributes. +// +// The left token specifies the name of the attribute in the rsx! macro, and the right string literal specifies the +// actual name of the attribute generated. +// +// This roughly follows the html spec +style_trait_methods! { + align_content: "align-content", + align_items: "align-items", + align_self: "align-self", + alignment_adjust: "alignment-adjust", + alignment_baseline: "alignment-baseline", + all: "all", + alt: "alt", + animation: "animation", + animation_delay: "animation-delay", + animation_direction: "animation-direction", + animation_duration: "animation-duration", + animation_fill_mode: "animation-fill-mode", + animation_iteration_count: "animation-iteration-count", + animation_name: "animation-name", + animation_play_state: "animation-play-state", + animation_timing_function: "animation-timing-function", + azimuth: "azimuth", + backface_visibility: "backface-visibility", + background: "background", + background_attachment: "background-attachment", + background_clip: "background-clip", + background_color: "background-color", + background_image: "background-image", + background_origin: "background-origin", + background_position: "background-position", + background_repeat: "background-repeat", + background_size: "background-size", + background_blend_mode: "background-blend-mode", + baseline_shift: "baseline-shift", + bleed: "bleed", + bookmark_label: "bookmark-label", + bookmark_level: "bookmark-level", + bookmark_state: "bookmark-state", + border: "border", + border_color: "border-color", + border_style: "border-style", + border_width: "border-width", + border_bottom: "border-bottom", + border_bottom_color: "border-bottom-color", + border_bottom_style: "border-bottom-style", + border_bottom_width: "border-bottom-width", + border_left: "border-left", + border_left_color: "border-left-color", + border_left_style: "border-left-style", + border_left_width: "border-left-width", + border_right: "border-right", + border_right_color: "border-right-color", + border_right_style: "border-right-style", + border_right_width: "border-right-width", + border_top: "border-top", + border_top_color: "border-top-color", + border_top_style: "border-top-style", + border_top_width: "border-top-width", + border_collapse: "border-collapse", + border_image: "border-image", + border_image_outset: "border-image-outset", + border_image_repeat: "border-image-repeat", + border_image_slice: "border-image-slice", + border_image_source: "border-image-source", + border_image_width: "border-image-width", + border_radius: "border-radius", + border_bottom_left_radius: "border-bottom-left-radius", + border_bottom_right_radius: "border-bottom-right-radius", + border_top_left_radius: "border-top-left-radius", + border_top_right_radius: "border-top-right-radius", + border_spacing: "border-spacing", + bottom: "bottom", + box_decoration_break: "box-decoration-break", + box_shadow: "box-shadow", + box_sizing: "box-sizing", + box_snap: "box-snap", + break_after: "break-after", + break_before: "break-before", + break_inside: "break-inside", + buffered_rendering: "buffered-rendering", + caption_side: "caption-side", + clear: "clear", + clear_side: "clear-side", + clip: "clip", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_adjust: "color-adjust", + color_correction: "color-correction", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + column_fill: "column-fill", + column_gap: "column-gap", + column_rule: "column-rule", + column_rule_color: "column-rule-color", + column_rule_style: "column-rule-style", + column_rule_width: "column-rule-width", + column_span: "column-span", + columns: "columns", + column_count: "column-count", + column_width: "column-width", + contain: "contain", + content: "content", + counter_increment: "counter-increment", + counter_reset: "counter-reset", + counter_set: "counter-set", + cue: "cue", + cue_after: "cue-after", + cue_before: "cue-before", + cursor: "cursor", + direction: "direction", + display: "display", + display_inside: "display-inside", + display_outside: "display-outside", + display_extras: "display-extras", + display_box: "display-box", + dominant_baseline: "dominant-baseline", + elevation: "elevation", + empty_cells: "empty-cells", + enable_background: "enable-background", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + float: "float", + float_defer_column: "float-defer-column", + float_defer_page: "float-defer-page", + float_offset: "float-offset", + float_wrap: "float-wrap", + flow_into: "flow-into", + flow_from: "flow-from", + flex: "flex", + flex_basis: "flex-basis", + flex_grow: "flex-grow", + flex_shrink: "flex-shrink", + flex_flow: "flex-flow", + flex_direction: "flex-direction", + flex_wrap: "flex-wrap", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font: "font", + font_family: "font-family", + font_size: "font-size", + font_stretch: "font-stretch", + font_style: "font-style", + font_weight: "font-weight", + font_feature_settings: "font-feature-settings", + font_kerning: "font-kerning", + font_language_override: "font-language-override", + font_size_adjust: "font-size-adjust", + font_synthesis: "font-synthesis", + font_variant: "font-variant", + font_variant_alternates: "font-variant-alternates", + font_variant_caps: "font-variant-caps", + font_variant_east_asian: "font-variant-east-asian", + font_variant_ligatures: "font-variant-ligatures", + font_variant_numeric: "font-variant-numeric", + font_variant_position: "font-variant-position", + footnote_policy: "footnote-policy", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + grid: "grid", + grid_auto_flow: "grid-auto-flow", + grid_auto_columns: "grid-auto-columns", + grid_auto_rows: "grid-auto-rows", + grid_template: "grid-template", + grid_template_areas: "grid-template-areas", + grid_template_columns: "grid-template-columns", + grid_template_rows: "grid-template-rows", + grid_area: "grid-area", + grid_column: "grid-column", + grid_column_start: "grid-column-start", + grid_column_end: "grid-column-end", + grid_row: "grid-row", + grid_row_start: "grid-row-start", + grid_row_end: "grid-row-end", + hanging_punctuation: "hanging-punctuation", + height: "height", + hyphenate_character: "hyphenate-character", + hyphenate_limit_chars: "hyphenate-limit-chars", + hyphenate_limit_last: "hyphenate-limit-last", + hyphenate_limit_lines: "hyphenate-limit-lines", + hyphenate_limit_zone: "hyphenate-limit-zone", + hyphens: "hyphens", + icon: "icon", + image_orientation: "image-orientation", + image_resolution: "image-resolution", + image_rendering: "image-rendering", + ime: "ime", + ime_align: "ime-align", + ime_mode: "ime-mode", + ime_offset: "ime-offset", + ime_width: "ime-width", + initial_letters: "initial-letters", + inline_box_align: "inline-box-align", + isolation: "isolation", + justify_content: "justify-content", + justify_items: "justify-items", + justify_self: "justify-self", + kerning: "kerning", + left: "left", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + line_box_contain: "line-box-contain", + line_break: "line-break", + line_grid: "line-grid", + line_height: "line-height", + line_slack: "line-slack", + line_snap: "line-snap", + list_style: "list-style", + list_style_image: "list-style-image", + list_style_position: "list-style-position", + list_style_type: "list-style-type", + margin: "margin", + margin_bottom: "margin-bottom", + margin_left: "margin-left", + margin_right: "margin-right", + margin_top: "margin-top", + marker: "marker", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_pattern: "marker-pattern", + marker_segment: "marker-segment", + marker_start: "marker-start", + marker_knockout_left: "marker-knockout-left", + marker_knockout_right: "marker-knockout-right", + marker_side: "marker-side", + marks: "marks", + marquee_direction: "marquee-direction", + marquee_play_count: "marquee-play-count", + marquee_speed: "marquee-speed", + marquee_style: "marquee-style", + mask: "mask", + mask_image: "mask-image", + mask_repeat: "mask-repeat", + mask_position: "mask-position", + mask_clip: "mask-clip", + mask_origin: "mask-origin", + mask_size: "mask-size", + mask_box: "mask-box", + mask_box_outset: "mask-box-outset", + mask_box_repeat: "mask-box-repeat", + mask_box_slice: "mask-box-slice", + mask_box_source: "mask-box-source", + mask_box_width: "mask-box-width", + mask_type: "mask-type", + max_height: "max-height", + max_lines: "max-lines", + max_width: "max-width", + min_height: "min-height", + min_width: "min-width", + mix_blend_mode: "mix-blend-mode", + nav_down: "nav-down", + nav_index: "nav-index", + nav_left: "nav-left", + nav_right: "nav-right", + nav_up: "nav-up", + object_fit: "object-fit", + object_position: "object-position", + offset_after: "offset-after", + offset_before: "offset-before", + offset_end: "offset-end", + offset_start: "offset-start", + opacity: "opacity", + order: "order", + orphans: "orphans", + outline: "outline", + outline_color: "outline-color", + outline_style: "outline-style", + outline_width: "outline-width", + outline_offset: "outline-offset", + overflow: "overflow", + overflow_x: "overflow-x", + overflow_y: "overflow-y", + overflow_style: "overflow-style", + overflow_wrap: "overflow-wrap", + padding: "padding", + padding_bottom: "padding-bottom", + padding_left: "padding-left", + padding_right: "padding-right", + padding_top: "padding-top", + page: "page", + page_break_after: "page-break-after", + page_break_before: "page-break-before", + page_break_inside: "page-break-inside", + paint_order: "paint-order", + pause: "pause", + pause_after: "pause-after", + pause_before: "pause-before", + perspective: "perspective", + perspective_origin: "perspective-origin", + pitch: "pitch", + pitch_range: "pitch-range", + play_during: "play-during", + pointer_events: "pointer-events", + position: "position", + quotes: "quotes", + region_fragment: "region-fragment", + resize: "resize", + rest: "rest", + rest_after: "rest-after", + rest_before: "rest-before", + richness: "richness", + right: "right", + ruby_align: "ruby-align", + ruby_merge: "ruby-merge", + ruby_position: "ruby-position", + scroll_behavior: "scroll-behavior", + scroll_snap_coordinate: "scroll-snap-coordinate", + scroll_snap_destination: "scroll-snap-destination", + scroll_snap_points_x: "scroll-snap-points-x", + scroll_snap_points_y: "scroll-snap-points-y", + scroll_snap_type: "scroll-snap-type", + shape_image_threshold: "shape-image-threshold", + shape_inside: "shape-inside", + shape_margin: "shape-margin", + shape_outside: "shape-outside", + shape_padding: "shape-padding", + shape_rendering: "shape-rendering", + size: "size", + speak: "speak", + speak_as: "speak-as", + speak_header: "speak-header", + speak_numeral: "speak-numeral", + speak_punctuation: "speak-punctuation", + speech_rate: "speech-rate", + stop_color: "stop-color", + stop_opacity: "stop-opacity", + stress: "stress", + string_set: "string-set", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + tab_size: "tab-size", + table_layout: "table-layout", + text_align: "text-align", + text_align_all: "text-align-all", + text_align_last: "text-align-last", + text_anchor: "text-anchor", + text_combine_upright: "text-combine-upright", + text_decoration: "text-decoration", + text_decoration_color: "text-decoration-color", + text_decoration_line: "text-decoration-line", + text_decoration_style: "text-decoration-style", + text_decoration_skip: "text-decoration-skip", + text_emphasis: "text-emphasis", + text_emphasis_color: "text-emphasis-color", + text_emphasis_style: "text-emphasis-style", + text_emphasis_position: "text-emphasis-position", + text_emphasis_skip: "text-emphasis-skip", + text_height: "text-height", + text_indent: "text-indent", + text_justify: "text-justify", + text_orientation: "text-orientation", + text_overflow: "text-overflow", + text_rendering: "text-rendering", + text_shadow: "text-shadow", + text_size_adjust: "text-size-adjust", + text_space_collapse: "text-space-collapse", + text_spacing: "text-spacing", + text_transform: "text-transform", + text_underline_position: "text-underline-position", + text_wrap: "text-wrap", + top: "top", + touch_action: "touch-action", + transform: "transform", + transform_box: "transform-box", + transform_origin: "transform-origin", + transform_style: "transform-style", + transition: "transition", + transition_delay: "transition-delay", + transition_duration: "transition-duration", + transition_property: "transition-property", + unicode_bidi: "unicode-bidi", + vector_effect: "vector-effect", + vertical_align: "vertical-align", + visibility: "visibility", + voice_balance: "voice-balance", + voice_duration: "voice-duration", + voice_family: "voice-family", + voice_pitch: "voice-pitch", + voice_range: "voice-range", + voice_rate: "voice-rate", + voice_stress: "voice-stress", + voice_volumn: "voice-volumn", + volume: "volume", + white_space: "white-space", + widows: "widows", + width: "width", + will_change: "will-change", + word_break: "word-break", + word_spacing: "word-spacing", + word_wrap: "word-wrap", + wrap_flow: "wrap-flow", + wrap_through: "wrap-through", + writing_mode: "writing-mode", + gap: "gap", + list_styler_type: "list-style-type", + row_gap: "row-gap", + transition_timing_function: "transition-timing-function", + user_select: "user-select", + webkit_user_select: "-webkit-user-select", + z_index : "z-index", +} +mapped_trait_methods! { + aria_current: "aria-current", + aria_details: "aria-details", + aria_disabled: "aria-disabled", + aria_hidden: "aria-hidden", + aria_invalid: "aria-invalid", + aria_keyshortcuts: "aria-keyshortcuts", + aria_label: "aria-label", + aria_roledescription: "aria-roledescription", + // Widget Attributes + aria_autocomplete: "aria-autocomplete", + aria_checked: "aria-checked", + aria_expanded: "aria-expanded", + aria_haspopup: "aria-haspopup", + aria_level: "aria-level", + aria_modal: "aria-modal", + aria_multiline: "aria-multiline", + aria_multiselectable: "aria-multiselectable", + aria_orientation: "aria-orientation", + aria_placeholder: "aria-placeholder", + aria_pressed: "aria-pressed", + aria_readonly: "aria-readonly", + aria_required: "aria-required", + aria_selected: "aria-selected", + aria_sort: "aria-sort", + aria_valuemax: "aria-valuemax", + aria_valuemin: "aria-valuemin", + aria_valuenow: "aria-valuenow", + aria_valuetext: "aria-valuetext", + // Live Region Attributes + aria_atomic: "aria-atomic", + aria_busy: "aria-busy", + aria_live: "aria-live", + aria_relevant: "aria-relevant", + + aria_dropeffect: "aria-dropeffect", + aria_grabbed: "aria-grabbed", + // Relationship Attributes + aria_activedescendant: "aria-activedescendant", + aria_colcount: "aria-colcount", + aria_colindex: "aria-colindex", + aria_colspan: "aria-colspan", + aria_controls: "aria-controls", + aria_describedby: "aria-describedby", + aria_errormessage: "aria-errormessage", + aria_flowto: "aria-flowto", + aria_labelledby: "aria-labelledby", + aria_owns: "aria-owns", + aria_posinset: "aria-posinset", + aria_rowcount: "aria-rowcount", + aria_rowindex: "aria-rowindex", + aria_rowspan: "aria-rowspan", + aria_setsize: "aria-setsize", +} + +pub mod svg { + mapped_trait_methods! { + accent_height: "accent-height", + accumulate: "accumulate", + additive: "additive", + alignment_baseline: "alignment-baseline", + alphabetic: "alphabetic", + amplitude: "amplitude", + arabic_form: "arabic-form", + ascent: "ascent", + attributeName: "attributeName", + attributeType: "attributeType", + azimuth: "azimuth", + baseFrequency: "baseFrequency", + baseline_shift: "baseline-shift", + baseProfile: "baseProfile", + bbox: "bbox", + begin: "begin", + bias: "bias", + by: "by", + calcMode: "calcMode", + cap_height: "cap-height", + class: "class", + clip: "clip", + clipPathUnits: "clipPathUnits", + clip_path: "clip-path", + clip_rule: "clip-rule", + color: "color", + color_interpolation: "color-interpolation", + color_interpolation_filters: "color-interpolation-filters", + color_profile: "color-profile", + color_rendering: "color-rendering", + contentScriptType: "contentScriptType", + contentStyleType: "contentStyleType", + crossorigin: "crossorigin", + cursor: "cursor", + cx: "cx", + cy: "cy", + d: "d", + decelerate: "decelerate", + descent: "descent", + diffuseConstant: "diffuseConstant", + direction: "direction", + display: "display", + divisor: "divisor", + dominant_baseline: "dominant-baseline", + dur: "dur", + dx: "dx", + dy: "dy", + edgeMode: "edgeMode", + elevation: "elevation", + enable_background: "enable-background", + end: "end", + exponent: "exponent", + fill: "fill", + fill_opacity: "fill-opacity", + fill_rule: "fill-rule", + filter: "filter", + filterRes: "filterRes", + filterUnits: "filterUnits", + flood_color: "flood-color", + flood_opacity: "flood-opacity", + font_family: "font-family", + font_size: "font-size", + font_size_adjust: "font-size-adjust", + font_stretch: "font-stretch", + font_style: "font-style", + font_variant: "font-variant", + font_weight: "font-weight", + format: "format", + from: "from", + fr: "fr", + fx: "fx", + fy: "fy", + g1: "g1", + g2: "g2", + glyph_name: "glyph-name", + glyph_orientation_horizontal: "glyph-orientation-horizontal", + glyph_orientation_vertical: "glyph-orientation-vertical", + glyphRef: "glyphRef", + gradientTransform: "gradientTransform", + gradientUnits: "gradientUnits", + hanging: "hanging", + height: "height", + href: "href", + hreflang: "hreflang", + horiz_adv_x: "horiz-adv-x", + horiz_origin_x: "horiz-origin-x", + id: "id", + ideographic: "ideographic", + image_rendering: "image-rendering", + _in: "_in", + in2: "in2", + intercept: "intercept", + k: "k", + k1: "k1", + k2: "k2", + k3: "k3", + k4: "k4", + kernelMatrix: "kernelMatrix", + kernelUnitLength: "kernelUnitLength", + kerning: "kerning", + keyPoints: "keyPoints", + keySplines: "keySplines", + keyTimes: "keyTimes", + lang: "lang", + lengthAdjust: "lengthAdjust", + letter_spacing: "letter-spacing", + lighting_color: "lighting-color", + limitingConeAngle: "limitingConeAngle", + local: "local", + marker_end: "marker-end", + marker_mid: "marker-mid", + marker_start: "marker_start", + markerHeight: "markerHeight", + markerUnits: "markerUnits", + markerWidth: "markerWidth", + mask: "mask", + maskContentUnits: "maskContentUnits", + maskUnits: "maskUnits", + mathematical: "mathematical", + max: "max", + media: "media", + method: "method", + min: "min", + mode: "mode", + name: "name", + numOctaves: "numOctaves", + offset: "offset", + opacity: "opacity", + operator: "operator", + order: "order", + orient: "orient", + orientation: "orientation", + origin: "origin", + overflow: "overflow", + overline_position: "overline-position", + overline_thickness: "overline-thickness", + panose_1: "panose-1", + paint_order: "paint-order", + path: "path", + pathLength: "pathLength", + patternContentUnits: "patternContentUnits", + patternTransform: "patternTransform", + patternUnits: "patternUnits", + ping: "ping", + pointer_events: "pointer-events", + points: "points", + pointsAtX: "pointsAtX", + pointsAtY: "pointsAtY", + pointsAtZ: "pointsAtZ", + preserveAlpha: "preserveAlpha", + preserveAspectRatio: "preserveAspectRatio", + primitiveUnits: "primitiveUnits", + r: "r", + radius: "radius", + referrerPolicy: "referrerPolicy", + refX: "refX", + refY: "refY", + rel: "rel", + rendering_intent: "rendering-intent", + repeatCount: "repeatCount", + repeatDur: "repeatDur", + requiredExtensions: "requiredExtensions", + requiredFeatures: "requiredFeatures", + restart: "restart", + result: "result", + role: "role", + rotate: "rotate", + rx: "rx", + ry: "ry", + scale: "scale", + seed: "seed", + shape_rendering: "shape-rendering", + slope: "slope", + spacing: "spacing", + specularConstant: "specularConstant", + specularExponent: "specularExponent", + speed: "speed", + spreadMethod: "spreadMethod", + startOffset: "startOffset", + stdDeviation: "stdDeviation", + stemh: "stemh", + stemv: "stemv", + stitchTiles: "stitchTiles", + stop_color: "stop_color", + stop_opacity: "stop_opacity", + strikethrough_position: "strikethrough-position", + strikethrough_thickness: "strikethrough-thickness", + string: "string", + stroke: "stroke", + stroke_dasharray: "stroke-dasharray", + stroke_dashoffset: "stroke-dashoffset", + stroke_linecap: "stroke-linecap", + stroke_linejoin: "stroke-linejoin", + stroke_miterlimit: "stroke-miterlimit", + stroke_opacity: "stroke-opacity", + stroke_width: "stroke-width", + style: "style", + surfaceScale: "surfaceScale", + systemLanguage: "systemLanguage", + tabindex: "tabindex", + tableValues: "tableValues", + target: "target", + targetX: "targetX", + targetY: "targetY", + text_anchor: "text-anchor", + text_decoration: "text-decoration", + text_rendering: "text-rendering", + textLength: "textLength", + to: "to", + transform: "transform", + transform_origin: "transform-origin", + r#type: "_type", + u1: "u1", + u2: "u2", + underline_position: "underline-position", + underline_thickness: "underline-thickness", + unicode: "unicode", + unicode_bidi: "unicode-bidi", + unicode_range: "unicode-range", + units_per_em: "units-per-em", + v_alphabetic: "v-alphabetic", + v_hanging: "v-hanging", + v_ideographic: "v-ideographic", + v_mathematical: "v-mathematical", + values: "values", + vector_effect: "vector-effect", + version: "version", + vert_adv_y: "vert-adv-y", + vert_origin_x: "vert-origin-x", + vert_origin_y: "vert-origin-y", + view_box: "viewBox", + view_target: "viewTarget", + visibility: "visibility", + width: "width", + widths: "widths", + word_spacing: "word-spacing", + writing_mode: "writing-mode", + x: "x", + x_height: "x-height", + x1: "x1", + x2: "x2", + xmlns: "xmlns", + x_channel_selector: "xChannelSelector", + y: "y", + y1: "y1", + y2: "y2", + y_channel_selector: "yChannelSelector", + z: "z", + zoomAndPan: "zoomAndPan", + } +} diff --git a/packages/rsx_interperter/src/captuered_context.rs b/packages/rsx_interperter/src/captuered_context.rs new file mode 100644 index 000000000..f3df4f341 --- /dev/null +++ b/packages/rsx_interperter/src/captuered_context.rs @@ -0,0 +1,137 @@ +use dioxus_core::VNode; +use dioxus_rsx::{BodyNode, CallBody, Component, ElementAttr, IfmtInput}; +use quote::{quote, ToTokens, TokenStreamExt}; +use std::collections::HashMap; +use syn::Expr; + +#[derive(Default)] +pub struct CapturedContextBuilder { + attributes: HashMap, + text: Vec, + components: Vec, + iterators: Vec, +} + +impl CapturedContextBuilder { + pub fn extend(&mut self, other: CapturedContextBuilder) { + self.attributes.extend(other.attributes); + self.text.extend(other.text); + self.components.extend(other.components); + } + + pub fn from_call_body(body: CallBody) -> Self { + let mut new = Self::default(); + for node in body.roots { + new.extend(Self::find_captured(node)); + } + new + } + + fn find_captured(node: BodyNode) -> Self { + let mut captured = CapturedContextBuilder::default(); + match node { + BodyNode::Element(el) => { + for attr in el.attributes { + let (name, value_tokens) = match attr.attr { + ElementAttr::AttrText { name, value } => { + (name.to_string(), value.to_token_stream()) + } + ElementAttr::AttrExpression { name, value } => { + (name.to_string(), value.to_token_stream()) + } + ElementAttr::CustomAttrText { name, value } => { + (name.value(), value.to_token_stream()) + } + ElementAttr::CustomAttrExpression { name, value } => { + (name.value(), value.to_token_stream()) + } + _ => continue, + }; + let formated: IfmtInput = syn::parse2(value_tokens).unwrap(); + captured.attributes.insert(name, formated); + } + for child in el.children { + captured.extend(Self::find_captured(child)); + } + } + BodyNode::Component(comp) => { + let fn_name = comp.name.segments.last().unwrap().ident.to_string(); + captured.components.push(comp); + } + BodyNode::Text(t) => { + let tokens = t.to_token_stream(); + let formated: IfmtInput = syn::parse2(tokens).unwrap(); + captured.text.push(formated); + } + BodyNode::RawExpr(expr) => captured.iterators.push(expr), + BodyNode::Meta(_) => (), + } + captured + } +} + +impl ToTokens for CapturedContextBuilder { + fn to_tokens(&self, tokens: &mut quote::__private::TokenStream) { + let CapturedContextBuilder { + attributes, + text, + components, + iterators, + } = self; + let captured: Vec<_> = attributes + .iter() + .map(|(_, fmt)| fmt.named_args.iter()) + .chain(text.iter().map(|fmt| fmt.named_args.iter())) + .flatten() + .collect(); + let captured_names = captured.iter().map(|(n, _)| n); + let captured_expr = captured.iter().map(|(_, e)| e); + tokens.append_all(quote! { + CapturedContext { + captured: IfmtArgs{ + named_args: &'static [#((#captured_names, #captured_expr)),*] + }, + components: vec![#(#components),*], + iterators: vec![#(#iterators),*], + } + }) + } +} + +struct CapturedComponentBuilder { + name: syn::Path, + function: String, +} + +pub struct CapturedContext<'a> { + // map of the attribute name to the formated value + captured: IfmtArgs, + // the only thing we can update in component is the children + components: Vec>, + // we can't reasonably interpert iterators, so they are staticly inserted + iterators: Vec>, +} + +pub struct IfmtArgs { + // live reload only supports named arguments + pub named_args: &'static [(&'static str, String)], +} + +enum IfmtSegment<'a> { + Static(&'a str), + Dynamic(&'a str), +} + +enum RsxNode<'a> { + Element { + name: String, + attributes: Vec<(String, IfmtSegment<'a>)>, + children: Vec>, + }, + Text { + text: Vec>, + }, + Component { + children: Vec>, + }, +} diff --git a/packages/rsx_interperter/src/elements.rs b/packages/rsx_interperter/src/elements.rs new file mode 100644 index 000000000..032c8f98e --- /dev/null +++ b/packages/rsx_interperter/src/elements.rs @@ -0,0 +1,1380 @@ +// find the mapped attribute name +pub fn element_to_static_str(element: &str) -> Option<(&'static str, Option<&'static str>)> { + ELEMENTS_WITH_MAPPED_ATTRIBUTES + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + .or_else(|| { + ELEMENTS_WITH_NAMESPACE + .iter() + .find(|(el, _, _)| *el == element) + .map(|(el, ns, _)| (*el, Some(*ns))) + }) + .or_else(|| { + ELEMENTS_WITHOUT_NAMESPACE + .iter() + .find(|(el, _)| *el == element) + .map(|(el, _)| (*el, None)) + }) +} + +macro_rules! builder_constructors { + ( + $( + $(#[$attr:meta])* + $name:ident { + $( + $(#[$attr_method:meta])* + $fil:ident: $vil:ident, + )* + }; + )* + ) => { + pub const ELEMENTS_WITHOUT_NAMESPACE: &'static [(&'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; + + ( $( + $(#[$attr:meta])* + $name:ident <> $namespace:tt { + $($fil:ident: $vil:ident,)* + }; + )* ) => { + pub const ELEMENTS_WITH_NAMESPACE: &'static [(&'static str, &'static str, &'static [&'static str])] = &[ + $( + ( + stringify!($name), + stringify!($namespace), + &[ + $( + stringify!($fil), + )* + ] + ), + )* + ]; + }; +} +pub const ELEMENTS_WITH_MAPPED_ATTRIBUTES: &'static [( + &'static str, + &'static [(&'static str, &'static str)], +)] = &[ + ("script", &[("r#type", "type"), ("r#script", "script")]), + ("button", &[("r#type", "type")]), + ("select", &[("value", "value")]), + ("option", &[("selected", "selected")]), + ("textarea", &[("value", "value")]), + ("label", &[("r#for", "for")]), + ("input", &[("r#type", "type"), ("value", "value")]), +]; + +// Organized in the same order as +// https://developer.mozilla.org/en-US/docs/Web/HTML/Element +// +// Does not include obsolete elements. +// +// This namespace represents a collection of modern HTML-5 compatiable elements. +// +// This list does not include obsolete, deprecated, experimental, or poorly supported elements. +builder_constructors! { + // Document metadata + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base) + /// element. + /// + base { + href: Uri, + target: Target, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/head) + /// element. + head {}; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/link) + /// element. + link { + // as: Mime, + crossorigin: CrossOrigin, + href: Uri, + hreflang: LanguageTag, + media: String, // FIXME media query + rel: LinkType, + sizes: String, // FIXME + title: String, // FIXME + r#type: Mime, + integrity: String, + }; + + /// Build a + /// [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta) + /// element. + meta { + charset: String, // FIXME IANA standard names + content: String, + http_equiv: HTTPEquiv, + name: Metadata, + }; + + /// Build a + /// [`