feat(format): select_wrap and join_wrap with when_long via Doc-IR
This commit is contained in:
@@ -30,6 +30,8 @@ join_indent_size: 1 # Number of extra indent levels for JOINs
|
||||
|
||||
# always | when_long | never
|
||||
where_wrap: always # Each AND/OR condition on its own line (when_long: only if the clause exceeds line_width)
|
||||
select_wrap: always # always | when_long | never — one SELECT/RETURNING column per line
|
||||
join_wrap: never # always | when_long | never — ON (and AND/OR) on their own lines under the JOIN
|
||||
line_width: 120 # Target width for when_long wrapping; 0 = unlimited
|
||||
|
||||
where_and_or_indent: true # AND/OR indented one level under WHERE
|
||||
|
||||
@@ -109,6 +109,9 @@ DataGrip enum conventions used below:
|
||||
| `FROM_ONLY_JOIN_INDENT` | `join_indent_size` | `1` | extra indent levels for JOINs |
|
||||
| `SET_ALIGN_EQUAL_SIGN` | `set_align_equal` | `false` | align `=` in UPDATE SET list |
|
||||
| `WHERE_EL_WRAP` + `WHERE_EL_LINE` | `where_wrap` | `always` | always \| when_long \| never — each AND/OR condition on its own line |
|
||||
| _(new)_ | `select_wrap` | `always` | always \| when_long \| never — one SELECT/RETURNING column per line |
|
||||
| _(new)_ | `join_wrap` | `never` | always \| when_long \| never — `ON` and AND/OR conditions on their own lines under the JOIN |
|
||||
| _(new)_ | `line_width` | `120` | target width for every `when_long` decision and `case_collapse`; 0 = unlimited |
|
||||
| _(no DataGrip equivalent)_ | `where_and_or_indent` | `true` | when true, AND/OR are indented one level under WHERE, not at WHERE's column |
|
||||
|
||||
### Subqueries
|
||||
|
||||
+1
-1
@@ -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, 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`). `select_wrap` (default `always`) and `join_wrap` (default `never`) also take `always|when_long|never`; `when_long` keeps the SELECT list / `JOIN … ON` on one line when it fits `line_width`, otherwise uses the existing one-column-per-line / ON-per-line layout. Items containing a subquery or wrapped CASE always use the broken layout. _Still TODO: move the remaining DML builders (subqueries, CTEs, VALUES) onto Doc._
|
||||
- _Still TODO: LSP range formatting._
|
||||
|
||||
## ✅ Config expansion — DataGrip settings parity
|
||||
|
||||
@@ -70,6 +70,8 @@ type Style struct {
|
||||
JoinIndentSize int // extra indent levels for JOINs (default 1)
|
||||
WhereWrap WrapMode // always|when_long|never — each AND/OR on its own line
|
||||
LineWidth int // target width for when_long wrapping (0 = unlimited)
|
||||
SelectWrap WrapMode // always|when_long|never — one SELECT/RETURNING column per line
|
||||
JoinWrap WrapMode // always|when_long|never — break JOIN … ON onto its own lines
|
||||
WhereAndOrIndent bool // AND/OR indented one level under WHERE
|
||||
|
||||
// --- Subqueries ---
|
||||
@@ -122,6 +124,8 @@ func Default() Style {
|
||||
IndentJoin: false,
|
||||
JoinIndentSize: 1,
|
||||
LineWidth: 120,
|
||||
SelectWrap: WrapAlways,
|
||||
JoinWrap: WrapNever,
|
||||
WhereWrap: WrapAlways,
|
||||
WhereAndOrIndent: true,
|
||||
|
||||
@@ -172,6 +176,8 @@ type yamlFile struct {
|
||||
JoinIndentSize *int `yaml:"join_indent_size"`
|
||||
WhereWrap *string `yaml:"where_wrap"`
|
||||
LineWidth *int `yaml:"line_width"`
|
||||
SelectWrap *string `yaml:"select_wrap"`
|
||||
JoinWrap *string `yaml:"join_wrap"`
|
||||
WhereAndOrIndent *bool `yaml:"where_and_or_indent"`
|
||||
|
||||
SubqueryOpening *string `yaml:"subquery_opening"`
|
||||
@@ -267,6 +273,20 @@ func Load(startDir string) (Style, error) {
|
||||
}
|
||||
st.LineWidth = *yf.LineWidth
|
||||
}
|
||||
if yf.SelectWrap != nil {
|
||||
wm := WrapMode(*yf.SelectWrap)
|
||||
if err := validWrap(wm); err != nil {
|
||||
return st, fmt.Errorf("pgtidy: %s: select_wrap: %w", path, err)
|
||||
}
|
||||
st.SelectWrap = wm
|
||||
}
|
||||
if yf.JoinWrap != nil {
|
||||
wm := WrapMode(*yf.JoinWrap)
|
||||
if err := validWrap(wm); err != nil {
|
||||
return st, fmt.Errorf("pgtidy: %s: join_wrap: %w", path, err)
|
||||
}
|
||||
st.JoinWrap = wm
|
||||
}
|
||||
if yf.WhereWrap != nil {
|
||||
wm := WrapMode(*yf.WhereWrap)
|
||||
if err := validWrap(wm); err != nil {
|
||||
|
||||
+37
-1
@@ -206,6 +206,27 @@ func dmlJoinClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||
if text != "" {
|
||||
line += " " + text
|
||||
}
|
||||
if onIdx := dmlKeywordIdx(body, 0, "on"); onIdx >= 0 && st.JoinWrap != config.WrapNever {
|
||||
nl := st.Newline
|
||||
var b strings.Builder
|
||||
b.WriteString(kwText)
|
||||
if head := dmlInline(body[:onIdx], st); head != "" {
|
||||
b.WriteString(" " + head)
|
||||
}
|
||||
for i, cond := range dmlSplitAndOr(body[onIdx+1:]) {
|
||||
b.WriteString(nl + st.Indent)
|
||||
if i == 0 {
|
||||
b.WriteString(caseText(body[onIdx].Tok, st) + " ")
|
||||
}
|
||||
b.WriteString(strings.ReplaceAll(dmlInline(cond, st), nl, nl+st.Indent))
|
||||
}
|
||||
broken := b.String()
|
||||
if st.JoinWrap == config.WrapAlways || strings.Contains(line, nl) {
|
||||
line = broken
|
||||
} else {
|
||||
line = Render(Group(IfBreak(Text(broken), Text(line))), st.LineWidth, st.Indent, nl)
|
||||
}
|
||||
}
|
||||
if !st.IndentJoin {
|
||||
return line
|
||||
}
|
||||
@@ -720,16 +741,31 @@ func dmlColListSelect(kwText string, items [][]cst.Tok, st config.Style) string
|
||||
}
|
||||
|
||||
texts := make([]string, len(items))
|
||||
multiline := false
|
||||
for i, item := range items {
|
||||
texts[i] = dmlInline(item, st)
|
||||
if strings.Contains(texts[i], st.Newline) {
|
||||
multiline = true
|
||||
}
|
||||
}
|
||||
flat := kwText + " " + strings.Join(texts, ", ")
|
||||
|
||||
// align_columns / select_align_as: pad expressions so AS and aliases align.
|
||||
if (st.AlignColumns || st.SelectAlignAs) && len(texts) > 1 {
|
||||
texts = alignSelectItems(texts, st)
|
||||
}
|
||||
|
||||
return dmlCommaList(kwText, texts, st)
|
||||
broken := dmlCommaList(kwText, texts, st)
|
||||
if multiline {
|
||||
return broken // an embedded subquery / wrapped CASE can't sit on one line
|
||||
}
|
||||
switch st.SelectWrap {
|
||||
case config.WrapNever:
|
||||
return flat
|
||||
case config.WrapWhenLong:
|
||||
return Render(Group(IfBreak(Text(broken), Text(flat))), st.LineWidth, st.Indent, st.Newline)
|
||||
}
|
||||
return broken
|
||||
}
|
||||
|
||||
// dmlColListSet formats an UPDATE SET column list with optional set_align_equal.
|
||||
|
||||
@@ -647,3 +647,50 @@ func TestDMLCaseCollapseUsesLineWidth(t *testing.T) {
|
||||
t.Errorf("exceeds 30, should wrap:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectWrap(t *testing.T) {
|
||||
src := "select a, b, c from t"
|
||||
st := config.Default()
|
||||
if got := File(parser.Parse(src), st); !strings.Contains(got, "\n ,b") {
|
||||
t.Errorf("default select_wrap=always should list one per line:\n%s", got)
|
||||
}
|
||||
st.SelectWrap = config.WrapNever
|
||||
if got := File(parser.Parse(src), st); !strings.Contains(got, "SELECT a, b, c") {
|
||||
t.Errorf("never:\n%s", got)
|
||||
}
|
||||
st.SelectWrap = config.WrapWhenLong
|
||||
st.LineWidth = 120
|
||||
if got := File(parser.Parse(src), st); !strings.Contains(got, "SELECT a, b, c") {
|
||||
t.Errorf("when_long, short:\n%s", got)
|
||||
}
|
||||
st.LineWidth = 8
|
||||
if got := File(parser.Parse(src), st); !strings.Contains(got, "\n ,b") {
|
||||
t.Errorf("when_long, long:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinWrap(t *testing.T) {
|
||||
src := "select a from t join u on t.id = u.id and t.x = u.x"
|
||||
st := config.Default()
|
||||
base := File(parser.Parse(src), st)
|
||||
if !strings.Contains(base, "JOIN u ON t.id = u.id AND t.x = u.x") {
|
||||
t.Fatalf("default join_wrap=never should stay inline:\n%s", base)
|
||||
}
|
||||
st.JoinWrap = config.WrapAlways
|
||||
got := File(parser.Parse(src), st)
|
||||
if !strings.Contains(got, "JOIN u\n ON t.id = u.id\n AND t.x = u.x") {
|
||||
t.Errorf("always:\n%s", got)
|
||||
}
|
||||
if twice := File(parser.Parse(got), st); twice != got {
|
||||
t.Errorf("not idempotent:\n%s\n---\n%s", got, twice)
|
||||
}
|
||||
st.JoinWrap = config.WrapWhenLong
|
||||
st.LineWidth = 120
|
||||
if File(parser.Parse(src), st) != base {
|
||||
t.Errorf("when_long short should equal inline")
|
||||
}
|
||||
st.LineWidth = 20
|
||||
if File(parser.Parse(src), st) != got {
|
||||
t.Errorf("when_long long should equal always")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user