From ab9fe8893b4e5eed40c48349ca8bc983a8847d8c Mon Sep 17 00:00:00 2001 From: Hein Date: Tue, 6 Oct 2026 14:48:28 +0200 Subject: [PATCH] feat(format): case_collapse uses line_width via Doc-IR --- docs/config/default.pgtidy.yaml | 2 +- docs/todo.md | 9 ++++----- pkg/format/dml.go | 12 +++++------- pkg/format/dml_test.go | 15 +++++++++++++++ 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/docs/config/default.pgtidy.yaml b/docs/config/default.pgtidy.yaml index 9d9895e..242c923 100644 --- a/docs/config/default.pgtidy.yaml +++ b/docs/config/default.pgtidy.yaml @@ -65,5 +65,5 @@ binary_op_align: false # Align =, <>, || etc. vertically in WHERE/exp space_after_comma_in_calls: false # Space after , in function calls: func(a, b) case_when_wrap: false # Each WHEN โ€ฆ THEN on its own line case_end: new_line # END placement: same_line | new_line -case_collapse: false # Collapse short CASE expressions to one line +case_collapse: false # Collapse a CASE to one line when it fits line_width record_space_before_paren: false # Space before ( in ROW(โ€ฆ) / record constructors diff --git a/docs/todo.md b/docs/todo.md index ac7b7ad..3792cc6 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -167,7 +167,7 @@ Legend: โœ… done ยท ๐Ÿšง in progress ยท โฌœ not started function call at the token level โ€” formatted without space (known limitation). - Note: SQL keywords inside PL/pgSQL function bodies remain lowercase (matching the corpus golden files); casing is applied only to top-level DML. -- โœ… Doc-IR core (`pkg/format/doc.go`: Text/Line/SoftLine/Group/Indent/IfBreak + `Render`) and `line_width` (default 120, 0 = unlimited). First consumer: `where_wrap: when_long` (`dmlWhereWhenLong`). _Still TODO: move the remaining DML builders (select lists, CASE, joins, subqueries) from string assembly onto Doc._ +- โœ… Doc-IR core (`pkg/format/doc.go`: Text/Line/SoftLine/Group/Indent/IfBreak + `Render`) and `line_width` (default 120, 0 = unlimited). First consumer: `where_wrap: when_long` (`dmlWhereWhenLong`). _Still TODO: move the remaining DML builders (select lists, joins, subqueries) from string assembly onto Doc._ - _Still TODO: LSP range formatting._ ## โœ… Config expansion โ€” DataGrip settings parity @@ -272,10 +272,9 @@ Both the simple (`CASE x WHEN ...`) and searched (`CASE WHEN ...`) forms are sup one level. - `case_end` (default `new_line`): placement of the closing `END` when wrapped โ€” `new_line` on its own line, `same_line` glued to the last WHEN/ELSE line. -- `case_collapse` (default `false`): when `true` *and* the fully-inlined rendering is - โ‰ค `caseCollapseWidth` (60 chars), keeps the wrapped CASE on one line anyway, overriding - `case_when_wrap`; longer CASEs still wrap. (No Doc-IR/line-width awareness exists yet, so - this is a fixed length threshold rather than a true "does it fit the line" check.) +- `case_collapse` (default `false`): when `true`, keeps the wrapped CASE on one line anyway if the + fully-inlined rendering fits in `line_width` (default 120), overriding `case_when_wrap`; + longer CASEs still wrap. Rendered through the Doc-IR (`Group(IfBreak(wrapped, inline))`). - `record_space_before_paren` (default `false`): scoped to the `ROW` keyword specifically (`ROW(1, 2)` vs `ROW (1, 2)`) โ€” bare `(a, b)` record literals are indistinguishable from grouping parens at the token level, so this setting only fires on an explicit `ROW(`. diff --git a/pkg/format/dml.go b/pkg/format/dml.go index d0e3225..bfaeaa9 100644 --- a/pkg/format/dml.go +++ b/pkg/format/dml.go @@ -861,10 +861,6 @@ func alignSetItems(texts []string) []string { return out } -// caseCollapseWidth is the inline-length threshold under which case_collapse -// keeps a CASE expression on one line even when case_when_wrap is set. -const caseCollapseWidth = 60 - // dmlIsCaseStart reports whether t is a CASE keyword token. func dmlIsCaseStart(t cst.Tok) bool { return t.Tok.Kind == lexer.Ident && lowerASCII(t.Tok.Text) == "case" @@ -1001,10 +997,12 @@ func dmlFormatCase(toks []cst.Tok, st config.Style) string { if !st.CaseWhenWrap { return inline } - if st.CaseCollapse && len(inline) <= caseCollapseWidth { - return inline + wrapped := dmlCaseWrapped(caseKw, operand, whens, elseKw, elseText, haveElse, endKw, st) + if st.CaseCollapse { + // Collapse when the one-line form fits in line_width. + return Render(Group(IfBreak(Text(wrapped), Text(inline))), st.LineWidth, st.Indent, st.Newline) } - return dmlCaseWrapped(caseKw, operand, whens, elseKw, elseText, haveElse, endKw, st) + return wrapped } // dmlCaseInline renders a CASE expression on a single line. diff --git a/pkg/format/dml_test.go b/pkg/format/dml_test.go index 38482b3..8cf1127 100644 --- a/pkg/format/dml_test.go +++ b/pkg/format/dml_test.go @@ -632,3 +632,18 @@ func TestWhereWhenLong(t *testing.T) { t.Errorf("broken when_long should equal always:\n%s\n---\n%s", long, want) } } + +func TestDMLCaseCollapseUsesLineWidth(t *testing.T) { + st := config.Default() + st.CaseWhenWrap = true + st.CaseCollapse = true + src := "select case when a = 1 then 'one' when a = 2 then 'two' else 'other' end from t;" + st.LineWidth = 120 + if got := File(parser.Parse(src), st); strings.Contains(got, "\n WHEN") { + t.Errorf("fits in 120, should collapse:\n%s", got) + } + st.LineWidth = 30 + if got := File(parser.Parse(src), st); !strings.Contains(got, "\n WHEN") { + t.Errorf("exceeds 30, should wrap:\n%s", got) + } +}