packages feed

ghcitui-0.2.0.0: lib/ghcitui-core/Ghcitui/Loc.hs

{-# LANGUAGE OverloadedRecordDot #-}

module Ghcitui.Loc
    ( -- * Code locations within a file

    -- Types and functions for handling code within a single file or module.
      SourceRange (..)
    , HasSourceRange (..)
    , unknownSourceRange
    , isLineInside
    , srFromLineNo
    , singleify
    , ColumnRange

      -- * Code in files and modules
      -- $modulesAndFiles
    , FileLoc (..)
    , ModuleLoc (..)
    , toModuleLoc
    , toFileLoc

      -- * Converting between modules and source files
    , ModuleFileMap
    , moduleFileMapFromList
    , moduleFileMapAssocs
    , getPathOfModule
    , getModuleOfPath
    ) where

import Control.Error (headMay)
import Data.Map.Strict as Map
import Data.Maybe (isNothing)
import qualified Data.Text as T

-- ------------------------------------------------------------------------------------------------

-- | Range, mapping start to end.
type ColumnRange = (Maybe Int, Maybe Int)

-- | Represents a multi-line range from one character to another in a source file.
data SourceRange = SourceRange
    { startLine :: !(Maybe Int)
    -- ^ Start of the source range, inclusive.
    , startCol :: !(Maybe Int)
    -- ^ Start column of the source range, inclusive.
    , endLine :: !(Maybe Int)
    -- ^ End of the source range, inclusive.
    , endCol :: !(Maybe Int)
    -- ^ End column of the source range, EXCLUSIVE.
    }
    deriving (Show, Eq, Ord)

-- | A source range that represents an unknown location.
unknownSourceRange :: SourceRange
unknownSourceRange = SourceRange Nothing Nothing Nothing Nothing

-- | Create a source range from a single line number.
srFromLineNo :: Int -> SourceRange
srFromLineNo lineno = unknownSourceRange{startLine = Just lineno, endLine = Just lineno}

{- | Return whether a given line number lies within a given source range.

>>> let sr = (srFromLineNo 1) { endLine = 3 }
>>> isLineInside sr <$> [0, 1, 2, 3, 5]
[False, True, True, True, False]
-}
isLineInside :: SourceRange -> Int -> Bool
isLineInside SourceRange{startLine = Just sl, endLine = Just el} num = num >= sl && num <= el
isLineInside SourceRange{startLine = Just sl, endLine = Nothing} num = num >= sl
isLineInside _ _ = False

-- | Convert a 'SourceRange' to potentially a single line and 'ColumnRange'.
singleify :: SourceRange -> Maybe (Int, ColumnRange)
singleify sr
    | isNothing sl = Nothing
    | sl == endLine sr = do
        lineno <- sl
        pure (lineno, (startCol sr, endCol sr))
    | otherwise = Nothing
  where
    sl = startLine sr

-- ------------------------------------------------------------------------------------------------

{- $modulesAndFiles
GHCi talks about code ranges in both files and modules inconsistently. 'ModuleLoc' and
'FileLoc' are types representing each code range. In general, locations as 'FileLoc's
are easier to manage.
-}

-- | Location in a module (may not have a corresponding source file).
data ModuleLoc = ModuleLoc
    { modName :: !T.Text
    , mSourceRange :: !SourceRange
    }
    deriving (Show, Eq, Ord)

-- | Location in a file (may not have a corresponding module).
data FileLoc = FileLoc
    { filepath :: !FilePath
    , fSourceRange :: !SourceRange
    }
    deriving (Show, Eq, Ord)

class HasSourceRange a where
    -- | Retrieve the source range from this source location.
    sourceRange :: a -> SourceRange

instance HasSourceRange FileLoc where
    sourceRange = fSourceRange

instance HasSourceRange ModuleLoc where
    sourceRange = mSourceRange

newtype ModuleFileMap = ModuleFileMap (Map.Map T.Text FilePath) deriving (Show, Eq)

instance Semigroup ModuleFileMap where
    ModuleFileMap a <> ModuleFileMap b = ModuleFileMap $ a <> b

instance Monoid ModuleFileMap where
    mempty = ModuleFileMap mempty

-- | Create a 'ModuleFileMap' from an association list.
moduleFileMapFromList :: [(T.Text, FilePath)] -> ModuleFileMap
moduleFileMapFromList = ModuleFileMap . Map.fromList

-- | Return mappings between a module name and a filepath.
moduleFileMapAssocs :: ModuleFileMap -> [(T.Text, FilePath)]
moduleFileMapAssocs (ModuleFileMap map_) = Map.assocs map_

-- | Convert a module to a @FilePath@.
getPathOfModule :: ModuleFileMap -> T.Text -> Maybe FilePath
getPathOfModule (ModuleFileMap ms) mod' = Map.lookup mod' ms

-- | Convert a @FilePath@ to a module name.
getModuleOfPath :: ModuleFileMap -> FilePath -> Maybe T.Text
getModuleOfPath (ModuleFileMap ms) fp = headMay [mod' | (mod', fp') <- Map.assocs ms, fp' == fp]

-- | Convert a 'FileLoc' to a 'ModuleLoc'.
toModuleLoc :: ModuleFileMap -> FileLoc -> Maybe ModuleLoc
toModuleLoc mfm fl = convert fl.filepath
  where
    makeModuleLoc txt' = ModuleLoc txt' (sourceRange fl)
    convert fp = makeModuleLoc <$> getModuleOfPath mfm fp

-- | Convert a 'ModuleLoc' to a 'FileLoc'.
toFileLoc :: ModuleFileMap -> ModuleLoc -> Maybe FileLoc
toFileLoc mfm ml = convert ml.modName
  where
    makeFileLoc txt' = FileLoc txt' (sourceRange ml)
    convert mn = makeFileLoc <$> getPathOfModule mfm mn