Skip to content

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

Merged
merged 24 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
bb742b3
resolve for overloaded-record-dot (checkpoint)
joyfulmantis Jun 13, 2023
be71eb9
resolve support works on VSCode (tests need to be redone)
joyfulmantis Jun 14, 2023
fb21134
Tests for both resolve and non resolve variants
joyfulmantis Jun 15, 2023
f347ebc
Added more tests
joyfulmantis Jun 19, 2023
c19480d
Fix merge mistakes; move function to hls-test-utils
joyfulmantis Jun 19, 2023
5e37f6f
Remove codeLens resolve
joyfulmantis Jun 19, 2023
4bcd45b
Don't use partial functions
joyfulmantis Jun 21, 2023
7d4f01e
Implement michaelpj's suggestions
joyfulmantis Jun 22, 2023
225152e
Make owned resolve data transparent to the plugins
joyfulmantis Jun 26, 2023
4b34265
Improve ord's resolve handler's error handling
joyfulmantis Jun 26, 2023
9985195
Oh well, if only we had MonadFail
joyfulmantis Jun 26, 2023
355e95c
Generic support for resolve in hls packages
joyfulmantis Jun 27, 2023
2e4d14c
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 27, 2023
fb49c31
Add a new code action resolve helper that falls backs to commands
joyfulmantis Jun 27, 2023
d1d299b
add resolve capability set to hls-test-utils
joyfulmantis Jun 28, 2023
e025840
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 28, 2023
0b57d5a
use caps defined at hls-test-utils
joyfulmantis Jun 28, 2023
735feca
Add code lens resolve support
joyfulmantis Jun 29, 2023
6b3b915
Merge branch 'master' into resolve-support
michaelpj Jun 29, 2023
1ba6098
Merge branch 'resolve-support' into ord-resolve
joyfulmantis Jun 29, 2023
7e9bf1d
Merge branch 'master' into ord-resolve
michaelpj Jun 29, 2023
0271ce2
Improve comments
joyfulmantis Jun 29, 2023
794034b
remove Benchmark as it wasn't that useful and triggered a lsp-test bug
joyfulmantis Jun 30, 2023
7790755
Merge branch 'master' into ord-resolve
joyfulmantis Jun 30, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -62,6 +62,7 @@ library
, opentelemetry >=0.4
, optparse-applicative
, regex-tdfa >=1.3.1.0
, row-types
, text
, transformers
, unordered-containers
Expand Down
130 changes: 100 additions & 30 deletions hls-plugin-api/src/Ide/Types.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{-# LANGUAGE MonadComprehensions #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE PolyKinds #-}
Expand Down Expand Up @@ -47,6 +48,8 @@ module Ide.Types
, installSigUsr1Handler
, responseError
, lookupCommandProvider
, OwnedResolveData(..)
, mkCodeActionHandlerWithResolve
)
where

Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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 _ =
Expand Down Expand Up @@ -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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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...

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 combineResponses should be able to throw an error in cases like this. For now I'd probably do the crappy "just take the first result" thing we do elsewhere.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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)

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 guess I could/should just throw an error, and that would be equivalent to returning Nothing?

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 pluginResponsible method for this, I think? We use it in a similar way to restrict the scope of some handlers. So if we put the responsible plugin ID in the data then I think we could restrict to just that plugin in pluginResponsible.

-- 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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -949,7 +967,7 @@ instance HasTracing WorkspaceSymbolParams where
instance HasTracing CallHierarchyIncomingCallsParams
instance HasTracing CallHierarchyOutgoingCallsParams
instance HasTracing CompletionItem

instance HasTracing CodeAction
-- ---------------------------------------------------------------------

{-# NOINLINE pROCESS_ID #-}
Expand Down Expand Up @@ -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
:: (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)
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 {
owner :: PluginId
, value :: Value
} deriving (Generic, Show)
instance ToJSON OwnedResolveData
instance FromJSON OwnedResolveData

pluginResolverResponsible :: Maybe Value -> PluginDescriptor c -> Bool
pluginResolverResponsible (Just val) pluginDesc =
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
-- 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
22 changes: 22 additions & 0 deletions hls-test-utils/src/Test/Hls.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module Test.Hls
defaultTestRunner,
goldenGitDiff,
goldenWithHaskellDoc,
goldenWithHaskellAndCaps,
goldenWithCabalDoc,
goldenWithHaskellDocFormatter,
goldenWithCabalDocFormatter,
Expand Down Expand Up @@ -143,6 +144,27 @@ goldenWithHaskellDoc
-> TestTree
goldenWithHaskellDoc = goldenWithDoc "haskell"

goldenWithHaskellAndCaps
:: Pretty b
=> ClientCapabilities
-> PluginTestDescriptor b
-> TestName
-> FilePath
-> FilePath
-> FilePath
-> FilePath
-> (TextDocumentIdentifier -> Session ())
-> TestTree
goldenWithHaskellAndCaps clientCaps plugin title testDataDir path desc ext act =
goldenGitDiff title (testDataDir </> path <.> desc <.> ext)
$ runSessionWithServerAndCaps plugin clientCaps testDataDir
$ TL.encodeUtf8 . TL.fromStrict
<$> do
doc <- openDoc (path <.> ext) "haskell"
void waitForBuildQueue
act doc
documentContents doc

goldenWithCabalDoc
:: Pretty b
=> PluginTestDescriptor b
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ library
exposed-modules: Ide.Plugin.OverloadedRecordDot
build-depends:
, base >=4.16 && <5
, aeson
, ghcide
, hls-plugin-api
, lsp
Expand Down Expand Up @@ -58,8 +59,12 @@ test-suite tests
build-depends:
, base
, filepath
, ghcide
, text
, hls-overloaded-record-dot-plugin
, lens
, lsp-test
, lsp-types
, row-types
, hls-test-utils

Loading