@@ -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+
214294func normalizeIndent (indent string , indentUnit , targetSize int ) string {
215295 if len (indent ) == 0 {
216296 return ""
0 commit comments