fix: some edge cases around autoformatting (#485)

* wip: remove comment support from autofmt

* fix: some edge cases around autoformatting

* fix: revert readme

* fix: failing test
This commit is contained in:
Jon Kelley authored and GitHub committed 2022-07-04 21:11:03 -04:00
1 parent 3a5d1a2ff8
commit 3a30b36f68
11 files changed
+536 -141

No files matched your search

+114 -33
View File
@@ -1,14 +1,34 @@
use std::fmt::{Result, Write};
use std::{
collections::HashMap,
fmt::{Result, Write},
};
use dioxus_rsx::BodyNode;
use dioxus_rsx::{BodyNode, ElementAttr, ElementAttrNamed};
use proc_macro2::{LineColumn, Span};
use syn::spanned::Spanned;
#[derive(Default, Debug)]
pub struct Buffer {
pub src: Vec<String>,
pub cached_formats: HashMap<Location, String>,
pub buf: String,
pub indent: usize,
}
#[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 Buffer {
// Create a new line and tab it to the current tab level
pub fn tabbed_line(&mut self) -> Result {
@@ -41,6 +61,7 @@ impl Buffer {
writeln!(self.buf)
}
// 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),
@@ -62,41 +83,12 @@ impl Buffer {
pub fn write_body_indented(&mut self, children: &[BodyNode]) -> Result {
self.indent += 1;
let mut comments = Vec::new();
// let mut had_comments = false;
// let mut comments = Vec::new();
for child in children {
// Exprs handle their own indenting/line breaks
if !matches!(child, BodyNode::RawExpr(_)) {
// collect all comments upwards
let start = child.span().start().line;
for (id, line) in self.src[..start - 1].iter().enumerate().rev() {
if line.trim().starts_with("//") || line.is_empty() {
comments.push(id);
} else {
break;
}
}
if comments.len() == 1 && self.src[comments[0]].is_empty() {
comments.pop();
}
let mut last_was_empty = false;
for comment_line in comments.drain(..).rev() {
let line = &self.src[comment_line];
if line.is_empty() {
if !last_was_empty {
self.new_line()?;
}
last_was_empty = true;
} else {
last_was_empty = false;
self.tabbed_line()?;
write!(self.buf, "{}", self.src[comment_line].trim())?;
}
}
self.tabbed_line()?;
}
@@ -106,4 +98,93 @@ impl Buffer {
self.indent -= 1;
Ok(())
}
pub(crate) fn is_short_attrs(&mut self, attributes: &[ElementAttrNamed]) -> usize {
attributes
.iter()
.map(|attr| match &attr.attr {
ElementAttr::AttrText { value, name } => {
value.value().len() + name.span().line_length() + 3
}
ElementAttr::AttrExpression { name, value } => {
value.span().line_length() + name.span().line_length() + 3
}
ElementAttr::CustomAttrText { value, name } => {
value.value().len() + name.value().len() + 3
}
ElementAttr::CustomAttrExpression { name, value } => {
name.value().len() + value.span().line_length() + 3
}
ElementAttr::EventTokens { tokens, name } => {
let location = Location::new(tokens.span().start());
let len = if let std::collections::hash_map::Entry::Vacant(e) =
self.cached_formats.entry(location)
{
let formatted = prettyplease::unparse_expr(tokens);
let len = if formatted.contains('\n') {
10000
} else {
formatted.len()
};
e.insert(formatted);
len
} else {
self.cached_formats[&location].len()
};
len + name.span().line_length() + 3
}
})
.sum()
}
pub fn retrieve_formatted_expr(&mut self, location: LineColumn) -> Option<String> {
self.cached_formats.remove(&Location::new(location))
}
}
trait SpanLength {
fn line_length(&self) -> usize;
}
impl SpanLength for Span {
fn line_length(&self) -> usize {
self.end().line - self.start().line
}
}
// collect all comments upwards
// let start = child.span().start().line;
// for (id, line) in self.src[..start - 1].iter().enumerate().rev() {
// if line.trim().starts_with("//") {
// // if line.trim().starts_with("//") || line.is_empty() {
// if line.is_empty() && id == 0 {
// break;
// }
// comments.push(id);
// } else {
// break;
// }
// }
// if comments.len() == 1 && self.src[comments[0]].is_empty() {
// comments.pop();
// }
// // let mut last_line_was_empty = false;
// had_comments = !comments.is_empty();
// for comment_line in comments.drain(..).rev() {
// let line = &self.src[comment_line];
// if line.is_empty() {
// continue;
// // if last_line_was_empty {
// // continue;
// // } else {
// // last_line_was_empty = true;
// // }
// }
// self.tabbed_line()?;
// write!(self.buf, "{}", self.src[comment_line].trim())?;
// }
+28 -12
View File
@@ -1,8 +1,8 @@
use crate::Buffer;
use crate::{buffer::Location, Buffer};
use dioxus_rsx::*;
use quote::ToTokens;
use std::fmt::{Result, Write};
use syn::AngleBracketedGenericArguments;
use syn::{spanned::Spanned, AngleBracketedGenericArguments};
enum ShortOptimization {
// Special because we want to print the closing bracket immediately
@@ -36,7 +36,8 @@ impl Buffer {
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let is_short_attr_list = self.is_short_fields(fields, manual_props).is_some();
let attr_len = self.field_len(fields, manual_props);
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
@@ -63,6 +64,11 @@ impl Buffer {
opt_level = ShortOptimization::Empty;
}
// multiline handlers bump everything down
if attr_len > 1000 {
opt_level = ShortOptimization::NoOpt;
}
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
@@ -179,17 +185,27 @@ impl Buffer {
Ok(())
}
pub fn is_short_fields(
&self,
pub fn field_len(
&mut self,
fields: &[ComponentField],
manual_props: &Option<syn::Expr>,
) -> Option<usize> {
) -> usize {
let attr_len = fields
.iter()
.map(|field| match &field.content {
ContentField::ManExpr(exp) => exp.to_token_stream().to_string().len(),
ContentField::Formatted(s) => s.value().len() ,
ContentField::OnHandlerRaw(_) => 100000,
ContentField::OnHandlerRaw(exp) | ContentField::ManExpr(exp) => {
let formatted = prettyplease::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>() + self.indent * 4;
@@ -197,18 +213,18 @@ impl Buffer {
Some(p) => {
let content = prettyplease::unparse_expr(p);
if content.len() + attr_len > 80 {
return None;
return 100000;
}
let mut lines = content.lines();
lines.next().unwrap();
if lines.next().is_none() {
Some(attr_len + content.len())
attr_len + content.len()
} else {
None
100000
}
}
None => Some(attr_len),
None => attr_len,
}
}
+56 -30
View File
@@ -1,7 +1,9 @@
use crate::{util::*, Buffer};
use crate::Buffer;
use dioxus_rsx::*;
use std::{fmt::Result, fmt::Write};
use syn::spanned::Spanned;
#[derive(Debug)]
enum ShortOptimization {
// Special because we want to print the closing bracket immediately
Empty,
@@ -41,7 +43,8 @@ impl Buffer {
let mut opt_level = ShortOptimization::NoOpt;
// check if we have a lot of attributes
let is_short_attr_list = is_short_attrs(attributes);
let attr_len = self.is_short_attrs(attributes);
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
@@ -68,6 +71,11 @@ impl Buffer {
opt_level = ShortOptimization::Empty;
}
// multiline handlers bump everything down
if attr_len > 1000 {
opt_level = ShortOptimization::NoOpt;
}
match opt_level {
ShortOptimization::Empty => {}
ShortOptimization::Oneliner => {
@@ -94,13 +102,22 @@ impl Buffer {
write!(self.buf, ",")?;
}
self.write_body_indented(children)?;
if !children.is_empty() {
self.write_body_indented(children)?;
}
self.tabbed_line()?;
}
ShortOptimization::NoOpt => {
self.write_attributes(attributes, key, false)?;
self.write_body_indented(children)?;
if !children.is_empty() && !attributes.is_empty() {
write!(self.buf, ",")?;
}
if !children.is_empty() {
self.write_body_indented(children)?;
}
self.tabbed_line()?;
}
}
@@ -171,7 +188,7 @@ impl Buffer {
}
ElementAttr::EventTokens { name, tokens } => {
let out = prettyplease::unparse_expr(tokens);
let out = self.retrieve_formatted_expr(tokens.span().start()).unwrap();
let mut lines = out.split('\n').peekable();
let first = lines.next().unwrap();
@@ -204,35 +221,42 @@ impl Buffer {
// returns the total line length if it's short
// returns none if the length exceeds the limit
// I think this eventually becomes quadratic :(
pub fn is_short_children(&self, children: &[BodyNode]) -> Option<usize> {
pub fn is_short_children(&mut self, children: &[BodyNode]) -> Option<usize> {
if children.is_empty() {
// todo: allow elements with comments but no children
// like div { /* comment */ }
return Some(0);
}
for child in children {
'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
match (line.trim().starts_with("//"), line.is_empty()) {
(true, _) => return None,
(_, true) => continue 'line,
_ => break 'line,
}
}
}
// for child in children {
// 'line: for line in self.src[..child.span().start().line - 1].iter().rev() {
// match (line.trim().starts_with("//"), line.is_empty()) {
// (true, _) => return None,
// (_, true) => continue 'line,
// _ => break 'line,
// }
// }
// }
match children {
[BodyNode::Text(ref text)] => Some(text.value().len()),
[BodyNode::Component(ref comp)] => {
let is_short_child = self.is_short_children(&comp.children);
let is_short_attrs = self.is_short_fields(&comp.fields, &comp.manual_props);
let attr_len = self.field_len(&comp.fields, &comp.manual_props);
match (is_short_child, is_short_attrs) {
(Some(child_len), Some(attrs_len)) => Some(child_len + attrs_len),
(Some(child_len), None) => Some(child_len),
(None, Some(attrs_len)) => Some(attrs_len),
(None, None) => None,
if attr_len > 80 {
None
} else {
self.is_short_children(&comp.children)
.map(|child_len| child_len + attr_len)
}
// let is_short_child = self.is_short_children(&comp.children);
// match (is_short_child, is_short_attrs) {
// (Some(child_len), Some(attrs_len)) => Some(child_len + attrs_len),
// (Some(child_len), None) => Some(child_len),
// (None, Some(attrs_len)) => Some(attrs_len),
// (None, None) => None,
// }
}
[BodyNode::RawExpr(ref _expr)] => {
// TODO: let rawexprs to be inlined
@@ -245,15 +269,17 @@ impl Buffer {
// }
None
}
[BodyNode::Element(ref el)] => self
.is_short_children(&el.children)
.map(|f| f + extract_attr_len(&el.attributes))
.and_then(|new_len| if new_len > 80 { None } else { Some(new_len) }),
[BodyNode::Element(ref el)] => {
let attr_len = self.is_short_attrs(&el.attributes);
if attr_len > 80 {
None
} else {
self.is_short_children(&el.children)
.map(|child_len| child_len + attr_len)
}
}
_ => None,
}
}
}
fn is_short_attrs(attrs: &[ElementAttrNamed]) -> bool {
extract_attr_len(attrs) < 80
}
+72 -6
View File
@@ -37,37 +37,62 @@ pub fn fmt_file(contents: &str) -> Vec<FormattedBlock> {
use triple_accel::{levenshtein_search, Match};
for Match { end, start, .. } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
for Match { end, start, k } in levenshtein_search(b"rsx! {", contents.as_bytes()) {
if k > 1 {
continue;
}
// ensure the marker is not nested
if start < last_bracket_end {
continue;
}
let indent_level = {
// walk backwards from start until we find a new line
let mut lines = contents[..start].lines().rev();
match lines.next() {
Some(line) => {
if line.starts_with("//") || line.starts_with("///") {
continue;
}
line.chars().take_while(|c| *c == ' ').count() / 4
}
None => 0,
}
};
let remaining = &contents[end - 1..];
let bracket_end = find_bracket_end(remaining).unwrap();
let sub_string = &contents[end..bracket_end + end - 1];
last_bracket_end = bracket_end + end - 1;
let new = fmt_block(sub_string).unwrap();
let stripped = &contents[end + 1..bracket_end + end - 1];
let mut new = fmt_block(sub_string, indent_level).unwrap();
if stripped == new {
if new.len() <= 80 && !new.contains('\n') {
new = format!(" {new} ");
}
let end_marker = end + bracket_end - indent_level * 4 - 1;
if new == contents[end..end_marker] {
continue;
}
formatted_blocks.push(FormattedBlock {
formatted: new,
start: end,
end: end + bracket_end - 1,
end: end_marker,
});
}
formatted_blocks
}
pub fn fmt_block(block: &str) -> Option<String> {
pub fn fmt_block(block: &str, indent_level: usize) -> Option<String> {
let mut buf = Buffer {
src: block.lines().map(|f| f.to_string()).collect(),
indent: indent_level,
..Buffer::default()
};
@@ -75,5 +100,46 @@ pub fn fmt_block(block: &str) -> Option<String> {
buf.write_body_indented(&body.roots).unwrap();
// writing idents leaves the final line ended at the end of the last ident
if buf.buf.contains('\n') {
buf.new_line().unwrap();
}
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);
dbg!(&block.formatted);
format!("{}{}{}", left, block.formatted, right)
}
// Apply all the blocks
pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
let mut out = String::new();
let mut last = 0;
for FormattedBlock {
formatted,
start,
end,
} in blocks
{
let prefix = &input[last..start];
out.push_str(prefix);
out.push_str(&formatted);
last = end;
}
let suffix = &input[last..];
out.push_str(suffix);
out
}
-16
View File
@@ -1,19 +1,3 @@
use dioxus_rsx::*;
// todo: use recursive or complete sizeing
pub fn extract_attr_len(attributes: &[ElementAttrNamed]) -> usize {
attributes
.iter()
.map(|attr| match &attr.attr {
ElementAttr::AttrText { value, .. } => value.value().len(),
ElementAttr::AttrExpression { .. } => 10,
ElementAttr::CustomAttrText { value, .. } => value.value().len(),
ElementAttr::CustomAttrExpression { .. } => 10,
ElementAttr::EventTokens { .. } => 1000000,
})
.sum()
}
pub fn find_bracket_end(contents: &str) -> Option<usize> {
let mut depth = 0;
+22
View File
@@ -0,0 +1,22 @@
#[test]
fn formats_file_properly() {
let src = include_str!("./samples/thing.rsx");
let formatted = dioxus_autofmt::fmt_file(src);
let out = dioxus_autofmt::apply_formats(src, formatted);
println!("{}", out);
}
#[test]
fn already_formatted_file_properly() {
let src = include_str!("./samples/pre.rsx");
let formatted = dioxus_autofmt::fmt_file(src);
dbg!(&formatted);
let out = dioxus_autofmt::apply_formats(src, formatted);
println!("{}", out);
}
+1 -1
View File
@@ -1,6 +1,6 @@
#![allow(unused)]
const SRC: &str = include_str!("./samples/all.rs");
const SRC: &str = include_str!("./samples/all.rsx");
fn body() -> &'static str {
&SRC[6..SRC.len() - 3]
File renamed without changes.
+3
View File
@@ -0,0 +1,3 @@
rsx! {
div {}
}
+227
View File
@@ -0,0 +1,227 @@
fn app(cx: Scope) -> Element {
cx.render(rsx! {
div {
div {
key: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
class: "asdasd",
}
}
h1 {"hi"}
h1 {"hi"}
h1 {"hi"}
h1 {"hi"}
div {
div {
key: "ddd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
class: "asd",
blah: 123,
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
onclick: move |_| {
let blah = 120;
true
},
div {
div { "hi" }
h2 { class: "asd" }
}
Component {}
// Generics
Component<Generic> {}
}
}
div { adsasd: "asd",
h1 { "asd" }
div {
div { "hello" }
div { "goodbye" }
div { class: "broccoli", div { "hi" } }
div { class: "broccolibroccolibroccolibroccolibroccolibroccolibroccolibroccolibroccolibroccoli",
div { "hi" }
}
div {
class: "alksdjasd",
onclick: move |_| {
liberty!();
},
div { "hi" }
}
commented {
// is unparalled
class: "asdasd",
// My genius
div { "hi" }
div {}
}
}
}
// Components
Component {
adsasd: "asd",
// this is a comment
onclick: move |_| {
let blah = 120;
let blah = 122;
}
}
div {
Component {
adsasd: "asd",
onclick: move |_| {
let a = a;
},
div { "thing" }
}
Component {
asdasd: "asdasd",
asdasd: "asdasdasdasdasdasdasdasdasdasd",
..Props {
a: 10,
b: 20
}
}
Component {
asdasd: "asdasd",
..Props {
a: 10,
b: 20,
c: {
fn main() {}
},
}
"content"
}
}
div {
a: "1234567891012345678910123456789101234567891012345678910123456789101234567891012345678910123456789101234567891012345678910",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123",
a: "123"
}
// Short attributes
div { a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123", a: "123" }
// Compression
h3 { class: "mb-2 text-xl font-bold", "Invite Member" }
a { class: "text-white", "Send invitation" }
// Props on tops
h3 { class: "mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold mb-2 text-xl font-bold",
"Invite Member"
}
// No children, minimal props
img { class: "mb-6 mx-auto h-24", src: "artemis-assets/images/friends.png", alt: "" }
// One level compression
div {
a { class: "py-2 px-3 bg-indigo-500 hover:bg-indigo-600 rounded text-xs text-white", href: "#", "Send invitation" }
}
// Tiny component
Component { a: 123 }
// Expressions
ul {
div {}
(0..10).map(|f| rsx! {
li { "hi" }
})
div {}
}
// Complex nesting with components
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" }
}
// Complex nesting with handlers
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 })
}
}
// No nesting
Component {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
}
// Component path
my::thing::Component {
adsasd: "asd",
onclick: move |_| {
let blah = 120;
}
}
})
}
+13 -43
View File
@@ -15,27 +15,18 @@ rsx! {
let formatted = fmt_file(src);
println!("{formatted:?}");
println!("{formatted:#?}");
}
#[test]
fn formats_valid_rust_src_with_indents() {
let mut src = r#"
let 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"}
}
div { h1 { "thing" } }
}
}
"#
@@ -43,12 +34,7 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape)
let formatted = fmt_file(&src);
let block = formatted.into_iter().next().unwrap();
src.replace_range(
block.start - 1..block.end + 1,
&format!("{{ {} }}", &block.formatted),
);
assert!(formatted.is_empty());
}
#[test]
@@ -59,23 +45,15 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape)
const ICON_SIZE: u32 = 36;
rsx! {
div {
h1 {"thing"}
}
div { h1 { "thing" } }
}
rsx! {
div {
Ball {
a: rsx!{
"asdasd"
}
a: rsx! {
"asdasd"
}
}
}
}
@@ -85,23 +63,15 @@ fn NavItem<'a>(cx: Scope, to: &'static str, children: Element<'a>, icon: Shape)
const ICON_SIZE: u32 = 36;
rsx! {
div {
h1 {"thing"}
}
div { h1 { "thing" } }
}
rsx! {
div {
Ball {
a: rsx!{
"asdasd"
}
a: rsx! {
"asdasd"
}
}
}
}
@@ -126,7 +96,7 @@ fn empty_blocks() {
let src = r###"
pub fn Alert(cx: Scope) -> Element {
cx.render(rsx! {
div { }
div {}
})
}
"###