Skip to content

Commit b8a731e

Browse files
authored
Introduce nicer style for records (#266)
1 parent ab85690 commit b8a731e

File tree

8 files changed

+449
-139
lines changed

8 files changed

+449
-139
lines changed

README.markdown

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ You can also install it using your package manager:
3333
- Replaces tabs by four spaces (turned off by default)
3434
- Replaces some ASCII sequences by their Unicode equivalents (turned off by
3535
default)
36+
- Format data constructors and fields in records.
3637

3738
Feature requests are welcome! Use the [issue tracker] for that.
3839

@@ -102,6 +103,61 @@ Use `stylish-haskell --defaults > .stylish-haskell.yaml` to dump a
102103
well-documented default configuration to a file, this way you can get started
103104
quickly.
104105

106+
## Record formatting
107+
108+
Basically, stylish-haskell supports 4 different styles of records, controlled by `records`
109+
in the config file.
110+
111+
Here's an example of all four styles:
112+
113+
```haskell
114+
-- equals: "indent 2", "first_field": "indent 2"
115+
data Foo a
116+
= Foo
117+
{ a :: Int
118+
, a2 :: String
119+
-- ^ some haddock
120+
}
121+
| Bar
122+
{ b :: a
123+
}
124+
deriving (Eq, Show)
125+
deriving (ToJSON) via Bar Foo
126+
127+
-- equals: "same_line", "first_field": "indent 2"
128+
data Foo a = Foo
129+
{ a :: Int
130+
, a2 :: String
131+
-- ^ some haddock
132+
}
133+
| Bar
134+
{ b :: a
135+
}
136+
deriving (Eq, Show)
137+
deriving (ToJSON) via Bar Foo
138+
139+
-- equals: "same_line", "first_field": "same_line"
140+
data Foo a = Foo { a :: Int
141+
, a2 :: String
142+
-- ^ some haddock
143+
}
144+
| Bar { b :: a
145+
}
146+
deriving (Eq, Show)
147+
deriving (ToJSON) via Bar Foo
148+
149+
-- equals: "indent 2", first_field: "same_line"
150+
data Foo a
151+
= Foo { a :: Int
152+
, a2 :: String
153+
-- ^ some haddock
154+
}
155+
| Bar { b :: a
156+
}
157+
deriving (Eq, Show)
158+
deriving (ToJSON) via Bar Foo
159+
```
160+
105161
## VIM integration
106162

107163
Since it works as a filter it is pretty easy to integrate this with VIM.

data/stylish-haskell.yaml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,33 @@ steps:
1515
# # true.
1616
# add_language_pragma: true
1717

18-
# Format record definitions
19-
- records: {}
18+
# Format record definitions. This is disabled by default.
19+
#
20+
# You can control the layout of record fields. The only rules that can't be configured
21+
# are these:
22+
#
23+
# - "|" is always aligned with "="
24+
# - "," in fields is always aligned with "{"
25+
# - "}" is likewise always aligned with "{"
26+
#
27+
# - records:
28+
# # How to format equals sign between type constructor and data constructor.
29+
# # Possible values:
30+
# # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor.
31+
# # - "indent N" -- insert a new line and N spaces from the beginning of the next line.
32+
# equals: "indent 2"
33+
#
34+
# # How to format first field of each record constructor.
35+
# # Possible values:
36+
# # - "same_line" -- "{" and first field goes on the same line as the data constructor.
37+
# # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor
38+
# first_field: "indent 2"
39+
#
40+
# # How many spaces to insert between the column with "," and the beginning of the comment in the next line.
41+
# field_comment: 2
42+
#
43+
# # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines.
44+
# deriving: 2
2045

2146
# Align the right hand side of some elements. This is quite conservative
2247
# and only applies to statements where each element occupies a single
@@ -225,9 +250,6 @@ steps:
225250
# simple_align but is a bit less conservative.
226251
# - squash: {}
227252

228-
# A common indentation setting. Different steps take this into account.
229-
indent: 4
230-
231253
# A common setting is the number of columns (parts of) code will be wrapped
232254
# to. Different steps take this into account.
233255
#

lib/Language/Haskell/Stylish/Config.hs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,14 @@ import Data.List (intercalate,
2424
import Data.Map (Map)
2525
import qualified Data.Map as M
2626
import Data.Maybe (fromMaybe)
27+
import qualified Data.Text as T
2728
import Data.YAML (prettyPosWithSource)
2829
import Data.YAML.Aeson (decode1Strict)
2930
import System.Directory
3031
import System.FilePath ((</>))
3132
import qualified System.IO as IO (Newline (..),
3233
nativeNewline)
34+
import Text.Read (readMaybe)
3335

3436

3537
--------------------------------------------------------------------------------
@@ -54,7 +56,6 @@ type Extensions = [String]
5456
--------------------------------------------------------------------------------
5557
data Config = Config
5658
{ configSteps :: [Step]
57-
, configIndent :: Int
5859
, configColumns :: Maybe Int
5960
, configLanguageExtensions :: [String]
6061
, configNewline :: IO.Newline
@@ -121,7 +122,6 @@ parseConfig (A.Object o) = do
121122
-- First load the config without the actual steps
122123
config <- Config
123124
<$> pure []
124-
<*> (o A..:? "indent" A..!= 4)
125125
<*> (o A..:! "columns" A..!= Just 80)
126126
<*> (o A..:? "language_extensions" A..!= [])
127127
<*> (o A..:? "newline" >>= parseEnum newlines IO.nativeNewline)
@@ -186,8 +186,25 @@ parseSimpleAlign c o = SimpleAlign.step
186186

187187
--------------------------------------------------------------------------------
188188
parseRecords :: Config -> A.Object -> A.Parser Step
189-
parseRecords c _ = Data.step
190-
<$> pure (configIndent c)
189+
parseRecords _ o = Data.step
190+
<$> (Data.Config
191+
<$> (o A..: "equals" >>= parseIndent)
192+
<*> (o A..: "first_field" >>= parseIndent)
193+
<*> (o A..: "field_comment")
194+
<*> (o A..: "deriving"))
195+
196+
197+
parseIndent :: A.Value -> A.Parser Data.Indent
198+
parseIndent = A.withText "Indent" $ \t ->
199+
if t == "same_line"
200+
then return Data.SameLine
201+
else
202+
if "indent " `T.isPrefixOf` t
203+
then
204+
case readMaybe (T.unpack $ T.drop 7 t) of
205+
Just n -> return $ Data.Indent n
206+
Nothing -> fail $ "Indent: not a number" <> T.unpack (T.drop 7 t)
207+
else fail $ "can't parse indent setting: " <> T.unpack t
191208

192209

193210
--------------------------------------------------------------------------------
Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
{-# LANGUAGE RecordWildCards #-}
2+
13
module Language.Haskell.Stylish.Step.Data where
24

35
import Data.List (find, intercalate)
4-
import Data.Maybe (maybeToList)
6+
import Data.Maybe (fromMaybe, maybeToList)
57
import qualified Language.Haskell.Exts as H
68
import Language.Haskell.Exts.Comments
79
import Language.Haskell.Stylish.Block
@@ -10,20 +12,36 @@ import Language.Haskell.Stylish.Step
1012
import Language.Haskell.Stylish.Util
1113
import Prelude hiding (init)
1214

15+
data Indent
16+
= SameLine
17+
| Indent !Int
18+
deriving (Show)
19+
20+
data Config = Config
21+
{ cEquals :: !Indent
22+
-- ^ Indent between type constructor and @=@ sign (measured from column 0)
23+
, cFirstField :: !Indent
24+
-- ^ Indent between data constructor and @{@ line (measured from column with data constructor name)
25+
, cFieldComment :: !Int
26+
-- ^ Indent between column with @{@ and start of field line comment (this line has @cFieldComment = 2@)
27+
, cDeriving :: !Int
28+
-- ^ Indent before @deriving@ lines (measured from column 0)
29+
} deriving (Show)
30+
1331
datas :: H.Module l -> [H.Decl l]
1432
datas (H.Module _ _ _ _ decls) = decls
1533
datas _ = []
1634

1735
type ChangeLine = Change String
1836

19-
step :: Int -> Step
20-
step indentSize = makeStep "Data" (step' indentSize)
37+
step :: Config -> Step
38+
step cfg = makeStep "Data" (step' cfg)
2139

22-
step' :: Int -> Lines -> Module -> Lines
23-
step' indentSize ls (module', allComments) = applyChanges changes ls
40+
step' :: Config -> Lines -> Module -> Lines
41+
step' cfg ls (module', allComments) = applyChanges changes ls
2442
where
2543
datas' = datas $ fmap linesFromSrcSpan module'
26-
changes = datas' >>= maybeToList . changeDecl allComments indentSize
44+
changes = datas' >>= maybeToList . changeDecl allComments cfg
2745

2846
findCommentOnLine :: LineBlock -> [Comment] -> Maybe Comment
2947
findCommentOnLine lb = find commentOnLine
@@ -43,9 +61,9 @@ commentsWithin lb = filter within
4361
within (Comment _ (H.SrcSpan _ start _ end _) _) =
4462
start >= blockStart lb && end <= blockEnd lb
4563

46-
changeDecl :: [Comment] -> Int -> H.Decl LineBlock -> Maybe ChangeLine
64+
changeDecl :: [Comment] -> Config -> H.Decl LineBlock -> Maybe ChangeLine
4765
changeDecl _ _ (H.DataDecl _ (H.DataType _) Nothing _ [] _) = Nothing
48-
changeDecl allComments indentSize (H.DataDecl block (H.DataType _) Nothing dhead decls derivings)
66+
changeDecl allComments cfg@Config{..} (H.DataDecl block (H.DataType _) Nothing dhead decls derivings)
4967
| hasRecordFields = Just $ change block (const $ concat newLines)
5068
| otherwise = Nothing
5169
where
@@ -54,27 +72,55 @@ changeDecl allComments indentSize (H.DataDecl block (H.DataType _) Nothing dhead
5472
(H.QualConDecl _ _ _ (H.RecDecl {})) -> True
5573
_ -> False)
5674
decls
57-
newLines = fmap constructors zipped ++ [fmap (indented . H.prettyPrint) derivings]
75+
76+
typeConstructor = "data " <> H.prettyPrint dhead
77+
78+
-- In any case set @pipeIndent@ such that @|@ is aligned with @=@.
79+
(firstLine, firstLineInit, pipeIndent) =
80+
case cEquals of
81+
SameLine -> (Nothing, typeConstructor <> " = ", length typeConstructor + 1)
82+
Indent n -> (Just [[typeConstructor]], indent n "= ", n)
83+
84+
newLines = fromMaybe [] firstLine ++ fmap constructors zipped <> [fmap (indent cDeriving . H.prettyPrint) derivings]
5885
zipped = zip decls ([1..] ::[Int])
59-
constructors (decl, 1) = processConstructor allComments typeConstructor indentSize decl
60-
constructors (decl, _) = processConstructor allComments (indented "| ") indentSize decl
61-
typeConstructor = "data " <> H.prettyPrint dhead <> " = "
62-
indented = indent indentSize
86+
87+
constructors (decl, 1) = processConstructor allComments firstLineInit cfg decl
88+
constructors (decl, _) = processConstructor allComments (indent pipeIndent "| ") cfg decl
6389
changeDecl _ _ _ = Nothing
6490

65-
processConstructor :: [Comment] -> String -> Int -> H.QualConDecl LineBlock -> [String]
66-
processConstructor allComments init indentSize (H.QualConDecl _ _ _ (H.RecDecl _ dname fields)) = do
67-
init <> H.prettyPrint dname : n1 ++ ns ++ [indented "}"]
91+
processConstructor :: [Comment] -> String -> Config -> H.QualConDecl LineBlock -> [String]
92+
processConstructor allComments init Config{..} (H.QualConDecl _ _ _ (H.RecDecl _ dname (f:fs))) = do
93+
fromMaybe [] firstLine <> n1 <> ns <> [indent fieldIndent "}"]
6894
where
69-
n1 = processName "{ " ( extractField $ head fields)
70-
ns = tail fields >>= (processName ", " . extractField)
95+
n1 = processName firstLinePrefix (extractField f)
96+
ns = fs >>= processName (indent fieldIndent ", ") . extractField
97+
98+
-- Set @fieldIndent@ such that @,@ is aligned with @{@.
99+
(firstLine, firstLinePrefix, fieldIndent) =
100+
case cFirstField of
101+
SameLine ->
102+
( Nothing
103+
, init <> H.prettyPrint dname <> " { "
104+
, length init + length (H.prettyPrint dname) + 1
105+
)
106+
Indent n ->
107+
( Just [init <> H.prettyPrint dname]
108+
, indent (length init + n) "{ "
109+
, length init + n
110+
)
111+
71112
processName prefix (fnames, _type, lineComment, commentBelowLine) =
72-
[indented prefix <> intercalate ", " (fmap H.prettyPrint fnames) <> " :: " <> H.prettyPrint _type <> addLineComment lineComment] ++ addCommentBelow commentBelowLine
113+
[prefix <> intercalate ", " (fmap H.prettyPrint fnames) <> " :: " <> H.prettyPrint _type <> addLineComment lineComment
114+
] ++ addCommentBelow commentBelowLine
115+
73116
addLineComment (Just (Comment _ _ c)) = " --" <> c
74117
addLineComment Nothing = ""
118+
119+
-- Field comment indent is measured from the column with @{@, hence adding of @fieldIndent@ here.
75120
addCommentBelow Nothing = []
76-
addCommentBelow (Just (Comment _ _ c)) = [indented "--" <> c]
121+
addCommentBelow (Just (Comment _ _ c)) = [indent (fieldIndent + cFieldComment) "--" <> c]
122+
77123
extractField (H.FieldDecl lb names _type) =
78124
(names, _type, findCommentOnLine lb allComments, findCommentBelowLine lb allComments)
79-
indented = indent indentSize
125+
80126
processConstructor _ init _ decl = [init <> trimLeft (H.prettyPrint decl)]

stylish-haskell.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ Library
6464
mtl >= 2.0 && < 2.3,
6565
semigroups >= 0.18 && < 0.20,
6666
syb >= 0.3 && < 0.8,
67+
text >= 1.2 && < 1.3,
6768
HsYAML-aeson >=0.2.0 && < 0.3,
6869
HsYAML >=0.2.0 && < 0.3
6970

@@ -148,6 +149,7 @@ Test-suite stylish-haskell-tests
148149
haskell-src-exts >= 1.18 && < 1.24,
149150
mtl >= 2.0 && < 2.3,
150151
syb >= 0.3 && < 0.8,
152+
text >= 1.2 && < 1.3,
151153
HsYAML-aeson >=0.2.0 && < 0.3,
152154
HsYAML >=0.2.0 && < 0.3
153155

tests/Language/Haskell/Stylish/Config/Tests.hs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,11 @@ dotStylish = unlines $
148148
, " align: false"
149149
, " remove_redundant: true"
150150
, " - trailing_whitespace: {}"
151-
, " - records: {}"
152-
, "indent: 2"
151+
, " - records:"
152+
, " equals: \"same_line\""
153+
, " first_field: \"indent 2\""
154+
, " field_comment: 2"
155+
, " deriving: 4"
153156
, "columns: 110"
154157
, "language_extensions:"
155158
, " - TemplateHaskell"

0 commit comments

Comments
 (0)