馃殌 Grammar-Aware Code Formatter: Structure through separation (supports Go, JavaScript, TypeScript, JSX, and TSX)
go
formatter
code-formatter
javascript
typescript
jsx
tsx
1package engine
2
3import "strings"
4
5type CommentMode int
6
7const (
8 CommentsFollow CommentMode = iota
9 CommentsPrecede
10 CommentsStandalone
11)
12
13type Engine struct {
14 CommentMode CommentMode
15 GroupSingleLineScopes bool
16}
17
18func (e *Engine) format(events []LineEvent, resultBuilder *strings.Builder) {
19 hasWrittenContent := false
20 previousWasOpenBrace := false
21 previousStatementType := ""
22 previousWasComment := false
23 previousWasTopLevel := false
24 previousWasScoped := false
25 previousWasSingleLineScope := false
26
27 for eventIndex, event := range events {
28 if event.InRawString {
29 if hasWrittenContent {
30 resultBuilder.WriteByte('\n')
31 }
32
33 resultBuilder.WriteString(event.Content)
34
35 hasWrittenContent = true
36
37 continue
38 }
39
40 if event.IsBlank {
41 continue
42 }
43
44 currentStatementType := event.StatementType
45
46 if event.IsPackageDecl {
47 currentStatementType = "package"
48 }
49
50 needsBlankLine := false
51 currentIsTopLevel := event.HasASTInfo && event.IsTopLevel
52 currentIsScoped := event.HasASTInfo && event.IsScoped
53 currentIsSingleLineScope := currentIsScoped && !event.IsOpeningBrace && !event.IsClosingBrace
54
55 if hasWrittenContent && !previousWasOpenBrace && !event.IsClosingBrace && !event.IsCaseLabel && !event.IsContinuation {
56 if currentIsTopLevel && previousWasTopLevel && currentStatementType != previousStatementType {
57 if e.CommentMode != CommentsFollow || !previousWasComment {
58 needsBlankLine = true
59 }
60 } else if event.HasASTInfo && (currentIsScoped || previousWasScoped) {
61 if e.GroupSingleLineScopes && currentIsSingleLineScope && previousWasSingleLineScope && currentStatementType == previousStatementType {
62 needsBlankLine = false
63 } else if e.CommentMode != CommentsFollow || !previousWasComment {
64 needsBlankLine = true
65 }
66 } else if currentStatementType != "" && previousStatementType != "" && currentStatementType != previousStatementType {
67 if e.CommentMode != CommentsFollow || !previousWasComment {
68 needsBlankLine = true
69 }
70 }
71
72 if e.CommentMode == CommentsFollow && event.IsCommentOnly && !previousWasComment {
73 nextIndex := e.findNextNonComment(events, eventIndex+1)
74
75 if nextIndex >= 0 {
76 nextNonCommentEvent := events[nextIndex]
77
78 if nextNonCommentEvent.HasASTInfo {
79 nextIsTopLevel := nextNonCommentEvent.IsTopLevel
80 nextIsScoped := nextNonCommentEvent.IsScoped
81
82 if nextIsTopLevel && previousWasTopLevel && nextNonCommentEvent.StatementType != previousStatementType {
83 needsBlankLine = true
84 } else if nextIsScoped || previousWasScoped {
85 needsBlankLine = true
86 } else if nextNonCommentEvent.StatementType != "" && previousStatementType != "" && nextNonCommentEvent.StatementType != previousStatementType {
87 needsBlankLine = true
88 }
89 }
90 }
91 }
92 }
93
94 if needsBlankLine {
95 resultBuilder.WriteByte('\n')
96 }
97
98 if hasWrittenContent {
99 resultBuilder.WriteByte('\n')
100 }
101
102 resultBuilder.WriteString(event.Content)
103
104 hasWrittenContent = true
105 previousWasOpenBrace = event.IsOpeningBrace || event.IsCaseLabel
106 previousWasComment = event.IsCommentOnly
107
108 if event.HasASTInfo {
109 previousStatementType = event.StatementType
110 previousWasTopLevel = event.IsTopLevel
111 previousWasScoped = event.IsScoped
112 previousWasSingleLineScope = currentIsSingleLineScope
113 } else if currentStatementType != "" {
114 previousStatementType = currentStatementType
115 previousWasTopLevel = false
116 previousWasScoped = false
117 previousWasSingleLineScope = false
118 }
119 }
120
121 resultBuilder.WriteByte('\n')
122}
123
124func (e *Engine) FormatToString(events []LineEvent) string {
125 var resultBuilder strings.Builder
126
127 resultBuilder.Grow(len(events) * 40)
128 e.format(events, &resultBuilder)
129
130 return resultBuilder.String()
131}
132
133func (e *Engine) FormatToBytes(events []LineEvent) []byte {
134 var resultBuilder strings.Builder
135
136 resultBuilder.Grow(len(events) * 40)
137 e.format(events, &resultBuilder)
138
139 return []byte(resultBuilder.String())
140}
141
142func (e *Engine) findNextNonComment(events []LineEvent, startIndex int) int {
143 for eventIndex := startIndex; eventIndex < len(events); eventIndex++ {
144 if events[eventIndex].IsBlank {
145 continue
146 }
147
148 if events[eventIndex].IsCommentOnly {
149 continue
150 }
151
152 return eventIndex
153 }
154
155 return -1
156}