-
Notifications
You must be signed in to change notification settings - Fork 25
Expand file tree
/
Copy pathConfigFile.hs
More file actions
250 lines (214 loc) · 9.05 KB
/
ConfigFile.hs
File metadata and controls
250 lines (214 loc) · 9.05 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
{-# LANGUAGE CPP #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
module Floskell.ConfigFile
( AppConfig(..)
, defaultAppConfig
, findAppConfig
, findAppConfigIn
, readAppConfig
, showStyle
, showLanguage
, showExtension
, showFixity
, lookupStyle
, lookupLanguage
, lookupExtension
, lookupFixity
, setStyle
, setLanguage
, setExtensions
, setFixities
) where
import Control.Applicative ( (<|>) )
import Data.Aeson
( (.:?), (.=), FromJSON(..), ToJSON(..) )
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Parser as JSON ( json' )
import qualified Data.Aeson.Types as JSON ( typeMismatch )
import qualified Data.Attoparsec.ByteString as AP
import qualified Data.ByteString as BS
import Data.Char ( isLetter, isSpace )
import Data.List ( (\\), inits )
import qualified Data.Text as T
import Floskell.Attoparsec ( parseOnly )
import Floskell.Styles ( Style(..), styles )
import GHC.Generics ( Generic )
import Language.Haskell.Exts
( Extension(..), Fixity(..), KnownExtension(..)
, Language(..), classifyExtension, classifyLanguage
, knownExtensions )
import qualified Language.Haskell.Exts as HSE
import System.Directory
( XdgDirectory(..), doesFileExist, findFileWith
, getAppUserDataDirectory, getCurrentDirectory
, getHomeDirectory, getXdgDirectory )
import System.FilePath
( joinPath, splitDirectories, takeDirectory )
{- floskell-disable -}
#if MIN_VERSION_aeson(2,0,0)
import qualified Data.Aeson.KeyMap as Map
#else
import qualified Data.HashMap.Lazy as Map
#endif
{- floskell-enable -}
data AppConfig = AppConfig { appStyle :: Style
, appLanguage :: Language
, appExtensions :: [Extension]
, appFixities :: [Fixity]
}
deriving ( Generic )
instance ToJSON AppConfig where
toJSON AppConfig{..} =
JSON.object [ "style" .= showStyle appStyle
, "language" .= showLanguage appLanguage
, "extensions" .= map showExtension appExtensions
, "fixities" .= map showFixity appFixities
, "formatting" .= styleConfig appStyle
]
instance FromJSON AppConfig where
parseJSON (JSON.Object o) = do
style <- maybe (appStyle defaultAppConfig) lookupStyle <$> o .:? "style"
language <- maybe (appLanguage defaultAppConfig) lookupLanguage
<$> o .:? "language"
extensions <- maybe (appExtensions defaultAppConfig)
((appExtensions defaultAppConfig ++) . map lookupExtension)
<$> o .:? "extensions"
fixities <- maybe (appFixities defaultAppConfig) (map lookupFixity)
<$> o .:? "fixities"
let fmt = styleConfig style
fmt' <- maybe fmt (updateConfig fmt) <$> o .:? "formatting"
let style' = style { styleConfig = fmt' }
return $ AppConfig style' language extensions fixities
where
updateConfig cfg v = case JSON.fromJSON $ mergeJSON (toJSON cfg) v of
JSON.Error e -> error e
JSON.Success x -> x
mergeJSON JSON.Null r = r
mergeJSON l JSON.Null = l
mergeJSON (JSON.Object l) (JSON.Object r) =
JSON.Object (Map.unionWith mergeJSON l r)
mergeJSON _ r = r
parseJSON v = JSON.typeMismatch "AppConfig" v
-- | Default program configuration.
defaultAppConfig :: AppConfig
defaultAppConfig = AppConfig (head styles) Haskell2010 defaultExtensions []
defaultExtensions :: [Extension]
defaultExtensions = [ e | e@EnableExtension{} <- knownExtensions ]
\\ map EnableExtension badExtensions
badExtensions :: [KnownExtension]
badExtensions =
[ Arrows -- steals proc
, TransformListComp -- steals the group keyword
, XmlSyntax
, RegularPatterns -- steals a-b
, UnboxedTuples -- breaks (#) lens operator
, PatternSynonyms -- steals the pattern keyword
, RecursiveDo -- steals the rec keyword
, DoRec -- same
, TypeApplications -- since GHC 8 and haskell-src-exts-1.19
]
-- | Show name of a style.
showStyle :: Style -> String
showStyle = T.unpack . styleName
-- | Show a Haskell language name.
showLanguage :: Language -> String
showLanguage = show
-- | Show a Haskell language extension.
showExtension :: Extension -> String
showExtension (EnableExtension x) = show x
showExtension (DisableExtension x) = "No" ++ show x
showExtension (UnknownExtension x) = x
-- | Show a fixity declaration.
showFixity :: Fixity -> String
showFixity (Fixity assoc prec op) =
showAssoc assoc ++ " " ++ show prec ++ " " ++ showOp op
where
showAssoc (HSE.AssocNone _) = "infix"
showAssoc (HSE.AssocLeft _) = "infixl"
showAssoc (HSE.AssocRight _) = "infixr"
showOp (HSE.UnQual _ (HSE.Symbol _ symbol)) = symbol
showOp (HSE.UnQual _ (HSE.Ident _ ident)) = "`" ++ ident ++ "`"
showOp _ = error "Operator in fixity list not supported"
-- | Lookup a style by name.
lookupStyle :: String -> Style
lookupStyle name = case filter ((== T.pack name) . styleName) styles of
[] -> error $ "Unknown style: " ++ name
x : _ -> x
-- | Lookup a language by name.
lookupLanguage :: String -> Language
lookupLanguage name = case classifyLanguage name of
UnknownLanguage _ -> error $ "Unknown language: " ++ name
x -> x
-- | Lookup an extension by name.
lookupExtension :: String -> Extension
lookupExtension "ImportQualifiedPost" = UnknownExtension "ImportQualifiedPost"
lookupExtension "NoImportQualifiedPost" = UnknownExtension "NoImportQualifiedPost"
lookupExtension name = case classifyExtension name of
UnknownExtension _ -> error $ "Unkown extension: " ++ name
x -> x
-- | Parse a fixity declaration.
lookupFixity :: String -> Fixity
lookupFixity decl =
let (assoc, decl') = break isSpace $ dropWhile isSpace decl
(prec, decl'') = break isSpace $ dropWhile isSpace decl'
(op, _) = break isSpace $ dropWhile isSpace decl''
in
Fixity (readAssoc assoc) (read prec) (readOp op)
where
readAssoc "infix" = HSE.AssocNone ()
readAssoc "infixl" = HSE.AssocLeft ()
readAssoc "infixr" = HSE.AssocRight ()
readAssoc assoc = error $ "Unknown associativity: " ++ assoc
readOp op = HSE.UnQual () $ case op of
'(' : op' -> HSE.Symbol () (init op')
'`' : op' -> HSE.Ident () (init op')
c : _ -> if isLetter c then HSE.Ident () op else HSE.Symbol () op
_ -> error "Missing operator in infix declaration"
-- | Try to find a configuration file based on current working
-- directory, or in one of the application configuration directories.
findAppConfig :: IO (Maybe FilePath)
findAppConfig = getCurrentDirectory >>= findAppConfigIn
findAppConfigIn :: FilePath -> IO (Maybe FilePath)
findAppConfigIn src = do
isFile <- doesFileExist src
let startFrom = if isFile then takeDirectory src else src
dotfilePaths <- sequence [ getHomeDirectory, getXdgDirectory XdgConfig "" ]
dotfileConfig <- findFileWith doesFileExist dotfilePaths ".floskell.json"
userPaths <- sequence [ getAppUserDataDirectory "floskell"
, getXdgDirectory XdgConfig "floskell"
]
userConfig <- findFileWith doesFileExist userPaths "config.json"
let localPaths =
map joinPath . reverse . drop 1 . inits . splitDirectories $
startFrom
localConfig <- findFileWith doesFileExist localPaths "floskell.json"
return $ localConfig <|> userConfig <|> dotfileConfig
-- | Load a configuration file.
readAppConfig :: FilePath -> IO AppConfig
readAppConfig file = do
text <- BS.readFile file
either (error . (++) (file ++ ": ")) return $ eitherDecodeStrict text
setStyle :: AppConfig -> Maybe String -> AppConfig
setStyle cfg mbStyle =
cfg { appStyle = maybe (appStyle cfg) lookupStyle mbStyle }
setLanguage :: AppConfig -> Maybe String -> AppConfig
setLanguage cfg mbLanguage =
cfg { appLanguage = maybe (appLanguage cfg) lookupLanguage mbLanguage }
setExtensions :: AppConfig -> [String] -> AppConfig
setExtensions cfg exts =
cfg { appExtensions = appExtensions cfg ++ map lookupExtension exts }
setFixities :: AppConfig -> [String] -> AppConfig
setFixities cfg fixities =
cfg { appFixities = appFixities cfg ++ map lookupFixity fixities }
eitherDecodeStrict :: FromJSON a => BS.ByteString -> Either String a
eitherDecodeStrict i = case parseOnly jsonEOF' i of
Right x -> case JSON.fromJSON x of
JSON.Error e -> Left e
JSON.Success x' -> Right x'
Left e -> Left e
where
jsonEOF' = JSON.json' <* skipSpace <* AP.endOfInput
skipSpace =
AP.skipWhile $ \w -> w == 0x20 || w == 0x0a || w == 0x0d || w == 0x09