on master help sign in
Refactor the comment rendering code and ‘newline’
a3a93ebmrkkrp
data/examples/other/comment-two-blocks-out.hsnew file+8
@@ -0,0 +1,8 @@
1+newNames :: [(String, String)]
2+newNames =
3+ let (*) = flip (,)
4+ in [ "Control" * "Monad"
5+ -- Foo
6+
7+ -- Bar
8+ ]
data/examples/other/comment-two-blocks.hsnew file+9
@@ -0,0 +1,9 @@
1+newNames :: [(String, String)]
2+newNames = let (*) = flip (,) in
3+ ["Control" * "Monad"
4+
5+ -- Foo
6+
7+ -- Bar
8+
9+ ]
data/examples/other/merging-comments-out.hsnew file+12
@@ -0,0 +1,12 @@
1+foo xs = baz
2+ where
3+ bar =
4+ catMaybes
5+ [ lookup langKey gets, -- 1
6+ lookup langKey cookies, -- 2
7+ lookupText langKey session -- 3
8+ ]
9+ ++ xs -- 4
10+
11+ -- Blah
12+ baz = addTwoLetters (id, Set.empty) bar
data/examples/other/merging-comments.hsnew file+9
@@ -0,0 +1,9 @@
1+foo xs = baz
2+ where
3+ bar = catMaybes [ lookup langKey gets -- 1
4+ , lookup langKey cookies -- 2
5+ , lookupText langKey session -- 3
6+ ] ++ xs -- 4
7+
8+ -- Blah
9+ baz = addTwoLetters (id, Set.empty) bar
default.nix+4−6
@@ -36,6 +36,7 @@ in {
36 "ShellCheck"
36 "ShellCheck"
37 "aeson"
37 "aeson"
38 "attoparsec"
38 "attoparsec"
39+ "aws"
39 "cassava"
40 "cassava"
40 "conduit"
41 "conduit"
41 "cryptonite"
42 "cryptonite"
@@ -43,34 +44,31 @@ in {
43 "distributed-process"
44 "distributed-process"
44 "esqueleto"
45 "esqueleto"
45 "fay"
46 "fay"
47+ "haxl"
46 "hedgehog"
48 "hedgehog"
47 "hlint"
49 "hlint"
48 "megaparsec"
50 "megaparsec"
51+ "ormolu"
49 "postgrest"
52 "postgrest"
50 "servant"
53 "servant"
51 "servant-server"
54 "servant-server"
52 "tensorflow"
55 "tensorflow"
56+ "yesod-core"
53
57
54 # Comment idempotence issue
58 # Comment idempotence issue
55
59
56 # "Agda"
60 # "Agda"
57− # "aws"
58 # "brick"
61 # "brick"
59 # "hakyll"
62 # "hakyll"
60− # "haxl"
61 # "hledger"
63 # "hledger"
62 # "http-client"
64 # "http-client"
63 # "idris"
65 # "idris"
64 # "intero"
66 # "intero"
65 # "leksah"
67 # "leksah"
66− # "lens"
67− # "ormolu"
68 # "pandoc"
68 # "pandoc"
69 # "pipes"
69 # "pipes"
70− # "purescript"
71 # "stack"
70 # "stack"
72 # "tls"
71 # "tls"
73− # "yesod-core"
74
72
75 # Missing language extension
73 # Missing language extension
76
74
src/Ormolu.hs+2−2
@@ -61,7 +61,7 @@ ormolu cfg path str = do
61 -- when we try to parse the rendered code back, inside of GHC monad
61 -- when we try to parse the rendered code back, inside of GHC monad
62 -- wrapper which will lead to error messages presenting the exceptions as
62 -- wrapper which will lead to error messages presenting the exceptions as
63 -- GHC bugs.
63 -- GHC bugs.
64− let !txt = printModule (cfgDebug cfg) result0
64+ let !txt = printModule result0
65 when (not (cfgUnsafe cfg) || cfgCheckIdempotency cfg) $ do
65 when (not (cfgUnsafe cfg) || cfgCheckIdempotency cfg) $ do
66 let pathRendered = path ++ "<rendered>"
66 let pathRendered = path ++ "<rendered>"
67 -- Parse the result of pretty-printing again and make sure that AST
67 -- Parse the result of pretty-printing again and make sure that AST
@@ -79,7 +79,7 @@ ormolu cfg path str = do
79 -- Try re-formatting the formatted result to check if we get exactly
79 -- Try re-formatting the formatted result to check if we get exactly
80 -- the same output.
80 -- the same output.
81 when (cfgCheckIdempotency cfg) $
81 when (cfgCheckIdempotency cfg) $
82− let txt2 = printModule False result1
82+ let txt2 = printModule result1
83 in case diffText txt txt2 pathRendered of
83 in case diffText txt txt2 pathRendered of
84 Nothing -> return ()
84 Nothing -> return ()
85 Just (loc, l, r) -> liftIO $
85 Just (loc, l, r) -> liftIO $
src/Ormolu/Printer.hs+3−5
@@ -18,12 +18,10 @@ import Ormolu.Printer.SpanStream
18-- | Render a module.
18-- | Render a module.
19
19
20printModule
20printModule
21− :: Bool -- ^ Whether to trace debugging information
22− -> ParseResult -- ^ Result of parsing
21+ :: ParseResult -- ^ Result of parsing
23 -> Text -- ^ Resulting rendition
22 -> Text -- ^ Resulting rendition
24−printModule debugOn ParseResult {..} =
23+printModule ParseResult {..} =
25− runR debugOn
26− (p_hsModule prExtensions prParsedSource)
24+ runR (p_hsModule prExtensions prParsedSource)
27 (mkSpanStream prParsedSource)
25 (mkSpanStream prParsedSource)
28 prCommentStream
26 prCommentStream
29 prAnns
27 prAnns
src/Ormolu/Printer/Combinators.hs−12
@@ -33,7 +33,6 @@ module Ormolu.Printer.Combinators
33 , dontUseBraces
33 , dontUseBraces
34 -- ** Wrapping
34 -- ** Wrapping
35 , sitcc
35 , sitcc
36− , line
37 , backticks
36 , backticks
38 , banana
37 , banana
39 , braces
38 , braces
@@ -181,17 +180,6 @@ sepSemi f xs = vlayout singleLine multiLine
181----------------------------------------------------------------------------
180----------------------------------------------------------------------------
182-- Wrapping
181-- Wrapping
183
182
184−-- | Finish given entity by a 'newline'.
185−--
186−-- > line m = do
187−-- > m
188−-- > newline
189−
190−line :: R () -> R ()
191−line m = do
192− m
193− newline
194−
195-- | Surround given entity by backticks.
183-- | Surround given entity by backticks.
196
184
197backticks :: R () -> R ()
185backticks :: R () -> R ()
src/Ormolu/Printer/Comments.hs+48−43
@@ -8,8 +8,6 @@ module Ormolu.Printer.Comments
8 ( spitPrecedingComments
8 ( spitPrecedingComments
9 , spitFollowingComments
9 , spitFollowingComments
10 , spitRemainingComments
10 , spitRemainingComments
11− , isNewlineModified
12− , hasMoreComments
13 )
11 )
14where
12where
15
13
@@ -58,7 +56,7 @@ spitPrecedingComment
58 :: Data a
56 :: Data a
59 => RealLocated a -- ^ AST element to attach comments to
57 => RealLocated a -- ^ AST element to attach comments to
60 -> Maybe RealSrcSpan -- ^ Location of last comment in the series
58 -> Maybe RealSrcSpan -- ^ Location of last comment in the series
61− -> R (Maybe RealSrcSpan) -- ^ Location of this comment
59+ -> R Bool -- ^ Are we done?
62spitPrecedingComment (L ref a) mlastSpn = do
60spitPrecedingComment (L ref a) mlastSpn = do
63 let p (L l _) = realSrcSpanEnd l <= realSrcSpanStart ref
61 let p (L l _) = realSrcSpanEnd l <= realSrcSpanStart ref
64 withPoppedComment p $ \l comment -> do
62 withPoppedComment p $ \l comment -> do
@@ -73,7 +71,7 @@ spitPrecedingComment (L ref a) mlastSpn = do
73 Nothing -> isLineDirty -- only for very first preceding comment
71 Nothing -> isLineDirty -- only for very first preceding comment
74 Just _ -> return False
72 Just _ -> return False
75 when (dirtyLine || needsNewlineBefore l mlastSpn) newline
73 when (dirtyLine || needsNewlineBefore l mlastSpn) newline
76− spitComment comment
74+ spitCommentNow l comment
77 if theSameLinePre l ref && not (isModule a)
75 if theSameLinePre l ref && not (isModule a)
78 then space
76 then space
79 else newline
77 else newline
@@ -85,43 +83,29 @@ spitFollowingComment
85 :: Data a
83 :: Data a
86 => RealLocated a -- ^ AST element to attach comments to
84 => RealLocated a -- ^ AST element to attach comments to
87 -> Maybe RealSrcSpan -- ^ Location of last comment in the series
85 -> Maybe RealSrcSpan -- ^ Location of last comment in the series
88− -> R (Maybe RealSrcSpan) -- ^ Location of this comment
86+ -> R Bool -- ^ Are we done?
89spitFollowingComment (L ref a) mlastSpn = do
87spitFollowingComment (L ref a) mlastSpn = do
90 mnSpn <- nextEltSpan
88 mnSpn <- nextEltSpan
91 -- Get first enclosing span that is not equal to reference span, i.e. it's
89 -- Get first enclosing span that is not equal to reference span, i.e. it's
92 -- truly something enclosing the AST element.
90 -- truly something enclosing the AST element.
93 meSpn <- getEnclosingSpan (/= ref)
91 meSpn <- getEnclosingSpan (/= ref)
94− newlineModified <- isNewlineModified
95− i <- getIndent
96 withPoppedComment (commentFollowsElt ref mnSpn meSpn mlastSpn) $ \l comment ->
92 withPoppedComment (commentFollowsElt ref mnSpn meSpn mlastSpn) $ \l comment ->
97 if theSameLinePost l ref && not (isModule a)
93 if theSameLinePost l ref && not (isModule a)
98− then modNewline $ \m -> setIndent i $ do
94+ then spitCommentPending OnTheSameLine l comment
99− if newlineModified
95+ else do
100− then do
101− -- This happens when we have several lines each with its own
102− -- comment and they get merged by the formatter.
103− m
104− spitComment comment
105− newline
106− else do
107− space
108− spitComment comment
109− m
110− else modNewline $ \m -> setIndent i $ do
111− m
112− when (needsNewlineBefore l mlastSpn) newline
96+ when (needsNewlineBefore l mlastSpn) $
113− spitComment comment
97+ registerPendingCommentLine OnNextLine ""
114− newline
98+ spitCommentPending OnNextLine l comment
115
99
116-- | Output a single remaining comment from the comment stream.
100-- | Output a single remaining comment from the comment stream.
117
101
118spitRemainingComment
102spitRemainingComment
119 :: Maybe RealSrcSpan -- ^ Location of last comment in the series
103 :: Maybe RealSrcSpan -- ^ Location of last comment in the series
120− -> R (Maybe RealSrcSpan) -- ^ Location of this comment
104+ -> R Bool -- ^ Are we done?
121spitRemainingComment mlastSpn =
105spitRemainingComment mlastSpn =
122 withPoppedComment (const True) $ \l comment -> do
106 withPoppedComment (const True) $ \l comment -> do
123 when (needsNewlineBefore l mlastSpn) newline
107 when (needsNewlineBefore l mlastSpn) newline
124− spitComment comment
108+ spitCommentNow l comment
125 newline
109 newline
126
110
127----------------------------------------------------------------------------
111----------------------------------------------------------------------------
@@ -130,30 +114,28 @@ spitRemainingComment mlastSpn =
130-- | Output series of comments.
114-- | Output series of comments.
131
115
132handleCommentSeries
116handleCommentSeries
133− :: (Maybe RealSrcSpan -> R (Maybe RealSrcSpan))
117+ :: (Maybe RealSrcSpan -> R Bool)
134 -- ^ Given location of previous comment, output the next comment
118 -- ^ Given location of previous comment, output the next comment
135− -- returning its location, or 'Nothing' if we are done
119+ -- returning 'True' if we're done
136 -> R ()
120 -> R ()
137−handleCommentSeries f = go Nothing
121+handleCommentSeries f = go
138 where
122 where
139− go mlastSpn = do
123+ go = do
140− r <- f mlastSpn
124+ done <- getLastCommentSpan >>= f
141− case r of
125+ unless done go
142− Nothing -> return ()
143− Just spn -> go (Just spn)
144
126
145-- | Try to pop a comment using given predicate and if there is a comment
127-- | Try to pop a comment using given predicate and if there is a comment
146-- matching the predicate, print it out.
128-- matching the predicate, print it out.
147
129
148withPoppedComment
130withPoppedComment
149 :: (RealLocated Comment -> Bool) -- ^ Comment predicate
131 :: (RealLocated Comment -> Bool) -- ^ Comment predicate
150− -> (RealSrcSpan -> Comment -> R ()) -- ^ Priting function
132+ -> (RealSrcSpan -> Comment -> R ()) -- ^ Printing function
151− -> R (Maybe RealSrcSpan)
133+ -> R Bool -- ^ Are we done?
152withPoppedComment p f = do
134withPoppedComment p f = do
153 r <- popComment p
135 r <- popComment p
154 case r of
136 case r of
155− Nothing -> return Nothing
137+ Nothing -> return True
156− Just (L l comment) -> Just l <$ f l comment
138+ Just (L l comment) -> False <$ f l comment
157
139
158-- | Determine if we need to insert a newline between current comment and
140-- | Determine if we need to insert a newline between current comment and
159-- last printed comment.
141-- last printed comment.
@@ -255,8 +237,31 @@ commentFollowsElt ref mnSpn meSpn mlastSpn (L l comment) =
255 Just nspn -> realSrcSpanEnd espn < realSrcSpanStart nspn
237 Just nspn -> realSrcSpanEnd espn < realSrcSpanStart nspn
256 in insideParent && nextOutsideParent
238 in insideParent && nextOutsideParent
257
239
258−-- | Output a 'Comment'. This is a low-level printing function.
240+-- | Output a 'Comment' immediately. This is a low-level printing function.
259−
241+
260−spitComment :: Comment -> R ()
242+spitCommentNow :: RealSrcSpan -> Comment -> R ()
261−spitComment =
243+spitCommentNow spn comment = do
244+ sitcc
245+ . sequence_
262− sitcc . sequence_ . NE.intersperse newline . fmap (txt . T.pack) . coerce
246+ . NE.intersperse newline
247+ . fmap (txt . T.pack)
248+ . coerce
249+ $ comment
250+ setLastCommentSpan spn
251+
252+-- | Output a 'Comment' at the end of correct line or after it depending on
253+-- 'CommentPosition'. Used for comments that may potentially follow on the
254+-- same line as something we just rendered, but not immediately after it.
255+
256+spitCommentPending :: CommentPosition -> RealSrcSpan -> Comment -> R ()
257+spitCommentPending position spn comment = do
258+ let wrapper = case position of
259+ OnTheSameLine -> sitcc
260+ OnNextLine -> id
261+ wrapper
262+ . sequence_
263+ . NE.toList
264+ . fmap (registerPendingCommentLine position . T.pack)
265+ . coerce
266+ $ comment
267+ setLastCommentSpan spn
src/Ormolu/Printer/Internal.hs+176−159
@@ -17,8 +17,6 @@ module Ormolu.Printer.Internal
17 , atom
17 , atom
18 , space
18 , space
19 , newline
19 , newline
20− , modNewline
21− , isNewlineModified
22 , isLineDirty
20 , isLineDirty
23 , inci
21 , inci
24 , sitcc
22 , sitcc
@@ -30,14 +28,15 @@ module Ormolu.Printer.Internal
30 , dontUseBraces
28 , dontUseBraces
31 , canUseBraces
29 , canUseBraces
32 -- * Special helpers for comment placement
30 -- * Special helpers for comment placement
31+ , CommentPosition (..)
32+ , registerPendingCommentLine
33 , trimSpanStream
33 , trimSpanStream
34 , nextEltSpan
34 , nextEltSpan
35 , popComment
35 , popComment
36− , hasMoreComments
37− , getIndent
38− , setIndent
39 , getEnclosingSpan
36 , getEnclosingSpan
40 , withEnclosingSpan
37 , withEnclosingSpan
38+ , setLastCommentSpan
39+ , getLastCommentSpan
41 -- * Annotations
40 -- * Annotations
42 , getAnns
41 , getAnns
43 )
42 )
@@ -47,10 +46,9 @@ import Control.Monad.Reader
47import Control.Monad.State.Strict
46import Control.Monad.State.Strict
48import Data.Bool (bool)
47import Data.Bool (bool)
49import Data.Coerce
48import Data.Coerce
50−import Data.Maybe (fromMaybe, isJust, listToMaybe)
49+import Data.Maybe (listToMaybe)
51import Data.Text (Text)
50import Data.Text (Text)
52import Data.Text.Lazy.Builder
51import Data.Text.Lazy.Builder
53−import Debug.Trace
54import GHC
52import GHC
55import Ormolu.Parser.Anns
53import Ormolu.Parser.Anns
56import Ormolu.Parser.CommentStream
54import Ormolu.Parser.CommentStream
@@ -70,16 +68,15 @@ import qualified Data.Text.Lazy as TL
70newtype R a = R (ReaderT RC (State SC) a)
68newtype R a = R (ReaderT RC (State SC) a)
71 deriving (Functor, Applicative, Monad)
69 deriving (Functor, Applicative, Monad)
72
70
73−-- | Reader context of 'R'.
71+-- | Reader context of 'R'. This should be used when we control rendering by
72+-- enclosing certain expressions with wrappers.
74
73
75data RC = RC
74data RC = RC
76 { rcIndent :: !Int
75 { rcIndent :: !Int
77− -- ^ Indentation level, as the column index we need to start from after a
76+ -- ^ Indentation level, as the column index we need to start from after
78− -- newline if we break lines
77+ -- a newline if we break lines
79 , rcLayout :: Layout
78 , rcLayout :: Layout
80 -- ^ Current layout
79 -- ^ Current layout
81− , rcDebug :: Bool
82− -- ^ Whether to print debugging info as we go
83 , rcEnclosingSpans :: [RealSrcSpan]
80 , rcEnclosingSpans :: [RealSrcSpan]
84 -- ^ Spans of enclosing elements of AST
81 -- ^ Spans of enclosing elements of AST
85 , rcAnns :: Anns
82 , rcAnns :: Anns
@@ -99,14 +96,28 @@ data SC = SC
99 -- ^ Span stream
96 -- ^ Span stream
100 , scCommentStream :: CommentStream
97 , scCommentStream :: CommentStream
101 -- ^ Comment stream
98 -- ^ Comment stream
102− , scNewline :: Maybe (R ())
99+ , scPendingComments :: ![(CommentPosition, Int, Text)]
103− -- ^ What to render as newline, or 'Nothing' ('newlineRaw' will be used)
100+ -- ^ Pending comment lines (in reverse order) to be inserted before next
101+ -- newline, 'Int' is the indentation level
104 , scDirtyLine :: !Bool
102 , scDirtyLine :: !Bool
105− -- ^ Whether the current line is “dirty”
103+ -- ^ Whether the current line is “dirty”, that is, already contains
106− , scSpace :: !Bool
104+ -- atoms that can have comments attached to them
105+ , scRequestedDelimiter :: !RequestedDelimiter
107 -- ^ Whether to output a space before the next output
106 -- ^ Whether to output a space before the next output
107+ , scLastCommentSpan :: !(Maybe RealSrcSpan)
108+ -- ^ Span of last output comment
108 }
109 }
109
110
111+-- | Make sure next output is delimited by one of the following.
112+
113+data RequestedDelimiter
114+ = RequestedSpace -- ^ A space
115+ | RequestedNewline -- ^ A newline
116+ | RequestedNothing -- ^ Nothing
117+ | AfterNewline -- ^ We just output a newline
118+ | VeryBeginning -- ^ We haven't printed anything yet
119+ deriving (Eq, Show)
120+
110-- | 'Layout' options.
121-- | 'Layout' options.
111
122
112data Layout
123data Layout
@@ -114,22 +125,27 @@ data Layout
114 | MultiLine -- ^ Use multiple lines
125 | MultiLine -- ^ Use multiple lines
115 deriving (Eq, Show)
126 deriving (Eq, Show)
116
127
128+-- | Modes for rendering of pending comments.
129+
130+data CommentPosition
131+ = OnTheSameLine -- ^ Put the comment on the same line
132+ | OnNextLine -- ^ Put the comment on next line
133+ deriving (Eq, Show)
134+
117-- | Run an 'R' monad.
135-- | Run an 'R' monad.
118
136
119runR
137runR
120− :: Bool -- ^ Whether to print debugging info
121− -> R () -- ^ Monad to run
138+ :: R () -- ^ Monad to run
122 -> SpanStream -- ^ Span stream
139 -> SpanStream -- ^ Span stream
123 -> CommentStream -- ^ Comment stream
140 -> CommentStream -- ^ Comment stream
124 -> Anns -- ^ Annotations
141 -> Anns -- ^ Annotations
125 -> Text -- ^ Resulting rendition
142 -> Text -- ^ Resulting rendition
126−runR debug (R m) sstream cstream anns =
143+runR (R m) sstream cstream anns =
127 TL.toStrict . toLazyText . scBuilder $ execState (runReaderT m rc) sc
144 TL.toStrict . toLazyText . scBuilder $ execState (runReaderT m rc) sc
128 where
145 where
129 rc = RC
146 rc = RC
130 { rcIndent = 0
147 { rcIndent = 0
131 , rcLayout = MultiLine
148 , rcLayout = MultiLine
132− , rcDebug = debug
133 , rcEnclosingSpans = []
149 , rcEnclosingSpans = []
134 , rcAnns = anns
150 , rcAnns = anns
135 , rcCanUseBraces = False
151 , rcCanUseBraces = False
@@ -139,9 +155,10 @@ runR debug (R m) sstream cstream anns =
139 , scBuilder = mempty
155 , scBuilder = mempty
140 , scSpanStream = sstream
156 , scSpanStream = sstream
141 , scCommentStream = cstream
157 , scCommentStream = cstream
142− , scNewline = Nothing
158+ , scPendingComments = []
143 , scDirtyLine = False
159 , scDirtyLine = False
144− , scSpace = False
160+ , scRequestedDelimiter = VeryBeginning
161+ , scLastCommentSpan = Nothing
145 }
162 }
146
163
147----------------------------------------------------------------------------
164----------------------------------------------------------------------------
@@ -156,9 +173,9 @@ runR debug (R m) sstream cstream anns =
156-- 'atom'.
173-- 'atom'.
157
174
158txt
175txt
159− :: Text -- ^ 'Text' to output
176+ :: Text -- ^ 'Text' to output
160 -> R ()
177 -> R ()
161−txt = spit False
178+txt = spit False False
162
179
163-- | Output 'Outputable' fragment of AST. This can be used to output numeric
180-- | Output 'Outputable' fragment of AST. This can be used to output numeric
164-- literals and similar. Everything that doesn't have inner structure but
181-- literals and similar. Everything that doesn't have inner structure but
@@ -168,28 +185,43 @@ atom
168 :: Outputable a
185 :: Outputable a
169 => a
186 => a
170 -> R ()
187 -> R ()
171−atom = spit True . T.pack . showOutputable
188+atom = spit True False . T.pack . showOutputable
172
189
173-- | Low-level non-public helper to define 'txt' and 'atom'.
190-- | Low-level non-public helper to define 'txt' and 'atom'.
174
191
175−spit :: Bool -> Text -> R ()
192+spit
193+ :: Bool -- ^ Should we mark the line as dirty?
194+ -> Bool -- ^ Used during outputting of pending comments?
195+ -> Text -- ^ 'Text' to output
196+ -> R ()
176−spit dirty x' = do
197+spit dirty printingComments txt' = do
198+ requestedDel <- R (gets scRequestedDelimiter)
199+ case requestedDel of
200+ RequestedNewline -> do
201+ R . modify $ \sc -> sc
202+ { scRequestedDelimiter = RequestedNothing }
203+ if printingComments
204+ then newlineRaw
205+ else newline
206+ _ -> return ()
207+ R $ do
177− i <- R (asks rcIndent)
208+ i <- asks rcIndent
178− c <- R (gets scColumn)
209+ c <- gets scColumn
179− needsSpace <- R (gets scSpace)
180− let spaces =
210+ let spaces =
181− if (c < i)
211+ if c < i
182− then T.replicate (i - c) " "
212+ then T.replicate (i - c) " "
183− else bool mempty " " needsSpace
213+ else bool mempty " " (requestedDel == RequestedSpace)
184− x = spaces <> x'
214+ indentedTxt = spaces <> txt'
185− traceR "spit_before" (Just x)
186− R . modify $ \sc -> sc
215+ modify $ \sc -> sc
187− { scBuilder = scBuilder sc <> fromText x
216+ { scBuilder = scBuilder sc <> fromText indentedTxt
188− , scColumn = scColumn sc + T.length x
217+ , scColumn = scColumn sc + T.length indentedTxt
189− , scDirtyLine = scDirtyLine sc || dirty
218+ , scDirtyLine = scDirtyLine sc || dirty
190− , scSpace = False
219+ , scRequestedDelimiter = RequestedNothing
191− }
220+ , scLastCommentSpan =
221+ if printingComments
222+ then scLastCommentSpan sc
192− traceR "spit_after" Nothing
223+ else Nothing
224+ }
193
225
194-- | This primitive /does not/ necessarily output a space. It just ensures
226-- | This primitive /does not/ necessarily output a space. It just ensures
195-- that the next thing that will be printed on the same line will be
227-- that the next thing that will be printed on the same line will be
@@ -202,55 +234,68 @@ spit dirty x' = do
202
234
203space :: R ()
235space :: R ()
204space = R . modify $ \sc -> sc
236space = R . modify $ \sc -> sc
205− { scSpace = True
237+ { scRequestedDelimiter =
238+ case scRequestedDelimiter sc of
239+ RequestedNothing -> RequestedSpace
240+ other -> other
206 }
241 }
207
242
208−-- | Output a newline.
243+-- | Output a newline. First time 'newline' is used after some non-'newline'
244+-- output it gets inserted immediately. Second use of 'newline' does not
245+-- output anything but makes sure that the next non-white space output will
246+-- be prefixed by a newline. Using 'newline' more than twice in a row has no
247+-- effect. Also, using 'newline' at the very beginning has no effect, this
248+-- is to avoid leading whitespace.
249+--
250+-- Similarly to 'space', this design prevents trailing newlines and makes it
251+-- hard to output more than one blank newline in a row.
209
252
210newline :: R ()
253newline :: R ()
211newline = do
254newline = do
212− n <- R (gets scNewline)
255+ cs <- reverse <$> R (gets scPendingComments)
256+ case cs of
257+ [] -> newlineRaw
258+ ((position, _, _):_) -> do
259+ case position of
260+ OnTheSameLine -> space
261+ OnNextLine -> newlineRaw
262+ R . forM_ cs $ \(_, indent, txt') ->
263+ let modRC rc = rc
264+ { rcIndent = indent
265+ }
266+ R m = do
267+ unless (T.null txt') $
268+ spit False True txt'
269+ newlineRaw
270+ in local modRC m
213− R . modify $ \sc -> sc
271+ R . modify $ \sc -> sc
214− { scNewline = Nothing
272+ { scPendingComments = []
215− }
273+ }
216− fromMaybe newlineRaw n
217
274
218-- | Low-level newline primitive. This one always just inserts a newline, no
275-- | Low-level newline primitive. This one always just inserts a newline, no
219-- hooks can be attached.
276-- hooks can be attached.
220
277
221newlineRaw :: R ()
278newlineRaw :: R ()
222−newlineRaw = do
279+newlineRaw = R . modify $ \sc ->
223− traceR "newline_before" (Just "\n")
280+ let requestedDel = scRequestedDelimiter sc
224− R . modify $ \sc -> sc
281+ builderSoFar = scBuilder sc
282+ in sc
225− { scBuilder = scBuilder sc <> "\n"
283+ { scBuilder =
284+ case requestedDel of
285+ AfterNewline -> builderSoFar
286+ RequestedNewline -> builderSoFar
287+ VeryBeginning -> builderSoFar
288+ _ -> builderSoFar <> "\n"
226 , scColumn = 0
289 , scColumn = 0
227 , scDirtyLine = False
290 , scDirtyLine = False
228− , scSpace = False
291+ , scRequestedDelimiter =
229− }
292+ case scRequestedDelimiter sc of
230− traceR "newline_after" Nothing
293+ AfterNewline -> RequestedNewline
231−
294+ RequestedNewline -> RequestedNewline
232−-- | The 'modNewline' function can be used to alter what will be inserted as
295+ VeryBeginning -> VeryBeginning
233−-- a newline. This is used to output comments following an element of AST
296+ _ -> AfterNewline
234−-- because we cannot output comments immediately, e.g. because we need to
235−-- close parentheses first, etc.
236−--
237−-- 'newline' auto-resets its modifications so the changes introduced with
238−-- 'modNewline' only have effect once.
239−--
240−-- The argument of the call-back is the version of 'newline' built so far.
241−
242−modNewline :: (R () -> R ()) -> R ()
243−modNewline f = R $ do
244− old <- gets scNewline
245− modify $ \sc -> sc
246− { scNewline = Just $ f (fromMaybe newlineRaw old)
247 }
297 }
248
298
249−-- | Check if newline is in modified state.
250−
251−isNewlineModified :: R Bool
252−isNewlineModified = isJust <$> R (gets scNewline)
253−
254-- | Check if the current line is “dirty”, that is, there is something on it
299-- | Check if the current line is “dirty”, that is, there is something on it
255-- that can have comments attached to it.
300-- that can have comments attached to it.
256
301
@@ -264,14 +309,11 @@ isLineDirty = R (gets scDirtyLine)
264-- effect, but with multi-line layout correct indentation levels matter.
309-- effect, but with multi-line layout correct indentation levels matter.
265
310
266inci :: R () -> R ()
311inci :: R () -> R ()
267−inci m' = do
312+inci (R m) = R (local modRC m)
268− traceR "inci_before" Nothing
313+ where
269− let R m = traceR "inci_inside" Nothing >> m'
270− modRC rc = rc
314+ modRC rc = rc
271− { rcIndent = rcIndent rc + indentStep
315+ { rcIndent = rcIndent rc + indentStep
272− }
316+ }
273− R (local modRC m)
274− traceR "inci_ended" Nothing
275
317
276-- | Set indentation level for the inner computation equal to current
318-- | Set indentation level for the inner computation equal to current
277-- column. This makes sure that the entire inner block is uniformly
319-- column. This makes sure that the entire inner block is uniformly
@@ -279,35 +321,30 @@ inci m' = do
279-- layout is multi-line.
321-- layout is multi-line.
280
322
281sitcc :: R () -> R ()
323sitcc :: R () -> R ()
282−sitcc m' = do
324+sitcc (R m) = do
283− traceR "sitcc_before" Nothing
325+ requestedDel <- R (gets scRequestedDelimiter)
284 i <- R (asks rcIndent)
326 i <- R (asks rcIndent)
285 c <- R (gets scColumn)
327 c <- R (gets scColumn)
286− needsSpace <- R (gets scSpace)
287− let R m = traceR "sitcc_inside" Nothing >> m'
288− modRC rc = rc
328+ let modRC rc = rc
289− { rcIndent = max i c + bool 0 1 needsSpace
329+ { rcIndent = max i c + bool 0 1 (requestedDel == RequestedSpace)
290 }
330 }
291− vlayout m' . R $ do
331+ vlayout (R m) . R $ do
292− modify $ \sc -> sc { scSpace = False }
332+ modify $ \sc -> sc
333+ { scRequestedDelimiter =
334+ case requestedDel of
335+ RequestedSpace -> RequestedNothing
336+ other -> other
337+ }
293 local modRC m
338 local modRC m
294− traceR "sitcc_ended" Nothing
295
339
296-- | Set 'Layout' for internal computation.
340-- | Set 'Layout' for internal computation.
297
341
298enterLayout :: Layout -> R () -> R ()
342enterLayout :: Layout -> R () -> R ()
299−enterLayout l (R m) = do
343+enterLayout l (R m) = R (local modRC m)
300− let label =
344+ where
301− case l of
302− SingleLine -> "single_line"
303− MultiLine -> "multi_line"
304− traceR ("lstart_" ++ label) Nothing
305− let modRC rc = rc
345+ modRC rc = rc
306− { rcLayout = l
346+ { rcLayout = l
307− }
347+ }
308− x <- R (local modRC m)
309− traceR ("lend_" ++ label) Nothing
310− return x
311
348
312-- | Do one or another thing depending on current 'Layout'.
349-- | Do one or another thing depending on current 'Layout'.
313
350
@@ -324,6 +361,21 @@ vlayout sline mline = do
324----------------------------------------------------------------------------
361----------------------------------------------------------------------------
325-- Special helpers for comment placement
362-- Special helpers for comment placement
326
363
364+-- | Register a comment line for outputting. It will be inserted right
365+-- before next newline. When the comment goes after something else on the
366+-- same line, a space will be inserted between preceding text and the
367+-- comment when necessary.
368+
369+registerPendingCommentLine
370+ :: CommentPosition -- ^ Comment position
371+ -> Text -- ^ 'Text' to output
372+ -> R ()
373+registerPendingCommentLine position txt' = R $ do
374+ i <- asks rcIndent
375+ modify $ \sc -> sc
376+ { scPendingComments = (position, i, txt') : scPendingComments sc
377+ }
378+
327-- | Drop elements that begin before or at the same place as given
379-- | Drop elements that begin before or at the same place as given
328-- 'SrcSpan'.
380-- 'SrcSpan'.
329
381
@@ -359,30 +411,6 @@ popComment f = R $ do
359 })
411 })
360 else return Nothing
412 else return Nothing
361
413
362−-- | Return 'True' if there are more comments in the 'CommentStream'.
363−
364−hasMoreComments :: R Bool
365−hasMoreComments = R $ do
366− CommentStream cstream <- gets scCommentStream
367− (return . not . null) cstream
368−
369−-- | Current indentation level.
370−
371−getIndent :: R Int
372−getIndent = R (asks rcIndent)
373−
374−-- | Set indentation level for the given computation.
375−
376−setIndent :: Int -> R () -> R ()
377−setIndent i m' = do
378− traceR "set_indent_before" Nothing
379− let R m = traceR "set_indent_inside" Nothing >> m'
380− modRC rc = rc
381− { rcIndent = i
382− }
383− R (local modRC m)
384− traceR "set_indent_after" Nothing
385−
386-- | Get the first enclosing 'RealSrcSpan' that satisfies given predicate.
414-- | Get the first enclosing 'RealSrcSpan' that satisfies given predicate.
387
415
388getEnclosingSpan
416getEnclosingSpan
@@ -394,11 +422,23 @@ getEnclosingSpan f =
394-- | Set 'RealSrcSpan' of enclosing span for the given computation.
422-- | Set 'RealSrcSpan' of enclosing span for the given computation.
395
423
396withEnclosingSpan :: RealSrcSpan -> R () -> R ()
424withEnclosingSpan :: RealSrcSpan -> R () -> R ()
397−withEnclosingSpan spn (R m) = do
425+withEnclosingSpan spn (R m) = R (local modRC m)
426+ where
398− let modRC rc = rc
427+ modRC rc = rc
399− { rcEnclosingSpans = spn : rcEnclosingSpans rc
428+ { rcEnclosingSpans = spn : rcEnclosingSpans rc
400− }
429+ }
401− R (local modRC m)
430+
431+-- | Set span of last output comment.
432+
433+setLastCommentSpan :: RealSrcSpan -> R ()
434+setLastCommentSpan spn = R . modify $ \sc -> sc
435+ { scLastCommentSpan = Just spn
436+ }
437+
438+-- | Get span of last output comment.
439+
440+getLastCommentSpan :: R (Maybe RealSrcSpan)
441+getLastCommentSpan = R (gets scLastCommentSpan)
402
442
403----------------------------------------------------------------------------
443----------------------------------------------------------------------------
404-- Annotations
444-- Annotations
@@ -428,29 +468,6 @@ dontUseBraces (R r) = R (local (\i -> i {rcCanUseBraces = False}) r)
428canUseBraces :: R Bool
468canUseBraces :: R Bool
429canUseBraces = R $ asks rcCanUseBraces
469canUseBraces = R $ asks rcCanUseBraces
430
470
431−----------------------------------------------------------------------------
432−-- Debug helpers
433−
434−-- | Tracing helper.
435−
436−traceR
437− :: String -- ^ Label
438− -> Maybe Text -- ^ Output, if any
439− -> R ()
440−traceR label moutput = R $ do
441− debug <- asks rcDebug
442− i <- asks rcIndent
443− c <- gets scColumn
444− when debug . traceM . concat $
445− [ replicate i ' '
446− , unwords [ label ++ ":"
447− , "i=" ++ show i
448− , "c=" ++ show c
449− ]
450− ] ++ case moutput of
451− Nothing -> []
452− Just output -> [" out=" ++ show output]
453−
454----------------------------------------------------------------------------
471----------------------------------------------------------------------------
455-- Constants
472-- Constants
456
473
src/Ormolu/Printer/Meat/Declaration/Value.hs+2−1
@@ -740,7 +740,8 @@ p_patSynBind PSB {..} = do
740 space
740 space
741 located psb_def p_pat
741 located psb_def p_pat
742 newline
742 newline
743− line (txt "where")
743+ txt "where"
744+ newline
744 inci (p_matchGroup (Function psb_id) mgroup)
745 inci (p_matchGroup (Function psb_id) mgroup)
745 txt "pattern"
746 txt "pattern"
746 case psb_args of
747 case psb_args of
src/Ormolu/Printer/Meat/ImportExport.hs+2−1
@@ -26,7 +26,7 @@ p_hsmodExports xs =
26 parens . sitcc $ sep (comma >> breakpoint) (sitcc . located' p_lie) xs
26 parens . sitcc $ sep (comma >> breakpoint) (sitcc . located' p_lie) xs
27
27
28p_hsmodImport :: ImportDecl GhcPs -> R ()
28p_hsmodImport :: ImportDecl GhcPs -> R ()
29−p_hsmodImport ImportDecl {..} = line $ do
29+p_hsmodImport ImportDecl {..} = do
30 txt "import"
30 txt "import"
31 space
31 space
32 when ideclSource (txt "{-# SOURCE #-}")
32 when ideclSource (txt "{-# SOURCE #-}")
@@ -58,6 +58,7 @@ p_hsmodImport ImportDecl {..} = line $ do
58 breakpoint
58 breakpoint
59 inci . parens . sitcc $
59 inci . parens . sitcc $
60 sep (comma >> breakpoint) (sitcc . located' p_lie) a
60 sep (comma >> breakpoint) (sitcc . located' p_lie) a
61+ newline
61p_hsmodImport (XImportDecl NoExt) = notImplemented "XImportDecl"
62p_hsmodImport (XImportDecl NoExt) = notImplemented "XImportDecl"
62
63
63p_lie :: IE GhcPs -> R ()
64p_lie :: IE GhcPs -> R ()
src/Ormolu/Printer/Meat/Module.hs+8−22
@@ -10,7 +10,6 @@ module Ormolu.Printer.Meat.Module
10where
10where
11
11
12import Control.Monad
12import Control.Monad
13−import Data.Maybe (isJust)
14import GHC
13import GHC
15import Ormolu.Imports
14import Ormolu.Imports
16import Ormolu.Parser.Pragma
15import Ormolu.Parser.Pragma
@@ -31,17 +30,11 @@ p_hsModule pragmas (L moduleSpan HsModule {..}) = do
31 deprecSpan = maybe [] (\(L s _) -> [s]) hsmodDeprecMessage
30 deprecSpan = maybe [] (\(L s _) -> [s]) hsmodDeprecMessage
32 spans' = exportSpans ++ deprecSpan ++ [moduleSpan]
31 spans' = exportSpans ++ deprecSpan ++ [moduleSpan]
33 switchLayout spans' $ do
32 switchLayout spans' $ do
34− let hasLangPragmas = not (null pragmas)
35− hasModuleHeader = isJust hsmodName
36− hasImports = not (null hsmodImports)
37− hasDecls = not (null hsmodDecls)
38 p_pragmas pragmas
33 p_pragmas pragmas
39− when (hasLangPragmas &&
40− (hasModuleHeader || hasImports || hasDecls)) $
41− newline
34+ newline
42 case hsmodName of
35 case hsmodName of
43− Nothing -> pure ()
36+ Nothing -> return ()
44− Just hsmodName' -> line $ do
37+ Just hsmodName' -> do
45 located hsmodName' p_hsmodName
38 located hsmodName' p_hsmodName
46 forM_ hsmodDeprecMessage $ \w -> do
39 forM_ hsmodDeprecMessage $ \w -> do
47 breakpoint
40 breakpoint
@@ -53,18 +46,11 @@ p_hsModule pragmas (L moduleSpan HsModule {..}) = do
53 inci (p_hsmodExports (unLoc hsmodExports'))
46 inci (p_hsmodExports (unLoc hsmodExports'))
54 breakpoint
47 breakpoint
55 txt "where"
48 txt "where"
56− when (hasImports || hasDecls) newline
49+ newline
50+ newline
57 forM_ (sortImports hsmodImports) (located' p_hsmodImport)
51 forM_ (sortImports hsmodImports) (located' p_hsmodImport)
58− when (hasImports && hasDecls) newline
52+ newline
59− switchLayout (map getLoc hsmodDecls) $ do
53+ switchLayout (getLoc <$> hsmodDecls) $ do
60 p_hsDecls Free hsmodDecls
54 p_hsDecls Free hsmodDecls
61− trailingComments <- hasMoreComments
55+ newline
62− when hasDecls $ do
63− newlineModified <- isNewlineModified
64− newline
65− -- In this case we need to insert a newline between the comments
66− -- output as a side effect of the previous newline and trailing
67− -- comments to prevent them from merging.
68− when (newlineModified && trailingComments) newline
69− when (trailingComments && hasModuleHeader) newline
70 spitRemainingComments
56 spitRemainingComments
src/Ormolu/Printer/Meat/Pragma.hs+2−1
@@ -26,7 +26,7 @@ p_pragmas ps =
26 in mapM_ (uncurry p_pragma) (S.toAscList . S.fromList . prepare $ ps)
26 in mapM_ (uncurry p_pragma) (S.toAscList . S.fromList . prepare $ ps)
27
27
28p_pragma :: PragmaTy -> String -> R ()
28p_pragma :: PragmaTy -> String -> R ()
29−p_pragma ty c = line $ do
29+p_pragma ty c = do
30 txt "{-# "
30 txt "{-# "
31 txt $ case ty of
31 txt $ case ty of
32 Language -> "LANGUAGE"
32 Language -> "LANGUAGE"
@@ -35,3 +35,4 @@ p_pragma ty c = line $ do
35 space
35 space
36 txt (T.pack c)
36 txt (T.pack c)
37 txt " #-}"
37 txt " #-}"
38+ newline
tests/Ormolu/Printer/CombinatorsSpec.hs+4−1
@@ -43,6 +43,9 @@ stdTest name m path = describe name $ do
43----------------------------------------------------------------------------
43----------------------------------------------------------------------------
44-- Test computations
44-- Test computations
45
45
46+line :: R () -> R ()
47+line m = m >> newline
48+
46rSimpleSig :: R ()
49rSimpleSig :: R ()
47rSimpleSig = line rFn
50rSimpleSig = line rFn
48
51
@@ -96,7 +99,7 @@ rModuleHeader = do
96
99
97shouldRender :: R () -> FilePath -> Expectation
100shouldRender :: R () -> FilePath -> Expectation
98shouldRender m path = do
101shouldRender m path = do
99− let rendered = runR False m mempty mempty emptyAnns
102+ let rendered = runR m mempty mempty emptyAnns
100 -- T.writeFile path rendered
103 -- T.writeFile path rendered
101 expected <- T.readFile path
104 expected <- T.readFile path
102 rendered `shouldBe` expected
105 rendered `shouldBe` expected