Pretty.hs 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  1. {-# LANGUAGE RecordWildCards #-}
  2. {-# LANGUAGE OverloadedStrings #-}
  3. {-# LANGUAGE ScopedTypeVariables #-}
  4. module Data.SCargot.Pretty
  5. ( LayoutOptions(..)
  6. , basicPrint
  7. , flatPrint
  8. , prettyPrintSExpr
  9. ) where
  10. import Data.Monoid ((<>))
  11. import Data.Text (Text)
  12. import qualified Data.Text as T
  13. import Data.SCargot.Repr
  14. -- | A 'LayoutOptions' value describes how to pretty-print a 'SExpr'.
  15. -- It describes how to print atoms, what horizontal space to fit
  16. -- it into, and other related options.
  17. --
  18. -- The 'swingIndent' value might require a big of explanation: in
  19. -- pretty-printing s-expressions, you have the option of whether
  20. -- to 'swing' expressions which get pushed to subsequent lines
  21. -- to the left, or to align them along the right. e.g. the
  22. -- s-expression @(foo a b)@ could use a non-swing indent as
  23. --
  24. -- > (foo arg-one
  25. -- > arg-two)
  26. --
  27. -- or a swing indent as
  28. --
  29. -- > (foo arg-one
  30. -- > arg-two)
  31. --
  32. -- often, in formatting Lisp code, control structures will
  33. -- swing subsequent expressions, as in
  34. --
  35. -- > (define (factorial n)
  36. -- > (if (= n 0)
  37. -- > 1
  38. -- > (* n (fact (- n 1)))))
  39. --
  40. -- but most functions will _not_ swing:
  41. --
  42. -- > (call-my-func arg-number-one
  43. -- > arg-number-two
  44. -- > arg-number-three)
  45. --
  46. -- The 'swingIndent' field lets you choose whether or not to
  47. -- swing subsequent s-expressions based on the atom in the car
  48. -- position of a list. You can default to always swinging subsequent
  49. -- expressions with @const True@ and never with @const False@, or
  50. -- choose based on some more advanced criteria. _If_ a swing happens,
  51. -- subsequent lines are indented based on the 'indentAmount' variable;
  52. -- otherwise, subsequent lines are indented based on the size of the
  53. -- @car@ of the list.
  54. data LayoutOptions a = LayoutOptions
  55. { atomPrinter :: a -> Text -- ^ How to serialize a given atom to 'Text'.
  56. , swingIndent :: SExpr a -> Bool -- ^ Whether or not to swing
  57. , indentAmount :: Int -- ^ How much to indent after a swing
  58. , maxWidth :: Maybe Int -- ^ The maximum width (if any)
  59. }
  60. flatPrint :: (a -> Text) -> LayoutOptions a
  61. flatPrint printer = LayoutOptions
  62. { atomPrinter = printer
  63. , swingIndent = const True
  64. , indentAmount = 2
  65. , maxWidth = Nothing
  66. }
  67. basicPrint :: (a -> Text) -> LayoutOptions a
  68. basicPrint printer = LayoutOptions
  69. { atomPrinter = printer
  70. , swingIndent = const True
  71. , indentAmount = 2
  72. , maxWidth = Just 80
  73. }
  74. -- Sort of like 'unlines' but without the trailing newline
  75. joinLines :: [Text] -> Text
  76. joinLines = T.intercalate "\n"
  77. -- Indents a line by n spaces
  78. indent :: Int -> Text -> Text
  79. indent n ts = T.replicate n " " <> ts
  80. -- Indents every line n spaces, and adds a newline to the beginning
  81. indentAll :: Int -> [Text] -> Text
  82. indentAll n = ("\n" <>) . joinLines . map (indent n)
  83. -- Indents every line but the first by some amount
  84. indentSubsequent :: Int -> [Text] -> Text
  85. indentSubsequent _ [] = ""
  86. indentSubsequent _ [t] = t
  87. indentSubsequent n (t:ts) = joinLines (t : go ts)
  88. where go = map (indent n)
  89. -- oh god this code is so disgusting
  90. -- i'm sorry to everyone i let down by writing this
  91. -- i swear i'll do better in the future i promise i have to
  92. -- for my sake and for everyone's
  93. prettyPrintSExpr :: LayoutOptions a -> SExpr a -> Text
  94. prettyPrintSExpr LayoutOptions { .. } = pHead 0
  95. where pHead _ SNil = "()"
  96. pHead _ (SAtom a) = atomPrinter a
  97. pHead ind (SCons x xs) = gather ind x xs id
  98. gather _ _ (SAtom _) _ = error "no dotted pretty printing yet!"
  99. gather ind h (SCons x xs) k = gather ind h xs (k . (x:))
  100. gather ind h SNil k = "(" <> hd <> body <> ")"
  101. where hd = indentSubsequent ind [pHead (ind+1) h]
  102. lst = k []
  103. flat = T.unwords (map (pHead (ind+1)) lst)
  104. headWidth = T.length hd + 1
  105. indented
  106. | swingIndent h =
  107. indentAll (ind + indentAmount)
  108. (map (pHead (ind + indentAmount)) lst)
  109. | otherwise =
  110. indentSubsequent (ind + headWidth + 1)
  111. (map (pHead (ind + headWidth + 1)) lst)
  112. body | length lst == 0 = ""
  113. | Just maxAmt <- maxWidth
  114. , (T.length flat + ind) > maxAmt = " " <> indented
  115. | otherwise = " " <> flat