Pretty.hs 4.9 KB

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