* fix: don't panic when writing blocks out * also fix serialization for hotreload * fix windows line endings
966 lines
30 KiB
Rust
966 lines
30 KiB
Rust
use crate::{buffer::Buffer, IndentOptions};
|
|
use dioxus_rsx::*;
|
|
use proc_macro2::{LineColumn, Span};
|
|
use quote::ToTokens;
|
|
use std::{
|
|
collections::{HashMap, VecDeque},
|
|
fmt::{Result, Write},
|
|
};
|
|
use syn::{spanned::Spanned, token::Brace, Expr};
|
|
|
|
#[derive(Debug)]
|
|
pub struct Writer<'a> {
|
|
pub raw_src: &'a str,
|
|
pub src: Vec<&'a str>,
|
|
pub cached_formats: HashMap<LineColumn, String>,
|
|
pub out: Buffer,
|
|
pub invalid_exprs: Vec<Span>,
|
|
}
|
|
|
|
impl<'a> Writer<'a> {
|
|
pub fn new(raw_src: &'a str, indent: IndentOptions) -> Self {
|
|
Self {
|
|
src: raw_src.lines().collect(),
|
|
raw_src,
|
|
out: Buffer {
|
|
indent,
|
|
..Default::default()
|
|
},
|
|
cached_formats: HashMap::new(),
|
|
invalid_exprs: Vec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn consume(self) -> Option<String> {
|
|
Some(self.out.buf)
|
|
}
|
|
|
|
pub fn write_rsx_call(&mut self, body: &TemplateBody) -> Result {
|
|
if body.roots.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
if Self::is_short_rsx_call(&body.roots) {
|
|
write!(self.out, " ")?;
|
|
self.write_ident(&body.roots[0])?;
|
|
write!(self.out, " ")?;
|
|
} else {
|
|
self.out.new_line()?;
|
|
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.write_text_node(text),
|
|
BodyNode::RawExpr(expr) => self.write_expr_node(expr),
|
|
BodyNode::ForLoop(forloop) => self.write_for_loop(forloop),
|
|
BodyNode::IfChain(ifchain) => self.write_if_chain(ifchain),
|
|
}?;
|
|
|
|
let span = Self::final_span_of_node(node);
|
|
|
|
self.write_inline_comments(span.end(), 0)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// Check if the rsx call is short enough to be inlined
|
|
pub(crate) fn is_short_rsx_call(roots: &[BodyNode]) -> bool {
|
|
// eventually I want to use the _text length, so shutup now
|
|
#[allow(clippy::match_like_matches_macro)]
|
|
match roots {
|
|
[] => true,
|
|
[BodyNode::Text(_text)] => true,
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
fn write_element(&mut self, el: &Element) -> Result {
|
|
let Element {
|
|
name,
|
|
raw_attributes: attributes,
|
|
children,
|
|
spreads,
|
|
brace,
|
|
..
|
|
} = el;
|
|
|
|
write!(self.out, "{name} ")?;
|
|
self.write_rsx_block(attributes, spreads, children, &brace.unwrap_or_default())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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.unwrap_or_default())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_text_node(&mut self, text: &TextNode) -> Result {
|
|
self.out.write_text(&text.input)
|
|
}
|
|
|
|
fn write_expr_node(&mut self, expr: &ExprNode) -> Result {
|
|
self.write_partial_expr(expr.expr.as_expr(), expr.span())
|
|
}
|
|
|
|
fn write_for_loop(&mut self, forloop: &ForLoop) -> std::fmt::Result {
|
|
write!(
|
|
self.out,
|
|
"for {} in ",
|
|
forloop.pat.clone().into_token_stream(),
|
|
)?;
|
|
|
|
self.write_inline_expr(&forloop.expr)?;
|
|
|
|
if forloop.body.is_empty() {
|
|
write!(self.out, "}}")?;
|
|
return Ok(());
|
|
}
|
|
|
|
self.out.new_line()?;
|
|
self.write_body_indented(&forloop.body.roots)?;
|
|
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "}}")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_if_chain(&mut self, ifchain: &IfChain) -> std::fmt::Result {
|
|
// Recurse in place by setting the next chain
|
|
let mut branch = Some(ifchain);
|
|
|
|
while let Some(chain) = branch {
|
|
let IfChain {
|
|
if_token,
|
|
cond,
|
|
then_branch,
|
|
else_if_branch,
|
|
else_branch,
|
|
..
|
|
} = chain;
|
|
|
|
write!(self.out, "{} ", if_token.to_token_stream(),)?;
|
|
|
|
self.write_inline_expr(cond)?;
|
|
|
|
self.out.new_line()?;
|
|
self.write_body_indented(&then_branch.roots)?;
|
|
|
|
if let Some(else_if_branch) = else_if_branch {
|
|
// write the closing bracket and else
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "}} else ")?;
|
|
|
|
branch = Some(else_if_branch);
|
|
} else if let Some(else_branch) = else_branch {
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "}} else {{")?;
|
|
|
|
self.out.new_line()?;
|
|
self.write_body_indented(&else_branch.roots)?;
|
|
branch = None;
|
|
} else {
|
|
branch = None;
|
|
}
|
|
}
|
|
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "}}")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// An expression within a for or if block that might need to be spread out across several lines
|
|
fn write_inline_expr(&mut self, expr: &Expr) -> std::fmt::Result {
|
|
let unparsed = self.unparse_expr(expr);
|
|
let mut lines = unparsed.lines();
|
|
let first_line = lines.next().ok_or(std::fmt::Error)?;
|
|
|
|
write!(self.out, "{first_line}")?;
|
|
|
|
let mut was_multiline = false;
|
|
|
|
for line in lines {
|
|
was_multiline = true;
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "{line}")?;
|
|
}
|
|
|
|
if was_multiline {
|
|
self.out.tabbed_line()?;
|
|
write!(self.out, "{{")?;
|
|
} else {
|
|
write!(self.out, " {{")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
// Push out the indent level and write each component, line by line
|
|
fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
|
|
self.out.indent_level += 1;
|
|
self.write_body_nodes(children)?;
|
|
self.out.indent_level -= 1;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn write_body_nodes(&mut self, children: &[BodyNode]) -> Result {
|
|
let mut iter = children.iter().peekable();
|
|
|
|
while let Some(child) = iter.next() {
|
|
if self.current_span_is_primary(child.span().start()) {
|
|
self.write_comments(child.span().start())?;
|
|
};
|
|
self.out.tab()?;
|
|
self.write_ident(child)?;
|
|
if iter.peek().is_some() {
|
|
self.out.new_line()?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
/// 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
|
|
fn write_rsx_block(
|
|
&mut self,
|
|
attributes: &[Attribute],
|
|
spreads: &[Spread],
|
|
children: &[BodyNode],
|
|
brace: &Brace,
|
|
) -> Result {
|
|
#[derive(Debug)]
|
|
enum ShortOptimization {
|
|
/// Special because we want to print the closing bracket immediately
|
|
///
|
|
/// IE
|
|
/// `div {}` instead of `div { }`
|
|
Empty,
|
|
|
|
/// Special optimization to put everything on the same line and add some buffer spaces
|
|
///
|
|
/// IE
|
|
///
|
|
/// `div { "asdasd" }` instead of a multiline variant
|
|
Oneliner,
|
|
|
|
/// Optimization where children flow but props remain fixed on top
|
|
PropsOnTop,
|
|
|
|
/// The noisiest optimization where everything flows
|
|
NoOpt,
|
|
}
|
|
|
|
// Write the opening brace
|
|
write!(self.out, "{{")?;
|
|
|
|
// 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, spreads);
|
|
let is_short_attr_list = (attr_len + self.out.indent_level * 4) < 80;
|
|
let children_len = self
|
|
.is_short_children(children)
|
|
.map_err(|_| std::fmt::Error)?;
|
|
let is_small_children = children_len.is_some();
|
|
|
|
// if we have one long attribute 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
|
|
// However if we have childrne we need to just spread them out for readability
|
|
if !is_short_attr_list && attributes.len() <= 1 && spreads.is_empty() {
|
|
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 {
|
|
if children_len.unwrap() + attr_len + self.out.indent_level * 4 < 100 {
|
|
opt_level = ShortOptimization::Oneliner;
|
|
} else {
|
|
opt_level = ShortOptimization::PropsOnTop;
|
|
}
|
|
}
|
|
|
|
// If there's nothing at all, empty optimization
|
|
if attributes.is_empty() && children.is_empty() && spreads.is_empty() {
|
|
opt_level = ShortOptimization::Empty;
|
|
|
|
// Write comments if they exist
|
|
self.write_inline_comments(brace.span.span().start(), 1)?;
|
|
self.write_todo_body(brace)?;
|
|
}
|
|
|
|
// multiline handlers bump everything down
|
|
if attr_len > 1000 || self.out.indent.split_line_attributes() {
|
|
opt_level = ShortOptimization::NoOpt;
|
|
}
|
|
|
|
let has_children = !children.is_empty();
|
|
|
|
match opt_level {
|
|
ShortOptimization::Empty => {}
|
|
ShortOptimization::Oneliner => {
|
|
write!(self.out, " ")?;
|
|
|
|
self.write_attributes(attributes, spreads, true, brace, has_children)?;
|
|
|
|
if !children.is_empty() && !attributes.is_empty() {
|
|
write!(self.out, " ")?;
|
|
}
|
|
|
|
let mut children_iter = children.iter().peekable();
|
|
while let Some(child) = children_iter.next() {
|
|
self.write_ident(child)?;
|
|
if children_iter.peek().is_some() {
|
|
write!(self.out, " ")?;
|
|
}
|
|
}
|
|
|
|
write!(self.out, " ")?;
|
|
}
|
|
|
|
ShortOptimization::PropsOnTop => {
|
|
if !attributes.is_empty() {
|
|
write!(self.out, " ")?;
|
|
}
|
|
|
|
self.write_attributes(attributes, spreads, true, brace, has_children)?;
|
|
|
|
if !children.is_empty() {
|
|
self.out.new_line()?;
|
|
self.write_body_indented(children)?;
|
|
}
|
|
|
|
self.out.tabbed_line()?;
|
|
}
|
|
|
|
ShortOptimization::NoOpt => {
|
|
self.write_inline_comments(brace.span.span().start(), 1)?;
|
|
self.out.new_line()?;
|
|
self.write_attributes(attributes, spreads, false, brace, has_children)?;
|
|
|
|
if !children.is_empty() {
|
|
self.out.new_line()?;
|
|
self.write_body_indented(children)?;
|
|
}
|
|
|
|
self.out.tabbed_line()?;
|
|
}
|
|
}
|
|
|
|
// Write trailing comments
|
|
if matches!(
|
|
opt_level,
|
|
ShortOptimization::NoOpt | ShortOptimization::PropsOnTop
|
|
) && self.leading_row_is_empty(brace.span.span().end())
|
|
{
|
|
let comments = self.accumulate_comments(brace.span.span().end());
|
|
if !comments.is_empty() {
|
|
self.apply_comments(comments)?;
|
|
self.out.tab()?;
|
|
}
|
|
}
|
|
|
|
write!(self.out, "}}")?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_attributes(
|
|
&mut self,
|
|
attributes: &[Attribute],
|
|
spreads: &[Spread],
|
|
props_same_line: bool,
|
|
brace: &Brace,
|
|
has_children: bool,
|
|
) -> Result {
|
|
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();
|
|
|
|
let has_attributes = !attributes.is_empty() || !spreads.is_empty();
|
|
|
|
while let Some(attr) = attr_iter.next() {
|
|
self.out.indent_level += 1;
|
|
|
|
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 !props_same_line {
|
|
self.out.indented_tab()?;
|
|
}
|
|
|
|
match attr {
|
|
AttrType::Attr(attr) => self.write_attribute(attr)?,
|
|
AttrType::Spread(attr) => self.write_spread_attribute(&attr.expr)?,
|
|
}
|
|
|
|
let span = match attr {
|
|
AttrType::Attr(attr) => attr
|
|
.comma
|
|
.as_ref()
|
|
.map(|c| c.span())
|
|
.unwrap_or_else(|| self.final_span_of_attr(attr)),
|
|
AttrType::Spread(attr) => attr.span(),
|
|
};
|
|
|
|
let has_more = attr_iter.peek().is_some();
|
|
let should_finish_comma = has_attributes && has_children || !props_same_line;
|
|
|
|
if has_more || should_finish_comma {
|
|
write!(self.out, ",")?;
|
|
}
|
|
|
|
if !props_same_line {
|
|
self.write_inline_comments(span.end(), 0)?;
|
|
}
|
|
|
|
if props_same_line && !has_more {
|
|
self.write_inline_comments(span.end(), 0)?;
|
|
}
|
|
|
|
if props_same_line && has_more {
|
|
write!(self.out, " ")?;
|
|
}
|
|
|
|
if !props_same_line && has_more {
|
|
self.out.new_line()?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
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!(),
|
|
}
|
|
}
|
|
|
|
fn write_attribute_value(&mut self, value: &AttributeValue) -> Result {
|
|
match value {
|
|
AttributeValue::IfExpr(if_chain) => {
|
|
self.write_attribute_if_chain(if_chain)?;
|
|
}
|
|
AttributeValue::AttrLiteral(value) => {
|
|
write!(self.out, "{value}")?;
|
|
}
|
|
AttributeValue::Shorthand(value) => {
|
|
write!(self.out, "{value}")?;
|
|
}
|
|
AttributeValue::EventTokens(closure) => {
|
|
self.out.indent_level += 1;
|
|
self.write_partial_expr(closure.as_expr(), closure.span())?;
|
|
self.out.indent_level -= 1;
|
|
}
|
|
AttributeValue::AttrExpr(value) => {
|
|
self.out.indent_level += 1;
|
|
self.write_partial_expr(value.as_expr(), value.span())?;
|
|
self.out.indent_level -= 1;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_attribute_if_chain(&mut self, if_chain: &IfAttributeValue) -> Result {
|
|
let cond = self.unparse_expr(&if_chain.condition);
|
|
write!(self.out, "if {cond} {{ ")?;
|
|
self.write_attribute_value(&if_chain.then_value)?;
|
|
write!(self.out, " }}")?;
|
|
match if_chain.else_value.as_deref() {
|
|
Some(AttributeValue::IfExpr(else_if_chain)) => {
|
|
write!(self.out, " else ")?;
|
|
self.write_attribute_if_chain(else_if_chain)?;
|
|
}
|
|
Some(other) => {
|
|
write!(self.out, " else {{ ")?;
|
|
self.write_attribute_value(other)?;
|
|
write!(self.out, " }}")?;
|
|
}
|
|
None => {}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_attr_comments(&mut self, brace: &Brace, attr_span: Span) -> Result {
|
|
// There's a chance this line actually shares the same line as the previous
|
|
// Only write comments if the comments actually belong to this line
|
|
//
|
|
// to do this, we check if the attr span starts on the same line as the brace
|
|
// if it doesn't, we write the comments
|
|
let brace_line = brace.span.span().start().line;
|
|
let attr_line = attr_span.start().line;
|
|
|
|
if brace_line != attr_line {
|
|
self.write_comments(attr_span.start())?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_inline_comments(&mut self, final_span: LineColumn, offset: usize) -> Result {
|
|
let line = final_span.line;
|
|
let column = final_span.column;
|
|
let Some(src_line) = self.src.get(line - 1) else {
|
|
return Ok(());
|
|
};
|
|
|
|
// the line might contain emoji or other unicode characters - this will cause issues
|
|
let Some(mut whitespace) = src_line.get(column..).map(|s| s.trim()) else {
|
|
return Ok(());
|
|
};
|
|
|
|
if whitespace.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
whitespace = whitespace[offset..].trim();
|
|
|
|
if whitespace.starts_with("//") {
|
|
write!(self.out, " {whitespace}")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
fn accumulate_comments(&mut self, loc: LineColumn) -> VecDeque<usize> {
|
|
// collect all comments upwards
|
|
// make sure we don't collect the comments of the node that we're currently under.
|
|
let start = loc;
|
|
let line_start = start.line - 1;
|
|
|
|
let mut comments = VecDeque::new();
|
|
|
|
let Some(lines) = self.src.get(..line_start) else {
|
|
return comments;
|
|
};
|
|
|
|
for (id, line) in lines.iter().enumerate().rev() {
|
|
if line.trim().starts_with("//") || line.is_empty() && id != 0 {
|
|
if id != 0 {
|
|
comments.push_front(id);
|
|
}
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
comments
|
|
}
|
|
fn apply_comments(&mut self, mut comments: VecDeque<usize>) -> Result {
|
|
while let Some(comment_line) = comments.pop_front() {
|
|
let Some(line) = self.src.get(comment_line) else {
|
|
continue;
|
|
};
|
|
|
|
let line = &line.trim();
|
|
|
|
if line.is_empty() {
|
|
self.out.new_line()?;
|
|
} else {
|
|
self.out.tab()?;
|
|
writeln!(self.out, "{}", line.trim())?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn write_comments(&mut self, loc: LineColumn) -> Result {
|
|
let comments = self.accumulate_comments(loc);
|
|
self.apply_comments(comments)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn attr_value_len(&mut self, value: &AttributeValue) -> usize {
|
|
match value {
|
|
AttributeValue::IfExpr(if_chain) => {
|
|
let condition_len = self.retrieve_formatted_expr(&if_chain.condition).len();
|
|
let value_len = self.attr_value_len(&if_chain.then_value);
|
|
let if_len = 2;
|
|
let brace_len = 2;
|
|
let space_len = 2;
|
|
let else_len = if_chain
|
|
.else_value
|
|
.as_ref()
|
|
.map(|else_value| self.attr_value_len(else_value) + 1)
|
|
.unwrap_or_default();
|
|
condition_len + value_len + if_len + brace_len + space_len + else_len
|
|
}
|
|
AttributeValue::AttrLiteral(lit) => lit.to_string().len(),
|
|
AttributeValue::Shorthand(expr) => {
|
|
let span = &expr.span();
|
|
span.end().line - span.start().line
|
|
}
|
|
AttributeValue::AttrExpr(expr) => expr
|
|
.as_expr()
|
|
.map(|expr| self.attr_expr_len(&expr))
|
|
.unwrap_or(100000),
|
|
AttributeValue::EventTokens(closure) => closure
|
|
.as_expr()
|
|
.map(|expr| self.attr_expr_len(&expr))
|
|
.unwrap_or(100000),
|
|
}
|
|
}
|
|
|
|
fn attr_expr_len(&mut self, expr: &Expr) -> usize {
|
|
let out = self.retrieve_formatted_expr(expr);
|
|
if out.contains('\n') {
|
|
100000
|
|
} else {
|
|
out.len()
|
|
}
|
|
}
|
|
|
|
fn is_short_attrs(&mut self, attributes: &[Attribute], spreads: &[Spread]) -> usize {
|
|
let mut total = 0;
|
|
|
|
// No more than 3 attributes before breaking the line
|
|
if attributes.len() > 3 {
|
|
return 100000;
|
|
}
|
|
|
|
for attr in attributes {
|
|
if self.current_span_is_primary(attr.span().start()) {
|
|
if let Some(lines) = self.src.get(..attr.span().start().line - 1) {
|
|
'line: for line in lines.iter().rev() {
|
|
match (line.trim().starts_with("//"), line.is_empty()) {
|
|
(true, _) => return 100000,
|
|
(_, true) => continue 'line,
|
|
_ => break 'line,
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
fn write_todo_body(&mut self, brace: &Brace) -> std::fmt::Result {
|
|
let span = brace.span.span();
|
|
let start = span.start();
|
|
let end = span.end();
|
|
|
|
if start.line == end.line {
|
|
return Ok(());
|
|
}
|
|
|
|
writeln!(self.out)?;
|
|
|
|
for idx in start.line..end.line {
|
|
let Some(line) = self.src.get(idx) else {
|
|
continue;
|
|
};
|
|
if line.trim().starts_with("//") {
|
|
for _ in 0..self.out.indent_level + 1 {
|
|
write!(self.out, " ")?
|
|
}
|
|
writeln!(self.out, "{}", line.trim())?;
|
|
}
|
|
}
|
|
|
|
for _ in 0..self.out.indent_level {
|
|
write!(self.out, " ")?
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_partial_expr(&mut self, expr: syn::Result<Expr>, src_span: Span) -> Result {
|
|
let Ok(expr) = expr else {
|
|
self.invalid_exprs.push(src_span);
|
|
return Err(std::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.tab()?;
|
|
write!(self.out, "{line}")?;
|
|
if lines.peek().is_none() {
|
|
write!(self.out, "")?;
|
|
} else {
|
|
writeln!(self.out)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn write_spread_attribute(&mut self, attr: &Expr) -> Result {
|
|
let formatted = self.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(())
|
|
}
|
|
|
|
// check if the children are short enough to be on the same line
|
|
// We don't have the notion of current line depth - each line tries to be < 80 total
|
|
// returns the total line length if it's short
|
|
// returns none if the length exceeds the limit
|
|
// I think this eventually becomes quadratic :(
|
|
fn is_short_children(&mut self, children: &[BodyNode]) -> syn::Result<Option<usize>> {
|
|
if children.is_empty() {
|
|
return Ok(Some(0));
|
|
}
|
|
|
|
// Any comments push us over the limit automatically
|
|
if self.children_have_comments(children) {
|
|
return Ok(None);
|
|
}
|
|
|
|
let res = match children {
|
|
[BodyNode::Text(ref text)] => Some(text.input.to_string_with_quotes().len()),
|
|
|
|
// TODO: let rawexprs to be inlined
|
|
[BodyNode::RawExpr(ref expr)] => {
|
|
let pretty = self.retrieve_formatted_expr(&expr.expr.as_expr()?);
|
|
if pretty.contains('\n') {
|
|
None
|
|
} else {
|
|
Some(pretty.len() + 2)
|
|
}
|
|
}
|
|
|
|
// TODO: let rawexprs to be inlined
|
|
[BodyNode::Component(ref comp)]
|
|
// basically if the component is completely empty, we can inline it
|
|
if comp.fields.is_empty()
|
|
&& comp.children.is_empty()
|
|
&& comp.spreads.is_empty() =>
|
|
{
|
|
Some(
|
|
comp.name
|
|
.segments
|
|
.iter()
|
|
.map(|s| s.ident.to_string().len() + 2)
|
|
.sum::<usize>(),
|
|
)
|
|
}
|
|
|
|
// Feedback on discord indicates folks don't like combining multiple children on the same line
|
|
// We used to do a lot of math to figure out if we should expand out the line, but folks just
|
|
// don't like it.
|
|
_ => None,
|
|
};
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
fn children_have_comments(&self, children: &[BodyNode]) -> bool {
|
|
for child in children {
|
|
if self.current_span_is_primary(child.span().start()) {
|
|
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
|
|
match (line.trim().starts_with("//"), line.is_empty()) {
|
|
(true, _) => return true,
|
|
(_, true) => continue 'line,
|
|
_ => break 'line,
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
false
|
|
}
|
|
|
|
// make sure the comments are actually relevant to this element.
|
|
// test by making sure this element is the primary element on this line (nothing else before it)
|
|
fn current_span_is_primary(&self, location: LineColumn) -> bool {
|
|
self.leading_row_is_empty(LineColumn {
|
|
line: location.line,
|
|
column: location.column + 1,
|
|
})
|
|
}
|
|
|
|
fn leading_row_is_empty(&self, location: LineColumn) -> bool {
|
|
let Some(line) = self.src.get(location.line - 1) else {
|
|
return false;
|
|
};
|
|
|
|
let Some(sub) = line.get(..location.column - 1) else {
|
|
return false;
|
|
};
|
|
|
|
sub.trim().is_empty()
|
|
}
|
|
|
|
#[allow(clippy::map_entry)]
|
|
fn retrieve_formatted_expr(&mut self, expr: &Expr) -> &str {
|
|
let loc = expr.span().start();
|
|
|
|
if !self.cached_formats.contains_key(&loc) {
|
|
let formatted = self.unparse_expr(expr);
|
|
self.cached_formats.insert(loc, formatted);
|
|
}
|
|
|
|
self.cached_formats
|
|
.get(&loc)
|
|
.expect("Just inserted the parsed expr, so it should be in the cache")
|
|
.as_str()
|
|
}
|
|
|
|
fn final_span_of_node(node: &BodyNode) -> Span {
|
|
// Write the trailing comments if there are any
|
|
// Get the ending span of the node
|
|
let span = match node {
|
|
BodyNode::Element(el) => el
|
|
.brace
|
|
.as_ref()
|
|
.map(|b| b.span.span())
|
|
.unwrap_or_else(|| el.name.span()),
|
|
BodyNode::Component(el) => el
|
|
.brace
|
|
.as_ref()
|
|
.map(|b| b.span.span())
|
|
.unwrap_or_else(|| el.name.span()),
|
|
BodyNode::Text(txt) => txt.input.span(),
|
|
BodyNode::RawExpr(exp) => exp.span(),
|
|
BodyNode::ForLoop(f) => f.brace.span.span(),
|
|
BodyNode::IfChain(i) => match i.else_brace {
|
|
Some(b) => b.span.span(),
|
|
None => i.then_brace.span.span(),
|
|
},
|
|
};
|
|
span
|
|
}
|
|
|
|
fn final_span_of_attr(&self, attr: &Attribute) -> Span {
|
|
match &attr.value {
|
|
AttributeValue::Shorthand(s) => s.span(),
|
|
AttributeValue::AttrLiteral(l) => l.span(),
|
|
AttributeValue::EventTokens(closure) => closure.body.span(),
|
|
AttributeValue::AttrExpr(exp) => exp.span(),
|
|
AttributeValue::IfExpr(ex) => ex
|
|
.else_value
|
|
.as_ref()
|
|
.map(|v| v.span())
|
|
.unwrap_or_else(|| ex.then_value.span()),
|
|
}
|
|
}
|
|
}
|