packages feed

ghcide-2.15.0.0: src/Development/IDE/Types/KnownTargets.hs

{-# LANGUAGE DeriveAnyClass     #-}
{-# LANGUAGE DerivingStrategies #-}
module Development.IDE.Types.KnownTargets ( KnownTargets(..)
                                          , emptyKnownTargets
                                          , mkKnownTargets
                                          , mkExtraKnownFiles
                                          , unionKnownTargets
                                          , tombstoneKnownFiles
                                          , Target(..)
                                          , toKnownFiles
                                          , toTargetFiles) where

import           Control.DeepSeq
import           Data.Hashable
import           Data.HashMap.Strict
import qualified Data.HashMap.Strict            as HMap
import           Data.HashSet
import qualified Data.HashSet                   as HSet
import           Development.IDE.GHC.Compat     (ModuleName)
import           Development.IDE.GHC.Orphans    ()
import           Development.IDE.Types.Location
import           GHC.Generics

-- | What HLS knows about the files of the workspace
data KnownTargets = KnownTargets
  { targetMap  :: !(HashMap Target (HashSet NormalizedFilePath))
    -- ^ What the session loader discovered: the modules the project is made of
  , knownExtra :: !(HashSet NormalizedFilePath)
    -- ^ Files reported present by the client that no target declares. See
    -- Note [Files that are not targets]
  , knownGone  :: !(HashSet NormalizedFilePath)
    -- ^ Files reported gone by the client. See Note [Tombstones]
  }
  deriving Show

{- Note [Files that are not targets]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
A workspace often has source files that belong to no session/component like test
fixtures and scratch files. The client reports them like any other and we have
to record them, so that we can maintain an accurate view of the filesystem state
based off which we can run and rerun rules, particularly the ones that resolve
imports like 'GetModulesPaths' and 'GetLocatedImports'.

They are not project modules though, so they are kept apart from the targets.
'toKnownFiles' includes these files, but 'toTargetFiles' does not, because a
file without a component has no session to be compiled in.
-}

{- Note [Tombstones]
~~~~~~~~~~~~~~~~~~~~
'targetMap' is only ever added to: 'extendKnownTargets' unions into it and the
session loader never removes anything. So a file that disappears cannot be
expressed by deleting an entry. Often there is no entry to delete, and deleting
one that is not there leaves 'KnownTargets' equal to what it was, hash and all,
so the early cutoff on 'GetKnownTargets' fires and nothing reruns: the deletion
would be invisible.

Recording the deletion always changes the value, so the rules reading it rerun.
'GetModulesPaths' has to, or an import keeps resolving to a file that is gone,
since 'toKnownFiles' is what tells it about files the file system scan cannot
see. 'GetModuleGraph' has to, or a deleted target stays a root of the graph.

Both 'toKnownFiles' and 'toTargetFiles' hide tombstoned files, which also covers
a target rediscovered by a later cradle load: 'extendKnownTargets' adds the
candidate locations of a 'TargetFile' without checking they exist. A file that
comes back clears its tombstone.
-}

unionKnownTargets :: KnownTargets -> KnownTargets -> KnownTargets
unionKnownTargets (KnownTargets tm extra gone) (KnownTargets tm' extra' gone') =
  KnownTargets (HMap.unionWith (<>) tm tm') (extra <> extra') (gone <> gone')

mkKnownTargets :: [(Target, HashSet NormalizedFilePath)] -> KnownTargets
mkKnownTargets vs = KnownTargets (HMap.fromList vs) HSet.empty HSet.empty

-- | See Note [Files that are not targets]
mkExtraKnownFiles :: HashSet NormalizedFilePath -> KnownTargets
mkExtraKnownFiles fs = KnownTargets HMap.empty fs HSet.empty

-- | Record files as gone, and files that came back as present again.
tombstoneKnownFiles
  :: HashSet NormalizedFilePath -- ^ gone
  -> HashSet NormalizedFilePath -- ^ back
  -> KnownTargets -> KnownTargets
tombstoneKnownFiles gone back kt =
  kt { knownGone = (knownGone kt `HSet.union` gone) `HSet.difference` back }

instance NFData KnownTargets where
  rnf (KnownTargets tm extra gone) = rnf tm `seq` rnf extra `seq` rnf gone `seq` ()

instance Eq KnownTargets where
  k1 == k2 = targetMap k1 == targetMap k2
          && knownExtra k1 == knownExtra k2
          && knownGone k1 == knownGone k2

instance Hashable KnownTargets where
  hashWithSalt s (KnownTargets hm extra gone) =
    hashWithSalt (hashWithSalt (hashWithSalt s hm) (HSet.toList extra)) (HSet.toList gone)

emptyKnownTargets :: KnownTargets
emptyKnownTargets = KnownTargets HMap.empty HSet.empty HSet.empty

data Target = TargetModule ModuleName | TargetFile NormalizedFilePath
  deriving ( Eq, Ord, Generic, Show )
  deriving anyclass (Hashable, NFData)

-- | Every file that is there, as far as we have been told.
toKnownFiles :: KnownTargets -> HashSet NormalizedFilePath
toKnownFiles kt = (targets `HSet.union` knownExtra kt) `HSet.difference` knownGone kt
  where targets = HSet.unions (HMap.elems (targetMap kt))

-- | The files of the project, as declared by the session loader.
-- See Note [Files that are not targets]
toTargetFiles :: KnownTargets -> HashSet NormalizedFilePath
toTargetFiles kt =
  HSet.unions (HMap.elems (targetMap kt)) `HSet.difference` knownGone kt