-
-
Notifications
You must be signed in to change notification settings - Fork 391
Resolve 1: Support for resolve in overloaded-record-dot #3658
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
Changes from 8 commits
bb742b3
be71eb9
fb21134
f347ebc
c19480d
5e37f6f
4bcd45b
7d4f01e
225152e
4b34265
9985195
355e95c
2e4d14c
fb49c31
d1d299b
e025840
0b57d5a
735feca
6b3b915
1ba6098
7e9bf1d
0271ce2
794034b
7790755
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,6 +12,7 @@ | |
{-# LANGUAGE MonadComprehensions #-} | ||
{-# LANGUAGE MultiParamTypeClasses #-} | ||
{-# LANGUAGE NamedFieldPuns #-} | ||
{-# LANGUAGE OverloadedLabels #-} | ||
{-# LANGUAGE OverloadedStrings #-} | ||
{-# LANGUAGE PatternSynonyms #-} | ||
{-# LANGUAGE PolyKinds #-} | ||
|
@@ -47,6 +48,8 @@ module Ide.Types | |
, installSigUsr1Handler | ||
, responseError | ||
, lookupCommandProvider | ||
, OwnedResolveData(..) | ||
, mkCodeActionHandlerWithResolve | ||
) | ||
where | ||
|
||
|
@@ -59,7 +62,9 @@ import System.Posix.Signals | |
#endif | ||
import Control.Applicative ((<|>)) | ||
import Control.Arrow ((&&&)) | ||
import Control.Lens ((.~), (^.)) | ||
import Control.Lens (_Just, (.~), (^.), (^?)) | ||
import Control.Monad.Trans.Class (lift) | ||
import Control.Monad.Trans.Except (ExceptT (..), runExceptT) | ||
import Data.Aeson hiding (Null, defaultOptions) | ||
import Data.Default | ||
import Data.Dependent.Map (DMap) | ||
|
@@ -74,6 +79,7 @@ import Data.List.NonEmpty (NonEmpty (..), toList) | |
import qualified Data.Map as Map | ||
import Data.Maybe | ||
import Data.Ord | ||
import Data.Row ((.!)) | ||
import Data.Semigroup | ||
import Data.String | ||
import qualified Data.Text as T | ||
|
@@ -85,7 +91,9 @@ import Ide.Plugin.Properties | |
import qualified Language.LSP.Protocol.Lens as L | ||
import Language.LSP.Protocol.Message | ||
import Language.LSP.Protocol.Types | ||
import Language.LSP.Server (LspM, getVirtualFile) | ||
import Language.LSP.Server (LspM, LspT, | ||
getClientCapabilities, | ||
getVirtualFile) | ||
import Language.LSP.VFS | ||
import Numeric.Natural | ||
import OpenTelemetry.Eventlog | ||
|
@@ -403,32 +411,10 @@ instance PluginMethod Request Method_TextDocumentCodeAction where | |
where | ||
uri = msgParams ^. L.textDocument . L.uri | ||
|
||
instance PluginRequestMethod Method_TextDocumentCodeAction where | ||
combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps = | ||
InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps | ||
where | ||
compat :: (Command |? CodeAction) -> (Command |? CodeAction) | ||
compat x@(InL _) = x | ||
compat x@(InR action) | ||
| Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport | ||
= x | ||
| otherwise = InL cmd | ||
where | ||
cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. L.title) (Just cmdParams) | ||
cmdParams = [toJSON (FallbackCodeActionParams (action ^. L.edit) (action ^. L.command))] | ||
|
||
wasRequested :: (Command |? CodeAction) -> Bool | ||
wasRequested (InL _) = True | ||
wasRequested (InR ca) | ||
| Nothing <- _only context = True | ||
| Just allowed <- _only context | ||
-- See https://github.com/microsoft/language-server-protocol/issues/970 | ||
-- This is somewhat vague, but due to the hierarchical nature of action kinds, we | ||
-- should check whether the requested kind is a *prefix* of the action kind. | ||
-- That means, for example, we will return actions with kinds `quickfix.import` and | ||
-- `quickfix.somethingElse` if the requested kind is `quickfix`. | ||
, Just caKind <- ca ^. L.kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed | ||
| otherwise = False | ||
instance PluginMethod Request Method_CodeActionResolve where | ||
pluginEnabled _ msgParams pluginDesc config = | ||
pluginResolverResponsible (msgParams ^. L.data_) pluginDesc | ||
&& pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) | ||
|
||
instance PluginMethod Request Method_TextDocumentDefinition where | ||
pluginEnabled _ msgParams pluginDesc _ = | ||
|
@@ -535,6 +521,38 @@ instance PluginMethod Request (Method_CustomMethod m) where | |
pluginEnabled _ _ _ _ = True | ||
|
||
--- | ||
instance PluginRequestMethod Method_TextDocumentCodeAction where | ||
combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps = | ||
InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps | ||
where | ||
compat :: (Command |? CodeAction) -> (Command |? CodeAction) | ||
compat x@(InL _) = x | ||
compat x@(InR action) | ||
| Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport | ||
= x | ||
| otherwise = InL cmd | ||
where | ||
cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. L.title) (Just cmdParams) | ||
cmdParams = [toJSON (FallbackCodeActionParams (action ^. L.edit) (action ^. L.command))] | ||
|
||
wasRequested :: (Command |? CodeAction) -> Bool | ||
wasRequested (InL _) = True | ||
wasRequested (InR ca) | ||
| Nothing <- _only context = True | ||
| Just allowed <- _only context | ||
-- See https://github.com/microsoft/language-server-protocol/issues/970 | ||
-- This is somewhat vague, but due to the hierarchical nature of action kinds, we | ||
-- should check whether the requested kind is a *prefix* of the action kind. | ||
-- That means, for example, we will return actions with kinds `quickfix.import` and | ||
-- `quickfix.somethingElse` if the requested kind is `quickfix`. | ||
, Just caKind <- ca ^. L.kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed | ||
| otherwise = False | ||
|
||
instance PluginRequestMethod Method_CodeActionResolve where | ||
-- CodeAction resolve is currently only used to changed the edit field, thus | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm, this made me realize that something funny may happen here. Suppose we have N code action handlers, each of which has a corresponding resolve handler. Now if one of the code action handlers produces a code action, then when the client asks to resolve it... it's going to get sent to every resolve handler. So we'll need to make sure that resolve handlers "know" if it's one of "their" code actions... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And indeed, I think we therefore don't want to combine the results of multiple resolve handlers firing. What would that even mean? Something has gone wrong if that has happened! We should get exactly one response. Hard to do for now, but I do think that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So even if every plugin's resolve handler used the same type theoretically Data.Unique should allow types to know whether a resolve belongs to them (resolvable ones eventually anyways). To make sure no extra processing was needed I was thinking of having the plugin record their name or id in the type serialized to the data field, and then first match on that, to make sure no extra processing was needed. Regarding combineResponses, I was actually originally just returning the unmodified codeAction if I was unable to resolve with the provided resolve data, which is why combineRespones would still be needed to find the modified codeAction from the list. I guess I could/should just throw an error, and that would be equivalent to returning Nothing? The plugin ultimately responsible for the resolve also needs to throw a ContentModified responseError if it can't resolve it. So not actually sure we can use pluginResponse in the end (or at least we need to modify it) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, if we use standard infrastructure and tag every code action with a Unique then we probably should be fairly okay. Although it requires some state on the plugin side somewhere to remember which uniques it produced. The advantage of the "just pass the CodeActionParams again" approach is that it's easier to be stateless if you want to. Not sure if it's possible to be stateless in general, though 🤔
I think that's fine. I think we run all the handlers, throw away any that failed and then combine the results of the rest. We could potentially also use the |
||
-- that's the only field we are combining. | ||
combineResponses _ _ _ codeAction (toList -> codeActions) = codeAction & L.edit .~ mconcat ((^. L.edit) <$> codeActions) | ||
|
||
instance PluginRequestMethod Method_TextDocumentDefinition where | ||
combineResponses _ _ _ _ (x :| _) = x | ||
|
||
|
@@ -848,7 +866,7 @@ type CommandFunction ideState a | |
|
||
newtype PluginId = PluginId T.Text | ||
deriving (Show, Read, Eq, Ord) | ||
deriving newtype (FromJSON, Hashable) | ||
deriving newtype (ToJSON, FromJSON, Hashable) | ||
|
||
instance IsString PluginId where | ||
fromString = PluginId . T.pack | ||
|
@@ -949,7 +967,7 @@ instance HasTracing WorkspaceSymbolParams where | |
instance HasTracing CallHierarchyIncomingCallsParams | ||
instance HasTracing CallHierarchyOutgoingCallsParams | ||
instance HasTracing CompletionItem | ||
|
||
instance HasTracing CodeAction | ||
-- --------------------------------------------------------------------- | ||
|
||
{-# NOINLINE pROCESS_ID #-} | ||
|
@@ -983,3 +1001,55 @@ getProcessID = fromIntegral <$> P.getProcessID | |
|
||
installSigUsr1Handler h = void $ installHandler sigUSR1 (Catch h) Nothing | ||
#endif | ||
|
||
-- |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 | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
:: (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null))) | ||
-> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction)) | ||
-> PluginHandlers 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 _) -> pure r | ||
(InL ls) -> do | ||
let mapper :: (Command |? CodeAction) -> ExceptT ResponseError (LspT Config IO) (Command |? CodeAction) | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
mapper c@(InL _) = pure c | ||
mapper (InR codeAction) = fmap (InR . dropData) $ ExceptT $ codeResolveMethod ideState pid codeAction | ||
if supportsResolve caps | ||
then pure $ InL ls | ||
else InL <$> traverse mapper ls --This is the actual part where we fill in the edit data for the client | ||
in mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod | ||
<> mkPluginHandler SMethod_CodeActionResolve codeResolveMethod | ||
where supportsResolve :: ClientCapabilities -> Bool | ||
supportsResolve 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 | ||
_ -> False | ||
dropData :: CodeAction -> CodeAction | ||
dropData ca = ca & L.data_ .~ Nothing | ||
|
||
-- |Allow plugins to "own" resolve data, allowing only them to be queried for | ||
-- the resolve action. This design has added flexibility at the cost of nested | ||
-- Value types | ||
data OwnedResolveData = ORD { | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
owner :: PluginId | ||
, value :: Value | ||
} deriving (Generic, Show) | ||
instance ToJSON OwnedResolveData | ||
instance FromJSON OwnedResolveData | ||
|
||
pluginResolverResponsible :: Maybe Value -> PluginDescriptor c -> Bool | ||
pluginResolverResponsible (Just val) pluginDesc = | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
case fromJSON val of | ||
(Success (ORD o _)) -> pluginId pluginDesc == o | ||
_ -> True -- We want to fail open in case our resolver is not using the ORD type | ||
joyfulmantis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
-- This is a wierd case, because anything that gets resolved should have a data | ||
-- field, but in any case, failing open is safe enough. | ||
pluginResolverResponsible Nothing _ = True |
Uh oh!
There was an error while loading. Please reload this page.