dioxus_autofmt/
lib.rs

1#![doc = include_str!("../README.md")]
2#![doc(html_logo_url = "https://avatars.githubusercontent.com/u/79236386")]
3#![doc(html_favicon_url = "https://avatars.githubusercontent.com/u/79236386")]
4
5use crate::writer::*;
6use dioxus_rsx::{BodyNode, CallBody};
7use proc_macro2::{LineColumn, Span};
8use syn::parse::Parser;
9
10mod buffer;
11mod collect_macros;
12mod indent;
13mod prettier_please;
14mod writer;
15
16pub use indent::{IndentOptions, IndentType};
17
18/// A modification to the original file to be applied by an IDE
19///
20/// Right now this re-writes entire rsx! blocks at a time, instead of precise line-by-line changes.
21///
22/// In a "perfect" world we would have tiny edits to preserve things like cursor states and selections. The API here makes
23/// it possible to migrate to a more precise modification approach in the future without breaking existing code.
24///
25/// Note that this is tailored to VSCode's TextEdit API and not a general Diff API. Line numbers are not accurate if
26/// multiple edits are applied in a single file without tracking text shifts.
27#[derive(serde::Deserialize, serde::Serialize, Clone, Debug, PartialEq, Eq, Hash)]
28pub struct FormattedBlock {
29    /// The new contents of the block
30    pub formatted: String,
31
32    /// The line number of the first line of the block.
33    pub start: usize,
34
35    /// The end of the block, exclusive.
36    pub end: usize,
37}
38
39/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
40///
41/// It accepts
42#[deprecated(note = "Use try_fmt_file instead - this function panics on error.")]
43pub fn fmt_file(contents: &str, indent: IndentOptions) -> Vec<FormattedBlock> {
44    let parsed =
45        syn::parse_file(contents).expect("fmt_file should only be called on valid syn::File files");
46    try_fmt_file(contents, &parsed, indent).expect("Failed to format file")
47}
48
49/// Format a file into a list of `FormattedBlock`s to be applied by an IDE for autoformatting.
50///
51/// This function expects a complete file, not just a block of code. To format individual rsx! blocks, use fmt_block instead.
52///
53/// The point here is to provide precise modifications of a source file so an accompanying IDE tool can map these changes
54/// back to the file precisely.
55///
56/// Nested blocks of RSX will be handled automatically
57///
58/// This returns an error if the rsx itself is invalid.
59///
60/// Will early return if any of the expressions are not complete. Even though we *could* return the
61/// expressions, eventually we'll want to pass off expression formatting to rustfmt which will reject
62/// those.
63pub fn try_fmt_file(
64    contents: &str,
65    parsed: &syn::File,
66    indent: IndentOptions,
67) -> syn::Result<Vec<FormattedBlock>> {
68    let mut formatted_blocks = Vec::new();
69
70    let macros = collect_macros::collect_from_file(parsed);
71
72    // No macros, no work to do
73    if macros.is_empty() {
74        return Ok(formatted_blocks);
75    }
76
77    let mut writer = Writer::new(contents, indent);
78
79    // Don't parse nested macros
80    let mut end_span = LineColumn { column: 0, line: 0 };
81    for item in macros {
82        let macro_path = &item.path.segments[0].ident;
83
84        // this macro is inside the last macro we parsed, skip it
85        if macro_path.span().start() < end_span {
86            continue;
87        }
88
89        let body = item.parse_body_with(CallBody::parse_strict)?;
90
91        let rsx_start = macro_path.span().start();
92
93        writer.out.indent_level = writer
94            .out
95            .indent
96            .count_indents(writer.src.get(rsx_start.line - 1).unwrap_or(&""));
97
98        // TESTME
99        // Writing *should* not fail but it's possible that it does
100        if writer.write_rsx_call(&body).is_err() {
101            let span = writer.invalid_exprs.pop().unwrap_or_else(Span::call_site);
102            return Err(syn::Error::new(span, "Failed emit valid rsx - likely due to partially complete expressions in the rsx! macro"));
103        }
104
105        // writing idents leaves the final line ended at the end of the last ident
106        if writer.out.buf.contains('\n') {
107            _ = writer.out.new_line();
108            _ = writer.out.tab();
109        }
110
111        let span = item.delimiter.span().join();
112        let mut formatted = writer.out.buf.split_off(0);
113
114        let start = collect_macros::byte_offset(contents, span.start()) + 1;
115        let end = collect_macros::byte_offset(contents, span.end()) - 1;
116
117        // Rustfmt will remove the space between the macro and the opening paren if the macro is a single expression
118        let body_is_solo_expr = body.body.roots.len() == 1
119            && matches!(body.body.roots[0], BodyNode::RawExpr(_) | BodyNode::Text(_));
120
121        // If it's short, and it's not a single expression, and it's not empty, then we can collapse it
122        if formatted.len() <= 80
123            && !formatted.contains('\n')
124            && !body_is_solo_expr
125            && !formatted.trim().is_empty()
126        {
127            formatted = format!(" {formatted} ");
128        }
129
130        end_span = span.end();
131
132        if contents[start..end] == formatted {
133            continue;
134        }
135
136        formatted_blocks.push(FormattedBlock {
137            formatted,
138            start,
139            end,
140        });
141    }
142
143    Ok(formatted_blocks)
144}
145
146/// Write a Callbody (the rsx block) to a string
147///
148/// If the tokens can't be formatted, this returns None. This is usually due to an incomplete expression
149/// that passed partial expansion but failed to parse.
150pub fn write_block_out(body: &CallBody) -> Option<String> {
151    let mut buf = Writer::new("", IndentOptions::default());
152    buf.write_rsx_call(body).ok()?;
153    buf.consume()
154}
155
156pub fn fmt_block(block: &str, indent_level: usize, indent: IndentOptions) -> Option<String> {
157    let body = CallBody::parse_strict.parse_str(block).unwrap();
158
159    let mut buf = Writer::new(block, indent);
160    buf.out.indent_level = indent_level;
161    buf.write_rsx_call(&body).ok()?;
162
163    // writing idents leaves the final line ended at the end of the last ident
164    if buf.out.buf.contains('\n') {
165        buf.out.new_line().unwrap();
166    }
167
168    buf.consume()
169}
170
171// Apply all the blocks
172pub fn apply_formats(input: &str, blocks: Vec<FormattedBlock>) -> String {
173    let mut out = String::new();
174
175    let mut last = 0;
176
177    for FormattedBlock {
178        formatted,
179        start,
180        end,
181    } in blocks
182    {
183        let prefix = &input[last..start];
184        out.push_str(prefix);
185        out.push_str(&formatted);
186        last = end;
187    }
188
189    let suffix = &input[last..];
190    out.push_str(suffix);
191
192    out
193}