Skip to content

fix(autofmt): preserve indentation inside multiline string literals#5659

Closed
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782992103-fix-fmt-multiline-string
Closed

fix(autofmt): preserve indentation inside multiline string literals#5659
devin-ai-integration[bot] wants to merge 1 commit into
mainfrom
devin/1782992103-fix-fmt-multiline-string

Conversation

@devin-ai-integration

Copy link
Copy Markdown

Summary

Fixes #5648. dx fmt was rewriting the interior of multiline string literals in rsx expressions (e.g. a format!("...\n ...") used as an attribute value), silently changing the string's value and shifting it further right (or left) on every format pass.

Two independent line-based transforms in dioxus-autofmt walked the pretty-printed expression line-by-line and blindly adjusted leading whitespace, without accounting for lines that fall inside a string literal:

  • writer::write_mulitiline_tokens added the current indent to every non-blank line → string grew right each pass.
  • prettier_please::unwrapped stripped the 4-space fn main() {} wrapper indent from every line → string shrank left each pass.

Fix: added a small string-literal state scanner and skip whitespace edits on lines that begin inside a string literal, so string contents are emitted verbatim.

// writer.rs (new, pub(crate))
enum StringScanState { Code, Str, RawStr(usize) }
fn scan_string_state(line: &str, start: StringScanState) -> StringScanState { ... }
// handles "...", r#"..."#, escapes, char literals, and // line comments
// write_mulitiline_tokens: only indent lines that start outside a string
if !starts_in_string && !line.trim().is_empty() { self.out.tab()?; }
string_state = scan_string_state(line, string_state);
// unwrapped: only strip the wrapper indent on lines that start outside a string
let out_line = if matches!(state, StringScanState::Code) {
    line.strip_prefix("    ").unwrap_or_default()
} else { line };
state = scan_string_state(line, state);

The reported input is now idempotent under dx fmt (both a normal and a raw multiline string are preserved exactly):

rsx! {
    input {
        placeholder: format!("bbb
                         {name}"),          // interior kept verbatim
        "data-shortcut": "alt+a",
    }
}

Testing

  • Added a twoway! regression fixture multiline-string (tests/wrong/multiline-string.{wrong.,}rsx, identical files → asserts idempotency) covering both a normal and a raw multiline string literal.
  • cargo test -p dioxus-autofmt (all samples + wrong-tests) and cargo clippy -p dioxus-autofmt pass.

Link to Devin session: https://app.devin.ai/sessions/9f5bdce719284acfb3022140fe900863
Requested by: @nicoburns

Co-Authored-By: Nico Burns <nico@nicoburns.com>
@devin-ai-integration devin-ai-integration Bot requested a review from a team as a code owner July 2, 2026 11:35
@nicoburns nicoburns self-assigned this Jul 2, 2026
@devin-ai-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@ealmloff

ealmloff commented Jul 2, 2026

Copy link
Copy Markdown
Member

We should avoid the issue in the first place instead of fixing it after we already format the code

@devin-ai-integration

Copy link
Copy Markdown
Author

Agreed that avoiding the corruption is nicer than repairing it — let me lay out what I found so we pick the right spot.

The string bytes get mangled by two independent, textual, line-by-line re-indent passes, and the offending literal usually lives inside a macro token stream (e.g. the "..." in format!(...)), so it isn't a syn Lit node we can easily reach from the expr AST:

  1. prettier_please::unwrapped — strips the 4-space fn main() { … } wrapper indent from every line (the // todo: set this to tab level hack).
  2. writer::{write_mulitiline_tokens, write_inline_expr, write_spread_attribute} — re-indents each line to slot the expr into the rsx tree.

So there's no single "format step" to guard; the value passes through both.

To truly avoid touching the bytes, the clean approach is mask-then-restore, mirroring the existing rsx! MARKER trick in prettier_please.rs: before handing the expr to prettyplease, walk the macro token streams (and AST lits) and swap every multiline string/byte/c-string Literal for a short single-line placeholder, then splice the original literal text back verbatim after all indentation is applied (at Writer::consume / in try_fmt_file). That requires threading a mask table through the Writer and recursing into Macro.tokens, and it has one behavioral side effect: prettyplease then makes line-wrapping decisions based on the short placeholder rather than the real string, so a call wrapping a long multiline string may format differently than today.

The current PR instead makes those two passes literal-aware (skip whitespace edits on lines that begin inside a string literal), which keeps prettyplease's wrapping faithful to the real content and is fully contained.

Happy to switch to the mask-then-restore version if you prefer that as the canonical fix — or if you had a specific location in mind for the guard, point me at it and I'll move it there.

@nicoburns nicoburns closed this Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

dx fmt bug

2 participants