From fb44e0031c6c767d67569e56af47d99427f665a6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C2=B7=F0=90=91=91=F0=90=91=B4=F0=90=91=95=F0=90=91=91?=
 =?UTF-8?q?=F0=90=91=A9=F0=90=91=A4?= <toastal@posteo.net>
Date: Thu, 25 Jun 2026 22:52:31 +0700
Subject: [PATCH] support tabs

---
 main/Main.hs         | 12 ++++++++----
 src/Nixfmt/Predoc.hs | 26 +++++++++++++-------------
 src/Nixfmt/Util.hs   |  7 +++++--
 3 files changed, 26 insertions(+), 19 deletions(-)

diff --git a/main/Main.hs b/main/Main.hs
index 92684f3..ad4f672 100644
--- a/main/Main.hs
+++ b/main/Main.hs
@@ -50,6 +50,7 @@ data Nixfmt = Nixfmt
   { files :: [FilePath],
     width :: Width,
     indent :: Int,
+    tabs :: Bool,
     check :: Bool,
     mergetool :: Bool,
     quiet :: Bool,
@@ -75,8 +76,9 @@ options =
          width =
            defaultWidth
              &= help (addDefaultHint defaultWidth "Maximum width in characters"),
-         indent = defaultIndent &= help (addDefaultHint defaultIndent "Number of spaces to use for indentation"),
-         check = False &= help "Check whether files are formatted without modifying them",
+          indent = defaultIndent &= help (addDefaultHint defaultIndent "Number of spaces to use for indentation"),
+          tabs = False &= help "Use tabs for indentation instead of spaces",
+          check = False &= help "Check whether files are formatted without modifying them",
          mergetool = False &= help "Whether to run in git mergetool mode, see https://github.com/NixOS/nixfmt?tab=readme-ov-file#git-mergetool for more info",
          quiet = False &= help "Do not report errors",
          strict = False &= help "Enable a stricter formatting mode that isn't influenced as much by how the input is formatted",
@@ -181,8 +183,10 @@ type Formatter = FilePath -> Text -> Either String Text
 toFormatter :: Nixfmt -> Formatter
 toFormatter Nixfmt{ast = True} = Nixfmt.printAst
 toFormatter Nixfmt{ir = True} = Nixfmt.printIR
-toFormatter Nixfmt{width, indent, verify = True, strict} = Nixfmt.formatVerify (layout width indent strict)
-toFormatter Nixfmt{width, indent, verify = False, strict} = Nixfmt.format (layout width indent strict)
+toFormatter Nixfmt{width, indent, tabs, verify = True, strict} =
+  Nixfmt.formatVerify (layout width (if tabs then 1 else indent) (if tabs then '\t' else ' ') strict)
+toFormatter Nixfmt{width, indent, tabs, verify = False, strict} =
+  Nixfmt.format (layout width (if tabs then 1 else indent) (if tabs then '\t' else ' ') strict)
 
 type Operation = Formatter -> Target -> IO Result
 

diff --git a/src/Nixfmt/Predoc.hs b/src/Nixfmt/Predoc.hs
index 8d8a02c..5c80523 100644
--- a/src/Nixfmt/Predoc.hs	1970-01-01 07:00:01.000000000 +0700
+++ b/src/Nixfmt/Predoc.hs	2026-07-10 23:38:56.053308065 +0700
@@ -47,11 +47,11 @@
 import Data.Functor (($>), (<&>))
 import Data.Functor.Identity (runIdentity)
 import Data.List (intersperse)
-import Data.List.NonEmpty (NonEmpty (..), singleton, (<|))
+import Data.List.NonEmpty (NonEmpty (..), (<|))
 import qualified Data.List.NonEmpty as NonEmpty
 import Data.Maybe (fromMaybe)
 import qualified Data.Sequence as Seq
-import Data.Text as Text (Text, concat, length, replicate, strip, stripStart, takeWhileEnd)
+import Data.Text as Text (Text, concat, length, replicate, singleton, strip, stripStart, takeWhileEnd)
 import GHC.Stack (HasCallStack)
 import Nixfmt.Types (
   Ann (..),
@@ -421,8 +421,8 @@
 mergeSpacings _ (Newlines x) = Newlines (x + 1)
 mergeSpacings _ y = y
 
-layout :: (Pretty a, LanguageElement a) => Int -> Int -> Bool -> a -> Text
-layout width indentWidth strict a = finalize $ layoutGreedy width indentWidth doc
+layout :: (Pretty a, LanguageElement a) => Int -> Int -> Char -> Bool -> a -> Text
+layout width indentWidth indentChar strict a = finalize $ layoutGreedy width indentWidth indentChar doc
   where
     doc =
       fixup
@@ -569,8 +569,8 @@
 newlines n = Text.replicate n "\n"
 
 -- | Create `n` spaces
-indent :: Int -> Text
-indent n = Text.replicate n " "
+indent :: Char -> Int -> Text
+indent c n = Text.replicate n (Text.singleton c)
 
 -- All state is (cc, indents, suppress)
 -- cc: current column (within the current line, *not including indentation*)
@@ -583,8 +583,8 @@
 type St = (Int, NonEmpty (Int, Int), Bool)
 
 -- tw   Target Width
-layoutGreedy :: Int -> Int -> Doc -> Text
-layoutGreedy tw iw doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, singleton (0, 0), False)
+layoutGreedy :: Int -> Int -> Char -> Doc -> Text
+layoutGreedy tw iw ic doc = Text.concat $ evalState (go [Group RegularG doc] []) (0, NonEmpty.singleton (0, 0), False)
   where
     -- Simple helpers around `put` with a tuple state
     -- NOTE: making putL strict removes the allocation for the Int
@@ -624,7 +624,7 @@
         go' = do
           (cc, (ci, _) :| _, _) <- get
           putL (if withIndent then cc + textWidth t else max 1 (cc + textWidth t))
-          let !i'd = indent (ci + textOffset)
+          let !i'd = indent ic (ci + textOffset)
           emit $! if cc == 0 && withIndent then [i'd, t] else [t]
 
     -- Simply put text without caring about line-start indentation

diff --git a/src/Nixfmt/Util.hs b/src/Nixfmt/Util.hs
index c2ae182..bc53845 100644
--- a/src/Nixfmt/Util.hs
+++ b/src/Nixfmt/Util.hs
@@ -69,12 +69,15 @@ commonPrefix a b =
     Nothing -> empty
     Just (prefix, _, _) -> prefix
 
+isIndentChar :: Char -> Bool
+isIndentChar c = c == ' ' || c == '\t'
+
 -- | The longest common prefix consisting of only whitespace. The longest common
 -- prefix of zero texts is infinite, represented as Nothing.
 commonIndentation :: [Text] -> Maybe Text
 commonIndentation [] = Nothing
-commonIndentation [x] = Just $ Text.takeWhile (== ' ') x
+commonIndentation [x] = Just $ Text.takeWhile isIndentChar x
 commonIndentation (x : y : xs) = commonIndentation (commonPrefix x y : xs)
 
 isSpaces :: Text -> Bool
-isSpaces = Text.all (== ' ')
+isSpaces = Text.all isIndentChar
-- 
2.54.0

