Working on
This commit is contained in:
211
internal/formatter/formatter.go
Normal file
211
internal/formatter/formatter.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package formatter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/marte-dev/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
|
||||
}
|
||||
Reference in New Issue
Block a user