Skip to content

Commit 7860df3

Browse files
authored
Resolve 0: Generic support for resolve in hls packages (#3678)
* Generic support for resolve in hls packages * Add a new code action resolve helper that falls backs to commands * add resolve capability set to hls-test-utils * Add code lens resolve support
1 parent e9c81e4 commit 7860df3

File tree

4 files changed

+199
-32
lines changed

4 files changed

+199
-32
lines changed

hls-plugin-api/hls-plugin-api.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ library
6262
, opentelemetry >=0.4
6363
, optparse-applicative
6464
, regex-tdfa >=1.3.1.0
65+
, row-types
6566
, text
6667
, transformers
6768
, unordered-containers

hls-plugin-api/src/Ide/Types.hs

Lines changed: 163 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
{-# LANGUAGE MonadComprehensions #-}
1313
{-# LANGUAGE MultiParamTypeClasses #-}
1414
{-# LANGUAGE NamedFieldPuns #-}
15+
{-# LANGUAGE OverloadedLabels #-}
1516
{-# LANGUAGE OverloadedStrings #-}
1617
{-# LANGUAGE PatternSynonyms #-}
1718
{-# LANGUAGE PolyKinds #-}
@@ -47,6 +48,9 @@ module Ide.Types
4748
, installSigUsr1Handler
4849
, responseError
4950
, lookupCommandProvider
51+
, OwnedResolveData(..)
52+
, mkCodeActionHandlerWithResolve
53+
, mkCodeActionWithResolveAndCommand
5054
)
5155
where
5256

@@ -59,7 +63,9 @@ import System.Posix.Signals
5963
#endif
6064
import Control.Applicative ((<|>))
6165
import Control.Arrow ((&&&))
62-
import Control.Lens ((.~), (^.))
66+
import Control.Lens (_Just, (.~), (?~), (^.), (^?))
67+
import Control.Monad.Trans.Class (lift)
68+
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
6369
import Data.Aeson hiding (Null, defaultOptions)
6470
import Data.Default
6571
import Data.Dependent.Map (DMap)
@@ -74,6 +80,7 @@ import Data.List.NonEmpty (NonEmpty (..), toList)
7480
import qualified Data.Map as Map
7581
import Data.Maybe
7682
import Data.Ord
83+
import Data.Row ((.!))
7784
import Data.Semigroup
7885
import Data.String
7986
import qualified Data.Text as T
@@ -85,7 +92,9 @@ import Ide.Plugin.Properties
8592
import qualified Language.LSP.Protocol.Lens as L
8693
import Language.LSP.Protocol.Message
8794
import Language.LSP.Protocol.Types
88-
import Language.LSP.Server (LspM, getVirtualFile)
95+
import Language.LSP.Server (LspM, LspT,
96+
getClientCapabilities,
97+
getVirtualFile)
8998
import Language.LSP.VFS
9099
import Numeric.Natural
91100
import OpenTelemetry.Eventlog
@@ -403,32 +412,10 @@ instance PluginMethod Request Method_TextDocumentCodeAction where
403412
where
404413
uri = msgParams ^. L.textDocument . L.uri
405414

406-
instance PluginRequestMethod Method_TextDocumentCodeAction where
407-
combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps =
408-
InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps
409-
where
410-
compat :: (Command |? CodeAction) -> (Command |? CodeAction)
411-
compat x@(InL _) = x
412-
compat x@(InR action)
413-
| Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport
414-
= x
415-
| otherwise = InL cmd
416-
where
417-
cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. L.title) (Just cmdParams)
418-
cmdParams = [toJSON (FallbackCodeActionParams (action ^. L.edit) (action ^. L.command))]
419-
420-
wasRequested :: (Command |? CodeAction) -> Bool
421-
wasRequested (InL _) = True
422-
wasRequested (InR ca)
423-
| Nothing <- _only context = True
424-
| Just allowed <- _only context
425-
-- See https://github.com/microsoft/language-server-protocol/issues/970
426-
-- This is somewhat vague, but due to the hierarchical nature of action kinds, we
427-
-- should check whether the requested kind is a *prefix* of the action kind.
428-
-- That means, for example, we will return actions with kinds `quickfix.import` and
429-
-- `quickfix.somethingElse` if the requested kind is `quickfix`.
430-
, Just caKind <- ca ^. L.kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed
431-
| otherwise = False
415+
instance PluginMethod Request Method_CodeActionResolve where
416+
pluginEnabled _ msgParams pluginDesc config =
417+
pluginResolverResponsible (msgParams ^. L.data_) pluginDesc
418+
&& pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc)
432419

433420
instance PluginMethod Request Method_TextDocumentDefinition where
434421
pluginEnabled _ msgParams pluginDesc _ =
@@ -464,6 +451,11 @@ instance PluginMethod Request Method_TextDocumentCodeLens where
464451
where
465452
uri = msgParams ^. L.textDocument . L.uri
466453

454+
instance PluginMethod Request Method_CodeLensResolve where
455+
pluginEnabled _ msgParams pluginDesc config =
456+
pluginResolverResponsible (msgParams ^. L.data_) pluginDesc
457+
&& pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc)
458+
467459
instance PluginMethod Request Method_TextDocumentRename where
468460
pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc
469461
&& pluginEnabledConfig plcRenameOn (configForPlugin config pluginDesc)
@@ -535,6 +527,38 @@ instance PluginMethod Request (Method_CustomMethod m) where
535527
pluginEnabled _ _ _ _ = True
536528

537529
---
530+
instance PluginRequestMethod Method_TextDocumentCodeAction where
531+
combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps =
532+
InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps
533+
where
534+
compat :: (Command |? CodeAction) -> (Command |? CodeAction)
535+
compat x@(InL _) = x
536+
compat x@(InR action)
537+
| Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport
538+
= x
539+
| otherwise = InL cmd
540+
where
541+
cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. L.title) (Just cmdParams)
542+
cmdParams = [toJSON (FallbackCodeActionParams (action ^. L.edit) (action ^. L.command))]
543+
544+
wasRequested :: (Command |? CodeAction) -> Bool
545+
wasRequested (InL _) = True
546+
wasRequested (InR ca)
547+
| Nothing <- _only context = True
548+
| Just allowed <- _only context
549+
-- See https://github.com/microsoft/language-server-protocol/issues/970
550+
-- This is somewhat vague, but due to the hierarchical nature of action kinds, we
551+
-- should check whether the requested kind is a *prefix* of the action kind.
552+
-- That means, for example, we will return actions with kinds `quickfix.import` and
553+
-- `quickfix.somethingElse` if the requested kind is `quickfix`.
554+
, Just caKind <- ca ^. L.kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed
555+
| otherwise = False
556+
557+
instance PluginRequestMethod Method_CodeActionResolve where
558+
-- CodeAction resolve is currently only used to changed the edit field, thus
559+
-- that's the only field we are combining.
560+
combineResponses _ _ _ codeAction (toList -> codeActions) = codeAction & L.edit .~ mconcat ((^. L.edit) <$> codeActions)
561+
538562
instance PluginRequestMethod Method_TextDocumentDefinition where
539563
combineResponses _ _ _ _ (x :| _) = x
540564

@@ -552,6 +576,10 @@ instance PluginRequestMethod Method_WorkspaceSymbol where
552576

553577
instance PluginRequestMethod Method_TextDocumentCodeLens where
554578

579+
instance PluginRequestMethod Method_CodeLensResolve where
580+
-- A resolve request should only ever get one response
581+
combineResponses _ _ _ _ (x :| _) = x
582+
555583
instance PluginRequestMethod Method_TextDocumentRename where
556584

557585
instance PluginRequestMethod Method_TextDocumentHover where
@@ -848,7 +876,7 @@ type CommandFunction ideState a
848876

849877
newtype PluginId = PluginId T.Text
850878
deriving (Show, Read, Eq, Ord)
851-
deriving newtype (FromJSON, Hashable)
879+
deriving newtype (ToJSON, FromJSON, Hashable)
852880

853881
instance IsString PluginId where
854882
fromString = PluginId . T.pack
@@ -949,7 +977,8 @@ instance HasTracing WorkspaceSymbolParams where
949977
instance HasTracing CallHierarchyIncomingCallsParams
950978
instance HasTracing CallHierarchyOutgoingCallsParams
951979
instance HasTracing CompletionItem
952-
980+
instance HasTracing CodeAction
981+
instance HasTracing CodeLens
953982
-- ---------------------------------------------------------------------
954983

955984
{-# NOINLINE pROCESS_ID #-}
@@ -983,3 +1012,107 @@ getProcessID = fromIntegral <$> P.getProcessID
9831012

9841013
installSigUsr1Handler h = void $ installHandler sigUSR1 (Catch h) Nothing
9851014
#endif
1015+
1016+
-- |When provided with both a codeAction provider and an affiliated codeAction
1017+
-- resolve provider, this function creates a handler that automatically uses
1018+
-- your resolve provider to fill out you original codeAction if the client doesn't
1019+
-- have codeAction resolve support. This means you don't have to check whether
1020+
-- the client supports resolve and act accordingly in your own providers.
1021+
mkCodeActionHandlerWithResolve
1022+
:: forall ideState. (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
1023+
-> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction))
1024+
-> PluginHandlers ideState
1025+
mkCodeActionHandlerWithResolve codeActionMethod codeResolveMethod =
1026+
let newCodeActionMethod ideState pid params = runExceptT $
1027+
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
1028+
caps <- lift getClientCapabilities
1029+
case codeActionReturn of
1030+
r@(InR Null) -> pure r
1031+
(InL ls) | -- If the client supports resolve, we will wrap the resolve data in a owned
1032+
-- resolve data type to allow the server to know who to send the resolve request to
1033+
supportsCodeActionResolve caps -> pure $ InL (wrapCodeActionResolveData pid <$> ls)
1034+
--This is the actual part where we call resolveCodeAction which fills in the edit data for the client
1035+
| otherwise -> InL <$> traverse (resolveCodeAction ideState pid) ls
1036+
newCodeResolveMethod ideState pid params =
1037+
codeResolveMethod ideState pid (unwrapCodeActionResolveData params)
1038+
in mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod
1039+
<> mkPluginHandler SMethod_CodeActionResolve newCodeResolveMethod
1040+
where
1041+
dropData :: CodeAction -> CodeAction
1042+
dropData ca = ca & L.data_ .~ Nothing
1043+
resolveCodeAction :: ideState -> PluginId -> (Command |? CodeAction) -> ExceptT ResponseError (LspT Config IO) (Command |? CodeAction)
1044+
resolveCodeAction _ideState _pid c@(InL _) = pure c
1045+
resolveCodeAction ideState pid (InR codeAction) =
1046+
fmap (InR . dropData) $ ExceptT $ codeResolveMethod ideState pid codeAction
1047+
1048+
-- |When provided with both a codeAction provider that includes both a command
1049+
-- and a data field and a resolve provider, this function creates a handler that
1050+
-- defaults to using your command if the client doesn't have code action resolve
1051+
-- support. This means you don't have to check whether the client supports resolve
1052+
-- and act accordingly in your own providers.
1053+
mkCodeActionWithResolveAndCommand
1054+
:: forall ideState. (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
1055+
-> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction))
1056+
-> PluginHandlers ideState
1057+
mkCodeActionWithResolveAndCommand codeActionMethod codeResolveMethod =
1058+
let newCodeActionMethod ideState pid params = runExceptT $
1059+
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
1060+
caps <- lift getClientCapabilities
1061+
case codeActionReturn of
1062+
r@(InR Null) -> pure r
1063+
(InL ls) | -- If the client supports resolve, we will wrap the resolve data in a owned
1064+
-- resolve data type to allow the server to know who to send the resolve request to
1065+
-- and dump the command fields.
1066+
supportsCodeActionResolve caps ->
1067+
pure $ InL (dropCommands . wrapCodeActionResolveData pid <$> ls)
1068+
-- If they do not we will drop the data field.
1069+
| otherwise -> pure $ InL $ dropData <$> ls
1070+
newCodeResolveMethod ideState pid params =
1071+
codeResolveMethod ideState pid (unwrapCodeActionResolveData params)
1072+
in mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod
1073+
<> mkPluginHandler SMethod_CodeActionResolve newCodeResolveMethod
1074+
where dropData :: Command |? CodeAction -> Command |? CodeAction
1075+
dropData ca = ca & _R . L.data_ .~ Nothing
1076+
dropCommands :: Command |? CodeAction -> Command |? CodeAction
1077+
dropCommands ca = ca & _R . L.command .~ Nothing
1078+
1079+
supportsCodeActionResolve :: ClientCapabilities -> Bool
1080+
supportsCodeActionResolve caps =
1081+
caps ^? L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just == Just True
1082+
&& case caps ^? L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just of
1083+
Just row -> "edit" `elem` row .! #properties
1084+
_ -> False
1085+
1086+
-- We don't wrap commands
1087+
wrapCodeActionResolveData :: PluginId -> (a |? CodeAction) -> a |? CodeAction
1088+
wrapCodeActionResolveData _pid c@(InL _) = c
1089+
wrapCodeActionResolveData pid (InR c@(CodeAction{_data_=Just x})) =
1090+
InR $ c & L.data_ ?~ toJSON (ORD pid x)
1091+
-- Neither do we wrap code actions's without data fields,
1092+
wrapCodeActionResolveData _pid c@(InR (CodeAction{_data_=Nothing})) = c
1093+
1094+
unwrapCodeActionResolveData :: CodeAction -> CodeAction
1095+
unwrapCodeActionResolveData c@CodeAction{_data_ = Just x}
1096+
| Success ORD {value = v} <- fromJSON x = c & L.data_ ?~ v
1097+
-- If we can't successfully decode the value as a ORD type than
1098+
-- we just return the codeAction untouched.
1099+
unwrapCodeActionResolveData c = c
1100+
1101+
-- |Allow plugins to "own" resolve data, allowing only them to be queried for
1102+
-- the resolve action. This design has added flexibility at the cost of nested
1103+
-- Value types
1104+
data OwnedResolveData = ORD {
1105+
owner :: PluginId
1106+
, value :: Value
1107+
} deriving (Generic, Show)
1108+
instance ToJSON OwnedResolveData
1109+
instance FromJSON OwnedResolveData
1110+
1111+
pluginResolverResponsible :: Maybe Value -> PluginDescriptor c -> Bool
1112+
pluginResolverResponsible (Just val) pluginDesc =
1113+
case fromJSON val of
1114+
(Success (ORD o _)) -> pluginId pluginDesc == o
1115+
_ -> True -- We want to fail open in case our resolver is not using the ORD type
1116+
-- This is a wierd case, because anything that gets resolved should have a data
1117+
-- field, but in any case, failing open is safe enough.
1118+
pluginResolverResponsible Nothing _ = True

hls-test-utils/src/Test/Hls.hs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ module Test.Hls
2020
defaultTestRunner,
2121
goldenGitDiff,
2222
goldenWithHaskellDoc,
23+
goldenWithHaskellAndCaps,
2324
goldenWithCabalDoc,
2425
goldenWithHaskellDocFormatter,
2526
goldenWithCabalDocFormatter,
@@ -143,6 +144,27 @@ goldenWithHaskellDoc
143144
-> TestTree
144145
goldenWithHaskellDoc = goldenWithDoc "haskell"
145146

147+
goldenWithHaskellAndCaps
148+
:: Pretty b
149+
=> ClientCapabilities
150+
-> PluginTestDescriptor b
151+
-> TestName
152+
-> FilePath
153+
-> FilePath
154+
-> FilePath
155+
-> FilePath
156+
-> (TextDocumentIdentifier -> Session ())
157+
-> TestTree
158+
goldenWithHaskellAndCaps clientCaps plugin title testDataDir path desc ext act =
159+
goldenGitDiff title (testDataDir </> path <.> desc <.> ext)
160+
$ runSessionWithServerAndCaps plugin clientCaps testDataDir
161+
$ TL.encodeUtf8 . TL.fromStrict
162+
<$> do
163+
doc <- openDoc (path <.> ext) "haskell"
164+
void waitForBuildQueue
165+
act doc
166+
documentContents doc
167+
146168
goldenWithCabalDoc
147169
:: Pretty b
148170
=> PluginTestDescriptor b

hls-test-utils/src/Test/Hls/Util.hs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@
1010
{-# LANGUAGE DataKinds #-}
1111
module Test.Hls.Util
1212
( -- * Test Capabilities
13-
codeActionSupportCaps
13+
codeActionResolveCaps
14+
, codeActionNoResolveCaps
15+
, codeActionSupportCaps
1416
, expectCodeAction
1517
-- * Environment specifications
1618
-- for ignoring tests
@@ -51,7 +53,7 @@ where
5153

5254
import Control.Applicative.Combinators (skipManyTill, (<|>))
5355
import Control.Exception (catch, throwIO)
54-
import Control.Lens ((&), (?~), (^.))
56+
import Control.Lens ((&), (?~), (^.), _Just, (.~))
5557
import Control.Monad
5658
import Control.Monad.IO.Class
5759
import qualified Data.Aeson as A
@@ -92,6 +94,15 @@ codeActionSupportCaps = def & L.textDocument ?~ textDocumentCaps
9294
codeActionCaps = CodeActionClientCapabilities (Just True) (Just literalSupport) (Just True) Nothing Nothing Nothing Nothing
9395
literalSupport = #codeActionKind .== (#valueSet .== [])
9496

97+
codeActionResolveCaps :: ClientCapabilities
98+
codeActionResolveCaps = Test.fullCaps
99+
& (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport . _Just) .~ (#properties .== ["edit"])
100+
& (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ True
101+
102+
codeActionNoResolveCaps :: ClientCapabilities
103+
codeActionNoResolveCaps = Test.fullCaps
104+
& (L.textDocument . _Just . L.codeAction . _Just . L.resolveSupport) .~ Nothing
105+
& (L.textDocument . _Just . L.codeAction . _Just . L.dataSupport . _Just) .~ False
95106
-- ---------------------------------------------------------------------
96107
-- Environment specification for ignoring tests
97108
-- ---------------------------------------------------------------------

0 commit comments

Comments
 (0)