packages feed

cabal-gild-0.2.1.1: source/library/CabalGild/Comments.hs

{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}

-- |
-- License: GPL-3.0-or-later
-- Copyright: Oleg Grenrus
module CabalGild.Comments where

import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BS8
import qualified Data.Map.Strict as Map
import qualified Data.Maybe as Maybe
import qualified Distribution.Fields as C
import qualified Distribution.Fields.Field as C
import qualified Distribution.Parsec as C

-------------------------------------------------------------------------------
-- Comments wrapper
-------------------------------------------------------------------------------

newtype Comments = Comments [BS.ByteString]
  deriving stock (Show)
  deriving newtype (Semigroup, Monoid)

unComments :: Comments -> [BS.ByteString]
unComments (Comments cs) = cs

nullComments :: Comments -> Bool
nullComments (Comments cs) = null cs

-------------------------------------------------------------------------------
-- Attach comments
-------------------------------------------------------------------------------

-- | Returns a 'C.Field' forest with comments attached.
--
-- * Comments are attached to the field after it.
-- * A glitch: comments "inside" the field are attached to the field after it.
-- * End-of-file comments are returned separately.
attachComments ::
  -- | source with comments
  BS.ByteString ->
  -- | parsed source fields
  [C.Field C.Position] ->
  ([C.Field (C.Position, Comments)], Comments)
attachComments input inputFields =
  (overAnn attach attach' inputFields, endComments)
  where
    inputFieldsU :: [(FieldPath, C.Field C.Position)]
    inputFieldsU = fieldUniverseN inputFields

    comments :: [(Int, Comments)]
    comments = extractComments input

    comments' :: Map.Map FieldPath Comments
    comments' =
      Map.fromListWith
        (flip (<>))
        [ (path, cs)
          | (l, cs) <- comments,
            path <- Maybe.maybeToList (findPath C.fieldAnn l inputFieldsU)
        ]

    endComments :: Comments
    endComments =
      mconcat
        [ cs
          | (l, cs) <- comments,
            Maybe.isNothing (findPath C.fieldAnn l inputFieldsU)
        ]

    attach :: FieldPath -> C.Position -> (C.Position, Comments)
    attach fp pos = (pos, Maybe.fromMaybe mempty (Map.lookup fp comments'))

    attach' :: C.Position -> (C.Position, Comments)
    attach' pos = (pos, mempty)

overAnn :: forall a b. (FieldPath -> a -> b) -> (a -> b) -> [C.Field a] -> [C.Field b]
overAnn f h = go' id
  where
    go :: (FieldPath -> FieldPath) -> Int -> C.Field a -> C.Field b
    go g i (C.Field (C.Name a name) fls) =
      C.Field (C.Name b name) (h <$$> fls)
      where
        b = f (g (Nth i End)) a
    go g i (C.Section (C.Name a name) args fls) =
      C.Section (C.Name b name) (h <$$> args) (go' (g . Nth i) fls)
      where
        b = f (g (Nth i End)) a

    go' :: (FieldPath -> FieldPath) -> [C.Field a] -> [C.Field b]
    go' g = zipWith (go g) [0 ..]

    (<$$>) :: (Functor f1, Functor f2) => (x -> y) -> f1 (f2 x) -> f1 (f2 y)
    x <$$> y = (x <$>) <$> y

-------------------------------------------------------------------------------
-- Find comments in the input
-------------------------------------------------------------------------------

extractComments :: BS.ByteString -> [(Int, Comments)]
extractComments = go . zip [1 ..] . map (BS.dropWhileEnd isCR . BS.dropWhile isSpace8) . BS8.lines
  where
    go :: [(Int, BS.ByteString)] -> [(Int, Comments)]
    go [] = []
    go ((n, bs) : rest)
      | isComment bs = case span ((isComment .|| BS.null) . snd) rest of
          (h, t) -> (n, Comments $ bs : map snd h) : go t
      | otherwise = go rest

    (.||) :: (t -> Bool) -> (t -> Bool) -> t -> Bool
    (f .|| g) x = f x || g x

    isSpace8 :: (Eq a, Num a) => a -> Bool
    isSpace8 w = w == 9 || w == 32

    isCR :: (Eq a, Num a) => a -> Bool
    isCR = (==) 13

    isComment :: BS.ByteString -> Bool
    isComment = BS.isPrefixOf "--"

-------------------------------------------------------------------------------
-- FieldPath
-------------------------------------------------------------------------------

-- | Paths input paths. Essentially a list of offsets. Own type ofr safety.
data FieldPath
  = End
  | Nth Int FieldPath -- nth field
  deriving (Eq, Ord, Show)

fieldUniverseN :: [C.Field ann] -> [(FieldPath, C.Field ann)]
fieldUniverseN = concat . zipWith g [0 ..]
  where
    g :: Int -> C.Field ann -> [(FieldPath, C.Field ann)]
    g n f' = [(Nth n p, f'') | (p, f'') <- fieldUniverse f']

fieldUniverse :: C.Field ann -> [(FieldPath, C.Field ann)]
fieldUniverse f@(C.Section _ _ fs) = (End, f) : concat (zipWith g [0 ..] fs)
  where
    g :: Int -> C.Field ann -> [(FieldPath, C.Field ann)]
    g n f' = [(Nth n p, f'') | (p, f'') <- fieldUniverse f']
fieldUniverse f@(C.Field _ _) = [(End, f)]

-- note: fieldUniverse* should produce 'FieldPath's in increasing order
-- that helps
findPath :: (a -> C.Position) -> Int -> [(FieldPath, a)] -> Maybe FieldPath
findPath _ _ [] = Nothing
findPath f l [(p, x)]
  | C.Position k _ <- f x =
      if l < k then Just p else Nothing
findPath f l ((_, x) : rest@((p, x') : _))
  | C.Position k _ <- f x,
    C.Position k' _ <- f x' =
      if k < l && l < k'
        then Just p
        else findPath f l rest