Skip to content

Commit c5f8ad7

Browse files
committed
fix: block comments, handling when source mixes tabs and spaces
1 parent 55e14dd commit c5f8ad7

1 file changed

Lines changed: 90 additions & 10 deletions

File tree

internal/formatter/formatter.go

Lines changed: 90 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,15 @@ func Format(src, language string, cfg *config.Config) (string, error) {
8080
lines = lines[:len(lines)-1]
8181
}
8282

83-
indentUnit := detectIndentUnit(lines)
83+
tabWidth := cfg.IndentSize
84+
if tabWidth <= 0 {
85+
tabWidth = 4
86+
}
87+
indentUnit := detectIndentUnit(lines, tabWidth)
8488
if indentUnit <= 0 {
8589
indentUnit = 1
8690
}
91+
useTabs := detectTabIndent(lines)
8792

8893
var out strings.Builder
8994
var group []row
@@ -99,21 +104,34 @@ func Format(src, language string, cfg *config.Config) (string, error) {
99104
var buf bytes.Buffer
100105
tw := tabwriter.NewWriter(&buf, 0, 1, padding, ' ', 0)
101106
for _, r := range group {
107+
// Feed spaces-only indent to tabwriter — a literal \t there would
108+
// be treated as a cell delimiter. We'll convert the leading
109+
// spaces back to tabs post-flush when the source uses tabs.
102110
tw.Write([]byte(r.indent + strings.Join(r.cells, "\t") + "\n"))
103111
}
104112
tw.Flush()
105-
out.WriteString(trimTrailingSpaces(buf.String()))
113+
flushed := trimTrailingSpaces(buf.String())
114+
if useTabs {
115+
flushed = leadingSpacesToTabs(flushed, cfg.IndentSize)
116+
}
117+
out.WriteString(flushed)
106118
group = group[:0]
107119
}
108120

109121
prevIndent := ""
110122
mlStringDelim := ""
111123
inBlockComment := false
112124
for _, line := range lines {
113-
indent, rest := SplitIndent(line)
125+
rawIndent, rest := SplitIndent(line)
126+
// Expand leading-indent tabs to spaces so (a) mixed tab/space files
127+
// normalize consistently and (b) literal tabs never reach tabwriter
128+
// as the indent prefix (it would treat them as cell delimiters).
129+
indent := expandIndentTabs(rawIndent, tabWidth)
114130

115-
// When AlignComments is disabled, emit block comment bodies verbatim.
116-
if !cfg.AlignComments && langCfg.BlockCommentOpen != "" {
131+
// Block comment bodies are always emitted verbatim — modifying comment
132+
// text would violate the whitespace-only invariant. (AlignComments
133+
// governs trailing `// comment` alignment, not block-comment bodies.)
134+
if langCfg.BlockCommentOpen != "" {
117135
if inBlockComment {
118136
flushGroup()
119137
out.WriteString(line + "\n")
@@ -190,7 +208,7 @@ func trimTrailingSpaces(s string) string {
190208
return strings.Join(lines, "\n")
191209
}
192210

193-
func detectIndentUnit(lines []string) int {
211+
func detectIndentUnit(lines []string, tabWidth int) int {
194212
for _, line := range lines {
195213
if strings.TrimSpace(line) == "" {
196214
continue
@@ -202,15 +220,77 @@ func detectIndentUnit(lines []string) int {
202220
if strings.HasPrefix(rest, "*") {
203221
continue
204222
}
205-
if strings.Contains(indent, "\t") {
206-
return 1
207-
}
208-
return len(indent)
223+
return len(expandIndentTabs(indent, tabWidth))
209224
}
210225
}
211226
return 1
212227
}
213228

229+
// detectTabIndent reports whether the source predominantly uses tab
230+
// indentation. When true, we emit tabs in the output indent so we respect
231+
// the file's existing style instead of forcing spaces.
232+
func detectTabIndent(lines []string) bool {
233+
tabCount, spaceCount := 0, 0
234+
for _, line := range lines {
235+
if len(line) == 0 {
236+
continue
237+
}
238+
switch line[0] {
239+
case '\t':
240+
tabCount++
241+
case ' ':
242+
spaceCount++
243+
}
244+
}
245+
return tabCount > spaceCount
246+
}
247+
248+
// leadingSpacesToTabs replaces leading space runs with tabs at unit-sized
249+
// stops. Applied after tabwriter has flushed, so inter-cell padding spaces
250+
// are preserved while the indent prefix is converted back to tabs.
251+
func leadingSpacesToTabs(s string, unit int) string {
252+
if unit <= 0 {
253+
return s
254+
}
255+
lines := strings.Split(s, "\n")
256+
for i, line := range lines {
257+
lead := 0
258+
for lead < len(line) && line[lead] == ' ' {
259+
lead++
260+
}
261+
tabs := lead / unit
262+
if tabs > 0 {
263+
lines[i] = strings.Repeat("\t", tabs) + line[tabs*unit:]
264+
}
265+
}
266+
return strings.Join(lines, "\n")
267+
}
268+
269+
// expandIndentTabs replaces tabs in the leading indent with spaces, using
270+
// tabWidth as the stop. Done so mixed tab/space files normalize uniformly
271+
// and literal tabs never reach tabwriter as the indent prefix.
272+
func expandIndentTabs(indent string, tabWidth int) string {
273+
if !strings.ContainsRune(indent, '\t') {
274+
return indent
275+
}
276+
if tabWidth <= 0 {
277+
tabWidth = 4
278+
}
279+
var b strings.Builder
280+
col := 0
281+
for _, c := range indent {
282+
if c == '\t' {
283+
n := tabWidth - (col % tabWidth)
284+
b.WriteString(strings.Repeat(" ", n))
285+
col += n
286+
continue
287+
}
288+
b.WriteRune(c)
289+
col++
290+
}
291+
return b.String()
292+
}
293+
214294
func normalizeIndent(indent string, indentUnit, targetSize int) string {
215295
if len(indent) == 0 {
216296
return ""

0 commit comments

Comments
 (0)