Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Resolve refactoring #3688

Merged
merged 14 commits into from
Jul 17, 2023
12 changes: 5 additions & 7 deletions ghcide/src/Development/IDE/Plugin/Completions.hs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeSta
descriptor recorder plId = (defaultPluginDescriptor plId)
{ pluginRules = produceCompletions recorder
, pluginHandlers = mkPluginHandler SMethod_TextDocumentCompletion getCompletionsLSP
<> mkPluginHandler SMethod_CompletionItemResolve resolveCompletion
, pluginResolveHandlers = mkResolveHandler SMethod_CompletionItemResolve resolveCompletion
, pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties}
, pluginPriority = ghcideCompletionsPluginPriority
}
Expand Down Expand Up @@ -119,11 +119,9 @@ dropListFromImportDecl iDecl = let
f x = x
in f <$> iDecl

resolveCompletion :: IdeState -> PluginId -> CompletionItem -> LSP.LspM Config (Either ResponseError CompletionItem)
resolveCompletion ide _ comp@CompletionItem{_detail,_documentation,_data_}
| Just resolveData <- _data_
, Success (CompletionResolveData uri needType (NameDetails mod occ)) <- fromJSON resolveData
, Just file <- uriToNormalizedFilePath $ toNormalizedUri uri
resolveCompletion :: ResolveFunction IdeState CompletionResolveData 'Method_CompletionItemResolve
resolveCompletion ide _pid comp@CompletionItem{_detail,_documentation,_data_} uri (CompletionResolveData _ needType (NameDetails mod occ))
| Just file <- uriToNormalizedFilePath $ toNormalizedUri uri
= liftIO $ runIdeAction "Completion resolve" (shakeExtras ide) $ do
msess <- useWithStaleFast GhcSessionDeps file
case msess of
Expand Down Expand Up @@ -160,7 +158,7 @@ resolveCompletion ide _ comp@CompletionItem{_detail,_documentation,_data_}
where
stripForall ty = case splitForAllTyCoVars ty of
(_,res) -> res
resolveCompletion _ _ comp = pure (Right comp)
resolveCompletion _ _ _ _ _ = pure $ Left $ ResponseError (InR ErrorCodes_InvalidParams) "Unable to get normalized file path for url" Nothing

-- | Generate code actions.
getCompletionsLSP
Expand Down
64 changes: 64 additions & 0 deletions ghcide/src/Development/IDE/Plugin/HLS.hs
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,16 @@ data Log
= LogPluginError PluginId ResponseError
| LogNoPluginForMethod (Some SMethod)
| LogInvalidCommandIdentifier
| LogNoResolveData
| LogParseError String (Maybe A.Value)
instance Pretty Log where
pretty = \case
LogPluginError (PluginId pId) err -> pretty pId <> ":" <+> prettyResponseError err
LogNoPluginForMethod (Some method) ->
"No plugin enabled for " <> pretty (show method)
LogInvalidCommandIdentifier-> "Invalid command identifier"
LogNoResolveData -> "No resolve data in resolve request"
LogParseError msg value -> "Error while parsing: " <> pretty msg <> ", value = " <> viaShow value

instance Show Log where show = renderString . layoutCompact . pretty

Expand Down Expand Up @@ -99,11 +103,19 @@ logAndReturnError recorder p errCode msg = do
logWith recorder Warning $ LogPluginError p err
pure $ Left err

-- | Build a ResponseError and log it before returning to the caller
logAndReturnError' :: Recorder (WithPriority Log) -> (LSPErrorCodes |? ErrorCodes) -> Log -> LSP.LspT Config IO (Either ResponseError a)
logAndReturnError' recorder errCode msg = do
let err = ResponseError errCode (T.pack $ show msg) Nothing
logWith recorder Warning $ msg
pure $ Left err

-- | Map a set of plugins to the underlying ghcide engine.
asGhcIdePlugin :: Recorder (WithPriority Log) -> IdePlugins IdeState -> Plugin Config
asGhcIdePlugin recorder (IdePlugins ls) =
mkPlugin rulesPlugins HLS.pluginRules <>
mkPlugin (executeCommandPlugins recorder) HLS.pluginCommands <>
mkPlugin (extensibleResolvePlugins recorder) id <>
mkPlugin (extensiblePlugins recorder) id <>
mkPlugin (extensibleNotificationPlugins recorder) id <>
mkPluginFromDescriptor dynFlagsPlugins HLS.pluginModifyDynflags
Expand Down Expand Up @@ -201,6 +213,46 @@ executeCommandHandlers recorder ecs = requestHandler SMethod_WorkspaceExecuteCom

-- ---------------------------------------------------------------------

extensibleResolvePlugins :: Recorder (WithPriority Log) -> [(PluginId, PluginDescriptor IdeState)] -> Plugin Config
extensibleResolvePlugins recorder xs = mempty { P.pluginHandlers = handlers }
where
IdeResolveHandlers handlers' = foldMap bakePluginId xs
bakePluginId :: (PluginId, PluginDescriptor IdeState) -> IdeResolveHandlers
bakePluginId (pid,pluginDesc) = IdeResolveHandlers $ DMap.map
(\f -> IdeResolveHandler [(pid,pluginDesc,f)])
hs
where
PluginResolveHandlers hs = HLS.pluginResolveHandlers pluginDesc
handlers = mconcat $ do
(ResolveMethod m :=> IdeResolveHandler fs') <- DMap.assocs handlers'
pure $ requestHandler m $ \ide params -> do
case A.fromJSON <$> (params ^. L.data_) of
(Just (A.Success (HLS.PluginResolveData owner uri value) )) -> do
-- Only run plugins that are allowed to run on this request
let fs = filter (\(pid,_ , _) -> pid == owner) fs'
-- Clients generally don't display ResponseErrors so instead we log any that we come across
case nonEmpty fs of
Nothing -> do
logWith recorder Warning (LogNoPluginForMethod $ Some m)
let err = ResponseError (InR ErrorCodes_InvalidRequest) msg Nothing
msg = pluginNotEnabled m fs'
return $ Left err
Just ((pid, _, ResolveHandler handler) NE.:| _) -> do
let msg e pid = "Exception in plugin " <> T.pack (show pid) <> " while processing " <> T.pack (show m) <> ": " <> T.pack (show e)
case A.fromJSON value of
A.Success decodedValue -> do
otTracedProvider pid (fromString $ show m) $ do
handler ide pid params uri decodedValue
`catchAny` (\e -> pure $ Left $ ResponseError (InR ErrorCodes_InternalError) (msg e pid) Nothing)
A.Error err -> do
logAndReturnError' recorder (InR ErrorCodes_ParseError) (LogParseError err (Just value))

Nothing -> do
logAndReturnError' recorder (InR ErrorCodes_InvalidParams) LogNoResolveData
(Just (A.Error str)) -> do
logAndReturnError' recorder (InR ErrorCodes_ParseError) (LogParseError str (params ^. L.data_))
-- ---------------------------------------------------------------------

extensiblePlugins :: Recorder (WithPriority Log) -> [(PluginId, PluginDescriptor IdeState)] -> Plugin Config
extensiblePlugins recorder xs = mempty { P.pluginHandlers = handlers }
where
Expand Down Expand Up @@ -286,13 +338,18 @@ combineErrors xs = ResponseError (InR ErrorCodes_InternalError) (T.pack (show x
newtype IdeHandler (m :: Method ClientToServer Request)
= IdeHandler [(PluginId, PluginDescriptor IdeState, IdeState -> MessageParams m -> LSP.LspM Config (NonEmpty (Either ResponseError (MessageResult m))))]

newtype IdeResolveHandler (m :: Method ClientToServer Request)
= IdeResolveHandler [(PluginId, PluginDescriptor IdeState, PluginResolveHandler IdeState m)]


-- | Combine the 'PluginHandler' for all plugins
newtype IdeNotificationHandler (m :: Method ClientToServer Notification)
= IdeNotificationHandler [(PluginId, PluginDescriptor IdeState, IdeState -> VFS -> MessageParams m -> LSP.LspM Config ())]
-- type NotificationHandler (m :: Method ClientToServer Notification) = MessageParams m -> IO ()`

-- | Combine the 'PluginHandlers' for all plugins
newtype IdeHandlers = IdeHandlers (DMap IdeMethod IdeHandler)
newtype IdeResolveHandlers = IdeResolveHandlers (DMap ResolveMethod IdeResolveHandler)
newtype IdeNotificationHandlers = IdeNotificationHandlers (DMap IdeNotification IdeNotificationHandler)

instance Semigroup IdeHandlers where
Expand All @@ -302,6 +359,13 @@ instance Semigroup IdeHandlers where
instance Monoid IdeHandlers where
mempty = IdeHandlers mempty

instance Semigroup IdeResolveHandlers where
(IdeResolveHandlers a) <> (IdeResolveHandlers b) = IdeResolveHandlers $ DMap.unionWithKey go a b
where
go _ (IdeResolveHandler a) (IdeResolveHandler b) = IdeResolveHandler (a <> b)
instance Monoid IdeResolveHandlers where
mempty = IdeResolveHandlers mempty

instance Semigroup IdeNotificationHandlers where
(IdeNotificationHandlers a) <> (IdeNotificationHandlers b) = IdeNotificationHandlers $ DMap.unionWithKey go a b
where
Expand Down
13 changes: 8 additions & 5 deletions ghcide/test/exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -1571,7 +1571,7 @@ completionTest name src pos expected = testSessionWait name $ do
[ (l, Just k, emptyToMaybe t, at) | (l,k,t,_,_,at) <- expected]
forM_ (zip compls expected) $ \(item, (_,_,_,expectedSig, expectedDocs, _)) -> do
CompletionItem{..} <-
if expectedSig || expectedDocs
if (expectedSig || expectedDocs) && isJust (item ^. L.data_)
then do
rsp <- request SMethod_CompletionItemResolve item
case rsp ^. L.result of
Expand Down Expand Up @@ -2081,10 +2081,13 @@ completionDocTests =
_ <- waitForDiagnostics
compls <- getCompletions doc pos
rcompls <- forM compls $ \item -> do
rsp <- request SMethod_CompletionItemResolve item
case rsp ^. L.result of
Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)
Right x -> pure x
if isJust (item ^. L.data_)
then do
rsp <- request SMethod_CompletionItemResolve item
case rsp ^. L.result of
Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err)
Right x -> pure x
else pure item
let compls' = [
-- We ignore doc uris since it points to the local path which determined by specific machines
case mn of
Expand Down
1 change: 1 addition & 0 deletions hls-plugin-api/hls-plugin-api.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ library
Ide.Plugin.ConfigUtils
Ide.Plugin.Properties
Ide.Plugin.RangeMap
Ide.Plugin.Resolve
Ide.PluginUtils
Ide.Types

Expand Down
149 changes: 149 additions & 0 deletions hls-plugin-api/src/Ide/Plugin/Resolve.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}

module Ide.Plugin.Resolve
(mkCodeActionHandlerWithResolve,
mkCodeActionWithResolveAndCommand) where

import Control.Lens (_Just, (&), (.~), (?~), (^.),
(^?))
import Control.Monad.Trans.Class (MonadTrans (lift))
import Control.Monad.Trans.Except (ExceptT (..), runExceptT,
throwE)
import qualified Data.Aeson as A
import Data.Row ((.!))
import qualified Data.Text as T
import GHC.Generics (Generic)
import Ide.Types
import qualified Language.LSP.Protocol.Lens as L
import Language.LSP.Protocol.Message
import Language.LSP.Protocol.Types
import Language.LSP.Server (LspM, LspT,
ProgressCancellable (Cancellable),
getClientCapabilities,
sendRequest,
withIndefiniteProgress)

-- |When provided with both a codeAction provider and an affiliated codeAction
-- resolve provider, this function creates a handler that automatically uses
-- your resolve provider to fill out you original codeAction if the client doesn't
-- have codeAction resolve support. This means you don't have to check whether
-- the client supports resolve and act accordingly in your own providers.
mkCodeActionHandlerWithResolve
:: forall ideState a. (A.FromJSON a) =>
(ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
-> (ideState -> PluginId -> CodeAction -> Uri -> a -> LspM Config (Either ResponseError CodeAction))
-> (PluginHandlers ideState, PluginResolveHandlers ideState)
mkCodeActionHandlerWithResolve codeActionMethod codeResolveMethod =
let newCodeActionMethod ideState pid params = runExceptT $
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
caps <- lift getClientCapabilities
case codeActionReturn of
r@(InR Null) -> pure r
(InL ls) | -- If the client supports resolve, we will wrap the resolve data in a owned
-- resolve data type to allow the server to know who to send the resolve request to
supportsCodeActionResolve caps -> pure $ InL ls
--This is the actual part where we call resolveCodeAction which fills in the edit data for the client
| otherwise -> InL <$> traverse (resolveCodeAction (params ^. L.textDocument . L.uri) ideState pid) ls
in (mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod
, mkResolveHandler SMethod_CodeActionResolve codeResolveMethod)
where
dropData :: CodeAction -> CodeAction
dropData ca = ca & L.data_ .~ Nothing
resolveCodeAction :: Uri -> ideState -> PluginId -> (Command |? CodeAction) -> ExceptT ResponseError (LspT Config IO) (Command |? CodeAction)
resolveCodeAction _uri _ideState _plId c@(InL _) = pure c
resolveCodeAction uri ideState pid (InR codeAction@CodeAction{_data_=Just value}) = do
case A.fromJSON value of
A.Error err -> throwE $ parseError (Just value) (T.pack err)
A.Success innerValueDecoded -> do
resolveResult <- ExceptT $ codeResolveMethod ideState pid codeAction uri innerValueDecoded
case resolveResult of
CodeAction {_edit = Just _ } -> do
pure $ InR $ dropData resolveResult
_ -> throwE $ invalidParamsError "Returned CodeAction has no data field"
resolveCodeAction _ _ _ (InR CodeAction{_data_=Nothing}) = throwE $ invalidParamsError "CodeAction has no data field"

-- |When provided with both a codeAction provider that includes both a command
-- and a data field and a resolve provider, this function creates a handler that
-- defaults to using your command if the client doesn't have code action resolve
-- support. This means you don't have to check whether the client supports resolve
-- and act accordingly in your own providers.
mkCodeActionWithResolveAndCommand
:: forall ideState a. (A.FromJSON a) =>
PluginId
-> (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
-> (ideState -> PluginId -> CodeAction -> Uri -> a -> LspM Config (Either ResponseError CodeAction))
-> ([PluginCommand ideState], PluginHandlers ideState, PluginResolveHandlers ideState)
mkCodeActionWithResolveAndCommand plId codeActionMethod codeResolveMethod =
let newCodeActionMethod ideState pid params = runExceptT $
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
caps <- lift getClientCapabilities
case codeActionReturn of
r@(InR Null) -> pure r
(InL ls) | -- If the client supports resolve, we will wrap the resolve data in a owned
-- resolve data type to allow the server to know who to send the resolve request to
supportsCodeActionResolve caps ->
pure $ InL ls
-- If they do not we will drop the data field, in addition we will populate the command
-- field with our command to execute the resolve, with the whole code action as it's argument.
| otherwise -> pure $ InL $ moveDataToCommand (params ^. L.textDocument . L.uri) <$> ls
in ([PluginCommand "codeActionResolve" "Executes resolve for code action" (executeResolveCmd (codeResolveMethod))],
mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod,
mkResolveHandler SMethod_CodeActionResolve codeResolveMethod)
where moveDataToCommand :: Uri -> Command |? CodeAction -> Command |? CodeAction
moveDataToCommand uri ca =
let dat = A.toJSON . wrapWithURI uri <$> ca ^? _R -- We need to take the whole codeAction
-- And put it in the argument for the Command, that way we can later
-- pas it to the resolve handler (which expects a whole code action)
cmd = mkLspCommand plId (CommandId "codeActionResolve") "Execute Code Action" (pure <$> dat)
in ca
& _R . L.data_ .~ Nothing -- Set the data field to nothing
& _R . L.command ?~ cmd -- And set the command to our previously created command
wrapWithURI :: Uri -> CodeAction -> CodeAction
wrapWithURI uri codeAction =
codeAction & L.data_ .~ (A.toJSON .WithURI uri <$> data_)
where data_ = codeAction ^? L.data_ . _Just
executeResolveCmd :: (ideState -> PluginId -> CodeAction -> Uri -> a -> LspM Config (Either ResponseError CodeAction))-> CommandFunction ideState CodeAction
executeResolveCmd resolveProvider ideState ca@CodeAction{_data_=Just value} = do
withIndefiniteProgress "Executing code action..." Cancellable $ do
case A.fromJSON value of
A.Error err -> pure $ Left $ parseError (Just value) (T.pack err)
A.Success (WithURI uri innerValue) -> do
case A.fromJSON innerValue of
A.Error err -> pure $ Left $ parseError (Just value) (T.pack err)
A.Success innerValueDecoded -> do
resolveResult <- resolveProvider ideState plId ca uri innerValueDecoded
case resolveResult of
Right CodeAction {_edit = Just wedits } -> do
_ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) (\_ -> pure ())
pure $ Right A.Null
Right _ -> pure $ Left $ invalidParamsError "Returned CodeAction has no data field"
Left err -> pure $ Left err
executeResolveCmd _ _ CodeAction{_data_= value} = pure $ Left $ invalidParamsError ("CodeAction data field empty: " <> (T.pack $ show value))


-- |To execute the resolve provider as a command, we need to additionally store
-- the URI that was provided to the original code action.
data WithURI = WithURI {
_uri :: Uri
, _value :: A.Value
} deriving (Generic, Show)
instance A.ToJSON WithURI
instance A.FromJSON WithURI

supportsCodeActionResolve :: ClientCapabilities -> Bool
supportsCodeActionResolve caps =
caps ^? L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just == Just True
&& case caps ^? L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just of
Just row -> "edit" `elem` row .! #properties
joyfulmantis marked this conversation as resolved.
Show resolved Hide resolved
_ -> False

invalidParamsError :: T.Text -> ResponseError
invalidParamsError msg = ResponseError (InR ErrorCodes_InternalError) ("Ide.Plugin.Resolve: " <> msg) Nothing

parseError :: Maybe A.Value -> T.Text -> ResponseError
parseError value errMsg = ResponseError (InR ErrorCodes_InternalError) ("Ide.Plugin.Resolve: Error parsing value:"<> (T.pack $ show value) <> " Error: "<> errMsg) Nothing
Loading
Loading