Hotreloading of for/if/body, formatted strings, literals, component props, nested rsx, light CLI rewrite, cli TUI (#2258)

Hotreload the contents of for loops, if chains, component bodies, props, attributes, and any literals discovered in rsx!
Add a TUI renderer to the CLI.
Improve the CLI build system to be async and parallel.
Refactor RSX to allow partial expansion of expressions.
Merge autofmt implementations for consistency.
Merge the representation of elements and components under the hood.
Add a diagnostics system for rsx for improved error messages.
Drop interprocess and move to websockets for communication between the CLI and the server.
Assign IDs to nodes and attributes in a stable way to be used in non compiler contexts.
Add hotreloading to any body of component/for loop/if chain/etc.

---------

Co-authored-by: Evan Almloff <evanalmloff@gmail.com>
Co-authored-by: Liam Mitchell <liamkarlmitchell@gmail.com>
This commit is contained in:
authored and GitHub committed 2024-07-17 19:11:18 -07:00
1 parent 176e67e5b7
commit 88af3e7eff
181 files changed
+12755 -10806

No files matched your search

+2 -2
View File
@@ -8,7 +8,7 @@ use std::fmt::{Result, Write};
use dioxus_rsx::IfmtInput;
use crate::{indent::IndentOptions, write_ifmt};
use crate::indent::IndentOptions;
/// The output buffer that tracks indent and string
#[derive(Debug, Default)]
@@ -51,7 +51,7 @@ impl Buffer {
}
pub fn write_text(&mut self, text: &IfmtInput) -> Result {
write_ifmt(text, &mut self.buf)
write!(self.buf, "{}", text.to_string_with_quotes())
}
}
+10 -4
View File
@@ -7,8 +7,15 @@ use syn::{visit::Visit, File, Macro};
type CollectedMacro<'a> = &'a Macro;
pub fn collect_from_file<'a>(file: &'a File, macros: &mut Vec<CollectedMacro<'a>>) {
MacroCollector::visit_file(&mut MacroCollector { macros }, file);
pub fn collect_from_file(file: &File) -> Vec<CollectedMacro<'_>> {
let mut macros = vec![];
MacroCollector::visit_file(
&mut MacroCollector {
macros: &mut macros,
},
file,
);
macros
}
struct MacroCollector<'a, 'b> {
@@ -46,7 +53,6 @@ pub fn byte_offset(input: &str, location: LineColumn) -> usize {
fn parses_file_and_collects_rsx_macros() {
let contents = include_str!("../tests/samples/long.rsx");
let parsed = syn::parse_file(contents).unwrap();
let mut macros = vec![];
collect_from_file(&parsed, &mut macros);
let macros = collect_from_file(&parsed);
assert_eq!(macros.len(), 3);
}
-291
View File
@@ -1,291 +0,0 @@
use crate::{ifmt_to_string, prettier_please::unparse_expr, writer::Location, Writer};
use dioxus_rsx::*;
use quote::ToTokens;
use std::fmt::{Result, Write};
use syn::{spanned::Spanned, AngleBracketedGenericArguments};
#[derive(Debug)]
enum ShortOptimization {
// Special because we want to print the closing bracket immediately
Empty,
// Special optimization to put everything on the same line
Oneliner,
// Optimization where children flow but props remain fixed on top
PropsOnTop,
// The noisiest optimization where everything flows
NoOpt,
}
impl Writer<'_> {
pub fn write_component(
&mut self,
Component {
name,
fields,
children,
manual_props,
prop_gen_args,
key,
..
}: &Component,
) -> Result {
self.write_component_name(name, prop_gen_args)?;
// decide if we have any special optimizations
// Default with none, opt the cases in one-by-one
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let attr_len = self.field_len(fields, manual_props) + self.key_len(key.as_ref());
let is_short_attr_list = attr_len < 80;
let is_small_children = self.is_short_children(children).is_some();
// if we have few attributes and a lot of children, place the attrs on top
if is_short_attr_list && !is_small_children {
opt_level = ShortOptimization::PropsOnTop;
}
// even if the attr is long, it should be put on one line
if !is_short_attr_list && (fields.len() <= 1 && manual_props.is_none()) {
if children.is_empty() {
opt_level = ShortOptimization::Oneliner;
} else {
opt_level = ShortOptimization::PropsOnTop;
}
}
// if we have few children and few attributes, make it a one-liner
if is_short_attr_list && is_small_children {
opt_level = ShortOptimization::Oneliner;
}
// If there's nothing at all, empty optimization
if fields.is_empty() && children.is_empty() && manual_props.is_none() && key.is_none() {
opt_level = ShortOptimization::Empty;
}
// multiline handlers bump everything down
if attr_len > 1000 || self.out.indent.split_line_attributes() {
opt_level = ShortOptimization::NoOpt;
}
// Useful for debugging
// dbg!(
// name.to_token_stream().to_string(),
// &opt_level,
// attr_len,
// is_short_attr_list,
// is_small_children
// );
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
write!(self.out, " ")?;
self.write_component_fields(fields, key.as_ref(), manual_props, true)?;
if !children.is_empty() && !fields.is_empty() {
write!(self.out, ", ")?;
}
for (id, child) in children.iter().enumerate() {
self.write_ident(child)?;
if id != children.len() - 1 && children.len() > 1 {
write!(self.out, ", ")?;
}
}
write!(self.out, " ")?;
}
ShortOptimization::PropsOnTop => {
write!(self.out, " ")?;
self.write_component_fields(fields, key.as_ref(), manual_props, true)?;
if !children.is_empty() && !fields.is_empty() {
write!(self.out, ",")?;
}
self.write_body_indented(children)?;
self.out.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_component_fields(fields, key.as_ref(), manual_props, false)?;
if !children.is_empty() && !fields.is_empty() {
write!(self.out, ",")?;
}
self.write_body_indented(children)?;
self.out.tabbed_line()?;
}
}
write!(self.out, "}}")?;
Ok(())
}
fn write_component_name(
&mut self,
name: &syn::Path,
generics: &Option<AngleBracketedGenericArguments>,
) -> Result {
let mut name = name.to_token_stream().to_string();
name.retain(|c| !c.is_whitespace());
write!(self.out, "{name}")?;
if let Some(generics) = generics {
let mut written = generics.to_token_stream().to_string();
written.retain(|c| !c.is_whitespace());
write!(self.out, "{written}")?;
}
write!(self.out, " {{")?;
Ok(())
}
fn write_component_fields(
&mut self,
fields: &[ComponentField],
key: Option<&IfmtInput>,
manual_props: &Option<syn::Expr>,
sameline: bool,
) -> Result {
let mut field_iter = fields.iter().peekable();
// write the key
if let Some(key) = key {
write!(self.out, "key: {}", ifmt_to_string(key))?;
if !fields.is_empty() {
write!(self.out, ",")?;
if sameline {
write!(self.out, " ")?;
}
}
}
while let Some(field) = field_iter.next() {
if !sameline {
self.out.indented_tabbed_line().unwrap();
}
let name = &field.name;
match &field.content {
ContentField::ManExpr(_exp) if field.can_be_shorthand() => {
write!(self.out, "{name}")?;
}
ContentField::ManExpr(exp) => {
let out = self.unparse_expr(exp);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
write!(self.out, "{name}: {first}")?;
for line in lines {
self.out.new_line()?;
self.out.indented_tab()?;
write!(self.out, "{line}")?;
}
}
ContentField::Formatted(s) => {
write!(
self.out,
"{}: {}",
name,
s.source.as_ref().unwrap().to_token_stream()
)?;
}
ContentField::Shorthand(e) => {
write!(self.out, "{}", e.to_token_stream())?;
}
}
if field_iter.peek().is_some() || manual_props.is_some() {
write!(self.out, ",")?;
if sameline {
write!(self.out, " ")?;
}
}
}
if let Some(exp) = manual_props {
if !sameline {
self.out.indented_tabbed_line().unwrap();
}
self.write_manual_props(exp)?;
}
Ok(())
}
pub fn field_len(
&mut self,
fields: &[ComponentField],
manual_props: &Option<syn::Expr>,
) -> usize {
let attr_len = fields
.iter()
.map(|field| match &field.content {
ContentField::Formatted(s) => ifmt_to_string(s).len() ,
ContentField::Shorthand(e) => e.to_token_stream().to_string().len(),
ContentField::ManExpr(exp) => {
let formatted = unparse_expr(exp);
let len = if formatted.contains('\n') {
10000
} else {
formatted.len()
};
self.cached_formats.insert(Location::new(exp.span().start()) , formatted);
len
},
} + 10)
.sum::<usize>();
match manual_props {
Some(p) => {
let content = unparse_expr(p);
if content.len() + attr_len > 80 {
return 100000;
}
let mut lines = content.lines();
lines.next().unwrap();
if lines.next().is_none() {
attr_len + content.len()
} else {
100000
}
}
None => attr_len,
}
}
fn write_manual_props(&mut self, exp: &syn::Expr) -> Result {
/*
We want to normalize the expr to the appropriate indent level.
*/
let formatted = self.unparse_expr(exp);
let mut lines = formatted.lines();
let first_line = lines.next().unwrap();
write!(self.out, "..{first_line}")?;
for line in lines {
self.out.indented_tabbed_line()?;
write!(self.out, "{line}")?;
}
Ok(())
}
}
-58
View File
@@ -1,58 +0,0 @@
//! pretty printer for rsx!
use std::fmt::{Result, Write};
use proc_macro2::Span;
use crate::{collect_macros::byte_offset, Writer};
impl Writer<'_> {
pub fn write_raw_expr(&mut self, placement: Span) -> Result {
/*
We want to normalize the expr to the appropriate indent level.
*/
let start = placement.start();
let end = placement.end();
// if the expr is on one line, just write it directly
if start.line == end.line {
// split counting utf8 chars
let start = byte_offset(self.raw_src, start);
let end = byte_offset(self.raw_src, end);
let row = self.raw_src[start..end].trim();
write!(self.out, "{row}")?;
return Ok(());
}
// If the expr is multiline, we want to collect all of its lines together and write them out properly
// This involves unshifting the first line if it's aligned
let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column..].trim_start())?;
let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?;
// check if this is the last line
let line = {
if id == (end.line - start.line) - 1 {
&line[..end.column]
} else {
line
}
};
// trim the leading whitespace
let previous_indent = self.out.indent.count_indents(line);
let offset = previous_indent.saturating_sub(prev_block_indent_level);
let required_indent = self.out.indent_level + offset;
self.out.write_tabs(required_indent)?;
let line = line.trim_start();
write!(self.out, "{line}")?;
}
Ok(())
}
}
+22 -80
View File
@@ -2,22 +2,16 @@
#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
use std::fmt::{Display, Write};
use crate::writer::*;
use collect_macros::byte_offset;
use dioxus_rsx::{BodyNode, CallBody, IfmtInput};
use dioxus_rsx::{BodyNode, CallBody};
use proc_macro2::LineColumn;
use quote::ToTokens;
use syn::{parse::Parser, ExprMacro, MacroDelimiter};
use syn::{parse::Parser, ExprMacro};
mod buffer;
mod collect_macros;
mod component;
mod element;
mod expr;
mod indent;
mod prettier_please;
mod rsx_block;
mod writer;
pub use indent::{IndentOptions, IndentType};
@@ -55,9 +49,7 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
let mut formatted_blocks = Vec::new();
let parsed = syn::parse_file(contents).unwrap();
let mut macros = vec![];
collect_macros::collect_from_file(&parsed, &mut macros);
let macros = collect_macros::collect_from_file(&parsed);
// No macros, no work to do
if macros.is_empty() {
@@ -86,7 +78,11 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
.indent
.count_indents(writer.src[rsx_start.line - 1]);
write_body(&mut writer, &body);
// TESTME
// If we fail to parse this macro then we have no choice to give up and return what we've got
if writer.write_rsx_call(&body.body).is_err() {
return formatted_blocks;
}
// writing idents leaves the final line ended at the end of the last ident
if writer.out.buf.contains('\n') {
@@ -94,23 +90,15 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
writer.out.tab().unwrap();
}
let span = match item.delimiter {
MacroDelimiter::Paren(b) => b.span,
MacroDelimiter::Brace(b) => b.span,
MacroDelimiter::Bracket(b) => b.span,
}
.join();
let span = item.delimiter.span().join();
let mut formatted = writer.out.buf.split_off(0);
let mut formatted = String::new();
std::mem::swap(&mut formatted, &mut writer.out.buf);
let start = byte_offset(contents, span.start()) + 1;
let end = byte_offset(contents, span.end()) - 1;
let start = collect_macros::byte_offset(contents, span.start()) + 1;
let end = collect_macros::byte_offset(contents, span.end()) - 1;
// Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression
let body_is_solo_expr = body.roots.len() == 1
&& matches!(body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
let body_is_solo_expr = body.body.roots.len() == 1
&& matches!(body.body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
if formatted.len() <= 80 && !formatted.contains('\n') && !body_is_solo_expr {
formatted = format!(" {formatted} ");
@@ -132,33 +120,20 @@ pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
formatted_blocks
}
/// Write a Callbody (the rsx block) to a string
///
/// If the tokens can't be formatted, this returns None. This is usually due to an incomplete expression
/// that passed partial expansion but failed to parse.
pub fn write_block_out(body: &CallBody) -> Option<String> {
let mut buf = Writer::new("");
write_body(&mut buf, body);
buf.write_rsx_call(&body.body).ok()?;
buf.consume()
}
fn write_body(buf: &mut Writer, body: &CallBody) {
match body.roots.len() {
0 => {}
1 if matches!(body.roots[0], BodyNode::Text(_)) => {
write!(buf.out, " ").unwrap();
buf.write_ident(&body.roots[0]).unwrap();
write!(buf.out, " ").unwrap();
}
_ => buf.write_body_indented(&body.roots).unwrap(),
}
}
pub fn fmt_block_from_expr(raw: &str, expr: ExprMacro) -> Option<String> {
let body = CallBody::parse_strict.parse2(expr.mac.tokens).unwrap();
let mut buf = Writer::new(raw);
write_body(&mut buf, &body);
buf.write_rsx_call(&body.body).ok()?;
buf.consume()
}
@@ -166,11 +141,9 @@ pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Opt
let body = CallBody::parse_strict.parse_str(block).unwrap();
let mut buf = Writer::new(block);
buf.out.indent = indent;
buf.out.indent_level = indent_level;
write_body(&mut buf, &body);
buf.write_rsx_call(&body.body).ok()?;
// writing idents leaves the final line ended at the end of the last ident
if buf.out.buf.contains('\n') {
@@ -180,16 +153,6 @@ pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Opt
buf.consume()
}
pub fn apply_format(input: &str, block: FormattedBlock) -> String {
let start = block.start;
let end = block.end;
let (left, _) = input.split_at(start);
let (_, right) = input.split_at(end);
format!("{}{}{}", left, block.formatted, right)
}
// Apply all the blocks
pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
let mut out = String::new();
@@ -213,24 +176,3 @@ pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
out
}
struct DisplayIfmt<'a>(&'a IfmtInput);
impl Display for DisplayIfmt<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let inner_tokens = self.0.source.as_ref().unwrap().to_token_stream();
inner_tokens.fmt(f)
}
}
pub(crate) fn ifmt_to_string(input: &IfmtInput) -> String {
let mut buf = String::new();
let display = DisplayIfmt(input);
write!(&mut buf, "{}", display).unwrap();
buf
}
pub(crate) fn write_ifmt(input: &IfmtInput, writable: &mut impl Write) -> std::fmt::Result {
let display = DisplayIfmt(input);
write!(writable, "{}", display)
}
@@ -1,4 +1,4 @@
use crate::{ifmt_to_string, prettier_please::unparse_expr, Writer};
use crate::{prettier_please::unparse_expr, Writer};
use dioxus_rsx::*;
use proc_macro2::Span;
use quote::ToTokens;
@@ -30,47 +30,24 @@ enum ShortOptimization {
NoOpt,
}
/*
// whitespace
div {
// some whitespace
class: "asdasd"
// whjiot
asdasd // whitespace
}
*/
impl Writer<'_> {
pub fn write_element(&mut self, el: &Element) -> Result {
let Element {
name,
key,
attributes,
children,
brace,
..
} = el;
let brace = brace
.as_ref()
.expect("braces should always be present in strict mode");
/*
1. Write the tag
2. Write the key
3. Write the attributes
4. Write the children
*/
write!(self.out, "{name} {{")?;
/// Basically elements and components are the same thing
///
/// This writes the contents out for both in one function, centralizing the annoying logic like
/// key handling, breaks, closures, etc
pub fn write_rsx_block(
&mut self,
attributes: &[Attribute],
spreads: &[Spread],
children: &[BodyNode],
brace: &Brace,
) -> Result {
// decide if we have any special optimizations
// Default with none, opt the cases in one-by-one
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let attr_len = self.is_short_attrs(attributes);
let attr_len = self.is_short_attrs(attributes, spreads);
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
let children_len = self.is_short_children(children);
let is_small_children = children_len.is_some();
@@ -82,7 +59,7 @@ impl Writer<'_> {
// even if the attr is long, it should be put on one line
// However if we have childrne we need to just spread them out for readability
if !is_short_attr_list && attributes.len() <= 1 {
if !is_short_attr_list && attributes.len() <= 1 && spreads.is_empty() {
if children.is_empty() {
opt_level = ShortOptimization::Oneliner;
} else {
@@ -100,7 +77,7 @@ impl Writer<'_> {
}
// If there's nothing at all, empty optimization
if attributes.is_empty() && children.is_empty() && key.is_none() {
if attributes.is_empty() && children.is_empty() && spreads.is_empty() {
opt_level = ShortOptimization::Empty;
// Write comments if they exist
@@ -112,49 +89,42 @@ impl Writer<'_> {
opt_level = ShortOptimization::NoOpt;
}
let has_children = !children.is_empty();
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
write!(self.out, " ")?;
self.write_attributes(brace, attributes, key, true)?;
self.write_attributes(attributes, spreads, true, brace, has_children)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ", ")?;
if !children.is_empty() && !attributes.is_empty() {
write!(self.out, " ")?;
}
for (id, child) in children.iter().enumerate() {
for child in children.iter() {
self.write_ident(child)?;
if id != children.len() - 1 && children.len() > 1 {
write!(self.out, ", ")?;
}
}
write!(self.out, " ")?;
}
ShortOptimization::PropsOnTop => {
if !attributes.is_empty() || key.is_some() {
if !attributes.is_empty() {
write!(self.out, " ")?;
}
self.write_attributes(brace, attributes, key, true)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ",")?;
}
self.write_attributes(attributes, spreads, true, brace, has_children)?;
if !children.is_empty() {
self.write_body_indented(children)?;
}
self.out.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_attributes(brace, attributes, key, false)?;
if !children.is_empty() && (!attributes.is_empty() || key.is_some()) {
write!(self.out, ",")?;
}
self.write_attributes(attributes, spreads, false, brace, has_children)?;
if !children.is_empty() {
self.write_body_indented(children)?;
@@ -164,76 +134,99 @@ impl Writer<'_> {
}
}
write!(self.out, "}}")?;
Ok(())
}
fn write_attributes(
&mut self,
attributes: &[Attribute],
spreads: &[Spread],
props_same_line: bool,
brace: &Brace,
attributes: &[AttributeType],
key: &Option<IfmtInput>,
sameline: bool,
has_children: bool,
) -> Result {
let mut attr_iter = attributes.iter().peekable();
if let Some(key) = key {
if !sameline {
self.out.indented_tabbed_line()?;
}
write!(self.out, "key: {}", ifmt_to_string(key))?;
if !attributes.is_empty() {
write!(self.out, ",")?;
if sameline {
write!(self.out, " ")?;
}
}
enum AttrType<'a> {
Attr(&'a Attribute),
Spread(&'a Spread),
}
let mut attr_iter = attributes
.iter()
.map(AttrType::Attr)
.chain(spreads.iter().map(AttrType::Spread))
.peekable();
while let Some(attr) = attr_iter.next() {
self.out.indent_level += 1;
if !sameline {
self.write_attr_comments(brace, attr.start())?;
if !props_same_line {
self.write_attr_comments(
brace,
match attr {
AttrType::Attr(attr) => attr.span(),
AttrType::Spread(attr) => attr.expr.span(),
},
)?;
}
self.out.indent_level -= 1;
if !sameline {
if !props_same_line {
self.out.indented_tabbed_line()?;
}
self.write_attribute(attr)?;
match attr {
AttrType::Attr(attr) => self.write_attribute(attr)?,
AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
}
if attr_iter.peek().is_some() {
write!(self.out, ",")?;
if sameline {
if props_same_line {
write!(self.out, " ")?;
}
}
}
Ok(())
}
let has_attributes = !attributes.is_empty() || !spreads.is_empty();
fn write_attribute_name(&mut self, attr: &ElementAttrName) -> Result {
match attr {
ElementAttrName::BuiltIn(name) => {
write!(self.out, "{}", name)?;
}
ElementAttrName::Custom(name) => {
write!(self.out, "{}", name.to_token_stream())?;
}
if has_attributes && has_children {
write!(self.out, ",")?;
}
Ok(())
}
fn write_attribute_value(&mut self, value: &ElementAttrValue) -> Result {
fn write_attribute(&mut self, attr: &Attribute) -> Result {
self.write_attribute_name(&attr.name)?;
// if the attribute is a shorthand, we don't need to write the colon, just the name
if !attr.can_be_shorthand() {
write!(self.out, ": ")?;
self.write_attribute_value(&attr.value)?;
}
Ok(())
}
fn write_attribute_name(&mut self, attr: &AttributeName) -> Result {
match attr {
AttributeName::BuiltIn(name) => {
write!(self.out, "{}", name)?;
}
AttributeName::Custom(name) => {
write!(self.out, "{}", name.to_token_stream())?;
}
AttributeName::Spread(_) => unreachable!(),
}
Ok(())
}
fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
match value {
ElementAttrValue::AttrOptionalExpr { condition, value } => {
AttributeValue::AttrOptionalExpr { condition, value } => {
write!(
self.out,
"if {condition} {{ ",
@@ -242,56 +235,47 @@ impl Writer<'_> {
self.write_attribute_value(value)?;
write!(self.out, " }}")?;
}
ElementAttrValue::AttrLiteral(value) => {
write!(self.out, "{value}", value = ifmt_to_string(value))?;
AttributeValue::AttrLiteral(value) => {
write!(self.out, "{value}")?;
}
ElementAttrValue::Shorthand(value) => {
write!(self.out, "{value}",)?;
AttributeValue::Shorthand(value) => {
write!(self.out, "{value}")?;
}
AttributeValue::EventTokens(closure) => {
self.write_partial_closure(closure)?;
}
ElementAttrValue::AttrExpr(value) => {
let out = self.unparse_expr(value);
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
// a one-liner for whatever reason
// Does not need a new line
AttributeValue::AttrExpr(value) => {
let Ok(expr) = value.as_expr() else {
return Err(fmt::Error);
};
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
self.write_mulitiline_tokens(pretty_expr)?;
}
}
Ok(())
}
fn write_mulitiline_tokens(&mut self, out: String) -> Result {
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
write!(self.out, "{first}")?;
} else {
writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "{first}")?;
write!(self.out, "")?;
} else {
writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "")?;
} else {
writeln!(self.out)?;
}
}
}
}
ElementAttrValue::EventTokens(tokens) => {
let out = self.retrieve_formatted_expr(tokens).to_string();
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
// a one-liner for whatever reason
// Does not need a new line
if lines.peek().is_none() {
write!(self.out, "{first}")?;
} else {
writeln!(self.out, "{first}")?;
while let Some(line) = lines.next() {
self.out.indented_tab()?;
write!(self.out, "{line}")?;
if lines.peek().is_none() {
write!(self.out, "")?;
} else {
writeln!(self.out)?;
}
}
writeln!(self.out)?;
}
}
}
@@ -299,28 +283,34 @@ impl Writer<'_> {
Ok(())
}
fn write_attribute(&mut self, attr: &AttributeType) -> Result {
match attr {
AttributeType::Named(attr) => self.write_named_attribute(attr),
AttributeType::Spread(attr) => self.write_spread_attribute(attr),
}
}
fn write_named_attribute(&mut self, attr: &ElementAttrNamed) -> Result {
self.write_attribute_name(&attr.attr.name)?;
// if the attribute is a shorthand, we don't need to write the colon, just the name
if !attr.attr.can_be_shorthand() {
write!(self.out, ": ")?;
self.write_attribute_value(&attr.attr.value)?;
/// Write out the special PartialClosure type from the rsx crate
/// Basically just write token by token until we hit the block and then try and format *that*
/// We can't just ToTokens
fn write_partial_closure(&mut self, closure: &PartialClosure) -> Result {
// Write the pretty version of the closure
if let Ok(expr) = closure.as_expr() {
let pretty_expr = self.retrieve_formatted_expr(&expr).to_string();
self.write_mulitiline_tokens(pretty_expr)?;
return Ok(());
}
Ok(())
// If we can't parse the closure, writing it is also a failure
// rustfmt won't be able to parse it either so no point in trying
Err(fmt::Error)
}
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
write!(self.out, "..")?;
write!(self.out, "{}", unparse_expr(attr))?;
let formatted = unparse_expr(attr);
let mut lines = formatted.lines();
let first_line = lines.next().unwrap();
write!(self.out, "..{first_line}")?;
for line in lines {
self.out.indented_tabbed_line()?;
write!(self.out, "{line}")?;
}
Ok(())
}
@@ -363,10 +353,10 @@ impl Writer<'_> {
}
match children {
[BodyNode::Text(ref text)] => Some(ifmt_to_string(text).len()),
[BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
// TODO: let rawexprs to be inlined
[BodyNode::RawExpr(ref expr)] => get_expr_length(expr),
[BodyNode::RawExpr(ref expr)] => Some(get_expr_length(expr.span())),
// TODO: let rawexprs to be inlined
[BodyNode::Component(ref comp)] if comp.fields.is_empty() => Some(
@@ -430,12 +420,11 @@ impl Writer<'_> {
}
}
fn get_expr_length(expr: &impl Spanned) -> Option<usize> {
let span = expr.span();
fn get_expr_length(span: Span) -> usize {
let (start, end) = (span.start(), span.end());
if start.line == end.line {
Some(end.column - start.column)
end.column - start.column
} else {
None
10000
}
}
+185 -94
View File
@@ -1,4 +1,9 @@
use dioxus_rsx::{AttributeType, BodyNode, ElementAttrValue, ForLoop, IfChain, IfmtInput};
use crate::buffer::Buffer;
use crate::collect_macros::byte_offset;
use dioxus_rsx::{
Attribute as AttributeType, AttributeName, AttributeValue as ElementAttrValue, BodyNode,
Component, Element, ForLoop, IfChain, Spread, TemplateBody,
};
use proc_macro2::{LineColumn, Span};
use quote::ToTokens;
use std::{
@@ -7,32 +12,15 @@ use std::{
};
use syn::{spanned::Spanned, token::Brace, Expr};
use crate::buffer::Buffer;
use crate::ifmt_to_string;
#[derive(Debug)]
pub struct Writer<'a> {
pub raw_src: &'a str,
pub src: Vec<&'a str>,
pub cached_formats: HashMap<Location, String>,
pub cached_formats: HashMap<LineColumn, String>,
pub comments: VecDeque<usize>,
pub out: Buffer,
}
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub struct Location {
pub line: usize,
pub col: usize,
}
impl Location {
pub fn new(start: LineColumn) -> Self {
Self {
line: start.line,
col: start.column,
}
}
}
impl<'a> Writer<'a> {
pub fn new(raw_src: &'a str) -> Self {
let src = raw_src.lines().collect();
@@ -45,20 +33,143 @@ impl<'a> Writer<'a> {
}
}
pub fn consume(self) -> Option<String> {
Some(self.out.buf)
}
pub fn write_rsx_call(&mut self, body: &TemplateBody) -> Result {
match body.roots.len() {
0 => {}
1 if matches!(body.roots[0], BodyNode::Text(_)) => {
write!(self.out, " ")?;
self.write_ident(&body.roots[0])?;
write!(self.out, " ")?;
}
_ => self.write_body_indented(&body.roots)?,
}
Ok(())
}
// Expects to be written directly into place
pub fn write_ident(&mut self, node: &BodyNode) -> Result {
match node {
BodyNode::Element(el) => self.write_element(el),
BodyNode::Component(component) => self.write_component(component),
BodyNode::Text(text) => self.out.write_text(text),
BodyNode::Text(text) => self.out.write_text(&text.input),
BodyNode::RawExpr(exp) => self.write_raw_expr(exp.span()),
BodyNode::ForLoop(forloop) => self.write_for_loop(forloop),
BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain),
}
}
pub fn consume(self) -> Option<String> {
Some(self.out.buf)
pub fn write_element(&mut self, el: &Element) -> Result {
let Element {
name,
raw_attributes: attributes,
children,
spreads,
brace,
..
} = el;
/*
1. Write the tag
2. Write the key
3. Write the attributes
4. Write the children
*/
write!(self.out, "{name} {{")?;
self.write_rsx_block(attributes, spreads, children, brace)?;
write!(self.out, "}}")?;
Ok(())
}
pub fn write_component(
&mut self,
Component {
name,
fields,
children,
generics,
spreads,
brace,
..
}: &Component,
) -> Result {
// Write the path by to_tokensing it and then removing all whitespace
let mut name = name.to_token_stream().to_string();
name.retain(|c| !c.is_whitespace());
write!(self.out, "{name}")?;
// Same idea with generics, write those via the to_tokens method and then remove all whitespace
if let Some(generics) = generics {
let mut written = generics.to_token_stream().to_string();
written.retain(|c| !c.is_whitespace());
write!(self.out, "{written}")?;
}
write!(self.out, " {{")?;
self.write_rsx_block(fields, spreads, &children.roots, brace)?;
write!(self.out, "}}")?;
Ok(())
}
pub fn write_raw_expr(&mut self, placement: Span) -> Result {
/*
We want to normalize the expr to the appropriate indent level.
*/
let start = placement.start();
let end = placement.end();
// if the expr is on one line, just write it directly
if start.line == end.line {
// split counting utf8 chars
let start = byte_offset(self.raw_src, start);
let end = byte_offset(self.raw_src, end);
let row = self.raw_src[start..end].trim();
write!(self.out, "{row}")?;
return Ok(());
}
// If the expr is multiline, we want to collect all of its lines together and write them out properly
// This involves unshifting the first line if it's aligned
let first_line = &self.src[start.line - 1];
write!(self.out, "{}", &first_line[start.column..].trim_start())?;
let prev_block_indent_level = self.out.indent.count_indents(first_line);
for (id, line) in self.src[start.line..end.line].iter().enumerate() {
writeln!(self.out)?;
// check if this is the last line
let line = {
if id == (end.line - start.line) - 1 {
&line[..end.column]
} else {
line
}
};
// trim the leading whitespace
let previous_indent = self.out.indent.count_indents(line);
let offset = previous_indent.saturating_sub(prev_block_indent_level);
let required_indent = self.out.indent_level + offset;
self.out.write_tabs(required_indent)?;
let line = line.trim_start();
write!(self.out, "{line}")?;
}
Ok(())
}
pub fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
@@ -123,28 +234,13 @@ impl<'a> Writer<'a> {
}
pub fn write_body_no_indent(&mut self, children: &[BodyNode]) -> Result {
let last_child = children.len();
let iter = children.iter().peekable().enumerate();
for (idx, child) in iter {
for child in children {
if self.current_span_is_primary(child.span()) {
self.write_comments(child.span())?;
}
};
match child {
// check if the expr is a short
BodyNode::RawExpr { .. } => {
self.out.tabbed_line()?;
self.write_ident(child)?;
if idx != last_child - 1 {
write!(self.out, ",")?;
}
}
_ => {
self.out.tabbed_line()?;
self.write_ident(child)?;
}
}
self.out.tabbed_line()?;
self.write_ident(child)?;
}
Ok(())
@@ -155,31 +251,35 @@ impl<'a> Writer<'a> {
ElementAttrValue::AttrOptionalExpr { condition, value } => {
let condition_len = self.retrieve_formatted_expr(condition).len();
let value_len = self.attr_value_len(value);
condition_len + value_len + 6
}
ElementAttrValue::AttrLiteral(lit) => ifmt_to_string(lit).len(),
ElementAttrValue::AttrLiteral(lit) => lit.to_string().len(),
ElementAttrValue::Shorthand(expr) => expr.span().line_length(),
ElementAttrValue::AttrExpr(expr) => {
let out = self.retrieve_formatted_expr(expr);
if out.contains('\n') {
100000
} else {
out.len()
}
}
ElementAttrValue::EventTokens(tokens) => {
let as_str = self.retrieve_formatted_expr(tokens);
if as_str.contains('\n') {
100000
} else {
as_str.len()
}
}
ElementAttrValue::AttrExpr(expr) => expr
.as_expr()
.map(|expr| self.attr_expr_len(&expr))
.unwrap_or(100000),
ElementAttrValue::EventTokens(closure) => closure
.as_expr()
.map(|expr| self.attr_expr_len(&expr))
.unwrap_or(100000),
}
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[AttributeType]) -> usize {
fn attr_expr_len(&mut self, expr: &Expr) -> usize {
let out = self.retrieve_formatted_expr(expr);
if out.contains('\n') {
100000
} else {
out.len()
}
}
pub(crate) fn is_short_attrs(
&mut self,
attributes: &[AttributeType],
spreads: &[Spread],
) -> usize {
let mut total = 0;
// No more than 3 attributes before breaking the line
@@ -188,8 +288,8 @@ impl<'a> Writer<'a> {
}
for attr in attributes {
if self.current_span_is_primary(attr.start()) {
'line: for line in self.src[..attr.start().start().line - 1].iter().rev() {
if self.current_span_is_primary(attr.span()) {
'line: for line in self.src[..attr.span().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return 100000,
(_, true) => continue 'line,
@@ -198,39 +298,37 @@ impl<'a> Writer<'a> {
}
}
match attr {
AttributeType::Named(attr) => {
let name_len = match &attr.attr.name {
dioxus_rsx::ElementAttrName::BuiltIn(name) => {
let name = name.to_string();
name.len()
}
dioxus_rsx::ElementAttrName::Custom(name) => name.value().len() + 2,
};
total += name_len;
//
if attr.attr.value.is_shorthand() {
total += 2;
} else {
total += self.attr_value_len(&attr.attr.value);
}
}
AttributeType::Spread(expr) => {
let expr_len = self.retrieve_formatted_expr(expr).len();
total += expr_len + 3;
let name_len = match &attr.name {
AttributeName::BuiltIn(name) => {
let name = name.to_string();
name.len()
}
AttributeName::Custom(name) => name.value().len() + 2,
AttributeName::Spread(_) => unreachable!(),
};
total += name_len;
//
if attr.can_be_shorthand() {
total += 2;
} else {
total += self.attr_value_len(&attr.value);
}
total += 6;
}
for spread in spreads {
let expr_len = self.retrieve_formatted_expr(&spread.expr).len();
total += expr_len + 3;
}
total
}
#[allow(clippy::map_entry)]
pub fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
let loc = Location::new(expr.span().start());
let loc = expr.span().start();
if !self.cached_formats.contains_key(&loc) {
let formatted = self.unparse_expr(expr);
@@ -254,7 +352,7 @@ impl<'a> Writer<'a> {
return Ok(());
}
self.write_body_indented(&forloop.body)?;
self.write_body_indented(&forloop.body.roots)?;
self.out.tabbed_line()?;
write!(self.out, "}}")?;
@@ -280,7 +378,7 @@ impl<'a> Writer<'a> {
self.write_inline_expr(cond)?;
self.write_body_indented(then_branch)?;
self.write_body_indented(&then_branch.roots)?;
if let Some(else_if_branch) = else_if_branch {
// write the closing bracket and else
@@ -292,7 +390,7 @@ impl<'a> Writer<'a> {
self.out.tabbed_line()?;
write!(self.out, "}} else {{")?;
self.write_body_indented(else_branch)?;
self.write_body_indented(&else_branch.roots)?;
branch = None;
} else {
branch = None;
@@ -329,13 +427,6 @@ impl<'a> Writer<'a> {
Ok(())
}
pub(crate) fn key_len(&self, key: Option<&IfmtInput>) -> usize {
match key {
Some(key) => ifmt_to_string(key).len() + 5,
None => 0,
}
}
}
pub(crate) trait SpanLength {