Files
marte_dev_tools/internal/formatter/formatter.go
2026-01-23 11:46:59 +01:00

212 lines
5.7 KiB
Go

package formatter
import (
"fmt"
"io"
"sort"
"strings"
"github.com/marte-community/marte-dev-tools/internal/parser"
)
type Insertable struct {
Position parser.Position
Text string
IsDoc bool
}
type Formatter struct {
insertables []Insertable
cursor int
writer io.Writer
}
func Format(config *parser.Configuration, w io.Writer) {
ins := []Insertable{}
for _, c := range config.Comments {
ins = append(ins, Insertable{Position: c.Position, Text: fixComment(c.Text), IsDoc: c.Doc})
}
for _, p := range config.Pragmas {
ins = append(ins, Insertable{Position: p.Position, Text: fixComment(p.Text)})
}
// Sort
sort.Slice(ins, func(i, j int) bool {
if ins[i].Position.Line != ins[j].Position.Line {
return ins[i].Position.Line < ins[j].Position.Line
}
return ins[i].Position.Column < ins[j].Position.Column
})
f := &Formatter{
insertables: ins,
writer: w,
}
f.formatConfig(config)
}
func fixComment(text string) string {
if strings.HasPrefix(text, "//!") {
if len(text) > 3 && text[3] != ' ' {
return "//! " + text[3:]
}
} else if strings.HasPrefix(text, "//#") {
if len(text) > 3 && text[3] != ' ' {
return "//# " + text[3:]
}
} else if strings.HasPrefix(text, "//") {
if len(text) > 2 && text[2] != ' ' && text[2] != '#' && text[2] != '!' {
return "// " + text[2:]
}
}
return text
}
func (f *Formatter) formatConfig(config *parser.Configuration) {
lastLine := 0
if config.Package != nil {
f.flushCommentsBefore(config.Package.Position, 0, false) // Package comments usually detached unless specifically doc
fmt.Fprintf(f.writer, "#package %s", config.Package.URI)
lastLine = config.Package.Position.Line
if f.hasTrailingComment(lastLine) {
fmt.Fprintf(f.writer, " %s", f.popComment())
}
fmt.Fprintln(f.writer)
fmt.Fprintln(f.writer)
}
for _, def := range config.Definitions {
f.flushCommentsBefore(def.Pos(), 0, true) // Stick to definition
lastLine = f.formatDefinition(def, 0)
if f.hasTrailingComment(lastLine) {
fmt.Fprintf(f.writer, " %s", f.popComment())
}
fmt.Fprintln(f.writer)
}
f.flushRemainingComments(0)
}
func (f *Formatter) formatDefinition(def parser.Definition, indent int) int {
indentStr := strings.Repeat(" ", indent)
switch d := def.(type) {
case *parser.Field:
fmt.Fprintf(f.writer, "%s%s = ", indentStr, d.Name)
endLine := f.formatValue(d.Value, indent)
return endLine
case *parser.ObjectNode:
fmt.Fprintf(f.writer, "%s%s = {", indentStr, d.Name)
if f.hasTrailingComment(d.Position.Line) {
fmt.Fprintf(f.writer, " %s", f.popComment())
}
fmt.Fprintln(f.writer)
f.formatSubnode(d.Subnode, indent+1)
fmt.Fprintf(f.writer, "%s}", indentStr)
return d.Subnode.EndPosition.Line
}
return 0
}
func (f *Formatter) formatSubnode(sub parser.Subnode, indent int) {
for _, def := range sub.Definitions {
f.flushCommentsBefore(def.Pos(), indent, true) // Stick to definition
lastLine := f.formatDefinition(def, indent)
if f.hasTrailingComment(lastLine) {
fmt.Fprintf(f.writer, " %s", f.popComment())
}
fmt.Fprintln(f.writer)
}
f.flushCommentsBefore(sub.EndPosition, indent, false)
}
func (f *Formatter) formatValue(val parser.Value, indent int) int {
switch v := val.(type) {
case *parser.StringValue:
if v.Quoted {
fmt.Fprintf(f.writer, "\"%s\"", v.Value)
} else {
// Should strictly parse unquoted as ReferenceValue or identifiers, but fallback here
fmt.Fprint(f.writer, v.Value)
}
return v.Position.Line
case *parser.IntValue:
fmt.Fprint(f.writer, v.Raw)
return v.Position.Line
case *parser.FloatValue:
fmt.Fprint(f.writer, v.Raw)
return v.Position.Line
case *parser.BoolValue:
fmt.Fprintf(f.writer, "%v", v.Value)
return v.Position.Line
case *parser.ReferenceValue:
fmt.Fprint(f.writer, v.Value)
return v.Position.Line
case *parser.ArrayValue:
fmt.Fprint(f.writer, "{ ")
for i, e := range v.Elements {
if i > 0 {
fmt.Fprint(f.writer, " ")
}
f.formatValue(e, indent)
}
fmt.Fprint(f.writer, " }")
if v.EndPosition.Line > 0 {
return v.EndPosition.Line
}
// Fallback if EndPosition not set (shouldn't happen with new parser)
if len(v.Elements) > 0 {
return v.Elements[len(v.Elements)-1].Pos().Line
}
return v.Position.Line
default:
return 0
}
}
func (f *Formatter) flushCommentsBefore(pos parser.Position, indent int, stick bool) {
indentStr := strings.Repeat(" ", indent)
for f.cursor < len(f.insertables) {
c := f.insertables[f.cursor]
if c.Position.Line < pos.Line || (c.Position.Line == pos.Line && c.Position.Column < pos.Column) {
fmt.Fprintf(f.writer, "%s%s\n", indentStr, c.Text)
f.cursor++
} else {
break
}
}
// If stick is true, we don't print extra newline.
// The caller will print the definition immediately after this function returns.
// If stick is false (e.g. end of block comments), we act normally.
// But actually, the previous implementation didn't print extra newlines between comments and code
// explicitly, it relied on the loop in formatConfig/formatSubnode to print newline AFTER definition.
// So comments naturally sat on top.
// The issue is if there WAS a blank line in source, we ignore it and squash. This implements "stick".
}
func (f *Formatter) flushRemainingComments(indent int) {
indentStr := strings.Repeat(" ", indent)
for f.cursor < len(f.insertables) {
c := f.insertables[f.cursor]
fmt.Fprintf(f.writer, "%s%s\n", indentStr, c.Text)
f.cursor++
}
}
func (f *Formatter) hasTrailingComment(line int) bool {
if f.cursor >= len(f.insertables) {
return false
}
c := f.insertables[f.cursor]
return c.Position.Line == line
}
func (f *Formatter) popComment() string {
if f.cursor >= len(f.insertables) {
return ""
}
c := f.insertables[f.cursor]
f.cursor++
return c.Text
}