content: {-
    Copyright (c) Meta Platforms, Inc. and affiliates.
    All rights reserved.
  
    This source code is licensed under the BSD-style license found in the
    LICENSE file in the root directory of this source tree.
  -}
  
  module Glean.Glass.Range
    (
    -- * working with ranges and bytespans, to and from Glean representations
       rangeContains
  
    -- ** high level
    , rangeSpanToLocationRange
  
    -- ** lower level
    , rangeSpanToRange
    , inclusiveRangeToExclusiveRange
  
    -- * File metadata
    , FileInfo(..)
    , getFileInfo
    , getFile
  
    ) where
  
  import Data.Default ( Default(def) )
  import Data.Function (on)
  import Data.Hashable
  import Data.Text
  
  import qualified Glean
  import qualified Glean.Util.Range as Range
  
  import qualified Glean.Glass.Types as Glass
  import qualified Glean.Glass.Query as Query
  import qualified Glean.Schema.Src.Types as Src
  import qualified Glean.Schema.Digest.Types as Digest
  
  import Glean.Glass.Types
      ( LocationRange(..),
        RepoName,
        Range(range_columnEnd, range_lineEnd, range_columnBegin, range_lineBegin),
        GlassExceptionReason (GlassExceptionReason_noSrcFileFact)
      )
  import Glean.Glass.Utils ( fetchDataRecursive )
  import Glean.Glass.Base ( GleanPath(..),  SymbolRepoPath(..))
  import Glean.Glass.Path ( fromGleanPath )
  import qualified Haxl.Core.Memo as Haxl
  import qualified Glean.Schema.CodemarkupTypes.Types as Code
  import qualified Glean.Schema.Glass.Types as Glass
  import Glean.Angle ( query )
  import qualified Glean.Haxl.Repos as Glean
  
  -- | @rangeContains x y@ is True iff the range of y is the same as x, or
  -- inside x.
  rangeContains :: Range -> Range -> Bool
  rangeContains big small =
    checkLE range_lineBegin range_columnBegin big small &&
    checkLE range_lineEnd range_columnEnd small big
    where
      checkLE line col a b = GT /= compareLineCol line col a b
      compareLineCol line col a b =
        (compare `on` line) a b <> (compare `on` col) a b
  
  -- | Convert Glean-side range span and file to the Glass range locations, with
  -- ranges in exclusive-end form, and paths adjusted to be repo-relative
  rangeSpanToLocationRange -- locationRangeFromCodeLocation
    :: RepoName
    -> Src.File
    -> Code.RangeSpan
    -> Glean.RepoHaxl u w Glass.LocationRange
  rangeSpanToLocationRange repo file rangespan = do
    range <- memoRangeSpanToRange file rangespan
    toLocationRange repo file range
  
  -- | Convert Glean-side rangeSpan to Glass Range, with memo filelines if needed
  -- Like rangeSpanToRange, but memoizes the file offsets for the span case.
  memoRangeSpanToRange
    :: Src.File
    -> Code.RangeSpan
    -> Glean.RepoHaxl u w Range
  memoRangeSpanToRange file rangespan@Code.RangeSpan_span{} = do
    mOffsets <- memoLineOffsets file
    return $ rangeSpanToRange mOffsets rangespan
  memoRangeSpanToRange _ rangespan =
    pure $ rangeSpanToRange Nothing rangespan
  
  -- | Glean provides either a bytespan or a range for a symbol location.
  -- Normalize it to Glass.Range (exclusive of end line/col spans)
  -- Only use this if you know the offsets are already memoized.
  rangeSpanToRange :: Maybe Range.LineOffsets -> Code.RangeSpan -> Range
  rangeSpanToRange offsets (Code.RangeSpan_span span) =
    fileByteSpanToExclusiveRange offsets span
  rangeSpanToRange _ (Code.RangeSpan_range range) =
    inclusiveRangeToExclusiveRange range
  rangeSpanToRange _ Code.RangeSpan_EMPTY =
    unexpected
  
  -- | Package of src.File metadata we typically need
  data FileInfo = FileInfo {
      fileRepo :: Glean.Repo,
      fileId :: {-# UNPACK #-} !(Glean.IdOf Src.File),
      srcFile :: !Src.File,
      offsets :: !(Maybe Range.LineOffsets),
      isIndexed :: !Bool,
      fileDigest :: Maybe Digest.Digest,
      indexFailure :: Maybe Src.IndexFailure_key
    }
  
  getFileInfo
    :: Glean.Repo
    -> GleanPath
    -> Glean.RepoHaxl u w (Either GlassExceptionReason FileInfo)
  getFileInfo fileRepo path = do
    minfo <- fetchDataRecursive (Query.fileInfo path)
    case minfo of
      Nothing -> return $ Left $
        GlassExceptionReason_noSrcFileFact $ "No src.File fact for "
          <> gleanPath path
      Just fileInfoP -> do
        Glass.FileInfo_key srcFile infos <- Glean.keyOf fileInfoP
        let fileId = Glean.getId srcFile
            Glass.FileMetadata isIndexed mLineOffsets fileDigest mFailure
              = infos
            indexFailure = Src.indexFailure_key =<< mFailure
        offsets <- memoLineOffsetsFileLines srcFile mLineOffsets
        return $ Right FileInfo{..}
  
  -- | Get the src.File fact for a path
  getFile
    :: GleanPath -> Glean.RepoHaxl u w (Either GlassExceptionReason Src.File)
  getFile path = do
    mfile <- Glean.getFirstResult (query (Query.srcFile path))
    return $ case mfile of
      Nothing -> Left $
        GlassExceptionReason_noSrcFileFact $ "No src.File fact for "
          <> gleanPath path
      Just srcFile -> Right srcFile
  
  -- | Glean's Src.Range is inclusive of start/end. Glass is exclusive of end
  inclusiveRangeToExclusiveRange :: Src.Range -> Glass.Range
  inclusiveRangeToExclusiveRange Src.Range{..} =
    Glass.Range {
      range_lineBegin = Glean.unNat range_lineBegin,
      range_columnBegin = Glean.unNat range_columnBegin,
      range_lineEnd = Glean.unNat range_lineEnd,
      range_columnEnd = Glean.unNat range_columnEnd + 1 -- n.b. exclusive end
    }
  
  --
  -- Internal stuff
  --
  
  unexpected :: a
  unexpected = error "unexpected RangeSpan"
  
  -- | (internal) Converts Glean-specific bytespan into client-visible line/col
  -- offsets convert Glean ranges into customer range types, with exclusive line
  -- end Warning: if the file doesn't have src.FileLines facts and the language
  -- schema uses span instead of range, the exclusive range will be incorrect (see
  -- type RangeSpan in codemarkup.angle)
  -- Note: This logic is similar to `Range.byteRangeToRange` except this function
  -- handles empty spans correctly. Inclusive bounds can't support empty spans.
  fileByteSpanToExclusiveRange
    :: Maybe Range.LineOffsets -> Src.ByteSpan -> Glass.Range
  fileByteSpanToExclusiveRange Nothing _ = def
  fileByteSpanToExclusiveRange (Just lineoffs) bytespan =
    let -- be careful subtracting Word64 to avoid wrap-around
        be@Range.ByteRange{..} = Range.byteSpanToRange bytespan
        -- Convert from 0-based col to 1-based col for Src.Range
        (range_lineBegin, range_columnBegin) =
          Range.byteOffsetToLineCol lineoffs byteRange_begin
        (range_lineEnd, range_columnEnd) =
          Range.byteOffsetToLineCol
            lineoffs (Range.byteRangeExclusiveEnd be)
    in Glass.Range {
      range_lineBegin = fromIntegral range_lineBegin,
      range_columnBegin = fromIntegral $ range_columnBegin + 1,
      range_lineEnd = fromIntegral range_lineEnd,
      range_columnEnd = fromIntegral $ range_columnEnd + 1
    }
  
  newtype Glass_LineOffsets_Cache_Key =
      Glass_LineOffsets_Cache_Key (Glean.Repo, Text)
    deriving (Eq, Hashable, Ord, Show)
  
  -- | Memoize the result of computing the line offsets on a file
  -- This provides up to 15x win for xrefs on large files
  -- (internal)
  memoLineOffsets :: Src.File -> Glean.RepoHaxl u w (Maybe Range.LineOffsets)
  memoLineOffsets file = do
    repo <- Glean.haxlRepo
    key <- Glean.keyOf file
    Haxl.memo (Glass_LineOffsets_Cache_Key (repo, key)) $ toLineOffsets file
  
  newtype Glass_FileLines_Conversion_Cache_Key =
      Glass_FileLines_Conversion_Cache_Key (Glean.Repo, Text)
    deriving (Eq, Hashable, Ord, Show)
  
  -- | Sometimes we already have the FileLines fact handy. Set the memo table
  -- directly in this case (internal)
  memoLineOffsetsFileLines
    :: Src.File
    -> Maybe Src.FileLines
    -> Glean.RepoHaxl u w (Maybe Range.LineOffsets)
  memoLineOffsetsFileLines file mFileLines = do
    repo <- Glean.haxlRepo
    key <- Glean.keyOf file
    Haxl.memo (Glass_FileLines_Conversion_Cache_Key (repo, key)) $
      fromFileLines mFileLines
  
  -- | (internal) Get the line offsets associated with a file
  -- Use the memoized version, memoLineOffsets
  toLineOffsets :: Src.File -> Glean.RepoHaxl u w (Maybe Range.LineOffsets)
  toLineOffsets file = do
    let fileId = Glean.getId file
    fromFileLines =<< Glean.getFirstResult (query (Query.fileLines fileId))
  
  -- | Just the file line endings to range conversion
  fromFileLines
    :: Maybe Src.FileLines -> Glean.RepoHaxl u w (Maybe Range.LineOffsets)
  fromFileLines mlines = case mlines of
    Nothing -> pure Nothing
    Just offs -> Just . Range.lengthsToLineOffsets <$> Glean.keyOf offs
  
  -- | (internal) Like toLocation, but for ranges.
  toLocationRange
    :: RepoName -> Src.File -> Range -> Glean.RepoHaxl u w LocationRange
  toLocationRange repo file range = do
    path <- GleanPath <$> Glean.keyOf file
    let SymbolRepoPath locationRange_repository locationRange_filepath =
          fromGleanPath repo path
    return $ LocationRange {
         locationRange_repository = locationRange_repository,
         locationRange_filepath = locationRange_filepath,
         locationRange_range = range
       }
  