diff --git a/crates/cli/tests/cli.rs b/crates/cli/tests/cli.rs index d2fded3..2a97799 100644 --- a/crates/cli/tests/cli.rs +++ b/crates/cli/tests/cli.rs @@ -350,7 +350,7 @@ fn explicit_rust_path_formats_sqlx_macros() { let out = std::fs::read_to_string(&file).expect("read"); assert!( out.contains( - "r#\"\n select id, name from users where org = $1 order by name\n \"#" + "r#\"\n select id, name\n from users\n where org = $1\n order by name\n \"#" ), "sqlx macro not formatted: {out}" ); @@ -390,7 +390,7 @@ fn directory_recursion_needs_embed_flag() { assert!( std::fs::read_to_string(dir.join("q.rs")) .expect("read") - .contains("select id, name from users"), + .contains("select id, name\n from users"), ); let status = squill() .args(["fmt", "--embed", "--check"]) @@ -432,7 +432,7 @@ fn go_host_files_format() { assert!( std::fs::read_to_string(&file) .expect("read") - .contains("`\n\tselect count(*) from t where a = 1\n\t`"), + .contains("`\n\tselect count(*)\n\tfrom t\n\twhere a = 1\n\t`"), ); let _ = std::fs::remove_dir_all(&dir); } diff --git a/crates/embed/src/lib.rs b/crates/embed/src/lib.rs index e31e959..adbde44 100644 --- a/crates/embed/src/lib.rs +++ b/crates/embed/src/lib.rs @@ -227,6 +227,9 @@ fn rewrite_literal( let host_indent = line_indent(source, literal_start); let mut format_options = *options; format_options.dialect = dialect; + // The author chose a multi-line literal: keep statements + // clause-per-line, never collapsed onto one line. + format_options.always_break_statements = true; format_options.indent_style = if host_indent.contains(' ') { formatter::IndentStyle::Spaces } else { diff --git a/crates/embed/tests/embed.rs b/crates/embed/tests/embed.rs index 5d48035..0319c45 100644 --- a/crates/embed/tests/embed.rs +++ b/crates/embed/tests/embed.rs @@ -110,11 +110,11 @@ fn multiline_literal_gets_quotes_on_own_lines() { let source = "fn main() {\n let q = sqlx::query!(\n r#\"SELECT id,name FROM users\n WHERE org = $1 ORDER BY name\"#\n );\n}\n"; let formatted = format_embedded(source, Host::Rust, RUST_SQLX_QUERY, &options()).expect("format"); - // The SQL fits on one line, so it stays one line — but on its own - // line between the quotes. + // Multi-line literals stay clause-per-line even when the SQL would + // fit on one line. assert_eq!( formatted, - "fn main() {\n let q = sqlx::query!(\n r#\"\n select id, name from users where org = $1 order by name\n \"#\n );\n}\n" + "fn main() {\n let q = sqlx::query!(\n r#\"\n select id, name\n from users\n where org = $1\n order by name\n \"#\n );\n}\n" ); assert!( !formatted.contains('\t'), @@ -156,7 +156,8 @@ fn go_smoke_test() { .to_string(); let formatted = format_embedded(&source, Host::Go, GO_DB_QUERY, &options()).expect("format"); assert!( - formatted.contains("`\n\tselect id, name from users where active order by name\n\t`"), + formatted + .contains("`\n\tselect id, name\n\tfrom users\n\twhere active\n\torder by name\n\t`"), "multi-line raw string not formatted: {formatted}" ); // Single-line strings stay byte-identical, even unformatted SQL. diff --git a/crates/formatter/src/lib.rs b/crates/formatter/src/lib.rs index c5a23cc..a4120ba 100644 --- a/crates/formatter/src/lib.rs +++ b/crates/formatter/src/lib.rs @@ -54,6 +54,11 @@ pub struct Options { /// sqlc-style `@name` parameters (see [`LexOptions::at_params`]); /// used when re-lexing for the safety check. pub at_params: bool, + /// Never collapse a statement onto one line (clause-per-line even + /// when it would fit). Used by embedding for multi-line string + /// literals, where the author already chose a vertical layout. Not + /// part of the CLI/config surface. + pub always_break_statements: bool, } impl Default for Options { @@ -65,6 +70,7 @@ impl Default for Options { quoting: IdentQuoting::default(), dialect: Dialect::default(), at_params: false, + always_break_statements: false, } } } @@ -116,7 +122,7 @@ fn format_cst_at(cst: &Cst, options: &Options, depth: u32) -> Formatted { let original = node.to_string(); let blank = pending_blank || leading_blank(&original); pending_blank = false; - match rules::lower_statement(node) { + match rules::lower_statement(node, options.always_break_statements) { Some(doc) => { let rendered = render(&doc, options); let safe = check::tokens_equivalent( @@ -348,7 +354,7 @@ fn relayout_statement(statement: &str, options: &Options, depth: u32) -> Option< if nodes.next().is_some() { return None; // expected exactly one statement } - let doc = rules::lower_statement(node)?; + let doc = rules::lower_statement(node, options.always_break_statements)?; let rendered = render(&doc, options); let piece = rendered.trim_end().to_string(); if !check::tokens_equivalent(statement, &piece, options.dialect, lex_options) @@ -373,5 +379,5 @@ fn line_indent(source: &str, offset: usize) -> String { /// Dev-tool access to the statement lowering (see examples/). #[doc(hidden)] pub fn debug_lower(node: &parser::syntax::SyntaxNode) -> Option { - rules::lower_statement(node) + rules::lower_statement(node, false) } diff --git a/crates/formatter/src/rules.rs b/crates/formatter/src/rules.rs index 2f2bd2a..ee002ae 100644 --- a/crates/formatter/src/rules.rs +++ b/crates/formatter/src/rules.rs @@ -17,7 +17,7 @@ use crate::doc::{ /// Lower one statement node to a document. Returns `None` for kinds the /// rules do not format (ErrorStatement — handled as verbatim upstream). -pub(crate) fn lower_statement(stmt: &SyntaxNode) -> Option { +pub(crate) fn lower_statement(stmt: &SyntaxNode, always_break: bool) -> Option { match stmt.kind() { SyntaxKind::SelectStmt | SyntaxKind::EmptyStmt @@ -97,6 +97,11 @@ pub(crate) fn lower_statement(stmt: &SyntaxNode) -> Option { _ => lowerer.dml_flow(&mut docs, stmt), } lowerer.flush_pending(&mut docs); + if always_break { + // Force the statement group broken: clause-per-line even + // when the statement would fit on one line. + docs.insert(0, break_parent()); + } header.push(group(concat(docs))); Some(concat(header)) }