feat(format): Doc-IR core, line_width (120) and where_wrap when_long
This commit is contained in:
@@ -244,6 +244,9 @@ func dmlWhereClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||
}
|
||||
|
||||
nl := st.Newline
|
||||
if st.WhereWrap == config.WrapWhenLong {
|
||||
return dmlWhereWhenLong(kwText, conditions, st)
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString(kwText)
|
||||
for i, cond := range conditions {
|
||||
@@ -261,6 +264,29 @@ func dmlWhereClause(kwText string, body []cst.Tok, st config.Style) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// dmlWhereWhenLong is the where_wrap: when_long layout: the whole clause stays
|
||||
// on one line when it fits in line_width, otherwise it breaks exactly as
|
||||
// where_wrap: always would.
|
||||
func dmlWhereWhenLong(kwText string, conditions [][]cst.Tok, st config.Style) string {
|
||||
prefix := ""
|
||||
if st.WhereAndOrIndent {
|
||||
prefix = st.Indent
|
||||
}
|
||||
parts := []Doc{Text(kwText)}
|
||||
for i, cond := range conditions {
|
||||
text := dmlInline(cond, st)
|
||||
pfx := prefix
|
||||
if i == 0 {
|
||||
pfx += " " // align with AND/OR token width
|
||||
}
|
||||
// Continuation lines of a multi-line condition (subquery) keep the
|
||||
// same prefix writeListItem would give them.
|
||||
text = strings.ReplaceAll(text, st.Newline, st.Newline+pfx)
|
||||
parts = append(parts, IfBreak(Concat(Text(st.Newline), Text(pfx)), Text(" ")), Text(text))
|
||||
}
|
||||
return Render(Group(Concat(parts...)), st.LineWidth, st.Indent, st.Newline)
|
||||
}
|
||||
|
||||
// dmlSplitAndOr splits toks at depth-0 AND/OR tokens, keeping the AND/OR with
|
||||
// the following condition.
|
||||
func dmlSplitAndOr(toks []cst.Tok) [][]cst.Tok {
|
||||
|
||||
@@ -611,3 +611,24 @@ func TestDMLRecordSpaceBeforeParen(t *testing.T) {
|
||||
t.Errorf("row() space_before_paren not idempotent:\n--- once ---\n%s\n--- twice ---\n%s", gotSpaced, twice)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhereWhenLong(t *testing.T) {
|
||||
src := "select a from t where a = 1 and b = 2"
|
||||
st := config.Default()
|
||||
st.WhereWrap = config.WrapWhenLong
|
||||
st.LineWidth = 80
|
||||
short := File(parser.Parse(src), st)
|
||||
if !strings.Contains(short, "WHERE a = 1 AND b = 2") {
|
||||
t.Errorf("short clause should stay on one line:\n%s", short)
|
||||
}
|
||||
st.LineWidth = 12
|
||||
long := File(parser.Parse(src), st)
|
||||
if strings.Contains(long, "WHERE a = 1") || !strings.Contains(long, "AND b = 2") {
|
||||
t.Errorf("long clause should break per condition:\n%s", long)
|
||||
}
|
||||
st.WhereWrap = config.WrapAlways
|
||||
want := File(parser.Parse(src), st)
|
||||
if long != want {
|
||||
t.Errorf("broken when_long should equal always:\n%s\n---\n%s", long, want)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package format
|
||||
|
||||
import "strings"
|
||||
|
||||
// Doc is a Wadler/Prettier-style layout document. Build one from Text, Line,
|
||||
// SoftLine, Group, Indent, IfBreak and Concat, then Render it at a line width:
|
||||
// a Group is printed flat (Lines become spaces, SoftLines vanish) when it fits
|
||||
// in the remaining width, otherwise broken (Lines become newlines).
|
||||
//
|
||||
// Rendering is relative to column 0; callers that splice the result mid-line
|
||||
// re-indent continuation lines themselves, so width checks ignore that offset.
|
||||
type Doc interface{ isDoc() }
|
||||
|
||||
type (
|
||||
docText string
|
||||
docLine struct{ soft bool }
|
||||
docConcat []Doc
|
||||
docIndent struct{ d Doc }
|
||||
docGroup struct{ d Doc }
|
||||
docBreak struct{ broken, flat Doc }
|
||||
)
|
||||
|
||||
func (docText) isDoc() {}
|
||||
func (docLine) isDoc() {}
|
||||
func (docConcat) isDoc() {}
|
||||
func (docIndent) isDoc() {}
|
||||
func (docGroup) isDoc() {}
|
||||
func (docBreak) isDoc() {}
|
||||
|
||||
// Text is literal text. If it contains a newline the enclosing group can never
|
||||
// be printed flat.
|
||||
func Text(s string) Doc { return docText(s) }
|
||||
|
||||
// Line is a space when flat and a newline (plus indentation) when broken.
|
||||
func Line() Doc { return docLine{} }
|
||||
|
||||
// SoftLine is nothing when flat and a newline (plus indentation) when broken.
|
||||
func SoftLine() Doc { return docLine{soft: true} }
|
||||
|
||||
// Concat joins docs in order.
|
||||
func Concat(ds ...Doc) Doc { return docConcat(ds) }
|
||||
|
||||
// Indent indents every line break inside d by one indent unit.
|
||||
func Indent(d Doc) Doc { return docIndent{d} }
|
||||
|
||||
// Group lays d out flat if it fits on the current line, otherwise broken.
|
||||
func Group(d Doc) Doc { return docGroup{d} }
|
||||
|
||||
// IfBreak renders broken when the enclosing group is broken, flat otherwise.
|
||||
func IfBreak(broken, flat Doc) Doc { return docBreak{broken, flat} }
|
||||
|
||||
type docCmd struct {
|
||||
indent string
|
||||
flat bool
|
||||
d Doc
|
||||
}
|
||||
|
||||
// Render lays out d. width <= 0 means unlimited (every group stays flat).
|
||||
func Render(d Doc, width int, unit, nl string) string {
|
||||
var b strings.Builder
|
||||
col := 0
|
||||
stack := []docCmd{{"", false, d}}
|
||||
for len(stack) > 0 {
|
||||
c := stack[len(stack)-1]
|
||||
stack = stack[:len(stack)-1]
|
||||
switch v := c.d.(type) {
|
||||
case docText:
|
||||
b.WriteString(string(v))
|
||||
if i := strings.LastIndexByte(string(v), '\n'); i >= 0 {
|
||||
col = len(v) - i - 1
|
||||
} else {
|
||||
col += len(v)
|
||||
}
|
||||
case docConcat:
|
||||
for i := len(v) - 1; i >= 0; i-- {
|
||||
stack = append(stack, docCmd{c.indent, c.flat, v[i]})
|
||||
}
|
||||
case docIndent:
|
||||
stack = append(stack, docCmd{c.indent + unit, c.flat, v.d})
|
||||
case docBreak:
|
||||
if c.flat {
|
||||
stack = append(stack, docCmd{c.indent, c.flat, v.flat})
|
||||
} else {
|
||||
stack = append(stack, docCmd{c.indent, c.flat, v.broken})
|
||||
}
|
||||
case docLine:
|
||||
if c.flat {
|
||||
if !v.soft {
|
||||
b.WriteByte(' ')
|
||||
col++
|
||||
}
|
||||
break
|
||||
}
|
||||
b.WriteString(nl)
|
||||
b.WriteString(c.indent)
|
||||
col = len(c.indent)
|
||||
case docGroup:
|
||||
flat := c.flat || width <= 0 || fitsFlat(v.d, width-col, c.indent, unit, stack)
|
||||
stack = append(stack, docCmd{c.indent, flat, v.d})
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// fitsFlat reports whether d, printed flat, plus whatever follows it up to the
|
||||
// next possible line break, fits in rem columns.
|
||||
func fitsFlat(d Doc, rem int, indent, unit string, rest []docCmd) bool {
|
||||
if rem < 0 {
|
||||
return false
|
||||
}
|
||||
type item struct {
|
||||
d Doc
|
||||
flat bool
|
||||
}
|
||||
work := []item{{d, true}}
|
||||
ri := len(rest) - 1
|
||||
for rem >= 0 {
|
||||
if len(work) == 0 {
|
||||
if ri < 0 {
|
||||
return true
|
||||
}
|
||||
work = append(work, item{rest[ri].d, rest[ri].flat})
|
||||
ri--
|
||||
continue
|
||||
}
|
||||
it := work[len(work)-1]
|
||||
work = work[:len(work)-1]
|
||||
switch v := it.d.(type) {
|
||||
case docText:
|
||||
if strings.ContainsRune(string(v), '\n') {
|
||||
return false
|
||||
}
|
||||
rem -= len(v)
|
||||
case docConcat:
|
||||
for i := len(v) - 1; i >= 0; i-- {
|
||||
work = append(work, item{v[i], it.flat})
|
||||
}
|
||||
case docIndent:
|
||||
work = append(work, item{v.d, it.flat})
|
||||
case docGroup:
|
||||
work = append(work, item{v.d, it.flat})
|
||||
case docBreak:
|
||||
if it.flat {
|
||||
work = append(work, item{v.flat, it.flat})
|
||||
} else {
|
||||
work = append(work, item{v.broken, it.flat})
|
||||
}
|
||||
case docLine:
|
||||
if !it.flat {
|
||||
return true // a real line break ends the measured run
|
||||
}
|
||||
if !v.soft {
|
||||
rem--
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package format
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDocGroupFlatWhenFits(t *testing.T) {
|
||||
d := Group(Concat(Text("WHERE"), Indent(Concat(Line(), Text("a = 1"), Line(), Text("AND b = 2")))))
|
||||
if got, want := Render(d, 80, " ", "\n"), "WHERE a = 1 AND b = 2"; got != want {
|
||||
t.Errorf("flat: got %q want %q", got, want)
|
||||
}
|
||||
if got, want := Render(d, 10, " ", "\n"), "WHERE\n a = 1\n AND b = 2"; got != want {
|
||||
t.Errorf("broken: got %q want %q", got, want)
|
||||
}
|
||||
if got, want := Render(d, 0, " ", "\n"), "WHERE a = 1 AND b = 2"; got != want {
|
||||
t.Errorf("unlimited: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocNewlineInTextForcesBreak(t *testing.T) {
|
||||
d := Group(Concat(Text("x"), Line(), Text("(\n y)")))
|
||||
if got, want := Render(d, 80, " ", "\n"), "x\n(\n y)"; got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocIfBreakAndSoftLine(t *testing.T) {
|
||||
d := Group(Concat(Text("f("), Indent(Concat(SoftLine(), Text("a, b"))), SoftLine(), IfBreak(Text(" -- long"), Text(""))))
|
||||
if got, want := Render(d, 80, " ", "\n"), "f(a, b"; got != want {
|
||||
t.Errorf("flat: got %q want %q", got, want)
|
||||
}
|
||||
if got, want := Render(d, 3, " ", "\n"), "f(\n a, b\n -- long"; got != want {
|
||||
t.Errorf("broken: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocTrailingTextCountsTowardFit(t *testing.T) {
|
||||
d := Concat(Group(Concat(Text("aaa"), Line(), Text("bbb"))), Text(";;;;"))
|
||||
if got, want := Render(d, 10, " ", "\n"), "aaa\nbbb;;;;"; got != want {
|
||||
t.Errorf("got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user