chore: more work done and planning with AI.
This commit is contained in:
+268
-53
@@ -92,10 +92,11 @@ func formatBodyInner(inner string, st config.Style) string {
|
||||
var b strings.Builder
|
||||
|
||||
// Emit verbatim up to and including DECLARE (keyword-cased).
|
||||
// Normalize CRLF in trivia so the output always uses st.Newline.
|
||||
for i := 0; i <= declareIdx; i++ {
|
||||
t := sig[i]
|
||||
for _, tr := range t.Lead {
|
||||
b.WriteString(tr.Text)
|
||||
b.WriteString(strings.ReplaceAll(tr.Text, "\r\n", nl))
|
||||
}
|
||||
if i == declareIdx {
|
||||
b.WriteString(applyCase(t.Tok.Text, st.KeywordCase))
|
||||
@@ -122,57 +123,30 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
|
||||
nl := st.Newline
|
||||
indent := st.Indent
|
||||
depth := 0
|
||||
var decls [][]cst.Tok
|
||||
var cur []cst.Tok
|
||||
var preComments []string
|
||||
var preCommentSets [][]string
|
||||
var curPreComments []string
|
||||
|
||||
emit := func() {
|
||||
collect := func() {
|
||||
if len(cur) == 0 {
|
||||
return
|
||||
}
|
||||
for _, c := range preComments {
|
||||
b.WriteString(indent)
|
||||
b.WriteString(c)
|
||||
b.WriteString(nl)
|
||||
}
|
||||
preComments = nil
|
||||
|
||||
// Graceful degradation: mid-declaration comments stay verbatim.
|
||||
if anyComment(cur[1:]) {
|
||||
b.WriteString(indent)
|
||||
b.WriteString(verbatimSpan(cur))
|
||||
b.WriteString(nl)
|
||||
cur = nil
|
||||
return
|
||||
}
|
||||
|
||||
body := cur
|
||||
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
|
||||
if hasSemi {
|
||||
body = body[:len(body)-1]
|
||||
}
|
||||
b.WriteString(indent)
|
||||
for i, t := range body {
|
||||
if i > 0 && needSpace(body[i-1].Tok, t.Tok) {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(caseText(t.Tok, st))
|
||||
}
|
||||
if hasSemi {
|
||||
b.WriteString(";")
|
||||
}
|
||||
b.WriteString(nl)
|
||||
decls = append(decls, cur)
|
||||
preCommentSets = append(preCommentSets, curPreComments)
|
||||
cur = nil
|
||||
curPreComments = nil
|
||||
}
|
||||
|
||||
for _, t := range toks {
|
||||
if len(cur) == 0 {
|
||||
for _, tr := range t.Lead {
|
||||
if tr.Kind == lexer.LineComment || tr.Kind == lexer.BlockComment {
|
||||
preComments = append(preComments, strings.TrimRight(tr.Text, " \t"))
|
||||
text := strings.TrimRight(strings.ReplaceAll(tr.Text, "\r", ""), " \t")
|
||||
curPreComments = append(curPreComments, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch t.Tok.Kind {
|
||||
case lexer.LParen, lexer.LBracket:
|
||||
depth++
|
||||
@@ -181,14 +155,195 @@ func formatDeclareVars(b *strings.Builder, toks []cst.Tok, st config.Style) {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
|
||||
cur = append(cur, t)
|
||||
|
||||
if t.Tok.Kind == lexer.Semicolon && depth == 0 {
|
||||
emit()
|
||||
collect()
|
||||
}
|
||||
}
|
||||
collect()
|
||||
|
||||
// Compute alignment widths when requested.
|
||||
var nameColW, typeColW int
|
||||
if st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq {
|
||||
for _, decl := range decls {
|
||||
if anyComment(decl[1:]) {
|
||||
continue
|
||||
}
|
||||
body := decl
|
||||
if len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon {
|
||||
body = body[:len(body)-1]
|
||||
}
|
||||
nw, tw := declareNameTypeWidth(body, st)
|
||||
if nw > nameColW {
|
||||
nameColW = nw
|
||||
}
|
||||
if tw > typeColW {
|
||||
typeColW = tw
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, cur := range decls {
|
||||
for _, c := range preCommentSets[i] {
|
||||
b.WriteString(indent)
|
||||
b.WriteString(c)
|
||||
b.WriteString(nl)
|
||||
}
|
||||
// Graceful degradation: mid-declaration comments stay verbatim.
|
||||
if anyComment(cur[1:]) {
|
||||
b.WriteString(indent)
|
||||
b.WriteString(verbatimSpan(cur))
|
||||
b.WriteString(nl)
|
||||
continue
|
||||
}
|
||||
|
||||
body := cur
|
||||
hasSemi := len(body) > 0 && body[len(body)-1].Tok.Kind == lexer.Semicolon
|
||||
if hasSemi {
|
||||
body = body[:len(body)-1]
|
||||
}
|
||||
b.WriteString(indent)
|
||||
if (st.PlpgsqlDeclareAlignType || st.PlpgsqlDeclareAlignEq) && nameColW > 0 {
|
||||
writeDeclareAligned(b, body, st, nameColW, typeColW)
|
||||
} else {
|
||||
for j, t := range body {
|
||||
if j > 0 && needSpace(body[j-1].Tok, t.Tok) {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(caseText(t.Tok, st))
|
||||
}
|
||||
}
|
||||
if hasSemi {
|
||||
b.WriteString(";")
|
||||
}
|
||||
b.WriteString(nl)
|
||||
}
|
||||
}
|
||||
|
||||
// declareNameTypeWidth returns the rendered width of the name and type portions
|
||||
// of a DECLARE variable declaration (without the default assignment).
|
||||
// Format is: [name type [:= default]] or [name type [DEFAULT default]].
|
||||
func declareNameTypeWidth(body []cst.Tok, st config.Style) (nameW, typeW int) {
|
||||
if len(body) < 2 {
|
||||
return 0, 0
|
||||
}
|
||||
// name is always the first token.
|
||||
name := caseText(body[0].Tok, st)
|
||||
nameW = len(name)
|
||||
|
||||
// type runs from body[1] until we hit := / DEFAULT / = at depth 0.
|
||||
var typeTokens []cst.Tok
|
||||
depth := 0
|
||||
for _, t := range body[1:] {
|
||||
switch t.Tok.Kind {
|
||||
case lexer.LParen, lexer.LBracket:
|
||||
depth++
|
||||
case lexer.RParen, lexer.RBracket:
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
if depth == 0 {
|
||||
low := lowerASCII(t.Tok.Text)
|
||||
if t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=") {
|
||||
break
|
||||
}
|
||||
if t.Tok.Kind == lexer.Ident && low == "default" {
|
||||
break
|
||||
}
|
||||
}
|
||||
typeTokens = append(typeTokens, t)
|
||||
}
|
||||
var tb strings.Builder
|
||||
for j, t := range typeTokens {
|
||||
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
|
||||
tb.WriteByte(' ')
|
||||
}
|
||||
tb.WriteString(caseText(t.Tok, st))
|
||||
}
|
||||
typeW = len(tb.String())
|
||||
return nameW, typeW
|
||||
}
|
||||
|
||||
// writeDeclareAligned writes a single DECLARE variable with aligned columns.
|
||||
func writeDeclareAligned(b *strings.Builder, body []cst.Tok, st config.Style, nameColW, typeColW int) {
|
||||
if len(body) == 0 {
|
||||
return
|
||||
}
|
||||
name := caseText(body[0].Tok, st)
|
||||
b.WriteString(name)
|
||||
if len(body) == 1 {
|
||||
return
|
||||
}
|
||||
|
||||
// Pad name to nameColW if align_type is requested.
|
||||
if st.PlpgsqlDeclareAlignType {
|
||||
pad := nameColW - len(name)
|
||||
for k := 0; k < pad; k++ {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
|
||||
// Collect type tokens.
|
||||
var typeTokens, restTokens []cst.Tok
|
||||
depth := 0
|
||||
pastType := false
|
||||
for _, t := range body[1:] {
|
||||
switch t.Tok.Kind {
|
||||
case lexer.LParen, lexer.LBracket:
|
||||
depth++
|
||||
case lexer.RParen, lexer.RBracket:
|
||||
if depth > 0 {
|
||||
depth--
|
||||
}
|
||||
}
|
||||
if !pastType && depth == 0 {
|
||||
low := lowerASCII(t.Tok.Text)
|
||||
if (t.Tok.Kind == lexer.Operator && (t.Tok.Text == ":=" || t.Tok.Text == "=")) ||
|
||||
(t.Tok.Kind == lexer.Ident && low == "default") {
|
||||
pastType = true
|
||||
restTokens = append(restTokens, t)
|
||||
continue
|
||||
}
|
||||
}
|
||||
if pastType {
|
||||
restTokens = append(restTokens, t)
|
||||
} else {
|
||||
typeTokens = append(typeTokens, t)
|
||||
}
|
||||
}
|
||||
|
||||
var typeStr strings.Builder
|
||||
for j, t := range typeTokens {
|
||||
if j > 0 && needSpace(typeTokens[j-1].Tok, t.Tok) {
|
||||
typeStr.WriteByte(' ')
|
||||
}
|
||||
typeStr.WriteString(caseText(t.Tok, st))
|
||||
}
|
||||
typeRendered := typeStr.String()
|
||||
|
||||
b.WriteByte(' ')
|
||||
b.WriteString(typeRendered)
|
||||
|
||||
if len(restTokens) > 0 {
|
||||
// Pad type to typeColW if align_eq is requested.
|
||||
if st.PlpgsqlDeclareAlignEq {
|
||||
pad := typeColW - len(typeRendered)
|
||||
for k := 0; k < pad; k++ {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
for j, t := range restTokens {
|
||||
prev := restTokens[0].Tok
|
||||
if j > 0 {
|
||||
prev = restTokens[j-1].Tok
|
||||
}
|
||||
if j == 0 || needSpace(prev, t.Tok) {
|
||||
b.WriteByte(' ')
|
||||
}
|
||||
b.WriteString(caseText(t.Tok, st))
|
||||
}
|
||||
}
|
||||
emit()
|
||||
}
|
||||
|
||||
// bline is one logical line within an accumulated statement.
|
||||
@@ -208,12 +363,17 @@ type bline struct {
|
||||
// 3. After EXCEPTION the formatter switches to verbatim-indent mode (original
|
||||
// leading whitespace is preserved) to avoid conflicts between styles that
|
||||
// put WHEN at col-0 vs indented.
|
||||
// 4. Blank-line counts from the original are preserved.
|
||||
// 4. Blank-line counts from the original are preserved (capped by PlpgsqlMaxBlankLines).
|
||||
func formatBodyStatements(text string, st config.Style) string {
|
||||
nl := st.Newline
|
||||
normalised := strings.ReplaceAll(text, "\r\n", "\n")
|
||||
rawLines := strings.Split(normalised, "\n")
|
||||
|
||||
maxBlanks := st.PlpgsqlMaxBlankLines
|
||||
if maxBlanks < 0 {
|
||||
maxBlanks = 0
|
||||
}
|
||||
|
||||
var (
|
||||
result strings.Builder
|
||||
stmt []bline
|
||||
@@ -228,7 +388,11 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
if len(stmt) == 0 {
|
||||
return
|
||||
}
|
||||
for i := 0; i < pendingBlanks; i++ {
|
||||
blanks := pendingBlanks
|
||||
if blanks > maxBlanks {
|
||||
blanks = maxBlanks
|
||||
}
|
||||
for i := 0; i < blanks; i++ {
|
||||
result.WriteString(nl)
|
||||
}
|
||||
pendingBlanks = 0
|
||||
@@ -259,8 +423,6 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
}
|
||||
effectiveDepth = blockDepth
|
||||
case "else", "elsif", "elseif":
|
||||
// Emit at one level up; blockDepth unchanged so the body continues
|
||||
// at the same depth (THEN will re-apply depthInc for elsif).
|
||||
if blockDepth > 0 {
|
||||
effectiveDepth = blockDepth - 1
|
||||
}
|
||||
@@ -271,7 +433,14 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
|
||||
baseIndent := strings.Repeat(st.Indent, effectiveDepth)
|
||||
|
||||
for i, ll := range stmt {
|
||||
// plpgsql_if_then_newline: when false, THEN stays on the same line as
|
||||
// the condition. When true (default) it's already on its own logical line.
|
||||
stmtLines := stmt
|
||||
if !st.PlpgsqlIfThenNewline && fw == "if" {
|
||||
stmtLines = joinThenToCondition(stmt)
|
||||
}
|
||||
|
||||
for i, ll := range stmtLines {
|
||||
if i == 0 || ll.indent == "" {
|
||||
result.WriteString(baseIndent)
|
||||
} else {
|
||||
@@ -301,8 +470,6 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
|
||||
fw := lowerASCII(firstBodyKeyword(stripped))
|
||||
isColZero := indent == ""
|
||||
// Only join when we are at paren-depth 0; content inside parens (e.g.
|
||||
// inside a CTE subquery) should not be merged across lines.
|
||||
joinToPrev := isColZero && parenDepth == 0 && len(stmt) > 0 && !sqlClauseKw[fw]
|
||||
|
||||
if joinToPrev {
|
||||
@@ -312,7 +479,6 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
stmt = append(stmt, bline{text: stripped, indent: indent})
|
||||
}
|
||||
|
||||
// Scan tokens to track paren depth and detect flush triggers.
|
||||
var lastD0Kw string
|
||||
for _, tok := range lexer.Lex(stripped) {
|
||||
if tok.IsTrivia() || tok.Kind == lexer.EOF {
|
||||
@@ -327,6 +493,13 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
}
|
||||
case lexer.Semicolon:
|
||||
if parenDepth == 0 {
|
||||
// plpgsql_loop_collapse: fold empty FOR … LOOP END LOOP; to one line.
|
||||
if st.PlpgsqlLoopCollapse && len(stmt) > 0 {
|
||||
collapsed, ok := tryCollapseLoop(stmt, st)
|
||||
if ok {
|
||||
stmt = []bline{{text: collapsed, indent: ""}}
|
||||
}
|
||||
}
|
||||
flush()
|
||||
}
|
||||
}
|
||||
@@ -335,13 +508,9 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
}
|
||||
}
|
||||
|
||||
// Structural keywords at the end of a line (paren depth 0) trigger a
|
||||
// flush and possibly a block-depth change.
|
||||
if parenDepth == 0 && len(stmt) > 0 {
|
||||
switch lastD0Kw {
|
||||
case "then", "loop", "begin":
|
||||
// ELSIF/ELSEIF headers end with THEN but must NOT increment depth
|
||||
// (blockDepth is already at the right level for the body).
|
||||
fw0 := lowerASCII(firstBodyKeyword(stmt[0].text))
|
||||
if fw0 != "elsif" && fw0 != "elseif" {
|
||||
depthInc = true
|
||||
@@ -357,6 +526,52 @@ func formatBodyStatements(text string, st config.Style) string {
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// joinThenToCondition merges a THEN line (on its own bline) into the preceding
|
||||
// condition line when plpgsql_if_then_newline is false.
|
||||
func joinThenToCondition(lines []bline) []bline {
|
||||
out := make([]bline, 0, len(lines))
|
||||
for i, ll := range lines {
|
||||
if i > 0 && strings.EqualFold(strings.TrimSpace(ll.text), "then") {
|
||||
out[len(out)-1].text = strings.TrimRight(out[len(out)-1].text, " \t") + " THEN"
|
||||
} else {
|
||||
out = append(out, ll)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// tryCollapseLoop tries to collapse an empty loop body to one line.
|
||||
// Detects: FOR … LOOP\n (empty or only blanks)\nEND LOOP;
|
||||
// Returns the collapsed line and true on success.
|
||||
func tryCollapseLoop(lines []bline, st config.Style) (string, bool) {
|
||||
if len(lines) < 2 {
|
||||
return "", false
|
||||
}
|
||||
first := strings.TrimSpace(lines[0].text)
|
||||
last := strings.TrimSpace(lines[len(lines)-1].text)
|
||||
firstLow := lowerASCII(first)
|
||||
lastLow := lowerASCII(last)
|
||||
|
||||
// Check last line is END LOOP; or LOOP (for WHILE/FOR empty bodies that end with LOOP).
|
||||
if !strings.HasPrefix(lastLow, "end loop") && lastLow != "end loop;" {
|
||||
return "", false
|
||||
}
|
||||
// Check middle lines are all empty.
|
||||
for _, mid := range lines[1 : len(lines)-1] {
|
||||
if strings.TrimSpace(mid.text) != "" {
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
// Check first line ends with LOOP.
|
||||
if !strings.HasSuffix(firstLow, "loop") {
|
||||
return "", false
|
||||
}
|
||||
_ = st
|
||||
_ = firstLow
|
||||
// Collapse to: <header> END LOOP;
|
||||
return strings.TrimRight(first, " \t") + " " + strings.ToUpper(last), true
|
||||
}
|
||||
|
||||
// firstBodyKeyword returns the text of the first identifier token in s
|
||||
// (lowercased), or "" if the first significant token is not an identifier.
|
||||
func firstBodyKeyword(s string) string {
|
||||
|
||||
Reference in New Issue
Block a user