{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} module Floskell.Config ( Indent(..) , LayoutContext(..) , Location(..) , WsLoc(..) , Whitespace(..) , Layout(..) , ConfigMapKey(..) , ConfigMap(..) , PenaltyConfig(..) , AlignConfig(..) , IndentConfig(..) , LayoutConfig(..) , OpConfig(..) , GroupConfig(..) , ImportsGroupOrder(..) , ImportsGroup(..) , SortImportsRule(..) , DeclarationConstruct(..) , OptionConfig(..) , Config(..) , defaultConfig , safeConfig , cfgMapFind , cfgOpWs , cfgGroupWs , inWs , wsSpace , wsLinebreak ) where import Data.Aeson ( FromJSON(..), ToJSON(..), genericParseJSON, genericToJSON ) import qualified Data.Aeson as JSON import Data.Aeson.Types as JSON ( Options(..), camelTo2, typeMismatch ) import Data.ByteString ( ByteString ) import Data.Default ( Default(..) ) import qualified Data.HashMap.Lazy as HashMap import Data.Map.Strict ( Map ) import qualified Data.Map.Strict as Map import Data.Set ( Set ) import qualified Data.Set as Set import qualified Data.Text as T import qualified Data.Text.Encoding as T ( decodeUtf8, encodeUtf8 ) import GHC.Generics data Indent = Align | IndentBy !Int | AlignOrIndentBy !Int deriving ( Eq, Ord, Show, Generic ) data LayoutContext = Declaration | Type | Pattern | Expression | Other deriving ( Eq, Ord, Bounded, Enum, Show, Generic ) data Location = Before | After deriving ( Eq, Ord, Bounded, Enum, Show, Generic ) data WsLoc = WsNone | WsBefore | WsAfter | WsBoth deriving ( Eq, Ord, Bounded, Enum, Show, Generic ) data Whitespace = Whitespace { wsSpaces :: !WsLoc , wsLinebreaks :: !WsLoc , wsForceLinebreak :: !Bool } deriving ( Show, Generic ) data Layout = Flex | Vertical | TryOneline deriving ( Eq, Ord, Bounded, Enum, Show, Generic ) data ConfigMapKey = ConfigMapKey !(Maybe ByteString) !(Maybe LayoutContext) deriving ( Eq, Ord, Show ) data ConfigMap a = ConfigMap { cfgMapDefault :: !a, cfgMapOverrides :: !(Map ConfigMapKey a) } deriving ( Generic ) data PenaltyConfig = PenaltyConfig { penaltyMaxLineLength :: !Int , penaltyLinebreak :: !Int , penaltyIndent :: !Int , penaltyOverfull :: !Int , penaltyOverfullOnce :: !Int } deriving ( Generic ) instance Default PenaltyConfig where def = PenaltyConfig { penaltyMaxLineLength = 80 , penaltyLinebreak = 100 , penaltyIndent = 1 , penaltyOverfull = 10 , penaltyOverfullOnce = 200 } data AlignConfig = AlignConfig { cfgAlignLimits :: !(Int, Int) , cfgAlignCase :: !Bool , cfgAlignClass :: !Bool , cfgAlignImportModule :: !Bool , cfgAlignImportSpec :: !Bool , cfgAlignLetBinds :: !Bool , cfgAlignMatches :: !Bool , cfgAlignRecordFields :: !Bool , cfgAlignWhere :: !Bool } deriving ( Generic ) instance Default AlignConfig where def = AlignConfig { cfgAlignLimits = (10, 25) , cfgAlignCase = False , cfgAlignClass = False , cfgAlignImportModule = False , cfgAlignImportSpec = False , cfgAlignLetBinds = False , cfgAlignMatches = False , cfgAlignRecordFields = False , cfgAlignWhere = False } data IndentConfig = IndentConfig { cfgIndentOnside :: !Int , cfgIndentDeriving :: !Int , cfgIndentWhere :: !Int , cfgIndentApp :: !Indent , cfgIndentCase :: !Indent , cfgIndentClass :: !Indent , cfgIndentDo :: !Indent , cfgIndentExportSpecList :: !Indent , cfgIndentIf :: !Indent , cfgIndentImportSpecList :: !Indent , cfgIndentLet :: !Indent , cfgIndentLetBinds :: !Indent , cfgIndentLetIn :: !Indent , cfgIndentMultiIf :: !Indent , cfgIndentTypesig :: !Indent , cfgIndentWhereBinds :: !Indent } deriving ( Generic ) instance Default IndentConfig where def = IndentConfig { cfgIndentOnside = 4 , cfgIndentDeriving = 4 , cfgIndentWhere = 2 , cfgIndentApp = IndentBy 4 , cfgIndentCase = IndentBy 4 , cfgIndentClass = IndentBy 4 , cfgIndentDo = IndentBy 4 , cfgIndentExportSpecList = IndentBy 4 , cfgIndentIf = IndentBy 4 , cfgIndentImportSpecList = IndentBy 4 , cfgIndentLet = IndentBy 4 , cfgIndentLetBinds = IndentBy 4 , cfgIndentLetIn = IndentBy 4 , cfgIndentMultiIf = IndentBy 4 , cfgIndentTypesig = IndentBy 4 , cfgIndentWhereBinds = IndentBy 2 } data LayoutConfig = LayoutConfig { cfgLayoutApp :: !Layout , cfgLayoutConDecls :: !Layout , cfgLayoutDeclaration :: !Layout , cfgLayoutExportSpecList :: !Layout , cfgLayoutIf :: !Layout , cfgLayoutImportSpecList :: !Layout , cfgLayoutInfixApp :: !Layout , cfgLayoutLet :: !Layout , cfgLayoutListComp :: !Layout , cfgLayoutRecord :: !Layout , cfgLayoutType :: !Layout } deriving ( Generic ) instance Default LayoutConfig where def = LayoutConfig { cfgLayoutApp = Flex , cfgLayoutConDecls = Flex , cfgLayoutDeclaration = Flex , cfgLayoutExportSpecList = Flex , cfgLayoutIf = Flex , cfgLayoutImportSpecList = Flex , cfgLayoutInfixApp = Flex , cfgLayoutLet = Flex , cfgLayoutListComp = Flex , cfgLayoutRecord = Flex , cfgLayoutType = Flex } newtype OpConfig = OpConfig { unOpConfig :: ConfigMap Whitespace } deriving ( Generic ) instance Default OpConfig where def = OpConfig ConfigMap { cfgMapDefault = Whitespace WsBoth WsBefore False , cfgMapOverrides = Map.empty } newtype GroupConfig = GroupConfig { unGroupConfig :: ConfigMap Whitespace } deriving ( Generic ) instance Default GroupConfig where def = GroupConfig ConfigMap { cfgMapDefault = Whitespace WsBoth WsAfter False , cfgMapOverrides = Map.empty } data ImportsGroupOrder = ImportsGroupKeep | ImportsGroupSorted | ImportsGroupGrouped deriving ( Generic ) data ImportsGroup = ImportsGroup { importsPrefixes :: ![String] , importsOrder :: !ImportsGroupOrder } deriving ( Generic ) data SortImportsRule = NoImportSort | SortImportsByPrefix | SortImportsByGroups ![ImportsGroup] data DeclarationConstruct = DeclModule | DeclClass | DeclInstance | DeclWhere deriving ( Eq, Ord, Generic ) data OptionConfig = OptionConfig { cfgOptionSortPragmas :: !Bool , cfgOptionSplitLanguagePragmas :: !Bool , cfgOptionSortImports :: !SortImportsRule , cfgOptionSortImportLists :: !Bool , cfgOptionAlignSumTypeDecl :: !Bool , cfgOptionFlexibleOneline :: !Bool , cfgOptionPreserveVerticalSpace :: !Bool , cfgOptionDeclNoBlankLines :: !(Set DeclarationConstruct) } deriving ( Generic ) instance Default OptionConfig where def = OptionConfig { cfgOptionSortPragmas = False , cfgOptionSplitLanguagePragmas = False , cfgOptionSortImports = NoImportSort , cfgOptionSortImportLists = False , cfgOptionAlignSumTypeDecl = False , cfgOptionFlexibleOneline = False , cfgOptionPreserveVerticalSpace = False , cfgOptionDeclNoBlankLines = Set.empty } data Config = Config { cfgPenalty :: !PenaltyConfig , cfgAlign :: !AlignConfig , cfgIndent :: !IndentConfig , cfgLayout :: !LayoutConfig , cfgOp :: !OpConfig , cfgGroup :: !GroupConfig , cfgOptions :: !OptionConfig } deriving ( Generic ) instance Default Config where def = Config { cfgPenalty = def , cfgAlign = def , cfgIndent = def , cfgLayout = def , cfgOp = def , cfgGroup = def , cfgOptions = def } defaultConfig :: Config defaultConfig = def { cfgOp = OpConfig ((unOpConfig def) { cfgMapOverrides = Map.fromList opWsOverrides }) } where opWsOverrides = [ (ConfigMapKey (Just ",") Nothing, Whitespace WsAfter WsBefore False) , ( ConfigMapKey (Just "record") Nothing , Whitespace WsAfter WsAfter False ) , ( ConfigMapKey (Just ".") (Just Type) , Whitespace WsAfter WsAfter False ) ] safeConfig :: Config -> Config safeConfig cfg = cfg { cfgGroup = group, cfgOp = op } where group = GroupConfig $ updateOverrides (unGroupConfig $ cfgGroup cfg) [ ("(#", Expression), ("(#", Pattern) ] op = OpConfig $ updateOverrides (unOpConfig $ cfgOp cfg) [ (".", Expression) ] updateOverrides config overrides = config { cfgMapOverrides = foldl (updateWs config) (cfgMapOverrides config) overrides } updateWs config m (key, ctx) = Map.insert (ConfigMapKey (Just key) (Just ctx)) (cfgMapFind ctx key config) { wsSpaces = WsBoth } m cfgMapFind :: LayoutContext -> ByteString -> ConfigMap a -> a cfgMapFind ctx key ConfigMap{..} = let value = cfgMapDefault value' = Map.findWithDefault value (ConfigMapKey Nothing (Just ctx)) cfgMapOverrides value'' = Map.findWithDefault value' (ConfigMapKey (Just key) Nothing) cfgMapOverrides value''' = Map.findWithDefault value'' (ConfigMapKey (Just key) (Just ctx)) cfgMapOverrides in value''' cfgOpWs :: LayoutContext -> ByteString -> OpConfig -> Whitespace cfgOpWs ctx op = cfgMapFind ctx op . unOpConfig cfgGroupWs :: LayoutContext -> ByteString -> GroupConfig -> Whitespace cfgGroupWs ctx op = cfgMapFind ctx op . unGroupConfig inWs :: Location -> WsLoc -> Bool inWs _ WsBoth = True inWs Before WsBefore = True inWs After WsAfter = True inWs _ _ = False wsSpace :: Location -> Whitespace -> Bool wsSpace loc ws = loc `inWs` wsSpaces ws wsLinebreak :: Location -> Whitespace -> Bool wsLinebreak loc ws = loc `inWs` wsLinebreaks ws ------------------------------------------------------------------------ readMaybe :: Read a => String -> Maybe a readMaybe str = case reads str of [ (x, "") ] -> Just x _ -> Nothing enumOptions :: Int -> Options enumOptions n = JSON.defaultOptions { constructorTagModifier = JSON.camelTo2 '-' . drop n } recordOptions :: Int -> Options recordOptions n = JSON.defaultOptions { fieldLabelModifier = JSON.camelTo2 '-' . drop n , unwrapUnaryRecords = True } instance ToJSON Indent where toJSON i = JSON.String $ case i of Align -> "align" IndentBy x -> "indent-by " `T.append` T.pack (show x) AlignOrIndentBy x -> "align-or-indent-by " `T.append` T.pack (show x) instance FromJSON Indent where parseJSON v@(JSON.String t) = maybe (JSON.typeMismatch "Indent" v) return $ if t == "align" then Just Align else if "indent-by " `T.isPrefixOf` t then IndentBy <$> readMaybe (T.unpack $ T.drop 10 t) else if "align-or-indent-by " `T.isPrefixOf` t then AlignOrIndentBy <$> readMaybe (T.unpack $ T.drop 19 t) else Nothing parseJSON v = JSON.typeMismatch "Indent" v instance ToJSON LayoutContext where toJSON = genericToJSON (enumOptions 0) instance FromJSON LayoutContext where parseJSON = genericParseJSON (enumOptions 0) instance ToJSON WsLoc where toJSON = genericToJSON (enumOptions 2) instance FromJSON WsLoc where parseJSON = genericParseJSON (enumOptions 2) instance ToJSON Whitespace where toJSON = genericToJSON (recordOptions 2) instance FromJSON Whitespace where parseJSON = genericParseJSON (recordOptions 2) instance ToJSON Layout where toJSON = genericToJSON (enumOptions 0) instance FromJSON Layout where parseJSON = genericParseJSON (enumOptions 0) layoutToText :: LayoutContext -> T.Text layoutToText Declaration = "declaration" layoutToText Type = "type" layoutToText Pattern = "pattern" layoutToText Expression = "expression" layoutToText Other = "other" textToLayout :: T.Text -> Maybe LayoutContext textToLayout "declaration" = Just Declaration textToLayout "type" = Just Type textToLayout "pattern" = Just Pattern textToLayout "expression" = Just Expression textToLayout "other" = Just Other textToLayout _ = Nothing keyToText :: ConfigMapKey -> T.Text keyToText (ConfigMapKey Nothing Nothing) = "default" keyToText (ConfigMapKey (Just n) Nothing) = T.decodeUtf8 n keyToText (ConfigMapKey Nothing (Just l)) = "* in " `T.append` layoutToText l keyToText (ConfigMapKey (Just n) (Just l)) = T.decodeUtf8 n `T.append` " in " `T.append` layoutToText l textToKey :: T.Text -> Maybe ConfigMapKey textToKey t = case T.splitOn " in " t of [ "default" ] -> Just (ConfigMapKey Nothing Nothing) [ "*", "*" ] -> Just (ConfigMapKey Nothing Nothing) [ name ] -> Just (ConfigMapKey (Just (T.encodeUtf8 name)) Nothing) [ name, "*" ] -> Just (ConfigMapKey (Just (T.encodeUtf8 name)) Nothing) [ "*", layout ] -> ConfigMapKey Nothing . Just <$> textToLayout layout [ name, layout ] -> ConfigMapKey (Just (T.encodeUtf8 name)) . Just <$> textToLayout layout _ -> Nothing instance ToJSON a => ToJSON (ConfigMap a) where toJSON ConfigMap{..} = toJSON $ Map.insert "default" cfgMapDefault $ Map.mapKeys keyToText cfgMapOverrides instance FromJSON a => FromJSON (ConfigMap a) where parseJSON value = do o <- parseJSON value cfgMapDefault <- maybe (fail "Missing key: default") return $ HashMap.lookup "default" o cfgMapOverrides <- either fail (return . Map.fromList) $ mapM toKey $ HashMap.toList $ HashMap.delete "default" o return ConfigMap { .. } where toKey (k, v) = case textToKey k of Just k' -> Right (k', v) Nothing -> Left ("Invalid key: " ++ T.unpack k) instance ToJSON PenaltyConfig where toJSON = genericToJSON (recordOptions 7) instance FromJSON PenaltyConfig where parseJSON = genericParseJSON (recordOptions 7) instance ToJSON AlignConfig where toJSON = genericToJSON (recordOptions 8) instance FromJSON AlignConfig where parseJSON = genericParseJSON (recordOptions 8) instance ToJSON IndentConfig where toJSON = genericToJSON (recordOptions 9) instance FromJSON IndentConfig where parseJSON = genericParseJSON (recordOptions 9) instance ToJSON LayoutConfig where toJSON = genericToJSON (recordOptions 9) instance FromJSON LayoutConfig where parseJSON = genericParseJSON (recordOptions 9) instance ToJSON OpConfig where toJSON = genericToJSON (recordOptions 0) instance FromJSON OpConfig where parseJSON = genericParseJSON (recordOptions 0) instance ToJSON GroupConfig where toJSON = genericToJSON (recordOptions 0) instance FromJSON GroupConfig where parseJSON = genericParseJSON (recordOptions 0) instance ToJSON ImportsGroupOrder where toJSON = genericToJSON (enumOptions 12) instance FromJSON ImportsGroupOrder where parseJSON = genericParseJSON (enumOptions 12) instance ToJSON ImportsGroup where toJSON = genericToJSON (recordOptions 7) instance FromJSON ImportsGroup where parseJSON x@JSON.Array{} = ImportsGroup <$> parseJSON x <*> pure ImportsGroupKeep parseJSON x = genericParseJSON (recordOptions 7) x instance ToJSON SortImportsRule where toJSON NoImportSort = toJSON False toJSON SortImportsByPrefix = toJSON True toJSON (SortImportsByGroups xs) = toJSON xs instance FromJSON SortImportsRule where parseJSON (JSON.Bool False) = return NoImportSort parseJSON (JSON.Bool True) = return SortImportsByPrefix parseJSON v = SortImportsByGroups <$> parseJSON v instance ToJSON DeclarationConstruct where toJSON = genericToJSON (enumOptions 4) instance FromJSON DeclarationConstruct where parseJSON = genericParseJSON (enumOptions 4) instance ToJSON OptionConfig where toJSON = genericToJSON (recordOptions 9) instance FromJSON OptionConfig where parseJSON = genericParseJSON (recordOptions 9) instance ToJSON Config where toJSON = genericToJSON (recordOptions 3) instance FromJSON Config where parseJSON = genericParseJSON (recordOptions 3)