{-# LANGUAGE FlexibleContexts, OverloadedStrings #-}
module Data.Text.Prettyprint.Location (
Pos (..)
, fromOffset
, Span(..)
, mergeSpan
, displayPos
, renderPos
, renderPosIO
, renderSpan
, renderSpanIO
) where
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
data Pos = Pos { posFile :: !FilePath
, posLine :: {-# UNPACK #-} !Int
, posCol :: {-# UNPACK #-} !Int
, posOffset :: {-# UNPACK #-} !Int
}
deriving(Show)
instance Eq Pos where
Pos { posFile = fp1
, posOffset = o1
} == Pos { posFile = fp2
, posOffset = o2
} = fp1 == fp2 && o1 == o2
fromOffset :: Monad m
=> (FilePath -> m Text)
-> FilePath
-> Int
-> m Pos
fromOffset readFile fp offset = do
file <- readFile fp
let lineFile = T.lines file
go ln (line : lines) offset
| offset <= T.length line =
Pos { posFile = fp
, posLine = ln
, posCol = offset + 1
, posOffset = offset
}
| otherwise = go (ln + 1) lines (offset - T.length line - 1)
go _ [] _ = error "The file is too short for the offset"
pure (go 1 lineFile offset)
data Span = Span { spanStart :: !Pos
, spanLen :: !Int
}
deriving(Eq, Show)
instance Semigroup Span where
(<>) = mergeSpan
mergeSpan :: Span -> Span -> Span
mergeSpan span1@Span { spanStart = start1
, spanLen = len1
} span2@Span { spanStart = start2
, spanLen = len2
} =
if posOffset start1 <= posOffset start2
then Span { spanStart = start1
, spanLen = posOffset start2 - posOffset start1 + len2
}
else mergeSpan span2 span1
displayPos :: Pos -> Text
displayPos Pos { posFile = fp
, posLine = ln
, posCol = cn
} = T.intercalate ":" [ T.pack fp
, T.pack (show ln)
, T.pack (show cn)
]
renderPos :: Monad m
=> (FilePath -> m Text)
-> Pos
-> m Text
renderPos readFile pos =
renderSpan readFile Span { spanStart = pos
, spanLen = 1
}
renderPosIO :: Pos -> IO Text
renderPosIO = renderPos T.readFile
renderSpan :: Monad m
=> (FilePath -> m Text)
-> Span
-> m Text
renderSpan readFile Span { spanStart = Pos { posFile = fp
, posLine = row
, posCol = col
, posOffset = offset
}
, spanLen = len
} = do
file <- readFile fp
let linedFile = T.lines file
highlightedPart = T.take len (T.drop offset file)
numberOfLinesToHightlight =
length $ T.lines highlightedPart
rendered =
if numberOfLinesToHightlight == 1
then let spikes = T.replicate (col - 1) " " <> T.replicate len "^"
str = if length linedFile < row - 1
then error "The file is too short to render this span"
else linedFile !! (row - 1)
in [str, spikes]
else let displayedLines =
take numberOfLinesToHightlight (drop (row - 1) linedFile)
in fmap ("> " <>) displayedLines
pure (T.unlines rendered)
renderSpanIO :: Span -> IO Text
renderSpanIO = renderSpan T.readFile