packages feed

fix-imports-2.4.0: src/FixImports/Format.hs

{-# LANGUAGE OverloadedStrings #-}
module FixImports.Format (
    formatGroups
    , showImport
) where
import qualified Data.List as List
import qualified Data.Maybe as Maybe
import qualified Text.PrettyPrint as PP
import           Text.PrettyPrint ((<+>))

import qualified FixImports.Config as Config
import qualified FixImports.Types as Types
import qualified FixImports.Util as Util


-- | Format import list.  Imports are alphabetized and grouped into sections
-- based on the top level module name (before the first dot).  Sections that
-- are too small are grouped with the section below them.
--
-- The local imports are sorted and grouped separately from the package
-- imports.  Rather than being alphabetical, they are sorted in a per-project
-- order that should be general-to-specific.
--
-- An unqualified import will follow a qualified one.  The Prelude, if
-- imported, always goes first.
formatGroups :: Config.Format -> Config.Order -> [Types.ImportLine] -> String
formatGroups format order imports =
    unlines $ joinGroups
        [ showGroups (group (Util.sortOn packagePrio package))
        , showGroups (group (Util.sortOn localPrio local))
        , showGroups [Util.sortOn name unqualified]
        ]
    where
    packagePrio import_ =
        ( name import_ /= prelude
        , name import_
        , qualifiedPrio import_
        )
    localPrio import_ =
        ( localPriority (Config._importOrder order) (name import_)
        , name import_
        , qualifiedPrio import_
        )
    qualifiedPrio = not . Types._importQualified . Types.importDecl
    name = Types._importName . Types.importDecl
    (unqualified, local, package) = Util.partition2
        ((Config._sortUnqualifiedLast order &&) . isUnqualified
            . Types.importDecl)
        ((==Types.Local) . Types.importSource)
        imports
    group
        | Config._groupImports format = collapse . Util.groupOn topModule
        | otherwise = (:[])
    topModule = takeWhile (/='.') . Types.moduleName . name
    collapse [] = []
    collapse (x:xs)
        | length x <= 2 = case collapse xs of
            [] -> [x]
            y : ys -> (x ++ y) : ys
        | otherwise = x : collapse xs
    showGroups = List.intercalate [""] . map (map (showImportLine format))
    joinGroups = List.intercalate [""] . filter (not . null)
    prelude = Types.ModuleName "Prelude"

isUnqualified :: Types.Import -> Bool
isUnqualified imp = not (Types._importQualified imp)
    && Maybe.isNothing (Types._importEntities imp)

-- | Modules whose top level element is in 'importFirst' go first, ones in
-- 'importLast' go last, and the rest go in the middle.
--
-- Like 'searchPrio' but for order.
localPriority :: Config.Priority Config.ModulePattern -> Types.ModuleName
    -> (Int, Maybe Int)
localPriority prio import_ =
    case List.findIndex (`Config.matchModule` import_) firsts of
        Just k -> (-1, Just k)
        Nothing -> case List.findIndex (`Config.matchModule` import_) lasts of
            Nothing -> (0, Nothing)
            Just k -> (1, Just k)
    where
    firsts = Config.high prio
    lasts = Config.low prio

showImportLine :: Config.Format -> Types.ImportLine -> String
showImportLine format (Types.ImportLine imp cmts _) = concat
    [ above
    , showImport format imp
    , (if null right then "" else ' ' : right)
    ]
    where
    above = concat [cmt ++ "\n" | Types.Comment Types.CmtAbove cmt <- cmts]
    right = Util.join "\n" [cmt | Types.Comment Types.CmtRight cmt <- cmts]

showImport :: Config.Format -> Types.Import -> String
showImport format
        (Types.Import name pkgQualifier source safe qualified as hiding
            entities _span) =
    PP.renderStyle style $ PP.hang
        (PP.hsep
            [ "import"
            , if qualified || not (Config._leaveSpaceForQualified format)
                    || source || safe
                then mempty
                else PP.text (replicate (length ("qualified" :: String)) ' ')
            , if source then "{-# SOURCE #-}" else mempty
            , if safe then "safe" else mempty
            , if qualified then "qualified" else mempty
            , maybe mempty (\s -> PP.text (show s)) pkgQualifier
            , PP.text $ Types.moduleName name
            , maybe mempty
                (\(Types.Qualification qual) -> "as" <+> PP.text qual) as
            , if hiding then "hiding" else mempty
            ])
        (Config._wrapIndent format) (maybe mempty prettyEntities entities)
    where
    style = PP.style
        { PP.lineLength = Config._columns format
        , PP.ribbonsPerLine = 1
        }

prettyEntities :: [Either Types.Error Types.Entity] -> PP.Doc
prettyEntities = parenList . map pp
    where
    -- This means 'extractEntity' hit something it didn't think should be
    -- possible.
    pp (Left err) = "{{parse error: " <> PP.text err <> "}}"
    pp (Right (Types.Entity qualifier var list)) =
        (maybe mempty PP.text qualifier <+> PP.text (Types.showName var))
        <> maybe mempty PP.text list

parenList :: [PP.Doc] -> PP.Doc
parenList = PP.parens . PP.fsep . PP.punctuate PP.comma