packages feed

ghc-tags-pipes-0.2.0.1: lib/GhcTags/Stream.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE LambdaCase          #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections       #-}

-- | Parse and combine a stream of tags.
--
module GhcTags.Stream
    ( tagParser
    , tagParserWithWarnings
    , ParsingError (..)
    , combineTagsStream
    , combineTagsPipe
    , runCombineTagsPipe
    ) where

#if MIN_VERSION_GLASGOW_HASKELL(9,6,0,0)
import           Control.Monad.State.Strict
#else
import           Control.Monad.State.Strict hiding (void)
#endif
import           Control.Exception (throwIO)
import           Data.ByteString (ByteString)
import           Data.Attoparsec.ByteString (Parser)
import           Data.ByteString.Builder (Builder)
import qualified Data.ByteString.Builder as BS
import           Data.Foldable (traverse_)
import           Data.Functor (($>))
import qualified Data.Text.Encoding as Text
import           System.IO

import           Pipes ((>->), (~>))
import qualified Pipes as Pipes
import qualified Pipes.Lift as Pipes
import qualified Pipes.Attoparsec as Pipes.AP
import           Pipes.Attoparsec (ParsingError (..))
import qualified Pipes.ByteString as Pipes.BS

import           GhcTags.Tag
import qualified GhcTags.Utils as Utils


-- | Parse a stream of tags, coming from a 'Text' producer.
-- Skips malformed lines and continues parsing. Use 'tagParserWithWarnings' to
-- report skipped records. I/O exceptions still propagate to the consumer.
--
tagParser :: MonadIO m
          => Parser (Maybe (Tag tk))
          -- ^ Parse a single tag.  For Vim this returns should parse a single
          -- line and return the tag, e.g  'parseTagLine'.
          -> Pipes.Producer ByteString m ()
          -> Pipes.Producer (Tag tk) m ()
tagParser = tagParserWithWarnings (const (pure ()))


-- | Recover a stream of tags and report every skipped line.
tagParserWithWarnings :: MonadIO m
                      => (String -> m ())
                      -> Parser (Maybe (Tag tk))
                      -> Pipes.Producer ByteString m ()
                      -> Pipes.Producer (Tag tk) m ()
tagParserWithWarnings report parser producer = do
      Pipes.for
        (Pipes.AP.parsed (Utils.parseLine parser) producer)
        (\case
          Left err -> Pipes.lift (report err)
          -- ignore header lines
          Right (Just tag) -> Pipes.yield tag
          Right Nothing   -> pure ())
  >>= \case
        Left (err, _) -> liftIO (throwIO err)
        Right ()      -> pure ()


-- | Merge a sorted stream of old tags with sorted new tags from one module.
-- Replaces that module's old tags and removes duplicate output tags, including
-- when the old stream is empty. The comparison must be consistent with equality.
combineTagsStream
    :: forall m (tk :: TAG_KIND). Monad m
    => (Tag tk -> Tag tk -> Ordering)
    -> RawFilePath -- ^ normalised module path, relative to the tags file directory
    -> [Tag tk]   -- ^ new tags, sorted by the supplied comparison
    -> Pipes.Producer (Tag tk) m () -- ^ sorted old tags
    -> Pipes.Producer (Tag tk) m ()
combineTagsStream compareFn modPath new old =
    merged >-> deduplicateTags compareFn modPath
  where
    merged :: Pipes.Producer (Tag tk) m ()
    merged = do
      ((), remaining) <- Pipes.runStateP new $
        Pipes.for (Pipes.hoist Pipes.lift old)
          (\tag -> Pipes.stateP $ fmap ((),) . combineTagsPipe compareFn modPath tag)
      traverse_ Pipes.yield remaining


-- Equal tags have equal sorting keys, so only the current comparison group
-- needs to be remembered. Keep distinct tags with equal keys: compareTags
-- does not compare fields or definitions.  Only deduplicate groups that
-- contain at least one new (modPath) tag; old-only groups are preserved as-is
-- because etags loses tagKind on round-trip, making formerly-distinct tags
-- look identical after parsing.
deduplicateTags :: Monad m
                => (Tag tk -> Tag tk -> Ordering)
                -> RawFilePath
                -> Pipes.Pipe (Tag tk) (Tag tk) m r
deduplicateTags compareFn modPath = go [] False
  where
    modPath' = rawFilePathToBS modPath
    isNew t  = modPath' == Text.encodeUtf8 (getRawFilePath (tagFilePath t))
    go seen hasNew = do
      tag <- Pipes.await
      let inGroup = case seen of
            previous : _ | compareFn previous tag == EQ -> True
            _ -> False
      if inGroup
        then let hasNew' = hasNew || isNew tag
             in if hasNew' && tag `elem` seen
                then go seen hasNew'
                else Pipes.yield tag >> go (tag : seen) hasNew'
        else Pipes.yield tag >> go [tag] (isNew tag)


-- | One merge step for an old tag. This does not deduplicate the complete
-- output; use 'combineTagsStream' for a complete merge.
--
combineTagsPipe
    :: forall m (tk :: TAG_KIND).  Applicative m
    => (Tag tk -> Tag tk -> Ordering)
    -> RawFilePath -- ^ normalised module path, relative to the tags file directory
    -> Tag tk      -- ^ tag read from disc
    -> [Tag tk]    -- ^ new tags
    -> Pipes.Producer (Tag tk) m [Tag tk]
combineTagsPipe compareFn modPath = go
  where
    modPath' = rawFilePathToBS modPath
    go :: Tag tk -> [Tag tk]
       -> Pipes.Producer (Tag tk) m [Tag tk]

    -- Omit only tags pointing to the exact module path. Both paths are
    -- normalised and relative to the tags file directory.
    go tag as
      | modPath' == Text.encodeUtf8 (getRawFilePath (tagFilePath tag))
      = pure as

    go tag as@(a : as')
      | otherwise = case a `compareFn` tag of
          LT -> Pipes.yield a >> go tag as'
          EQ -> Pipes.yield a $> as'
          GT -> Pipes.yield tag $> as

    go tag [] = Pipes.yield tag $> []


-- | run 'combineTagsPipe' taking care of the state.
--
runCombineTagsPipe
    :: MonadIO m
    => Handle
    -> (Tag tk -> Tag tk -> Ordering)
    -> (Tag tk -> Builder)
    -> RawFilePath
    -> Tag tk
    -> Pipes.Effect (StateT [Tag tk] m) ()
runCombineTagsPipe writeHandle compareFn formatTag modPath =
       (\tag -> Pipes.stateP $ fmap ((),) . combineTagsPipe compareFn modPath tag)
    ~> Pipes.yield . BS.toLazyByteString . formatTag
    ~> (\bs -> Pipes.BS.fromLazy bs)
    ~> (\bs -> Pipes.yield bs >-> Pipes.BS.toHandle writeHandle)