Skip to content

Commit d14d9e5

Browse files
authored
Support for resolve in hls-hlint-plugin (#3679)
1 parent 6f775e9 commit d14d9e5

File tree

3 files changed

+145
-105
lines changed

3 files changed

+145
-105
lines changed

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

+35-14
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import Control.Lens (_Just, (.~), (?~), (^.), (^?))
6767
import Control.Monad.Trans.Class (lift)
6868
import Control.Monad.Trans.Except (ExceptT (..), runExceptT)
6969
import Data.Aeson hiding (Null, defaultOptions)
70+
import qualified Data.Aeson
7071
import Data.Default
7172
import Data.Dependent.Map (DMap)
7273
import qualified Data.Dependent.Map as DMap
@@ -93,8 +94,10 @@ import qualified Language.LSP.Protocol.Lens as L
9394
import Language.LSP.Protocol.Message
9495
import Language.LSP.Protocol.Types
9596
import Language.LSP.Server (LspM, LspT,
97+
ProgressCancellable (Cancellable),
9698
getClientCapabilities,
97-
getVirtualFile)
99+
getVirtualFile, sendRequest,
100+
withIndefiniteProgress)
98101
import Language.LSP.VFS
99102
import Numeric.Natural
100103
import OpenTelemetry.Eventlog
@@ -1051,30 +1054,48 @@ mkCodeActionHandlerWithResolve codeActionMethod codeResolveMethod =
10511054
-- support. This means you don't have to check whether the client supports resolve
10521055
-- and act accordingly in your own providers.
10531056
mkCodeActionWithResolveAndCommand
1054-
:: forall ideState. (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
1057+
:: forall ideState.
1058+
PluginId
1059+
-> (ideState -> PluginId -> CodeActionParams -> LspM Config (Either ResponseError ([Command |? CodeAction] |? Null)))
10551060
-> (ideState -> PluginId -> CodeAction -> LspM Config (Either ResponseError CodeAction))
1056-
-> PluginHandlers ideState
1057-
mkCodeActionWithResolveAndCommand codeActionMethod codeResolveMethod =
1061+
-> ([PluginCommand ideState], PluginHandlers ideState)
1062+
mkCodeActionWithResolveAndCommand plId codeActionMethod codeResolveMethod =
10581063
let newCodeActionMethod ideState pid params = runExceptT $
10591064
do codeActionReturn <- ExceptT $ codeActionMethod ideState pid params
10601065
caps <- lift getClientCapabilities
10611066
case codeActionReturn of
10621067
r@(InR Null) -> pure r
10631068
(InL ls) | -- If the client supports resolve, we will wrap the resolve data in a owned
10641069
-- resolve data type to allow the server to know who to send the resolve request to
1065-
-- and dump the command fields.
10661070
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
1071+
pure $ InL (wrapCodeActionResolveData pid <$> ls)
1072+
-- If they do not we will drop the data field, in addition we will populate the command
1073+
-- field with our command to execute the resolve, with the whole code action as it's argument.
1074+
| otherwise -> pure $ InL $ moveDataToCommand <$> ls
10701075
newCodeResolveMethod ideState pid params =
10711076
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
1077+
in ([PluginCommand "codeActionResolve" "Executes resolve for code action" (executeResolveCmd plId codeResolveMethod)],
1078+
mkPluginHandler SMethod_TextDocumentCodeAction newCodeActionMethod
1079+
<> mkPluginHandler SMethod_CodeActionResolve newCodeResolveMethod)
1080+
where moveDataToCommand :: Command |? CodeAction -> Command |? CodeAction
1081+
moveDataToCommand ca =
1082+
let dat = toJSON <$> ca ^? _R -- We need to take the whole codeAction
1083+
-- And put it in the argument for the Command, that way we can later
1084+
-- pas it to the resolve handler (which expects a whole code action)
1085+
cmd = mkLspCommand plId (CommandId "codeActionResolve") "Execute Code Action" (pure <$> dat)
1086+
in ca
1087+
& _R . L.data_ .~ Nothing -- Set the data field to nothing
1088+
& _R . L.command ?~ cmd -- And set the command to our previously created command
1089+
executeResolveCmd :: PluginId -> PluginMethodHandler ideState Method_CodeActionResolve -> CommandFunction ideState CodeAction
1090+
executeResolveCmd pluginId resolveProvider ideState ca = do
1091+
withIndefiniteProgress "Executing code action..." Cancellable $ do
1092+
resolveResult <- resolveProvider ideState pluginId ca
1093+
case resolveResult of
1094+
Right CodeAction {_edit = Just wedits } -> do
1095+
_ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) (\_ -> pure ())
1096+
pure $ Right Data.Aeson.Null
1097+
Right _ -> pure $ Left $ responseError "No edit in CodeAction"
1098+
Left err -> pure $ Left err
10781099

10791100
supportsCodeActionResolve :: ClientCapabilities -> Bool
10801101
supportsCodeActionResolve caps =

plugins/hls-hlint-plugin/src/Ide/Plugin/Hlint.hs

+67-89
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ import Control.Arrow ((&&&))
4040
import Control.Concurrent.STM
4141
import Control.DeepSeq
4242
import Control.Exception
43-
import Control.Lens ((^.))
43+
import Control.Lens ((?~), (^.))
4444
import Control.Monad
4545
import Control.Monad.IO.Class
4646
import Control.Monad.Trans.Except
@@ -127,10 +127,7 @@ import Language.LSP.Protocol.Message
127127
import Language.LSP.Protocol.Types hiding
128128
(Null)
129129
import qualified Language.LSP.Protocol.Types as LSP
130-
import Language.LSP.Server (ProgressCancellable (Cancellable),
131-
getVersionedTextDoc,
132-
sendRequest,
133-
withIndefiniteProgress)
130+
import Language.LSP.Server (getVersionedTextDoc)
134131

135132
import qualified Development.IDE.Core.Shake as Shake
136133
import Development.IDE.Spans.Pragmas (LineSplitTextEdits (LineSplitTextEdits),
@@ -146,6 +143,8 @@ import GHC.Generics (Generic)
146143
import System.Environment (setEnv,
147144
unsetEnv)
148145
#endif
146+
import Data.Aeson (Result (Error, Success),
147+
fromJSON)
149148
import Text.Regex.TDFA.Text ()
150149
-- ---------------------------------------------------------------------
151150

@@ -188,13 +187,12 @@ fromStrictMaybe Strict.Nothing = Nothing
188187
#endif
189188

190189
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
191-
descriptor recorder plId = (defaultPluginDescriptor plId)
190+
descriptor recorder plId =
191+
let (pluginCommands, pluginHandlers) = mkCodeActionWithResolveAndCommand plId codeActionProvider (resolveProvider recorder)
192+
in (defaultPluginDescriptor plId)
192193
{ pluginRules = rules recorder plId
193-
, pluginCommands =
194-
[ PluginCommand "applyOne" "Apply a single hint" (applyOneCmd recorder)
195-
, PluginCommand "applyAll" "Apply all hints to the file" (applyAllCmd recorder)
196-
]
197-
, pluginHandlers = mkPluginHandler SMethod_TextDocumentCodeAction codeActionProvider
194+
, pluginCommands = pluginCommands
195+
, pluginHandlers = pluginHandlers
198196
, pluginConfigDescriptor = defaultConfigDescriptor
199197
{ configHasDiagnostics = True
200198
, configCustomConfig = mkCustomConfig properties
@@ -396,21 +394,9 @@ getHlintConfig pId =
396394
Config
397395
<$> usePropertyAction #flags pId properties
398396

399-
runHlintAction
400-
:: (Eq k, Hashable k, Show k, Show (RuleResult k), Typeable k, Typeable (RuleResult k), NFData k, NFData (RuleResult k))
401-
=> IdeState
402-
-> NormalizedFilePath -> String -> k -> IO (Maybe (RuleResult k))
403-
runHlintAction ideState normalizedFilePath desc rule = runAction desc ideState $ use rule normalizedFilePath
404-
405-
runGetFileContentsAction :: IdeState -> NormalizedFilePath -> IO (Maybe (FileVersion, Maybe T.Text))
406-
runGetFileContentsAction ideState normalizedFilePath = runHlintAction ideState normalizedFilePath "Hlint.GetFileContents" GetFileContents
407-
408-
runGetModSummaryAction :: IdeState -> NormalizedFilePath -> IO (Maybe ModSummaryResult)
409-
runGetModSummaryAction ideState normalizedFilePath = runHlintAction ideState normalizedFilePath "Hlint.GetModSummary" GetModSummary
410-
411397
-- ---------------------------------------------------------------------
412398
codeActionProvider :: PluginMethodHandler IdeState Method_TextDocumentCodeAction
413-
codeActionProvider ideState pluginId (CodeActionParams _ _ documentId _ context)
399+
codeActionProvider ideState _pluginId (CodeActionParams _ _ documentId _ context)
414400
| let TextDocumentIdentifier uri = documentId
415401
, Just docNormalizedFilePath <- uriToNormalizedFilePath (toNormalizedUri uri)
416402
= do
@@ -427,16 +413,7 @@ codeActionProvider ideState pluginId (CodeActionParams _ _ documentId _ context)
427413
[diagnostic | diagnostic <- diags
428414
, validCommand diagnostic
429415
]
430-
file <- runGetFileContentsAction ideState docNormalizedFilePath
431-
singleHintCodeActions <-
432-
if | Just (_, source) <- file -> do
433-
modSummaryResult <- runGetModSummaryAction ideState docNormalizedFilePath
434-
pure if | Just modSummaryResult <- modSummaryResult
435-
, Just source <- source
436-
, let dynFlags = ms_hspp_opts $ msrModSummary modSummaryResult ->
437-
diags >>= diagnosticToCodeActions dynFlags source pluginId verTxtDocId
438-
| otherwise -> []
439-
| otherwise -> pure []
416+
let singleHintCodeActions = diags >>= diagnosticToCodeActions verTxtDocId
440417
if numHintsInDoc > 1 && numHintsInContext > 0 then do
441418
pure $ singleHintCodeActions ++ [applyAllAction verTxtDocId]
442419
else
@@ -446,9 +423,8 @@ codeActionProvider ideState pluginId (CodeActionParams _ _ documentId _ context)
446423

447424
where
448425
applyAllAction verTxtDocId =
449-
let args = Just [toJSON verTxtDocId]
450-
cmd = mkLspCommand pluginId "applyAll" "Apply all hints" args
451-
in LSP.CodeAction "Apply all hints" (Just LSP.CodeActionKind_QuickFix) Nothing Nothing Nothing Nothing (Just cmd) Nothing
426+
let args = Just $ toJSON (AA verTxtDocId)
427+
in LSP.CodeAction "Apply all hints" (Just LSP.CodeActionKind_QuickFix) Nothing Nothing Nothing Nothing Nothing args
452428

453429
-- |Some hints do not have an associated refactoring
454430
validCommand (LSP.Diagnostic _ _ (Just (InR code)) _ (Just "hlint") _ _ _ _) =
@@ -458,44 +434,57 @@ codeActionProvider ideState pluginId (CodeActionParams _ _ documentId _ context)
458434

459435
diags = context ^. LSP.diagnostics
460436

437+
resolveProvider :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_CodeActionResolve
438+
resolveProvider recorder ideState _pluginId ca@CodeAction {_data_ = Just data_} = pluginResponse $ do
439+
case fromJSON data_ of
440+
(Success (AA verTxtDocId@(VersionedTextDocumentIdentifier uri _))) -> do
441+
file <- getNormalizedFilePath uri
442+
edit <- ExceptT $ liftIO $ applyHint recorder ideState file Nothing verTxtDocId
443+
pure $ ca & LSP.edit ?~ edit
444+
(Success (AO verTxtDocId@(VersionedTextDocumentIdentifier uri _) pos hintTitle)) -> do
445+
let oneHint = OneHint pos hintTitle
446+
file <- getNormalizedFilePath uri
447+
edit <- ExceptT $ liftIO $ applyHint recorder ideState file (Just oneHint) verTxtDocId
448+
pure $ ca & LSP.edit ?~ edit
449+
(Success (IH verTxtDocId@(VersionedTextDocumentIdentifier uri _) hintTitle )) -> do
450+
file <- getNormalizedFilePath uri
451+
edit <- ExceptT $ liftIO $ ignoreHint recorder ideState file verTxtDocId hintTitle
452+
pure $ ca & LSP.edit ?~ edit
453+
Error s-> throwE ("JSON decoding error: " <> s)
454+
resolveProvider _ _ _ _ = pluginResponse $ throwE "CodeAction with no data field"
455+
461456
-- | Convert a hlint diagnostic into an apply and an ignore code action
462457
-- if applicable
463-
diagnosticToCodeActions :: DynFlags -> T.Text -> PluginId -> VersionedTextDocumentIdentifier -> LSP.Diagnostic -> [LSP.CodeAction]
464-
diagnosticToCodeActions dynFlags fileContents pluginId verTxtDocId diagnostic
458+
diagnosticToCodeActions :: VersionedTextDocumentIdentifier -> LSP.Diagnostic -> [LSP.CodeAction]
459+
diagnosticToCodeActions verTxtDocId diagnostic
465460
| LSP.Diagnostic{ _source = Just "hlint", _code = Just (InR code), _range = LSP.Range start _ } <- diagnostic
466461
, let isHintApplicable = "refact:" `T.isPrefixOf` code
467462
, let hint = T.replace "refact:" "" code
468463
, let suppressHintTitle = "Ignore hint \"" <> hint <> "\" in this module"
469-
, let suppressHintTextEdits = mkSuppressHintTextEdits dynFlags fileContents hint
470-
, let suppressHintWorkspaceEdit =
471-
LSP.WorkspaceEdit
472-
(Just (M.singleton (verTxtDocId ^. LSP.uri) suppressHintTextEdits))
473-
Nothing
474-
Nothing
464+
, let suppressHintArguments = IH verTxtDocId hint
475465
= catMaybes
476466
-- Applying the hint is marked preferred because it addresses the underlying error.
477467
-- Disabling the rule isn't, because less often used and configuration can be adapted.
478468
[ if | isHintApplicable
479469
, let applyHintTitle = "Apply hint \"" <> hint <> "\""
480-
applyHintArguments = [toJSON (AOP verTxtDocId start hint)]
481-
applyHintCommand = mkLspCommand pluginId "applyOne" applyHintTitle (Just applyHintArguments) ->
482-
Just (mkCodeAction applyHintTitle diagnostic Nothing (Just applyHintCommand) True)
470+
applyHintArguments = AO verTxtDocId start hint ->
471+
Just (mkCodeAction applyHintTitle diagnostic (Just (toJSON applyHintArguments)) True)
483472
| otherwise -> Nothing
484-
, Just (mkCodeAction suppressHintTitle diagnostic (Just suppressHintWorkspaceEdit) Nothing False)
473+
, Just (mkCodeAction suppressHintTitle diagnostic (Just (toJSON suppressHintArguments)) False)
485474
]
486475
| otherwise = []
487476

488-
mkCodeAction :: T.Text -> LSP.Diagnostic -> Maybe LSP.WorkspaceEdit -> Maybe LSP.Command -> Bool -> LSP.CodeAction
489-
mkCodeAction title diagnostic workspaceEdit command isPreferred =
477+
mkCodeAction :: T.Text -> LSP.Diagnostic -> Maybe Value -> Bool -> LSP.CodeAction
478+
mkCodeAction title diagnostic data_ isPreferred =
490479
LSP.CodeAction
491480
{ _title = title
492481
, _kind = Just LSP.CodeActionKind_QuickFix
493482
, _diagnostics = Just [diagnostic]
494483
, _isPreferred = Just isPreferred
495484
, _disabled = Nothing
496-
, _edit = workspaceEdit
497-
, _command = command
498-
, _data_ = Nothing
485+
, _edit = Nothing
486+
, _command = Nothing
487+
, _data_ = data_
499488
}
500489

501490
mkSuppressHintTextEdits :: DynFlags -> T.Text -> T.Text -> [LSP.TextEdit]
@@ -519,28 +508,32 @@ mkSuppressHintTextEdits dynFlags fileContents hint =
519508
combinedTextEdit : lineSplitTextEditList
520509
-- ---------------------------------------------------------------------
521510

522-
applyAllCmd :: Recorder (WithPriority Log) -> CommandFunction IdeState VersionedTextDocumentIdentifier
523-
applyAllCmd recorder ide verTxtDocId = do
524-
let file = maybe (error $ show (verTxtDocId ^. LSP.uri) ++ " is not a file.")
525-
toNormalizedFilePath'
526-
(uriToFilePath' (verTxtDocId ^. LSP.uri))
527-
withIndefiniteProgress "Applying all hints" Cancellable $ do
528-
res <- liftIO $ applyHint recorder ide file Nothing verTxtDocId
529-
logWith recorder Debug $ LogApplying file res
530-
case res of
531-
Left err -> pure $ Left (responseError (T.pack $ "hlint:applyAll: " ++ show err))
532-
Right fs -> do
533-
_ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing fs) (\_ -> pure ())
534-
pure $ Right Null
511+
ignoreHint :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> VersionedTextDocumentIdentifier -> HintTitle -> IO (Either String WorkspaceEdit)
512+
ignoreHint _recorder ideState nfp verTxtDocId ignoreHintTitle = do
513+
(_, fileContents) <- runAction "Hlint.GetFileContents" ideState $ getFileContents nfp
514+
(msr, _) <- runAction "Hlint.GetModSummaryWithoutTimestamps" ideState $ useWithStale_ GetModSummaryWithoutTimestamps nfp
515+
case fileContents of
516+
Just contents -> do
517+
let dynFlags = ms_hspp_opts $ msrModSummary msr
518+
textEdits = mkSuppressHintTextEdits dynFlags contents ignoreHintTitle
519+
workspaceEdit =
520+
LSP.WorkspaceEdit
521+
(Just (M.singleton (verTxtDocId ^. LSP.uri) textEdits))
522+
Nothing
523+
Nothing
524+
pure $ Right workspaceEdit
525+
Nothing -> pure $ Left "Unable to get fileContents"
535526

536527
-- ---------------------------------------------------------------------
537-
538-
data ApplyOneParams = AOP
539-
{ verTxtDocId :: VersionedTextDocumentIdentifier
540-
, start_pos :: Position
541-
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them.
542-
, hintTitle :: HintTitle
543-
} deriving (Eq,Show,Generic,FromJSON,ToJSON)
528+
data HlintResolveCommands = AA { verTxtDocId :: VersionedTextDocumentIdentifier}
529+
| AO { verTxtDocId :: VersionedTextDocumentIdentifier
530+
, start_pos :: Position
531+
-- | There can be more than one hint suggested at the same position, so HintTitle is used to distinguish between them.
532+
, hintTitle :: HintTitle
533+
}
534+
| IH { verTxtDocId :: VersionedTextDocumentIdentifier
535+
, ignoreHintTitle :: HintTitle
536+
} deriving (Generic, ToJSON, FromJSON)
544537

545538
type HintTitle = T.Text
546539

@@ -549,21 +542,6 @@ data OneHint = OneHint
549542
, oneHintTitle :: HintTitle
550543
} deriving (Eq, Show)
551544

552-
applyOneCmd :: Recorder (WithPriority Log) -> CommandFunction IdeState ApplyOneParams
553-
applyOneCmd recorder ide (AOP verTxtDocId pos title) = do
554-
let oneHint = OneHint pos title
555-
let file = maybe (error $ show (verTxtDocId ^. LSP.uri) ++ " is not a file.") toNormalizedFilePath'
556-
(uriToFilePath' (verTxtDocId ^. LSP.uri))
557-
let progTitle = "Applying hint: " <> title
558-
withIndefiniteProgress progTitle Cancellable $ do
559-
res <- liftIO $ applyHint recorder ide file (Just oneHint) verTxtDocId
560-
logWith recorder Debug $ LogApplying file res
561-
case res of
562-
Left err -> pure $ Left (responseError (T.pack $ "hlint:applyOne: " ++ show err))
563-
Right fs -> do
564-
_ <- sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing fs) (\_ -> pure ())
565-
pure $ Right Null
566-
567545
applyHint :: Recorder (WithPriority Log) -> IdeState -> NormalizedFilePath -> Maybe OneHint -> VersionedTextDocumentIdentifier -> IO (Either String WorkspaceEdit)
568546
applyHint recorder ide nfp mhint verTxtDocId =
569547
runExceptT $ do

0 commit comments

Comments
 (0)