{-# 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
    { SourceRange -> Maybe Int
startLine :: !(Maybe Int)
    -- ^ Start of the source range, inclusive.
    , SourceRange -> Maybe Int
startCol :: !(Maybe Int)
    -- ^ Start column of the source range, inclusive.
    , SourceRange -> Maybe Int
endLine :: !(Maybe Int)
    -- ^ End of the source range, inclusive.
    , SourceRange -> Maybe Int
endCol :: !(Maybe Int)
    -- ^ End column of the source range, EXCLUSIVE.
    }
    deriving (Int -> SourceRange -> ShowS
[SourceRange] -> ShowS
SourceRange -> FilePath
(Int -> SourceRange -> ShowS)
-> (SourceRange -> FilePath)
-> ([SourceRange] -> ShowS)
-> Show SourceRange
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> SourceRange -> ShowS
showsPrec :: Int -> SourceRange -> ShowS
$cshow :: SourceRange -> FilePath
show :: SourceRange -> FilePath
$cshowList :: [SourceRange] -> ShowS
showList :: [SourceRange] -> ShowS
Show, SourceRange -> SourceRange -> Bool
(SourceRange -> SourceRange -> Bool)
-> (SourceRange -> SourceRange -> Bool) -> Eq SourceRange
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: SourceRange -> SourceRange -> Bool
== :: SourceRange -> SourceRange -> Bool
$c/= :: SourceRange -> SourceRange -> Bool
/= :: SourceRange -> SourceRange -> Bool
Eq, Eq SourceRange
Eq SourceRange =>
(SourceRange -> SourceRange -> Ordering)
-> (SourceRange -> SourceRange -> Bool)
-> (SourceRange -> SourceRange -> Bool)
-> (SourceRange -> SourceRange -> Bool)
-> (SourceRange -> SourceRange -> Bool)
-> (SourceRange -> SourceRange -> SourceRange)
-> (SourceRange -> SourceRange -> SourceRange)
-> Ord SourceRange
SourceRange -> SourceRange -> Bool
SourceRange -> SourceRange -> Ordering
SourceRange -> SourceRange -> SourceRange
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: SourceRange -> SourceRange -> Ordering
compare :: SourceRange -> SourceRange -> Ordering
$c< :: SourceRange -> SourceRange -> Bool
< :: SourceRange -> SourceRange -> Bool
$c<= :: SourceRange -> SourceRange -> Bool
<= :: SourceRange -> SourceRange -> Bool
$c> :: SourceRange -> SourceRange -> Bool
> :: SourceRange -> SourceRange -> Bool
$c>= :: SourceRange -> SourceRange -> Bool
>= :: SourceRange -> SourceRange -> Bool
$cmax :: SourceRange -> SourceRange -> SourceRange
max :: SourceRange -> SourceRange -> SourceRange
$cmin :: SourceRange -> SourceRange -> SourceRange
min :: SourceRange -> SourceRange -> SourceRange
Ord)

-- | A source range that represents an unknown location.
unknownSourceRange :: SourceRange
unknownSourceRange :: SourceRange
unknownSourceRange = Maybe Int -> Maybe Int -> Maybe Int -> Maybe Int -> SourceRange
SourceRange Maybe Int
forall a. Maybe a
Nothing Maybe Int
forall a. Maybe a
Nothing Maybe Int
forall a. Maybe a
Nothing Maybe Int
forall a. Maybe a
Nothing

-- | Create a source range from a single line number.
srFromLineNo :: Int -> SourceRange
srFromLineNo :: Int -> SourceRange
srFromLineNo Int
lineno = SourceRange
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 -> Int -> Bool
isLineInside SourceRange{$sel:startLine:SourceRange :: SourceRange -> Maybe Int
startLine = Just Int
sl, $sel:endLine:SourceRange :: SourceRange -> Maybe Int
endLine = Just Int
el} Int
num = Int
num Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
sl Bool -> Bool -> Bool
&& Int
num Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
<= Int
el
isLineInside SourceRange{$sel:startLine:SourceRange :: SourceRange -> Maybe Int
startLine = Just Int
sl, $sel:endLine:SourceRange :: SourceRange -> Maybe Int
endLine = Maybe Int
Nothing} Int
num = Int
num Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
>= Int
sl
isLineInside SourceRange
_ Int
_ = Bool
False

-- | Convert a 'SourceRange' to potentially a single line and 'ColumnRange'.
singleify :: SourceRange -> Maybe (Int, ColumnRange)
singleify :: SourceRange -> Maybe (Int, ColumnRange)
singleify SourceRange
sr
    | Maybe Int -> Bool
forall a. Maybe a -> Bool
isNothing Maybe Int
sl = Maybe (Int, ColumnRange)
forall a. Maybe a
Nothing
    | Maybe Int
sl Maybe Int -> Maybe Int -> Bool
forall a. Eq a => a -> a -> Bool
== SourceRange -> Maybe Int
endLine SourceRange
sr = do
        Int
lineno <- Maybe Int
sl
        (Int, ColumnRange) -> Maybe (Int, ColumnRange)
forall a. a -> Maybe a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Int
lineno, (SourceRange -> Maybe Int
startCol SourceRange
sr, SourceRange -> Maybe Int
endCol SourceRange
sr))
    | Bool
otherwise = Maybe (Int, ColumnRange)
forall a. Maybe a
Nothing
  where
    sl :: Maybe Int
sl = SourceRange -> Maybe Int
startLine SourceRange
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
    { ModuleLoc -> Text
modName :: !T.Text
    , ModuleLoc -> SourceRange
mSourceRange :: !SourceRange
    }
    deriving (Int -> ModuleLoc -> ShowS
[ModuleLoc] -> ShowS
ModuleLoc -> FilePath
(Int -> ModuleLoc -> ShowS)
-> (ModuleLoc -> FilePath)
-> ([ModuleLoc] -> ShowS)
-> Show ModuleLoc
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ModuleLoc -> ShowS
showsPrec :: Int -> ModuleLoc -> ShowS
$cshow :: ModuleLoc -> FilePath
show :: ModuleLoc -> FilePath
$cshowList :: [ModuleLoc] -> ShowS
showList :: [ModuleLoc] -> ShowS
Show, ModuleLoc -> ModuleLoc -> Bool
(ModuleLoc -> ModuleLoc -> Bool)
-> (ModuleLoc -> ModuleLoc -> Bool) -> Eq ModuleLoc
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ModuleLoc -> ModuleLoc -> Bool
== :: ModuleLoc -> ModuleLoc -> Bool
$c/= :: ModuleLoc -> ModuleLoc -> Bool
/= :: ModuleLoc -> ModuleLoc -> Bool
Eq, Eq ModuleLoc
Eq ModuleLoc =>
(ModuleLoc -> ModuleLoc -> Ordering)
-> (ModuleLoc -> ModuleLoc -> Bool)
-> (ModuleLoc -> ModuleLoc -> Bool)
-> (ModuleLoc -> ModuleLoc -> Bool)
-> (ModuleLoc -> ModuleLoc -> Bool)
-> (ModuleLoc -> ModuleLoc -> ModuleLoc)
-> (ModuleLoc -> ModuleLoc -> ModuleLoc)
-> Ord ModuleLoc
ModuleLoc -> ModuleLoc -> Bool
ModuleLoc -> ModuleLoc -> Ordering
ModuleLoc -> ModuleLoc -> ModuleLoc
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: ModuleLoc -> ModuleLoc -> Ordering
compare :: ModuleLoc -> ModuleLoc -> Ordering
$c< :: ModuleLoc -> ModuleLoc -> Bool
< :: ModuleLoc -> ModuleLoc -> Bool
$c<= :: ModuleLoc -> ModuleLoc -> Bool
<= :: ModuleLoc -> ModuleLoc -> Bool
$c> :: ModuleLoc -> ModuleLoc -> Bool
> :: ModuleLoc -> ModuleLoc -> Bool
$c>= :: ModuleLoc -> ModuleLoc -> Bool
>= :: ModuleLoc -> ModuleLoc -> Bool
$cmax :: ModuleLoc -> ModuleLoc -> ModuleLoc
max :: ModuleLoc -> ModuleLoc -> ModuleLoc
$cmin :: ModuleLoc -> ModuleLoc -> ModuleLoc
min :: ModuleLoc -> ModuleLoc -> ModuleLoc
Ord)

-- | Location in a file (may not have a corresponding module).
data FileLoc = FileLoc
    { FileLoc -> FilePath
filepath :: !FilePath
    , FileLoc -> SourceRange
fSourceRange :: !SourceRange
    }
    deriving (Int -> FileLoc -> ShowS
[FileLoc] -> ShowS
FileLoc -> FilePath
(Int -> FileLoc -> ShowS)
-> (FileLoc -> FilePath) -> ([FileLoc] -> ShowS) -> Show FileLoc
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> FileLoc -> ShowS
showsPrec :: Int -> FileLoc -> ShowS
$cshow :: FileLoc -> FilePath
show :: FileLoc -> FilePath
$cshowList :: [FileLoc] -> ShowS
showList :: [FileLoc] -> ShowS
Show, FileLoc -> FileLoc -> Bool
(FileLoc -> FileLoc -> Bool)
-> (FileLoc -> FileLoc -> Bool) -> Eq FileLoc
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: FileLoc -> FileLoc -> Bool
== :: FileLoc -> FileLoc -> Bool
$c/= :: FileLoc -> FileLoc -> Bool
/= :: FileLoc -> FileLoc -> Bool
Eq, Eq FileLoc
Eq FileLoc =>
(FileLoc -> FileLoc -> Ordering)
-> (FileLoc -> FileLoc -> Bool)
-> (FileLoc -> FileLoc -> Bool)
-> (FileLoc -> FileLoc -> Bool)
-> (FileLoc -> FileLoc -> Bool)
-> (FileLoc -> FileLoc -> FileLoc)
-> (FileLoc -> FileLoc -> FileLoc)
-> Ord FileLoc
FileLoc -> FileLoc -> Bool
FileLoc -> FileLoc -> Ordering
FileLoc -> FileLoc -> FileLoc
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: FileLoc -> FileLoc -> Ordering
compare :: FileLoc -> FileLoc -> Ordering
$c< :: FileLoc -> FileLoc -> Bool
< :: FileLoc -> FileLoc -> Bool
$c<= :: FileLoc -> FileLoc -> Bool
<= :: FileLoc -> FileLoc -> Bool
$c> :: FileLoc -> FileLoc -> Bool
> :: FileLoc -> FileLoc -> Bool
$c>= :: FileLoc -> FileLoc -> Bool
>= :: FileLoc -> FileLoc -> Bool
$cmax :: FileLoc -> FileLoc -> FileLoc
max :: FileLoc -> FileLoc -> FileLoc
$cmin :: FileLoc -> FileLoc -> FileLoc
min :: FileLoc -> FileLoc -> FileLoc
Ord)

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

instance HasSourceRange FileLoc where
    sourceRange :: FileLoc -> SourceRange
sourceRange = FileLoc -> SourceRange
fSourceRange

instance HasSourceRange ModuleLoc where
    sourceRange :: ModuleLoc -> SourceRange
sourceRange = ModuleLoc -> SourceRange
mSourceRange

newtype ModuleFileMap = ModuleFileMap (Map.Map T.Text FilePath) deriving (Int -> ModuleFileMap -> ShowS
[ModuleFileMap] -> ShowS
ModuleFileMap -> FilePath
(Int -> ModuleFileMap -> ShowS)
-> (ModuleFileMap -> FilePath)
-> ([ModuleFileMap] -> ShowS)
-> Show ModuleFileMap
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ModuleFileMap -> ShowS
showsPrec :: Int -> ModuleFileMap -> ShowS
$cshow :: ModuleFileMap -> FilePath
show :: ModuleFileMap -> FilePath
$cshowList :: [ModuleFileMap] -> ShowS
showList :: [ModuleFileMap] -> ShowS
Show, ModuleFileMap -> ModuleFileMap -> Bool
(ModuleFileMap -> ModuleFileMap -> Bool)
-> (ModuleFileMap -> ModuleFileMap -> Bool) -> Eq ModuleFileMap
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ModuleFileMap -> ModuleFileMap -> Bool
== :: ModuleFileMap -> ModuleFileMap -> Bool
$c/= :: ModuleFileMap -> ModuleFileMap -> Bool
/= :: ModuleFileMap -> ModuleFileMap -> Bool
Eq)

instance Semigroup ModuleFileMap where
    ModuleFileMap Map Text FilePath
a <> :: ModuleFileMap -> ModuleFileMap -> ModuleFileMap
<> ModuleFileMap Map Text FilePath
b = Map Text FilePath -> ModuleFileMap
ModuleFileMap (Map Text FilePath -> ModuleFileMap)
-> Map Text FilePath -> ModuleFileMap
forall a b. (a -> b) -> a -> b
$ Map Text FilePath
a Map Text FilePath -> Map Text FilePath -> Map Text FilePath
forall a. Semigroup a => a -> a -> a
<> Map Text FilePath
b

instance Monoid ModuleFileMap where
    mempty :: ModuleFileMap
mempty = Map Text FilePath -> ModuleFileMap
ModuleFileMap Map Text FilePath
forall a. Monoid a => a
mempty

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

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

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

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

-- | Convert a 'FileLoc' to a 'ModuleLoc'.
toModuleLoc :: ModuleFileMap -> FileLoc -> Maybe ModuleLoc
toModuleLoc :: ModuleFileMap -> FileLoc -> Maybe ModuleLoc
toModuleLoc ModuleFileMap
mfm FileLoc
fl = FilePath -> Maybe ModuleLoc
convert FileLoc
fl.filepath
  where
    makeModuleLoc :: Text -> ModuleLoc
makeModuleLoc Text
txt' = Text -> SourceRange -> ModuleLoc
ModuleLoc Text
txt' (FileLoc -> SourceRange
forall a. HasSourceRange a => a -> SourceRange
sourceRange FileLoc
fl)
    convert :: FilePath -> Maybe ModuleLoc
convert FilePath
fp = Text -> ModuleLoc
makeModuleLoc (Text -> ModuleLoc) -> Maybe Text -> Maybe ModuleLoc
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ModuleFileMap -> FilePath -> Maybe Text
getModuleOfPath ModuleFileMap
mfm FilePath
fp

-- | Convert a 'ModuleLoc' to a 'FileLoc'.
toFileLoc :: ModuleFileMap -> ModuleLoc -> Maybe FileLoc
toFileLoc :: ModuleFileMap -> ModuleLoc -> Maybe FileLoc
toFileLoc ModuleFileMap
mfm ModuleLoc
ml = Text -> Maybe FileLoc
convert ModuleLoc
ml.modName
  where
    makeFileLoc :: FilePath -> FileLoc
makeFileLoc FilePath
txt' = FilePath -> SourceRange -> FileLoc
FileLoc FilePath
txt' (ModuleLoc -> SourceRange
forall a. HasSourceRange a => a -> SourceRange
sourceRange ModuleLoc
ml)
    convert :: Text -> Maybe FileLoc
convert Text
mn = FilePath -> FileLoc
makeFileLoc (FilePath -> FileLoc) -> Maybe FilePath -> Maybe FileLoc
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> ModuleFileMap -> Text -> Maybe FilePath
getPathOfModule ModuleFileMap
mfm Text
mn