{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneDeriving #-}
module Ide.Plugin.CallHierarchy.Internal (
prepareCallHierarchy
, incomingCalls
, outgoingCalls
) where
import Control.Lens ((^.))
import Control.Monad.IO.Class
import Data.Aeson as A
import Data.List (groupBy, sortBy)
import qualified Data.Map as M
import Data.Maybe
import qualified Data.Set as S
import qualified Data.Text as T
import Data.Tuple.Extra
import Development.IDE
import Development.IDE.Core.Shake
import Development.IDE.GHC.Compat as Compat
import Development.IDE.Spans.AtPoint
import HieDb (Symbol (Symbol))
import qualified Ide.Plugin.CallHierarchy.Query as Q
import Ide.Plugin.CallHierarchy.Types
import Ide.PluginUtils (getNormalizedFilePath,
handleMaybe, pluginResponse,
throwPluginError)
import Ide.Types
import Language.LSP.Types
import qualified Language.LSP.Types.Lens as L
import Text.Read (readMaybe)
-- | Render prepare call hierarchy request.
prepareCallHierarchy :: PluginMethodHandler IdeState TextDocumentPrepareCallHierarchy
prepareCallHierarchy state _ param = pluginResponse $ do
nfp <- getNormalizedFilePath (param ^. L.textDocument ^. L.uri)
items <- liftIO
$ runAction "CallHierarchy.prepareHierarchy" state
$ prepareCallHierarchyItem nfp (param ^. L.position)
pure $ List <$> pure items
prepareCallHierarchyItem :: NormalizedFilePath -> Position -> Action [CallHierarchyItem]
prepareCallHierarchyItem nfp pos = use GetHieAst nfp >>= \case
Nothing -> pure mempty
Just (HAR _ hf _ _ _) -> pure $ prepareByAst hf pos nfp
prepareByAst :: HieASTs a -> Position -> NormalizedFilePath -> [CallHierarchyItem]
prepareByAst hf pos nfp =
case listToMaybe $ pointCommand hf pos extract of
Nothing -> mempty
Just infos -> mapMaybe (construct nfp hf) infos
extract :: HieAST a -> [(Identifier, [ContextInfo], Span)]
extract ast = let span = nodeSpan ast
infos = M.toList $ M.map (S.toList . identInfo) (Compat.getNodeIds ast)
in [(ident, contexts, span) | (ident, contexts) <- infos]
recFieldInfo, declInfo, valBindInfo, classTyDeclInfo,
useInfo, patternBindInfo, tyDeclInfo, matchBindInfo :: [ContextInfo] -> Maybe ContextInfo
recFieldInfo ctxs = listToMaybe [ctx | ctx@RecField{} <- ctxs]
declInfo ctxs = listToMaybe [ctx | ctx@Decl{} <- ctxs]
valBindInfo ctxs = listToMaybe [ctx | ctx@ValBind{} <- ctxs]
classTyDeclInfo ctxs = listToMaybe [ctx | ctx@ClassTyDecl{} <- ctxs]
useInfo ctxs = listToMaybe [Use | Use <- ctxs]
patternBindInfo ctxs = listToMaybe [ctx | ctx@PatternBind{} <- ctxs]
tyDeclInfo ctxs = listToMaybe [TyDecl | TyDecl <- ctxs]
matchBindInfo ctxs = listToMaybe [MatchBind | MatchBind <- ctxs]
construct :: NormalizedFilePath -> HieASTs a -> (Identifier, [ContextInfo], Span) -> Maybe CallHierarchyItem
construct nfp hf (ident, contexts, ssp)
| isInternalIdentifier ident = Nothing
| Just (RecField RecFieldDecl _) <- recFieldInfo contexts
-- ignored type span
= Just $ mkCallHierarchyItem' ident SkField ssp ssp
| isJust (matchBindInfo contexts) && isNothing (valBindInfo contexts)
= Just $ mkCallHierarchyItem' ident SkFunction ssp ssp
| Just ctx <- valBindInfo contexts
= Just $ case ctx of
ValBind _ _ span -> mkCallHierarchyItem' ident SkFunction (renderSpan span) ssp
_ -> mkCallHierarchyItem' ident skUnknown ssp ssp
| Just ctx <- declInfo contexts
= Just $ case ctx of
Decl ClassDec span -> mkCallHierarchyItem' ident SkInterface (renderSpan span) ssp
Decl ConDec span -> mkCallHierarchyItem' ident SkConstructor (renderSpan span) ssp
Decl DataDec span -> mkCallHierarchyItem' ident SkStruct (renderSpan span) ssp
Decl FamDec span -> mkCallHierarchyItem' ident SkFunction (renderSpan span) ssp
Decl InstDec span -> mkCallHierarchyItem' ident SkInterface (renderSpan span) ssp
Decl SynDec span -> mkCallHierarchyItem' ident SkTypeParameter (renderSpan span) ssp
_ -> mkCallHierarchyItem' ident skUnknown ssp ssp
| Just (ClassTyDecl span) <- classTyDeclInfo contexts
= Just $ mkCallHierarchyItem' ident SkMethod (renderSpan span) ssp
| Just (PatternBind _ _ span) <- patternBindInfo contexts
= Just $ mkCallHierarchyItem' ident SkFunction (renderSpan span) ssp
| Just _ <- useInfo contexts = Just $ mkCallHierarchyItem' ident SkInterface ssp ssp
| Just _ <- tyDeclInfo contexts = renderTyDecl
| otherwise = Nothing
where
renderSpan (Just span) = span
renderSpan _ = ssp
-- https://github.com/haskell/lsp/blob/e11b7c09658610f6d815d04db08a64e7cf6b4467/lsp-types/src/Language/LSP/Types/DocumentSymbol.hs#L97
skUnknown = SkUnknown 27 -- 27 is the first unused number while ToJSON
mkCallHierarchyItem' = mkCallHierarchyItem nfp
isInternalIdentifier = \case
Left _ -> False
Right name -> isInternalName name
renderTyDecl = case ident of
Left _ -> Nothing
Right name -> case getNameBinding name (getAsts hf) of
Nothing -> Nothing
Just sp -> listToMaybe $ prepareByAst hf (realSrcSpanToRange sp ^. L.start) nfp
mkCallHierarchyItem :: NormalizedFilePath -> Identifier -> SymbolKind -> Span -> Span -> CallHierarchyItem
mkCallHierarchyItem nfp ident kind span selSpan =
CallHierarchyItem
(T.pack $ optimizeDisplay $ identifierName ident)
kind
Nothing
(Just $ T.pack $ identifierToDetail ident)
(fromNormalizedUri $ normalizedFilePathToUri nfp)
(realSrcSpanToRange span)
(realSrcSpanToRange selSpan)
(toJSON . show <$> mkSymbol ident)
where
identifierToDetail :: Identifier -> String
identifierToDetail = \case
Left modName -> moduleNameString modName
Right name -> (moduleNameString . moduleName . nameModule) name
identifierName :: Identifier -> String
identifierName = \case
Left modName -> moduleNameString modName
Right name -> occNameString $ nameOccName name
optimizeDisplay :: String -> String
optimizeDisplay name -- Optimize display for DuplicateRecordFields
| "$sel:" == take 5 name = drop 5 name
| otherwise = name
mkSymbol :: Identifier -> Maybe Symbol
mkSymbol = \case
Left _ -> Nothing
Right name -> Just $ Symbol (occName name) (nameModule name)
----------------------------------------------------------------------
-------------- Incoming calls and outgoing calls ---------------------
----------------------------------------------------------------------
deriving instance Ord SymbolKind
deriving instance Ord SymbolTag
deriving instance Ord CallHierarchyItem
#if !MIN_VERSION_aeson(1,5,2)
deriving instance Ord Value
#endif
-- | Render incoming calls request.
incomingCalls :: PluginMethodHandler IdeState CallHierarchyIncomingCalls
incomingCalls state pluginId param = pluginResponse $ do
calls <- liftIO
$ runAction "CallHierarchy.incomingCalls" state
$ queryCalls
(param ^. L.item)
Q.incomingCalls
mkCallHierarchyIncomingCall
(mergeCalls CallHierarchyIncomingCall L.from)
pure $ Just $ List calls
where
mkCallHierarchyIncomingCall :: Vertex -> Action (Maybe CallHierarchyIncomingCall)
mkCallHierarchyIncomingCall = mkCallHierarchyCall CallHierarchyIncomingCall
-- | Render outgoing calls request.
outgoingCalls :: PluginMethodHandler IdeState CallHierarchyOutgoingCalls
outgoingCalls state pluginId param = pluginResponse $ do
calls <- liftIO
$ runAction "CallHierarchy.outgoingCalls" state
$ queryCalls
(param ^. L.item)
Q.outgoingCalls
mkCallHierarchyOutgoingCall
(mergeCalls CallHierarchyOutgoingCall L.to)
pure $ Just $ List calls
where
mkCallHierarchyOutgoingCall :: Vertex -> Action (Maybe CallHierarchyOutgoingCall)
mkCallHierarchyOutgoingCall = mkCallHierarchyCall CallHierarchyOutgoingCall
-- | Merge calls from the same place
mergeCalls constructor target =
concatMap merge
. groupBy (\a b -> a ^. target == b ^. target)
. sortBy (\a b -> (a ^. target) `compare` (b ^. target))
where
merge [] = []
merge calls@(call:_) =
let ranges = concatMap ((\(List x) -> x) . (^. L.fromRanges)) calls
in [constructor (call ^. target) (List ranges)]
mkCallHierarchyCall :: (CallHierarchyItem -> List Range -> a) -> Vertex -> Action (Maybe a)
mkCallHierarchyCall mk v@Vertex{..} = do
let pos = Position (fromIntegral $ sl - 1) (fromIntegral $ sc - 1)
nfp = toNormalizedFilePath' hieSrc
range = mkRange
(fromIntegral $ casl - 1)
(fromIntegral $ casc - 1)
(fromIntegral $ cael - 1)
(fromIntegral $ caec - 1)
prepareCallHierarchyItem nfp pos >>=
\case
[item] -> pure $ Just $ mk item (List [range])
_ -> do
ShakeExtras{withHieDb} <- getShakeExtras
sps <- liftIO (withHieDb (`Q.getSymbolPosition` v))
case sps of
(x:_) -> do
items <- prepareCallHierarchyItem
nfp
(Position (fromIntegral $ psl x - 1) (fromIntegral $ psc x - 1))
case items of
[item] -> pure $ Just $ mk item (List [range])
_ -> pure Nothing
_ -> pure Nothing
-- | Unified queries include incoming calls and outgoing calls.
queryCalls :: (Show a)
=> CallHierarchyItem
-> (HieDb -> Symbol -> IO [Vertex])
-> (Vertex -> Action (Maybe a))
-> ([a] -> [a])
-> Action [a]
queryCalls item queryFunc makeFunc merge
| Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do
ShakeExtras{withHieDb} <- getShakeExtras
maySymbol <- getSymbol nfp
case maySymbol of
Nothing -> pure mempty
Just symbol -> do
vs <- liftIO $ withHieDb (`queryFunc` symbol)
items <- catMaybes <$> mapM makeFunc vs
pure $ merge items
| otherwise = pure mempty
where
uri = item ^. L.uri
xdata = item ^. L.xdata
pos = item ^. (L.selectionRange . L.start)
getSymbol nfp = case item ^. L.xdata of
Just xdata -> case fromJSON xdata of
A.Success (symbolStr :: String) -> maybe (getSymbolFromAst nfp pos) (pure . pure) $ readMaybe symbolStr
A.Error _ -> getSymbolFromAst nfp pos
Nothing -> getSymbolFromAst nfp pos -- Fallback if xdata lost, some editor(VSCode) will drop it
getSymbolFromAst :: NormalizedFilePath -> Position -> Action (Maybe Symbol)
getSymbolFromAst nfp pos = use GetHieAst nfp >>= \case
Nothing -> pure Nothing
Just (HAR _ hf _ _ _) -> do
case listToMaybe $ pointCommand hf pos extract of
Just infos -> maybe (pure Nothing) pure $ mkSymbol . fst3 <$> listToMaybe infos
Nothing -> pure Nothing