diff --git a/ghcide/src/Development/IDE/Plugin/Completions.hs b/ghcide/src/Development/IDE/Plugin/Completions.hs index 2a1841131c..4f6b8cfa97 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions.hs @@ -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 + <> mkResolveHandler SMethod_CompletionItemResolve resolveCompletion , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} , pluginPriority = ghcideCompletionsPluginPriority } @@ -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 @@ -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 diff --git a/ghcide/test/exe/Main.hs b/ghcide/test/exe/Main.hs index 1b825e9d0d..208871a933 100644 --- a/ghcide/test/exe/Main.hs +++ b/ghcide/test/exe/Main.hs @@ -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 @@ -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 diff --git a/hls-plugin-api/hls-plugin-api.cabal b/hls-plugin-api/hls-plugin-api.cabal index 64d1aa8263..4a4c370f5d 100644 --- a/hls-plugin-api/hls-plugin-api.cabal +++ b/hls-plugin-api/hls-plugin-api.cabal @@ -38,6 +38,7 @@ library Ide.Plugin.ConfigUtils Ide.Plugin.Properties Ide.Plugin.RangeMap + Ide.Plugin.Resolve Ide.PluginUtils Ide.Types diff --git a/hls-plugin-api/src/Ide/Plugin/Resolve.hs b/hls-plugin-api/src/Ide/Plugin/Resolve.hs new file mode 100644 index 0000000000..9f5ab76014 --- /dev/null +++ b/hls-plugin-api/src/Ide/Plugin/Resolve.hs @@ -0,0 +1,190 @@ +{-# 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.Maybe (catMaybes) +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 +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 with a data field and a resolve +-- provider, this function creates a handler that creates a command that uses +-- your resolve 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. see Note [Code action resolve fallback to commands] +-- Also: This helper only works with workspace edits, not commands. Any command set +-- either in the original code action or in the resolve will be ignored. +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) +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 + -- pass it to the resolve handler (which expects a whole code action) + -- It should be noted that mkLspCommand already specifies the command + -- to the plugin, so we don't need to do that here. + 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 "Applying edits for 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 ca2@CodeAction {_edit = Just wedits } | diffCodeActions ca ca2 == ["edit"] -> do + _ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) (\_ -> pure ()) + pure $ Right $ InR Null + Right ca2@CodeAction {_edit = Just _ } -> + pure $ Left $ + internalError $ + "The resolve provider unexpectedly returned a code action with the following differing fields: " + <> (T.pack $ show $ diffCodeActions ca ca2) + Right _ -> pure $ Left $ internalError "The resolve provider unexpectedly returned a result with no data field" + Left err -> pure $ Left err + executeResolveCmd _ _ CodeAction{_data_= value} = pure $ Left $ invalidParamsError ("The code action to resolve has an illegal data field: " <> (T.pack $ show value)) + + +-- TODO: Remove once provided by lsp-types +-- |Compares two CodeActions and returns a list of fields that are not equal +diffCodeActions :: CodeAction -> CodeAction -> [T.Text] +diffCodeActions ca ca2 = + let titleDiff = if ca ^. L.title == ca2 ^. L.title then Nothing else Just "title" + kindDiff = if ca ^. L.kind == ca2 ^. L.kind then Nothing else Just "kind" + diagnosticsDiff = if ca ^. L.diagnostics == ca2 ^. L.diagnostics then Nothing else Just "diagnostics" + commandDiff = if ca ^. L.command == ca2 ^. L.command then Nothing else Just "diagnostics" + isPreferredDiff = if ca ^. L.isPreferred == ca2 ^. L.isPreferred then Nothing else Just "isPreferred" + dataDiff = if ca ^. L.data_ == ca2 ^. L.data_ then Nothing else Just "data" + disabledDiff = if ca ^. L.disabled == ca2 ^. L.disabled then Nothing else Just "disabled" + editDiff = if ca ^. L.edit == ca2 ^. L.edit then Nothing else Just "edit" + in catMaybes [titleDiff, kindDiff, diagnosticsDiff, commandDiff, isPreferredDiff, dataDiff, disabledDiff, editDiff] + +-- |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 + +-- |Checks if the the client supports resolve for code action. We currently only check +-- whether resolve for the edit field is supported, because that's the only one we care +-- about at the moment. +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 + _ -> False + +internalError :: T.Text -> ResponseError +internalError msg = ResponseError (InR ErrorCodes_InternalError) ("Ide.Plugin.Resolve: Internal Error : " <> msg) Nothing + +invalidParamsError :: T.Text -> ResponseError +invalidParamsError msg = ResponseError (InR ErrorCodes_InvalidParams) ("Ide.Plugin.Resolve: : " <> msg) Nothing + +parseError :: Maybe A.Value -> T.Text -> ResponseError +parseError value errMsg = ResponseError (InR ErrorCodes_ParseError) ("Ide.Plugin.Resolve: Error parsing value:"<> (T.pack $ show value) <> " Error: "<> errMsg) Nothing + +{- Note [Code action resolve fallback to commands] + To make supporting code action resolve easy for plugins, we want to let them + provide one implementation that can be used both when clients support + resolve, and when they don't. + The way we do this is to have them always implement a resolve handler. + Then, if the client doesn't support resolve, we instead install the resolve + handler as a _command_ handler, passing the code action literal itself + as the command argument. This allows the command handler to have + the same interface as the resolve handler! + -} diff --git a/hls-plugin-api/src/Ide/Types.hs b/hls-plugin-api/src/Ide/Types.hs index 245322b224..bd35a3312d 100644 --- a/hls-plugin-api/src/Ide/Types.hs +++ b/hls-plugin-api/src/Ide/Types.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-} +{-# LANGUAGE BlockArguments #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DefaultSignatures #-} @@ -48,9 +49,8 @@ module Ide.Types , installSigUsr1Handler , responseError , lookupCommandProvider -, OwnedResolveData(..) -, mkCodeActionHandlerWithResolve -, mkCodeActionWithResolveAndCommand +, ResolveFunction +, mkResolveHandler ) where @@ -64,10 +64,7 @@ import System.Posix.Signals import Control.Applicative ((<|>)) import Control.Arrow ((&&&)) import Control.Lens (_Just, (.~), (?~), (^.), (^?)) -import Control.Monad.Trans.Class (lift) -import Control.Monad.Trans.Except (ExceptT (..), runExceptT) import Data.Aeson hiding (Null, defaultOptions) -import qualified Data.Aeson import Data.Default import Data.Dependent.Map (DMap) import qualified Data.Dependent.Map as DMap @@ -81,7 +78,6 @@ 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 @@ -93,11 +89,7 @@ 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, LspT, - ProgressCancellable (Cancellable), - getClientCapabilities, - getVirtualFile, sendRequest, - withIndefiniteProgress) +import Language.LSP.Server (LspM, LspT, getVirtualFile) import Language.LSP.VFS import Numeric.Natural import OpenTelemetry.Eventlog @@ -416,6 +408,7 @@ instance PluginMethod Request Method_TextDocumentCodeAction where uri = msgParams ^. L.textDocument . L.uri instance PluginMethod Request Method_CodeActionResolve where + -- See Note [Resolve in PluginHandlers] pluginEnabled _ msgParams pluginDesc config = pluginResolverResponsible (msgParams ^. L.data_) pluginDesc && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) @@ -455,6 +448,7 @@ instance PluginMethod Request Method_TextDocumentCodeLens where uri = msgParams ^. L.textDocument . L.uri instance PluginMethod Request Method_CodeLensResolve where + -- See Note [Resolve in PluginHandlers] pluginEnabled _ msgParams pluginDesc config = pluginResolverResponsible (msgParams ^. L.data_) pluginDesc && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) @@ -477,7 +471,9 @@ instance PluginMethod Request Method_TextDocumentDocumentSymbol where uri = msgParams ^. L.textDocument . L.uri instance PluginMethod Request Method_CompletionItemResolve where - pluginEnabled _ msgParams pluginDesc config = pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) + -- See Note [Resolve in PluginHandlers] + pluginEnabled _ msgParams pluginDesc config = pluginResolverResponsible (msgParams ^. L.data_) pluginDesc + && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) instance PluginMethod Request Method_TextDocumentCompletion where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc @@ -558,9 +554,9 @@ instance PluginRequestMethod Method_TextDocumentCodeAction where | otherwise = False instance PluginRequestMethod Method_CodeActionResolve where - -- CodeAction resolve is currently only used to changed the edit field, thus - -- that's the only field we are combining. - combineResponses _ _ _ codeAction (toList -> codeActions) = codeAction & L.edit .~ mconcat ((^. L.edit) <$> codeActions) + -- A resolve request should only have one response. + -- See Note [Resolve in PluginHandlers]. + combineResponses _ _ _ _ (x :| _) = x instance PluginRequestMethod Method_TextDocumentDefinition where combineResponses _ _ _ _ (x :| _) = x @@ -580,7 +576,8 @@ instance PluginRequestMethod Method_WorkspaceSymbol where instance PluginRequestMethod Method_TextDocumentCodeLens where instance PluginRequestMethod Method_CodeLensResolve where - -- A resolve request should only ever get one response + -- A resolve request should only ever get one response. + -- See note Note [Resolve in PluginHandlers] combineResponses _ _ _ _ (x :| _) = x instance PluginRequestMethod Method_TextDocumentRename where @@ -624,16 +621,9 @@ instance PluginRequestMethod Method_TextDocumentDocumentSymbol where in [si] <> children' instance PluginRequestMethod Method_CompletionItemResolve where - -- resolving completions can only change the detail, additionalTextEdit or documentation fields - combineResponses _ _ _ _ (x :| xs) = go x xs - where go :: CompletionItem -> [CompletionItem] -> CompletionItem - go !comp [] = comp - go !comp1 (comp2:xs) - = go (comp1 - & L.detail .~ comp1 ^. L.detail <> comp2 ^. L.detail - & L.documentation .~ ((comp1 ^. L.documentation) <|> (comp2 ^. L.documentation)) -- difficult to write generic concatentation for docs - & L.additionalTextEdits .~ comp1 ^. L.additionalTextEdits <> comp2 ^. L.additionalTextEdits) - xs + -- A resolve request should only have one response. + -- See Note [Resolve in PluginHandlers] + combineResponses _ _ _ _ (x :| _) = x instance PluginRequestMethod Method_TextDocumentCompletion where combineResponses _ conf _ _ (toList -> xs) = snd $ consumeCompletionResponse limit $ combine xs @@ -790,15 +780,42 @@ type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> LspM Config ( type PluginNotificationMethodHandler a m = a -> VFS -> PluginId -> MessageParams m -> LspM Config () --- | Make a handler for plugins with no extra data +-- | Make a handler for plugins. For how resolve works with this see +-- Note [Resolve in PluginHandlers] mkPluginHandler - :: PluginRequestMethod m + :: forall ideState m. PluginRequestMethod m => SClientMethod m -> PluginMethodHandler ideState m -> PluginHandlers ideState -mkPluginHandler m f = PluginHandlers $ DMap.singleton (IdeMethod m) (PluginHandler f') +mkPluginHandler m f = PluginHandlers $ DMap.singleton (IdeMethod m) (PluginHandler (f' m)) where - f' pid ide params = pure <$> f ide pid params + f' :: SMethod m -> PluginId -> ideState -> MessageParams m -> LspT Config IO (NonEmpty (Either ResponseError (MessageResult m))) + -- We need to have separate functions for each method that supports resolve, so far we only support CodeActions + -- CodeLens, and Completion methods. + f' SMethod_TextDocumentCodeAction pid ide params@CodeActionParams{_textDocument=TextDocumentIdentifier {_uri}} = + pure . fmap (wrapCodeActions pid _uri) <$> f ide pid params + f' SMethod_TextDocumentCodeLens pid ide params@CodeLensParams{_textDocument=TextDocumentIdentifier {_uri}} = + pure . fmap (wrapCodeLenses pid _uri) <$> f ide pid params + f' SMethod_TextDocumentCompletion pid ide params@CompletionParams{_textDocument=TextDocumentIdentifier {_uri}} = + pure . fmap (wrapCompletions pid _uri) <$> f ide pid params + + -- This is the default case for all other methods + f' _ pid ide params = pure <$> f ide pid params + + -- Todo: use fancy pancy lenses to make this a few lines + wrapCodeActions pid uri (InL ls) = + let wrapCodeActionItem pid uri (InR c) = InR $ wrapResolveData pid uri c + wrapCodeActionItem _ _ command@(InL _) = command + in InL $ wrapCodeActionItem pid uri <$> ls + wrapCodeActions _ _ (InR r) = InR r + + wrapCodeLenses pid uri (InL ls) = InL $ wrapResolveData pid uri <$> ls + wrapCodeLenses _ _ (InR r) = InR r + + wrapCompletions pid uri (InL ls) = InL $ wrapResolveData pid uri <$> ls + wrapCompletions pid uri (InR (InL cl@(CompletionList{_items}))) = + InR $ InL $ cl & L.items .~ (wrapResolveData pid uri <$> _items) + wrapCompletions _ _ (InR (InR r)) = InR $ InR r -- | Make a handler for plugins with no extra data mkPluginNotificationHandler @@ -877,6 +894,59 @@ type CommandFunction ideState a -- --------------------------------------------------------------------- +type ResolveFunction ideState a (m :: Method ClientToServer Request) = + ideState + -> PluginId + -> MessageParams m + -> Uri + -> a + -> LspM Config (Either ResponseError (MessageResult m)) + +-- | Make a handler for resolve methods. In here we take your provided ResolveFunction +-- and turn it into a PluginHandlers. See Note [Resolve in PluginHandlers] +mkResolveHandler + :: forall ideState a m. (FromJSON a, PluginRequestMethod m, L.HasData_ (MessageParams m) (Maybe Value)) + => SClientMethod m + -> (ideState + ->PluginId + -> MessageParams m + -> Uri + -> a + -> LspM Config (Either ResponseError (MessageResult m))) + -> PluginHandlers ideState +mkResolveHandler m f = mkPluginHandler m $ \ideState plId params -> do + case fromJSON <$> (params ^. L.data_) of + (Just (Success (PluginResolveData owner uri value) )) -> do + if owner == plId + then + case fromJSON value of + Success decodedValue -> + let newParams = params & L.data_ ?~ value + in f ideState plId newParams uri decodedValue + Error err -> + pure $ Left $ ResponseError (InR ErrorCodes_ParseError) (parseError value err) Nothing + else pure $ Left $ ResponseError (InR ErrorCodes_InternalError) invalidRequest Nothing + (Just (Error err)) -> pure $ Left $ ResponseError (InR ErrorCodes_ParseError) (parseError (params ^. L.data_) err) Nothing + _ -> pure $ Left $ ResponseError (InR ErrorCodes_InternalError) invalidRequest Nothing + where invalidRequest = "The resolve request incorrectly got routed to the wrong resolve handler!" + parseError value err = "Unable to decode: " <> (T.pack $ show value) <> ". Error: " <> (T.pack $ show err) + +wrapResolveData :: L.HasData_ a (Maybe Value) => PluginId -> Uri -> a -> a +wrapResolveData pid uri hasData = + hasData & L.data_ .~ (toJSON . PluginResolveData pid uri <$> data_) + where data_ = hasData ^? L.data_ . _Just + +-- |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 PluginResolveData = PluginResolveData { + resolvePlugin :: PluginId +, resolveURI :: Uri +, resolveValue :: Value +} + deriving (Generic, Show) + deriving anyclass (ToJSON, FromJSON) + newtype PluginId = PluginId T.Text deriving (Show, Read, Eq, Ord) deriving newtype (ToJSON, FromJSON, Hashable) @@ -979,11 +1049,16 @@ instance HasTracing WorkspaceSymbolParams where traceWithSpan sp (WorkspaceSymbolParams _ _ query) = setTag sp "query" (encodeUtf8 query) instance HasTracing CallHierarchyIncomingCallsParams instance HasTracing CallHierarchyOutgoingCallsParams -instance HasTracing CompletionItem + +-- Instances for resolve types instance HasTracing CodeAction instance HasTracing CodeLens +instance HasTracing CompletionItem +instance HasTracing DocumentLink +instance HasTracing InlayHint +instance HasTracing WorkspaceSymbol -- --------------------------------------------------------------------- - +--Experimental resolve refactoring {-# NOINLINE pROCESS_ID #-} pROCESS_ID :: T.Text pROCESS_ID = unsafePerformIO getPid @@ -1016,124 +1091,39 @@ 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 - :: forall ideState. (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 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 (wrapCodeActionResolveData pid <$> ls) - --This is the actual part where we call resolveCodeAction which fills in the edit data for the client - | otherwise -> InL <$> traverse (resolveCodeAction ideState pid) ls - newCodeResolveMethod ideState pid params = - codeResolveMethod ideState pid (unwrapCodeActionResolveData params) - in mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod - <> mkPluginHandler SMethod_CodeActionResolve newCodeResolveMethod - where - dropData :: CodeAction -> CodeAction - dropData ca = ca & L.data_ .~ Nothing - resolveCodeAction :: ideState -> PluginId -> (Command |? CodeAction) -> ExceptT ResponseError (LspT Config IO) (Command |? CodeAction) - resolveCodeAction _ideState _pid c@(InL _) = pure c - resolveCodeAction ideState pid (InR codeAction) = - fmap (InR . dropData) $ ExceptT $ codeResolveMethod ideState pid codeAction - --- |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. - PluginId - -> (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null))) - -> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction)) - -> ([PluginCommand ideState], PluginHandlers 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 (wrapCodeActionResolveData pid <$> 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 <$> ls - newCodeResolveMethod ideState pid params = - codeResolveMethod ideState pid (unwrapCodeActionResolveData params) - in ([PluginCommand "codeActionResolve" "Executes resolve for code action" (executeResolveCmd plId codeResolveMethod)], - mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod - <> mkPluginHandler SMethod_CodeActionResolve newCodeResolveMethod) - where moveDataToCommand :: Command |? CodeAction -> Command |? CodeAction - moveDataToCommand ca = - let dat = toJSON <$> 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 - executeResolveCmd :: PluginId -> PluginMethodHandler ideState Method_CodeActionResolve -> CommandFunction ideState CodeAction - executeResolveCmd pluginId resolveProvider ideState ca = do - withIndefiniteProgress "Executing code action..." Cancellable $ do - resolveResult <- resolveProvider ideState pluginId ca - case resolveResult of - Right CodeAction {_edit = Just wedits } -> do - _ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) (\_ -> pure ()) - pure $ Right $ InR Null - Right _ -> pure $ Left $ responseError "No edit in CodeAction" - Left err -> pure $ Left err - -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 - _ -> False - --- We don't wrap commands -wrapCodeActionResolveData :: PluginId -> (a |? CodeAction) -> a |? CodeAction -wrapCodeActionResolveData _pid c@(InL _) = c -wrapCodeActionResolveData pid (InR c@(CodeAction{_data_=Just x})) = - InR $ c & L.data_ ?~ toJSON (ORD pid x) --- Neither do we wrap code actions's without data fields, -wrapCodeActionResolveData _pid c@(InR (CodeAction{_data_=Nothing})) = c - -unwrapCodeActionResolveData :: CodeAction -> CodeAction -unwrapCodeActionResolveData c@CodeAction{_data_ = Just x} - | Success ORD {value = v} <- fromJSON x = c & L.data_ ?~ v --- If we can't successfully decode the value as a ORD type than --- we just return the codeAction untouched. -unwrapCodeActionResolveData c = c - --- |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 - +-- |Determine whether this request should be routed to the plugin. Fails closed +-- if we can't determine which plugin it should be routed to. 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 +pluginResolverResponsible (Just (fromJSON -> (Success (PluginResolveData o _ _)))) pluginDesc = + pluginId pluginDesc == o +-- We want to fail closed +pluginResolverResponsible _ _ = False + +{- Note [Resolve in PluginHandlers] + Resolve methods have a few guarantees that need to be made by HLS, + specifically they need to only be called once, as neither their errors nor + their responses can be easily combined. Whereas commands, which similarly have + the same requirements have their own codepaths for execution, for resolve + methods we are relying on the standard PluginHandlers codepath. + That isn't a problem, but it does mean we need to do some things extra for + these methods. + - First of all, whenever a handler that can be resolved sets the data_ field + in their response, we need to intercept it, and wrap it in a data type + PluginResolveData that allows us to route the future resolve request to the + specific plugin which is responsible for it. (We also throw in the URI for + convenience, because everyone needs that). We do that in mkPluginHandler. + - When we get any resolve requests we check their data field for our + PluginResolveData that will allow us to route the request to the right + plugin. If we can't find out which plugin to route the request to, then we + just don't route it at all. This is done in pluginEnabled, and + pluginResolverResponsible. + - Finally we have mkResolveHandler, which takes the resolve request and + unwraps the plugins data from our PluginResolveData, parses it and passes it + it on to the registered handler. + It should be noted that there are some restrictions with this approach: First, + if a plugin does not set the data_ field, than the request will not be able + to be resolved. This is because we only wrap data_ fields that have been set + with our PluginResolvableData tag. Second, if a plugin were to register two + resolve handlers for the same method, than our assumptions that we never have + two responses break, and behavior is undefined. + -} diff --git a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs index c99ff2ee1d..5a3e47bf5e 100644 --- a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs +++ b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs @@ -6,7 +6,6 @@ {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE TypeApplications #-} {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE ViewPatterns #-} @@ -25,9 +24,7 @@ import Control.Monad.IO.Class import Control.Monad.Trans.Class (lift) import Control.Monad.Trans.Except (ExceptT) import Control.Monad.Trans.Maybe -import qualified Data.Aeson as A (Result (..), - ToJSON (toJSON), - fromJSON) +import qualified Data.Aeson as A (ToJSON (toJSON)) import Data.Aeson.Types (FromJSON) import qualified Data.IntMap as IM (IntMap, elems, fromList, (!?)) @@ -47,6 +44,7 @@ import Development.IDE.Graph.Classes import GHC.Generics (Generic) import Ide.Plugin.RangeMap (filterByRange) import qualified Ide.Plugin.RangeMap as RM (RangeMap, fromList) +import Ide.Plugin.Resolve import Ide.PluginUtils (getNormalizedFilePath, handleMaybe, handleMaybeM, @@ -92,7 +90,7 @@ descriptorForModules recorder modFilter plId = pluginHandlers = -- This plugin provides code lenses mkPluginHandler SMethod_TextDocumentCodeLens (lensProvider recorder) - <> mkPluginHandler SMethod_CodeLensResolve (lensResolveProvider recorder) + <> mkResolveHandler SMethod_CodeLensResolve (lensResolveProvider recorder) -- This plugin provides code actions <> mkCodeActionHandlerWithResolve (codeActionProvider recorder) (codeActionResolveProvider recorder) @@ -139,8 +137,8 @@ lensProvider _ state _ CodeLensParams {_textDocument = TextDocumentIdentifier { , _range = range , _command = Nothing } -lensResolveProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_CodeLensResolve -lensResolveProvider _ ideState plId cl@(CodeLens {_data_ = Just data_@(A.fromJSON -> A.Success (ResolveOne uri uid))}) +lensResolveProvider :: Recorder (WithPriority Log) -> ResolveFunction IdeState EIResolveData 'Method_CodeLensResolve +lensResolveProvider _ ideState plId cl uri rd@(ResolveOne _ uid) = pluginResponse $ do nfp <- getNormalizedFilePath uri (MinimalImportsResult{forResolve}) <- @@ -153,14 +151,9 @@ lensResolveProvider _ ideState plId cl@(CodeLens {_data_ = Just data_@(A.fromJSO where mkCommand :: PluginId -> TextEdit -> Command mkCommand pId TextEdit{_newText} = let title = abbreviateImportTitle _newText - _arguments = Just [data_] - in mkLspCommand pId importCommandId title _arguments -lensResolveProvider _ _ _ (CodeLens {_data_ = Just (A.fromJSON -> A.Success (ResolveAll _))}) = do + in mkLspCommand pId importCommandId title (Just $ [A.toJSON rd]) +lensResolveProvider _ _ _ _ _ (ResolveAll _) = do pure $ Left $ ResponseError (InR ErrorCodes_InvalidParams) "Unexpected argument for lens resolve handler: ResolveAll" Nothing -lensResolveProvider _ _ _ (CodeLens {_data_ = Just (A.fromJSON @EIResolveData -> (A.Error (T.pack -> str)))}) = - pure $ Left $ ResponseError (InR ErrorCodes_ParseError) str Nothing -lensResolveProvider _ _ _ (CodeLens {_data_ = v}) = do - pure $ Left $ ResponseError (InR ErrorCodes_InvalidParams) ("Unexpected argument for lens resolve handler: " <> (T.pack $ show v)) Nothing -- | If there are any implicit imports, provide both one code action per import -- to make that specific import explicit, and one code action to turn them all @@ -191,15 +184,11 @@ codeActionProvider _ ideState _pId (CodeActionParams _ _ TextDocumentIdentifier , _disabled = Nothing , _data_ = data_} -codeActionResolveProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState 'Method_CodeActionResolve -codeActionResolveProvider _ ideState _ ca@(CodeAction{_data_= Just (A.fromJSON -> A.Success rd)}) = +codeActionResolveProvider :: Recorder (WithPriority Log) -> ResolveFunction IdeState EIResolveData 'Method_CodeActionResolve +codeActionResolveProvider _ ideState _ ca _ rd = pluginResponse $ do wedit <- resolveWTextEdit ideState rd pure $ ca & L.edit ?~ wedit -codeActionResolveProvider _ _ _ (CodeAction{_data_= Just (A.fromJSON @EIResolveData -> A.Error (T.pack -> str))}) = - pure $ Left $ ResponseError (InR ErrorCodes_ParseError) str Nothing -codeActionResolveProvider _ _ _ (CodeAction {_data_ = v}) = do - pure $ Left $ ResponseError (InR ErrorCodes_InvalidParams) ("Unexpected argument for code action resolve handler: " <> (T.pack $ show v)) Nothing -------------------------------------------------------------------------------- resolveWTextEdit :: IdeState -> EIResolveData -> ExceptT String (LspT Config IO) WorkspaceEdit diff --git a/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs b/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs index 81d991db2b..62f2e08094 100644 --- a/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs +++ b/plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs @@ -117,6 +117,7 @@ import qualified Refact.Fixity as Refact import Ide.Plugin.Config hiding (Config) import Ide.Plugin.Properties +import Ide.Plugin.Resolve import Ide.PluginUtils import Ide.Types hiding (Config) @@ -143,8 +144,6 @@ import GHC.Generics (Generic) import System.Environment (setEnv, unsetEnv) #endif -import Data.Aeson (Result (Error, Success), - fromJSON) import Text.Regex.TDFA.Text () -- --------------------------------------------------------------------- @@ -434,22 +433,16 @@ codeActionProvider ideState _pluginId (CodeActionParams _ _ documentId _ context diags = context ^. LSP.diagnostics -resolveProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_CodeActionResolve -resolveProvider recorder ideState _ - ca@CodeAction {_data_ = Just (fromJSON -> (Success (ApplyHint verTxtDocId oneHint)))} = pluginResponse $ do - file <- getNormalizedFilePath (verTxtDocId ^. LSP.uri) +resolveProvider :: Recorder (WithPriority Log) -> ResolveFunction IdeState HlintResolveCommands Method_CodeActionResolve +resolveProvider recorder ideState _plId ca uri resolveValue = pluginResponse $ do + file <- getNormalizedFilePath uri + case resolveValue of + (ApplyHint verTxtDocId oneHint) -> do edit <- ExceptT $ liftIO $ applyHint recorder ideState file oneHint verTxtDocId pure $ ca & LSP.edit ?~ edit -resolveProvider recorder ideState _ - ca@CodeAction {_data_ = Just (fromJSON -> (Success (IgnoreHint verTxtDocId hintTitle)))} = pluginResponse $ do - file <- getNormalizedFilePath (verTxtDocId ^. LSP.uri) + (IgnoreHint verTxtDocId hintTitle ) -> do edit <- ExceptT $ liftIO $ ignoreHint recorder ideState file verTxtDocId hintTitle pure $ ca & LSP.edit ?~ edit -resolveProvider _ _ _ - CodeAction {_data_ = Just (fromJSON @HlintResolveCommands -> (Error (T.pack -> str)))} = - pure $ Left $ ResponseError (InR ErrorCodes_ParseError) str Nothing -resolveProvider _ _ _ CodeAction {_data_ = _} = - pure $ Left $ ResponseError (InR ErrorCodes_InvalidParams) "Unexpected argument for code action resolve handler: (Probably Nothing)" Nothing -- | Convert a hlint diagnostic into an apply and an ignore code action -- if applicable diff --git a/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs b/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs index 7a743bcdd5..174358e79e 100644 --- a/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs +++ b/plugins/hls-overloaded-record-dot-plugin/src/Ide/Plugin/OverloadedRecordDot.hs @@ -76,14 +76,15 @@ import Development.IDE.Types.Logger (Priority (..), import GHC.Generics (Generic) import Ide.Plugin.RangeMap (RangeMap) import qualified Ide.Plugin.RangeMap as RangeMap +import Ide.Plugin.Resolve (mkCodeActionHandlerWithResolve) import Ide.PluginUtils (getNormalizedFilePath, handleMaybeM, pluginResponse) import Ide.Types (PluginDescriptor (..), PluginId (..), PluginMethodHandler, + ResolveFunction, defaultPluginDescriptor, - mkCodeActionHandlerWithResolve, mkPluginHandler) import Language.LSP.Protocol.Lens (HasChanges (changes)) import qualified Language.LSP.Protocol.Lens as L @@ -167,28 +168,25 @@ instance FromJSON ORDResolveData descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder plId = (defaultPluginDescriptor plId) - { pluginHandlers = - mkCodeActionHandlerWithResolve codeActionProvider resolveProvider +descriptor recorder plId = let pluginHandler = mkCodeActionHandlerWithResolve codeActionProvider resolveProvider + in (defaultPluginDescriptor plId) + { pluginHandlers = pluginHandler , pluginRules = collectRecSelsRule recorder } -resolveProvider :: PluginMethodHandler IdeState 'Method_CodeActionResolve -resolveProvider ideState pId ca@(CodeAction _ _ _ _ _ _ _ (Just resData)) = - pluginResponse $ do - case fromJSON resData of - Success (ORDRD uri int) -> do - nfp <- getNormalizedFilePath uri - CRSR _ crsDetails exts <- collectRecSelResult ideState nfp - pragma <- getFirstPragma pId ideState nfp - case IntMap.lookup int crsDetails of - Just rse -> pure $ ca {_edit = mkWorkspaceEdit uri rse exts pragma} - -- We need to throw a content modified error here, see - -- https://github.com/microsoft/language-server-protocol/issues/1738 - -- but we need fendor's plugin error response pr to make it - -- convenient to use here, so we will wait to do that till that's merged - _ -> throwE "Content Modified Error" - _ -> throwE "Unable to deserialize the data" +resolveProvider :: ResolveFunction IdeState ORDResolveData 'Method_CodeActionResolve +resolveProvider ideState plId ca uri (ORDRD _ int) = + pluginResponse $ do + nfp <- getNormalizedFilePath uri + CRSR _ crsDetails exts <- collectRecSelResult ideState nfp + pragma <- getFirstPragma plId ideState nfp + case IntMap.lookup int crsDetails of + Just rse -> pure $ ca {_edit = mkWorkspaceEdit uri rse exts pragma} + -- We need to throw a content modified error here, see + -- https://github.com/microsoft/language-server-protocol/issues/1738 + -- but we need fendor's plugin error response pr to make it + -- convenient to use here, so we will wait to do that till that's merged + _ -> throwE "Content Modified Error" codeActionProvider :: PluginMethodHandler IdeState 'Method_TextDocumentCodeAction codeActionProvider ideState pId (CodeActionParams _ _ caDocId caRange _) = diff --git a/test/functional/Completion.hs b/test/functional/Completion.hs index 08280d4c4f..0511e75fcc 100644 --- a/test/functional/Completion.hs +++ b/test/functional/Completion.hs @@ -7,6 +7,7 @@ import Control.Lens hiding ((.=)) import Control.Monad import Data.Aeson (object, (.=)) import Data.Foldable (find) +import Data.Maybe (isJust) import Data.Row.Records (focus) import qualified Data.Text as T import Ide.Plugin.Config (maxCompletions) @@ -18,10 +19,13 @@ getResolvedCompletions :: TextDocumentIdentifier -> Position -> Session [Complet getResolvedCompletions doc pos = do xs <- getCompletions doc pos forM xs $ \item -> do - rsp <- request SMethod_CompletionItemResolve item - case rsp ^. result of - Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err) - Right x -> pure x + if isJust (item ^. data_) + then do + rsp <- request SMethod_CompletionItemResolve item + case rsp ^. result of + Left err -> liftIO $ assertFailure ("completionItem/resolve failed with: " <> show err) + Right x -> pure x + else pure item tests :: TestTree tests = testGroup "completions" [