packages feed

headroom-0.4.0.0: src/Headroom/SourceCode.hs

{-# LANGUAGE DerivingStrategies         #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoImplicitPrelude          #-}
{-# LANGUAGE StrictData                 #-}
{-# LANGUAGE TypeApplications           #-}
{-# LANGUAGE ViewPatterns               #-}

{-|
Module      : Headroom.SourceCode
Description : Type safe representation of analyzed source code
Copyright   : (c) 2019-2021 Vaclav Svejcar
License     : BSD-3-Clause
Maintainer  : vaclav.svejcar@gmail.com
Stability   : experimental
Portability : POSIX

This module contains data types and function used for analysis and type safe
representation of source code files.
-}

module Headroom.SourceCode
  ( -- * Data Types
    LineType(..)
  , CodeLine
  , SourceCode(..)
    -- * Functions
  , fromText
  , toText
  , firstMatching
  , lastMatching
  , stripStart
  , stripEnd
  , cut
  )
where

import           Control.Monad.State                 ( State
                                                     , evalState
                                                     )
import           Headroom.Data.Coerce                ( coerce
                                                     , inner
                                                     )
import           Headroom.Data.Text                  ( fromLines
                                                     , toLines
                                                     )
import           RIO
import qualified RIO.List                           as L
import qualified RIO.Text                           as T


---------------------------------  DATA TYPES  ---------------------------------

-- | Represents type of the line in source code.
data LineType
  = Code    -- ^ Line of code
  | Comment -- ^ Line of comment
  deriving (Eq, Show)

-- | Type alias for analyzed line of code.
type CodeLine = (LineType, Text)

-- | Represents analyzed source code.
newtype SourceCode
  = SourceCode [CodeLine]
  deriving stock (Eq, Show)
  deriving newtype (Semigroup, Monoid)


------------------------------  PUBLIC FUNCTIONS  ------------------------------

-- | Converts 'Text' into 'SourceCode' using the given function to analyze
-- each line's 'LineType'. The analyzing function can hold any state that is
-- accumulated as the text is processed, for example to hold some info about
-- already processed lines.
fromText :: a
         -- ^ initial state of analyzing function
         -> (Text -> State a LineType)
         -- ^ function that analyzes currently processed line
         -> Text
         -- ^ raw source code to analyze
         -> SourceCode
         -- ^ analyzed 'SourceCode'
fromText s0 f (toLines -> ls) = coerce $ zip (evalState (mapM f ls) s0) ls


-- | Converts analyzed 'SourceCode' back into 'Text'.
toText :: SourceCode
       -- ^ source code to convert back to plain text
       -> Text
       -- ^ resulting plain text
toText (SourceCode sc) = fromLines . fmap snd $ sc


-- | Finds very first line matching given predicate and optionally performs some
-- operation over it.
firstMatching :: (CodeLine -> Maybe a)
              -- ^ predicate (and transform) function
              -> SourceCode
              -- ^ source code to search in
              -> Maybe (Int, a)
              -- ^ first matching line (if found)
firstMatching f sc = go (coerce sc) 0
 where
  go [] _ = Nothing
  go (x : xs) i | Just res <- f x = Just (i, res)
                | otherwise       = go xs (i + 1)


-- | Finds very last line matching given predicate and optionally performs some
-- operation over it.
lastMatching :: (CodeLine -> Maybe a)
             -- ^ predicate (and transform) function
             -> SourceCode
             -- ^ source code to search in
             -> Maybe (Int, a)
             -- ^ last matching line (if found)
lastMatching f sc =
  let matching = firstMatching f . inner @_ @[CodeLine] reverse $ sc
      lastIdx  = length (coerce sc :: [CodeLine]) - 1
  in  fmap (first (lastIdx -)) matching


-- | Strips empty lines at the beginning of source code.
--
-- >>> stripStart $ SourceCode [(Code, ""), (Code, "foo"), (Code, "")]
-- SourceCode [(Code,"foo"),(Code,"")]
stripStart :: SourceCode
           -- ^ source code to strip
           -> SourceCode
           -- ^ stripped source code
stripStart = inner @_ @[CodeLine] (L.dropWhile (T.null . T.strip . snd))


-- | Strips empty lines at the end of source code.
--
-- >>> stripEnd $ SourceCode [(Code, ""), (Code, "foo"), (Code, "")]
-- SourceCode [(Code,""),(Code,"foo")]
stripEnd :: SourceCode
         -- ^ source code to strip
         -> SourceCode
         -- ^ stripped source code
stripEnd = inner @_ @[CodeLine] (L.dropWhileEnd (T.null . T.strip . snd))


-- | Cuts snippet from the source code using the given start and end position.
--
-- >>> cut 1 3 $ SourceCode [(Code, "1"), (Code, "2"),(Code, "3"),(Code, "4")]
-- SourceCode [(Code,"2"),(Code,"3")]
cut :: Int
    -- ^ index of first line to be included into the snippet
    -> Int
    -- ^ index of the first line after the snippet
    -> SourceCode
    -- ^ source code to cut
    -> SourceCode
    -- ^ cut snippet
cut s e = inner @_ @[CodeLine] (L.take (e - s) . L.drop s)