diff --git a/plugins/hls-tactics-plugin/hls-tactics-plugin.cabal b/plugins/hls-tactics-plugin/hls-tactics-plugin.cabal index 3d62a50c1e..f4d11fd892 100644 --- a/plugins/hls-tactics-plugin/hls-tactics-plugin.cabal +++ b/plugins/hls-tactics-plugin/hls-tactics-plugin.cabal @@ -27,6 +27,9 @@ library hs-source-dirs: src exposed-modules: Ide.Plugin.Tactic + Wingman.AbstractLSP + Wingman.AbstractLSP.TacticActions + Wingman.AbstractLSP.Types Wingman.Auto Wingman.CaseSplit Wingman.CodeGen diff --git a/plugins/hls-tactics-plugin/src/Ide/Plugin/Tactic.hs b/plugins/hls-tactics-plugin/src/Ide/Plugin/Tactic.hs index 35ecf0dcfe..de93d03ed0 100644 --- a/plugins/hls-tactics-plugin/src/Ide/Plugin/Tactic.hs +++ b/plugins/hls-tactics-plugin/src/Ide/Plugin/Tactic.hs @@ -1,9 +1,5 @@ -- | A plugin that uses tactics to synthesize code -module Ide.Plugin.Tactic - ( descriptor - , tacticTitle - , TacticCommand (..) - ) where +module Ide.Plugin.Tactic (descriptor) where import Wingman.Plugin diff --git a/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP.hs b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP.hs new file mode 100644 index 0000000000..0baf96a6d3 --- /dev/null +++ b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP.hs @@ -0,0 +1,263 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StandaloneDeriving #-} + +{-# LANGUAGE NoMonoLocalBinds #-} + +{-# OPTIONS_GHC -Wno-orphans #-} + +module Wingman.AbstractLSP (installInteractions) where + +import Control.Monad (void) +import Control.Monad.IO.Class +import Control.Monad.Trans (lift) +import Control.Monad.Trans.Maybe (MaybeT, mapMaybeT) +import qualified Data.Aeson as A +import Data.Foldable (traverse_) +import qualified Data.Text as T +import Data.Tuple.Extra (uncurry3) +import Development.IDE (IdeState) +import Development.IDE.Core.UseStale +import Development.IDE.GHC.ExactPrint (GetAnnotatedParsedSource(GetAnnotatedParsedSource)) +import qualified Ide.Plugin.Config as Plugin +import Ide.Types +import Language.LSP.Server (LspM, sendRequest, getClientCapabilities) +import qualified Language.LSP.Types as LSP +import Language.LSP.Types hiding (CodeLens, CodeAction) +import Wingman.AbstractLSP.Types +import Wingman.EmptyCase (fromMaybeT) +import Wingman.LanguageServer (getTacticConfig, getIdeDynflags, mkWorkspaceEdits, runStaleIde, showLspMessage, mkShowMessageParams) +import Wingman.Types + + +------------------------------------------------------------------------------ +-- | Attact the 'Interaction's to a 'PluginDescriptor'. Interactions are +-- self-contained request/response pairs that abstract over the LSP, and +-- provide a unified interface for doing interesting things, without needing to +-- dive into the underlying API too directly. +installInteractions + :: [Interaction] + -> PluginDescriptor IdeState + -> PluginDescriptor IdeState +installInteractions is desc = + let plId = pluginId desc + in desc + { pluginCommands = pluginCommands desc <> fmap (buildCommand plId) is + , pluginHandlers = pluginHandlers desc <> buildHandlers is + } + + +------------------------------------------------------------------------------ +-- | Extract 'PluginHandlers' from 'Interaction's. +buildHandlers + :: [Interaction] + -> PluginHandlers IdeState +buildHandlers cs = + flip foldMap cs $ \(Interaction (c :: Continuation sort target b)) -> + case c_makeCommand c of + SynthesizeCodeAction k -> + mkPluginHandler STextDocumentCodeAction $ codeActionProvider @target (c_sort c) k + SynthesizeCodeLens k -> + mkPluginHandler STextDocumentCodeLens $ codeLensProvider @target (c_sort c) k + + +------------------------------------------------------------------------------ +-- | Extract a 'PluginCommand' from an 'Interaction'. +buildCommand + :: PluginId + -> Interaction + -> PluginCommand IdeState +buildCommand plId (Interaction (c :: Continuation sort target b)) = + PluginCommand + { commandId = toCommandId $ c_sort c + , commandDesc = T.pack "" + , commandFunc = runContinuation plId c + } + + +------------------------------------------------------------------------------ +-- | Boilerplate for running a 'Continuation' as part of an LSP command. +runContinuation + :: forall sort a b + . IsTarget a + => PluginId + -> Continuation sort a b + -> CommandFunction IdeState (FileContext, b) +runContinuation plId cont state (fc, b) = do + fromMaybeT + (Left $ ResponseError + { _code = InternalError + , _message = T.pack "TODO(sandy)" + , _xdata = Nothing + } ) $ do + env@LspEnv{..} <- buildEnv state plId fc + let stale a = runStaleIde "runContinuation" state (fc_nfp le_fileContext) a + args <- fetchTargetArgs @a env + c_runCommand cont env args fc b >>= \case + ErrorMessages errs -> do + traverse_ showUserFacingMessage errs + pure $ Right A.Null + RawEdit edits -> do + sendEdits edits + pure $ Right A.Null + GraftEdit gr -> do + ccs <- lift getClientCapabilities + TrackedStale pm _ <- mapMaybeT liftIO $ stale GetAnnotatedParsedSource + case mkWorkspaceEdits le_dflags ccs (fc_uri le_fileContext) (unTrack pm) gr of + Left errs -> + pure $ Left $ ResponseError + { _code = InternalError + , _message = T.pack $ show errs + , _xdata = Nothing + } + Right edits -> do + sendEdits edits + pure $ Right A.Null + + +------------------------------------------------------------------------------ +-- | Push a 'WorkspaceEdit' to the client. +sendEdits :: WorkspaceEdit -> MaybeT (LspM Plugin.Config) () +sendEdits edits = + void $ lift $ + sendRequest + SWorkspaceApplyEdit + (ApplyWorkspaceEditParams Nothing edits) + (const $ pure ()) + + +------------------------------------------------------------------------------ +-- | Push a 'UserFacingMessage' to the client. +showUserFacingMessage + :: UserFacingMessage + -> MaybeT (LspM Plugin.Config) () +showUserFacingMessage ufm = + void $ lift $ showLspMessage $ mkShowMessageParams ufm + + +------------------------------------------------------------------------------ +-- | Build an 'LspEnv', which contains the majority of things we need to know +-- in a 'Continuation'. +buildEnv + :: IdeState + -> PluginId + -> FileContext + -> MaybeT (LspM Plugin.Config) LspEnv +buildEnv state plId fc = do + cfg <- lift $ getTacticConfig plId + dflags <- mapMaybeT liftIO $ getIdeDynflags state $ fc_nfp fc + pure $ LspEnv + { le_ideState = state + , le_pluginId = plId + , le_dflags = dflags + , le_config = cfg + , le_fileContext = fc + } + + +------------------------------------------------------------------------------ +-- | Lift a 'Continuation' into an LSP CodeAction. +codeActionProvider + :: forall target sort b + . (IsContinuationSort sort, A.ToJSON b, IsTarget target) + => sort + -> ( LspEnv + -> TargetArgs target + -> MaybeT (LspM Plugin.Config) [(Metadata, b)] + ) + -> PluginMethodHandler IdeState TextDocumentCodeAction +codeActionProvider sort k state plId + (CodeActionParams _ _ (TextDocumentIdentifier uri) range _) + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do + fromMaybeT (Right $ List []) $ do + let fc = FileContext + { fc_uri = uri + , fc_nfp = nfp + , fc_range = Just $ unsafeMkCurrent range + } + env <- buildEnv state plId fc + args <- fetchTargetArgs @target env + actions <- k env args + pure + $ Right + $ List + $ fmap (InR . uncurry (makeCodeAction plId fc sort)) actions +codeActionProvider _ _ _ _ _ = pure $ Right $ List [] + + +------------------------------------------------------------------------------ +-- | Lift a 'Continuation' into an LSP CodeLens. +codeLensProvider + :: forall target sort b + . (IsContinuationSort sort, A.ToJSON b, IsTarget target) + => sort + -> ( LspEnv + -> TargetArgs target + -> MaybeT (LspM Plugin.Config) [(Range, Metadata, b)] + ) + -> PluginMethodHandler IdeState TextDocumentCodeLens +codeLensProvider sort k state plId + (CodeLensParams _ _ (TextDocumentIdentifier uri)) + | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do + fromMaybeT (Right $ List []) $ do + let fc = FileContext + { fc_uri = uri + , fc_nfp = nfp + , fc_range = Nothing + } + env <- buildEnv state plId fc + args <- fetchTargetArgs @target env + actions <- k env args + pure + $ Right + $ List + $ fmap (uncurry3 $ makeCodeLens plId sort fc) actions +codeLensProvider _ _ _ _ _ = pure $ Right $ List [] + + +------------------------------------------------------------------------------ +-- | Build a 'LSP.CodeAction'. +makeCodeAction + :: (A.ToJSON b, IsContinuationSort sort) + => PluginId + -> FileContext + -> sort + -> Metadata + -> b + -> LSP.CodeAction +makeCodeAction plId fc sort (Metadata title kind preferred) b = + let cmd_id = toCommandId sort + cmd = mkLspCommand plId cmd_id title $ Just [A.toJSON (fc, b)] + in LSP.CodeAction + { _title = title + , _kind = Just kind + , _diagnostics = Nothing + , _isPreferred = Just preferred + , _disabled = Nothing + , _edit = Nothing + , _command = Just cmd + , _xdata = Nothing + } + + +------------------------------------------------------------------------------ +-- | Build a 'LSP.CodeLens'. +makeCodeLens + :: (A.ToJSON b, IsContinuationSort sort) + => PluginId + -> sort + -> FileContext + -> Range + -> Metadata + -> b + -> LSP.CodeLens +makeCodeLens plId sort fc range (Metadata title _ _) b = + let fc' = fc { fc_range = Just $ unsafeMkCurrent range } + cmd_id = toCommandId sort + cmd = mkLspCommand plId cmd_id title $ Just [A.toJSON (fc', b)] + in LSP.CodeLens + { _range = range + , _command = Just cmd + , _xdata = Nothing + } + diff --git a/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/TacticActions.hs b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/TacticActions.hs new file mode 100644 index 0000000000..62f51f7a34 --- /dev/null +++ b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/TacticActions.hs @@ -0,0 +1,157 @@ +{-# LANGUAGE RecordWildCards #-} + +{-# LANGUAGE NoMonoLocalBinds #-} + +module Wingman.AbstractLSP.TacticActions where + +import Control.Monad (when) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans (lift) +import Control.Monad.Trans.Maybe (mapMaybeT) +import Data.Foldable +import Data.Maybe (listToMaybe) +import Data.Proxy +import Development.IDE hiding (rangeToRealSrcSpan) +import Development.IDE.Core.UseStale +import Development.IDE.GHC.Compat +import Development.IDE.GHC.ExactPrint +import Generics.SYB.GHC (mkBindListT, everywhereM') +import GhcPlugins (occName) +import System.Timeout (timeout) +import Wingman.AbstractLSP.Types +import Wingman.CaseSplit +import Wingman.GHC (liftMaybe, isHole, pattern AMatch, unXPat) +import Wingman.Judgements (jNeedsToBindArgs) +import Wingman.LanguageServer (runStaleIde) +import Wingman.LanguageServer.TacticProviders +import Wingman.Machinery (runTactic, scoreSolution) +import Wingman.Range +import Wingman.Types + + +------------------------------------------------------------------------------ +-- | An 'Interaction' for a 'TacticCommand'. +makeTacticInteraction + :: TacticCommand + -> Interaction +makeTacticInteraction cmd = + Interaction $ Continuation @_ @HoleTarget cmd + (SynthesizeCodeAction $ \env@LspEnv{..} hj -> do + pure $ commandProvider cmd $ + TacticProviderData + { tpd_lspEnv = env + , tpd_jdg = hj_jdg hj + , tpd_hole_sort = hj_hole_sort hj + } + ) + $ \LspEnv{..} HoleJudgment{..} FileContext{..} var_name -> do + let stale a = runStaleIde "tacticCmd" le_ideState fc_nfp a + + let span = fmap (rangeToRealSrcSpan (fromNormalizedFilePath fc_nfp)) hj_range + TrackedStale _ pmmap <- mapMaybeT liftIO $ stale GetAnnotatedParsedSource + pm_span <- liftMaybe $ mapAgeFrom pmmap span + let t = commandTactic cmd var_name + + res <- liftIO $ timeout (cfg_timeout_seconds le_config * seconds) $ do + runTactic hj_ctx hj_jdg t >>= \case + Left err -> pure $ ErrorMessages $ pure $ mkUserFacingMessage err + Right rtr -> + case rtr_extract rtr of + L _ (HsVar _ (L _ rdr)) | isHole (occName rdr) -> + pure $ ErrorMessages [NothingToDo] + _ -> do + for_ (rtr_other_solns rtr) $ \soln -> do + traceMX "other solution" $ syn_val soln + traceMX "with score" $ scoreSolution soln (rtr_jdg rtr) [] + traceMX "solution" $ rtr_extract rtr + pure $ GraftEdit $ graftHole (RealSrcSpan $ unTrack pm_span) rtr + + pure $ case res of + Nothing -> ErrorMessages $ pure TimedOut + Just c -> c + + +------------------------------------------------------------------------------ +-- | The number of microseconds in a second +seconds :: Num a => a +seconds = 1e6 + + +------------------------------------------------------------------------------ +-- | Transform some tactic errors into a 'UserFacingMessage'. +mkUserFacingMessage :: [TacticError] -> UserFacingMessage +mkUserFacingMessage errs + | elem OutOfGas errs = NotEnoughGas +mkUserFacingMessage _ = TacticErrors + + +------------------------------------------------------------------------------ +-- | Graft a 'RunTacticResults' into the correct place in an AST. Correctly +-- deals with top-level holes, in which we might need to fiddle with the +-- 'Match's that bind variables. +graftHole + :: SrcSpan + -> RunTacticResults + -> Graft (Either String) ParsedSource +graftHole span rtr + | _jIsTopHole (rtr_jdg rtr) + = genericGraftWithSmallestM + (Proxy @(Located [LMatch GhcPs (LHsExpr GhcPs)])) span + $ \dflags matches -> + everywhereM' + $ mkBindListT $ \ix -> + graftDecl dflags span ix $ \name pats -> + splitToDecl + (case not $ jNeedsToBindArgs (rtr_jdg rtr) of + -- If the user has explicitly bound arguments, use the + -- fixity they wrote. + True -> matchContextFixity . m_ctxt . unLoc + =<< listToMaybe matches + -- Otherwise, choose based on the name of the function. + False -> Nothing + ) + (occName name) + $ iterateSplit + $ mkFirstAgda (fmap unXPat pats) + $ unLoc + $ rtr_extract rtr +graftHole span rtr + = graft span + $ rtr_extract rtr + + +------------------------------------------------------------------------------ +-- | Keep a fixity if one was present in the 'HsMatchContext'. +matchContextFixity :: HsMatchContext p -> Maybe LexicalFixity +matchContextFixity (FunRhs _ l _) = Just l +matchContextFixity _ = Nothing + + +------------------------------------------------------------------------------ +-- | Helper function to route 'mergeFunBindMatches' into the right place in an +-- AST --- correctly dealing with inserting into instance declarations. +graftDecl + :: DynFlags + -> SrcSpan + -> Int + -> (RdrName -> [Pat GhcPs] -> LHsDecl GhcPs) + -> LMatch GhcPs (LHsExpr GhcPs) + -> TransformT (Either String) [LMatch GhcPs (LHsExpr GhcPs)] +graftDecl dflags dst ix make_decl (L src (AMatch (FunRhs (L _ name) _ _) pats _)) + | dst `isSubspanOf` src = do + L _ dec <- annotateDecl dflags $ make_decl name pats + case dec of + ValD _ (FunBind { fun_matches = MG { mg_alts = L _ alts@(first_match : _)} + }) -> do + -- For whatever reason, ExactPrint annotates newlines to the ends of + -- case matches and type signatures, but only allows us to insert + -- them at the beginning of those things. Thus, we need want to + -- insert a preceeding newline (done in 'annotateDecl') on all + -- matches, except for the first one --- since it gets its newline + -- from the line above. + when (ix == 0) $ + setPrecedingLinesT first_match 0 0 + pure alts + _ -> lift $ Left "annotateDecl didn't produce a funbind" +graftDecl _ _ _ _ x = pure $ pure x + diff --git a/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/Types.hs b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/Types.hs new file mode 100644 index 0000000000..8b00f0b021 --- /dev/null +++ b/plugins/hls-tactics-plugin/src/Wingman/AbstractLSP/Types.hs @@ -0,0 +1,171 @@ +{-# LANGUAGE AllowAmbiguousTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StandaloneDeriving #-} +{-# LANGUAGE TypeFamilies #-} + +{-# OPTIONS_GHC -Wno-orphans #-} + +module Wingman.AbstractLSP.Types where + +import Control.Monad.IO.Class +import Control.Monad.Trans.Maybe (MaybeT (MaybeT), mapMaybeT) +import qualified Data.Aeson as A +import Data.Text (Text) +import Development.IDE (IdeState) +import Development.IDE.GHC.ExactPrint (Graft) +import Development.IDE.Core.UseStale +import Development.IDE.GHC.Compat hiding (Target) +import GHC.Generics (Generic) +import qualified Ide.Plugin.Config as Plugin +import Ide.Types +import Language.LSP.Server (LspM) +import Language.LSP.Types hiding (CodeLens, CodeAction) +import Wingman.LanguageServer (judgementForHole) +import Wingman.Types + + +------------------------------------------------------------------------------ +-- | An 'Interaction' is an existential 'Continuation', which handles both +-- sides of the request/response interaction for LSP. +data Interaction where + Interaction + :: (IsTarget target, IsContinuationSort sort, A.ToJSON b, A.FromJSON b) + => Continuation sort target b + -> Interaction + + +------------------------------------------------------------------------------ +-- | Metadata for a command. Used by both code actions and lenses, though for +-- lenses, only 'md_title' is currently used. +data Metadata + = Metadata + { md_title :: Text + , md_kind :: CodeActionKind + , md_preferred :: Bool + } + deriving stock (Eq, Show) + + +------------------------------------------------------------------------------ +-- | Whether we're defining a CodeAction or CodeLens. +data SynthesizeCommand a b + = SynthesizeCodeAction + ( LspEnv + -> TargetArgs a + -> MaybeT (LspM Plugin.Config) [(Metadata, b)] + ) + | SynthesizeCodeLens + ( LspEnv + -> TargetArgs a + -> MaybeT (LspM Plugin.Config) [(Range, Metadata, b)] + ) + + +------------------------------------------------------------------------------ +-- | Transform a "continuation sort" into a 'CommandId'. +class IsContinuationSort a where + toCommandId :: a -> CommandId + +instance IsContinuationSort CommandId where + toCommandId = id + +instance IsContinuationSort Text where + toCommandId = CommandId + + +------------------------------------------------------------------------------ +-- | Ways a 'Continuation' can resolve. +data ContinuationResult + = -- | Produce some error messages. + ErrorMessages [UserFacingMessage] + -- | Produce an explicit 'WorkspaceEdit'. + | RawEdit WorkspaceEdit + -- | Produce a 'Graft', corresponding to a transformation of the current + -- AST. + | GraftEdit (Graft (Either String) ParsedSource) + + +------------------------------------------------------------------------------ +-- | A 'Continuation' is a single object corresponding to an action that users +-- can take via LSP. It generalizes codeactions and codelenses, allowing for +-- a significant amount of code reuse. +-- +-- Given @Continuation sort target payload@: +-- +-- the @sort@ corresponds to a 'CommandId', allowing you to namespace actions +-- rather than working directly with text. This functionality is driven via +-- 'IsContinuationSort'. +-- +-- the @target@ is used to fetch data from LSP on both sides of the +-- request/response barrier. For example, you can use it to resolve what node +-- in the AST the incoming range refers to. This functionality is driven via +-- 'IsTarget'. +-- +-- the @payload@ is used for data you'd explicitly like to send from the +-- request to the response. It's like @target@, but only gets computed once. +-- This is beneficial if you can do it, but requires that your data is +-- serializable via JSON. +data Continuation sort target payload = Continuation + { c_sort :: sort + , c_makeCommand :: SynthesizeCommand target payload + , c_runCommand + :: LspEnv + -> TargetArgs target + -> FileContext + -> payload + -> MaybeT (LspM Plugin.Config) ContinuationResult + } + + +------------------------------------------------------------------------------ +-- | What file are we looking at, and what bit of it? +data FileContext = FileContext + { fc_uri :: Uri + , fc_nfp :: NormalizedFilePath + , fc_range :: Maybe (Tracked 'Current Range) + -- ^ For code actions, this is 'Just'. For code lenses, you'll get + -- a 'Nothing' in the request, and a 'Just' in the response. + } + deriving stock (Eq, Ord, Show, Generic) + deriving anyclass (A.ToJSON, A.FromJSON) + +deriving anyclass instance A.ToJSON NormalizedFilePath +deriving anyclass instance A.ToJSON NormalizedUri +deriving anyclass instance A.FromJSON NormalizedFilePath +deriving anyclass instance A.FromJSON NormalizedUri + + +------------------------------------------------------------------------------ +-- | Everything we need to resolve continuations. +data LspEnv = LspEnv + { le_ideState :: IdeState + , le_pluginId :: PluginId + , le_dflags :: DynFlags + , le_config :: Config + , le_fileContext :: FileContext + } + + +------------------------------------------------------------------------------ +-- | Extract some information from LSP, so it can be passed to the requests and +-- responses of a 'Continuation'. +class IsTarget t where + type TargetArgs t + fetchTargetArgs + :: LspEnv + -> MaybeT (LspM Plugin.Config) (TargetArgs t) + +------------------------------------------------------------------------------ +-- | A 'HoleTarget' is a target (see 'IsTarget') which succeeds if the given +-- range is an HsExpr hole. It gives continuations access to the resulting +-- tactic judgement. +data HoleTarget = HoleTarget + deriving stock (Eq, Ord, Show, Enum, Bounded) + +instance IsTarget HoleTarget where + type TargetArgs HoleTarget = HoleJudgment + fetchTargetArgs LspEnv{..} = do + let FileContext{..} = le_fileContext + range <- MaybeT $ pure fc_range + mapMaybeT liftIO $ judgementForHole le_ideState fc_nfp range le_config + diff --git a/plugins/hls-tactics-plugin/src/Wingman/EmptyCase.hs b/plugins/hls-tactics-plugin/src/Wingman/EmptyCase.hs index 8335642a4e..568ca69ca1 100644 --- a/plugins/hls-tactics-plugin/src/Wingman/EmptyCase.hs +++ b/plugins/hls-tactics-plugin/src/Wingman/EmptyCase.hs @@ -1,4 +1,6 @@ {-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TypeFamilies #-} {-# LANGUAGE NoMonoLocalBinds #-} @@ -9,7 +11,6 @@ import Control.Monad import Control.Monad.Except (runExcept) import Control.Monad.Trans import Control.Monad.Trans.Maybe -import Data.Aeson import Data.Generics.Aliases (mkQ, GenericQ) import Data.Generics.Schemes (everything) import Data.Maybe @@ -31,6 +32,7 @@ import OccName import Prelude hiding (span) import Prelude hiding (span) import TcRnTypes (tcg_binds) +import Wingman.AbstractLSP.Types import Wingman.CodeGen (destructionFor) import Wingman.GHC import Wingman.Judgements @@ -38,59 +40,51 @@ import Wingman.LanguageServer import Wingman.Types ------------------------------------------------------------------------------- --- | The 'CommandId' for the empty case completion. -emptyCaseLensCommandId :: CommandId -emptyCaseLensCommandId = CommandId "wingman.emptyCase" - - ------------------------------------------------------------------------------- --- | A command function that just applies a 'WorkspaceEdit'. -workspaceEditHandler :: CommandFunction IdeState WorkspaceEdit -workspaceEditHandler _ideState wedit = do - _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedit) (\_ -> pure ()) - return $ Right Null - - ------------------------------------------------------------------------------- --- | Provide the "empty case completion" code lens -codeLensProvider :: PluginMethodHandler IdeState TextDocumentCodeLens -codeLensProvider state plId (CodeLensParams _ _ (TextDocumentIdentifier uri)) - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do - let stale a = runStaleIde "codeLensProvider" state nfp a - - ccs <- getClientCapabilities - liftIO $ fromMaybeT (Right $ List []) $ do - dflags <- getIdeDynflags state nfp - TrackedStale pm _ <- stale GetAnnotatedParsedSource - TrackedStale binds bind_map <- stale GetBindings - holes <- emptyCaseScrutinees state nfp - - fmap (Right . List) $ for holes $ \(ss, ty) -> do - binds_ss <- liftMaybe $ mapAgeFrom bind_map ss - let bindings = getLocalScope (unTrack binds) $ unTrack binds_ss - range = realSrcSpanToRange $ unTrack ss - matches <- - liftMaybe $ - destructionFor - (foldMap (hySingleton . occName . fst) bindings) - ty - edits <- liftMaybe $ hush $ - mkWorkspaceEdits dflags ccs uri (unTrack pm) $ - graftMatchGroup (RealSrcSpan $ unTrack ss) $ - noLoc matches - - pure $ - CodeLens range - (Just - $ mkLspCommand - plId - emptyCaseLensCommandId - (mkEmptyCaseLensDesc ty) - $ Just $ pure $ toJSON $ edits - ) - Nothing -codeLensProvider _ _ _ = pure $ Right $ List [] +data EmptyCaseT = EmptyCaseT + +instance IsContinuationSort EmptyCaseT where + toCommandId _ = CommandId "wingman.emptyCase" + +instance IsTarget EmptyCaseT where + type TargetArgs EmptyCaseT = () + fetchTargetArgs _ = pure () + +emptyCaseInteraction :: Interaction +emptyCaseInteraction = Interaction $ + Continuation @EmptyCaseT @EmptyCaseT @WorkspaceEdit EmptyCaseT + (SynthesizeCodeLens $ \LspEnv{..} _ -> do + let FileContext{..} = le_fileContext + + let stale a = runStaleIde "codeLensProvider" le_ideState fc_nfp a + + ccs <- lift getClientCapabilities + TrackedStale pm _ <- mapMaybeT liftIO $ stale GetAnnotatedParsedSource + TrackedStale binds bind_map <- mapMaybeT liftIO $ stale GetBindings + holes <- mapMaybeT liftIO $ emptyCaseScrutinees le_ideState fc_nfp + + for holes $ \(ss, ty) -> do + binds_ss <- liftMaybe $ mapAgeFrom bind_map ss + let bindings = getLocalScope (unTrack binds) $ unTrack binds_ss + range = realSrcSpanToRange $ unTrack ss + matches <- + liftMaybe $ + destructionFor + (foldMap (hySingleton . occName . fst) bindings) + ty + edits <- liftMaybe $ hush $ + mkWorkspaceEdits le_dflags ccs fc_uri (unTrack pm) $ + graftMatchGroup (RealSrcSpan $ unTrack ss) $ + noLoc matches + pure + ( range + , Metadata + (mkEmptyCaseLensDesc ty) + (CodeActionUnknown "refactor.wingman.completeEmptyCase") + False + , edits + ) + ) + $ (\ _ _ _ we -> pure $ RawEdit we) scrutinzedType :: EmptyCaseSort Type -> Maybe Type diff --git a/plugins/hls-tactics-plugin/src/Wingman/LanguageServer/TacticProviders.hs b/plugins/hls-tactics-plugin/src/Wingman/LanguageServer/TacticProviders.hs index ebc460f6d3..5a0844b73c 100644 --- a/plugins/hls-tactics-plugin/src/Wingman/LanguageServer/TacticProviders.hs +++ b/plugins/hls-tactics-plugin/src/Wingman/LanguageServer/TacticProviders.hs @@ -1,38 +1,29 @@ {-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} +{-# OPTIONS_GHC -Wno-orphans #-} module Wingman.LanguageServer.TacticProviders ( commandProvider , commandTactic - , tcCommandId - , TacticParams (..) , TacticProviderData (..) - , useNameFromHypothesis ) where import Control.Monad -import Data.Aeson import Data.Bool (bool) import Data.Coerce import Data.Maybe import Data.Monoid import qualified Data.Set as S import qualified Data.Text as T -import Data.Traversable import DataCon (dataConName) -import Development.IDE.Core.UseStale (Tracked, Age(..)) import Development.IDE.GHC.Compat -import GHC.Generics import GHC.LanguageExtensions.Type (Extension (LambdaCase)) -import Ide.PluginUtils import Ide.Types -import Language.LSP.Types hiding - (SemanticTokenAbsolute (length, line), - SemanticTokenRelative (length), - SemanticTokensEdit (_start)) +import Language.LSP.Types hiding (SemanticTokenAbsolute (..), SemanticTokenRelative (..)) import OccName import Prelude hiding (span) +import Wingman.AbstractLSP.Types import Wingman.Auto import Wingman.GHC import Wingman.Judgements @@ -192,40 +183,27 @@ guardLength f as = bool [] as $ f $ length as -- UI. type TacticProvider = TacticProviderData - -> IO [Command |? CodeAction] + -> [(Metadata, T.Text)] data TacticProviderData = TacticProviderData - { tpd_dflags :: DynFlags - , tpd_config :: Config - , tpd_plid :: PluginId - , tpd_uri :: Uri - , tpd_range :: Tracked 'Current Range + { tpd_lspEnv :: LspEnv , tpd_jdg :: Judgement , tpd_hole_sort :: HoleSort } -data TacticParams = TacticParams - { tp_file :: Uri -- ^ Uri of the file to fill the hole in - , tp_range :: Tracked 'Current Range -- ^ The range of the hole - , tp_var_name :: T.Text - } - deriving stock (Show, Eq, Generic) - deriving anyclass (ToJSON, FromJSON) - - requireHoleSort :: (HoleSort -> Bool) -> TacticProvider -> TacticProvider requireHoleSort p tp tpd = case p $ tpd_hole_sort tpd of True -> tp tpd - False -> pure [] + False -> [] withMetaprogram :: (T.Text -> TacticProvider) -> TacticProvider withMetaprogram tp tpd = case tpd_hole_sort tpd of Metaprogram mp -> tp mp tpd - _ -> pure [] + _ -> [] ------------------------------------------------------------------------------ @@ -233,9 +211,9 @@ withMetaprogram tp tpd = -- predicate holds for the goal. requireExtension :: Extension -> TacticProvider -> TacticProvider requireExtension ext tp tpd = - case xopt ext $ tpd_dflags tpd of + case xopt ext $ le_dflags $ tpd_lspEnv tpd of True -> tp tpd - False -> pure [] + False -> [] ------------------------------------------------------------------------------ @@ -245,7 +223,7 @@ filterGoalType :: (Type -> Bool) -> TacticProvider -> TacticProvider filterGoalType p tp tpd = case p $ unCType $ jGoal $ tpd_jdg tpd of True -> tp tpd - False -> pure [] + False -> [] ------------------------------------------------------------------------------ @@ -266,11 +244,11 @@ filterBindingType p tp tpd = let jdg = tpd_jdg tpd hy = jLocalHypothesis jdg g = jGoal jdg - in fmap join $ for (unHypothesis hy) $ \hi -> + in unHypothesis hy >>= \hi -> let ty = unCType $ hi_type hi in case p (unCType g) ty of True -> tp (hi_name hi) ty tpd - False -> pure [] + False -> [] ------------------------------------------------------------------------------ @@ -281,37 +259,22 @@ filterTypeProjection -> (a -> TacticProvider) -> TacticProvider filterTypeProjection p tp tpd = - fmap join $ for (p $ unCType $ jGoal $ tpd_jdg tpd) $ \a -> + (p $ unCType $ jGoal $ tpd_jdg tpd) >>= \a -> tp a tpd ------------------------------------------------------------------------------ -- | Get access to the 'Config' when building a 'TacticProvider'. withConfig :: (Config -> TacticProvider) -> TacticProvider -withConfig tp tpd = tp (tpd_config tpd) tpd +withConfig tp tpd = tp (le_config $ tpd_lspEnv tpd) tpd ------------------------------------------------------------------------------ -- | Terminal constructor for providing context-sensitive tactics. Tactics -- given by 'provide' are always available. provide :: TacticCommand -> T.Text -> TacticProvider -provide tc name TacticProviderData{..} = do - let title = tacticTitle tc name - params = TacticParams { tp_file = tpd_uri , tp_range = tpd_range , tp_var_name = name } - cmd = mkLspCommand tpd_plid (tcCommandId tc) title (Just [toJSON params]) - pure - $ pure - $ InR - $ CodeAction - { _title = title - , _kind = Just $ mkTacticKind tc - , _diagnostics = Nothing - , _isPreferred = Just $ tacticPreferred tc - , _disabled = Nothing - , _edit = Nothing - , _command = Just cmd - , _xdata = Nothing - } +provide tc name _ = + pure $ (Metadata (tacticTitle tc name) (mkTacticKind tc) (tacticPreferred tc), name) ------------------------------------------------------------------------------ @@ -345,7 +308,7 @@ liftLambdaCase nil f t = -- algebraic types. destructFilter :: Type -> Type -> Bool destructFilter _ (algebraicTyCon -> Just _) = True -destructFilter _ _ = False +destructFilter _ _ = False ------------------------------------------------------------------------------ @@ -354,5 +317,9 @@ destructFilter _ _ = False destructPunFilter :: Type -> Type -> Bool destructPunFilter _ (algebraicTyCon -> Just tc) = any (not . null . dataConFieldLabels) $ tyConDataCons tc -destructPunFilter _ _ = False +destructPunFilter _ _ = False + + +instance IsContinuationSort TacticCommand where + toCommandId = tcCommandId diff --git a/plugins/hls-tactics-plugin/src/Wingman/Plugin.hs b/plugins/hls-tactics-plugin/src/Wingman/Plugin.hs index 8cd6fa1c9d..909ee6c26e 100644 --- a/plugins/hls-tactics-plugin/src/Wingman/Plugin.hs +++ b/plugins/hls-tactics-plugin/src/Wingman/Plugin.hs @@ -2,255 +2,34 @@ {-# LANGUAGE RecordWildCards #-} -- | A plugin that uses tactics to synthesize code -module Wingman.Plugin - ( descriptor - , tacticTitle - , TacticCommand (..) - ) where +module Wingman.Plugin where import Control.Monad -import Control.Monad.Trans -import Control.Monad.Trans.Maybe -import Data.Aeson -import Data.Data -import Data.Foldable (for_) -import Data.Maybe -import qualified Data.Text as T import Development.IDE.Core.Shake (IdeState (..)) -import Development.IDE.Core.UseStale (Tracked, TrackedStale(..), unTrack, mapAgeFrom, unsafeMkCurrent) -import Development.IDE.GHC.Compat -import Development.IDE.GHC.ExactPrint -import Generics.SYB.GHC import Ide.Types -import Language.LSP.Server import Language.LSP.Types -import Language.LSP.Types.Capabilities -import OccName import Prelude hiding (span) -import System.Timeout -import Wingman.CaseSplit +import Wingman.AbstractLSP +import Wingman.AbstractLSP.TacticActions (makeTacticInteraction) import Wingman.EmptyCase -import Wingman.GHC -import Wingman.Judgements (jNeedsToBindArgs) import Wingman.LanguageServer import Wingman.LanguageServer.Metaprogram (hoverProvider) -import Wingman.LanguageServer.TacticProviders -import Wingman.Machinery (scoreSolution) -import Wingman.Range import Wingman.StaticPlugin -import Wingman.Tactics -import Wingman.Types descriptor :: PluginId -> PluginDescriptor IdeState -descriptor plId = (defaultPluginDescriptor plId) - { pluginCommands - = mconcat - [ fmap (\tc -> - PluginCommand - (tcCommandId tc) - (tacticDesc $ tcCommandName tc) - (tacticCmd (commandTactic tc) plId)) - [minBound .. maxBound] - , pure $ - PluginCommand - emptyCaseLensCommandId - "Complete the empty case" - workspaceEditHandler - ] - , pluginHandlers = mconcat - [ mkPluginHandler STextDocumentCodeAction codeActionProvider - , mkPluginHandler STextDocumentCodeLens codeLensProvider - , mkPluginHandler STextDocumentHover hoverProvider - ] - , pluginRules = wingmanRules plId - , pluginConfigDescriptor = - defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} - , pluginModifyDynflags = staticPlugin - } - - -codeActionProvider :: PluginMethodHandler IdeState TextDocumentCodeAction -codeActionProvider state plId (CodeActionParams _ _ (TextDocumentIdentifier uri) (unsafeMkCurrent -> range) _ctx) - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do - cfg <- getTacticConfig plId - liftIO $ fromMaybeT (Right $ List []) $ do - HoleJudgment{..} <- judgementForHole state nfp range cfg - actions <- lift $ - -- This foldMap is over the function monoid. - foldMap commandProvider [minBound .. maxBound] $ TacticProviderData - { tpd_dflags = hj_dflags - , tpd_config = cfg - , tpd_plid = plId - , tpd_uri = uri - , tpd_range = range - , tpd_jdg = hj_jdg - , tpd_hole_sort = hj_hole_sort +descriptor plId + = installInteractions + ( emptyCaseInteraction + : fmap makeTacticInteraction [minBound .. maxBound] + ) + $ (defaultPluginDescriptor plId) + { pluginHandlers = mkPluginHandler STextDocumentHover hoverProvider + , pluginRules = wingmanRules plId + , pluginConfigDescriptor = + defaultConfigDescriptor + { configCustomConfig = mkCustomConfig properties } - pure $ Right $ List actions -codeActionProvider _ _ _ = pure $ Right $ List [] - - -showUserFacingMessage - :: MonadLsp cfg m - => UserFacingMessage - -> m (Either ResponseError a) -showUserFacingMessage ufm = do - showLspMessage $ mkShowMessageParams ufm - pure $ Left $ mkErr InternalError $ T.pack $ show ufm - - -mkUserFacingMessage :: [TacticError] -> UserFacingMessage -mkUserFacingMessage errs - | elem OutOfGas errs = NotEnoughGas -mkUserFacingMessage _ = TacticErrors - - -tacticCmd - :: (T.Text -> TacticsM ()) - -> PluginId - -> CommandFunction IdeState TacticParams -tacticCmd tac pId state (TacticParams uri range var_name) - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do - let stale a = runStaleIde "tacticCmd" state nfp a - - ccs <- getClientCapabilities - cfg <- getTacticConfig pId - res <- liftIO $ runMaybeT $ do - HoleJudgment{..} <- judgementForHole state nfp range cfg - let span = fmap (rangeToRealSrcSpan (fromNormalizedFilePath nfp)) hj_range - TrackedStale pm pmmap <- stale GetAnnotatedParsedSource - pm_span <- liftMaybe $ mapAgeFrom pmmap span - let t = tac var_name - - timingOut (cfg_timeout_seconds cfg * seconds) $ do - res <- liftIO $ runTactic hj_ctx hj_jdg t - pure $ join $ case res of - Left errs -> do - traceMX "errs" errs - Left $ mkUserFacingMessage errs - Right rtr -> - case rtr_extract rtr of - L _ (HsVar _ (L _ rdr)) | isHole (occName rdr) -> - Left NothingToDo - _ -> pure $ mkTacticResultEdits pm_span hj_dflags ccs uri pm rtr - - case res of - Nothing -> do - showUserFacingMessage TimedOut - Just (Left ufm) -> do - showUserFacingMessage ufm - Just (Right edit) -> do - _ <- sendRequest - SWorkspaceApplyEdit - (ApplyWorkspaceEditParams Nothing edit) - (const $ pure ()) - pure $ Right Null -tacticCmd _ _ _ _ = - pure $ Left $ mkErr InvalidRequest "Bad URI" - - ------------------------------------------------------------------------------- --- | The number of microseconds in a second -seconds :: Num a => a -seconds = 1e6 - - -timingOut - :: Int -- ^ Time in microseconds - -> IO a -- ^ Computation to run - -> MaybeT IO a -timingOut t m = MaybeT $ timeout t m - - -mkErr :: ErrorCode -> T.Text -> ResponseError -mkErr code err = ResponseError code err Nothing - - ------------------------------------------------------------------------------- --- | Turn a 'RunTacticResults' into concrete edits to make in the source --- document. -mkTacticResultEdits - :: Tracked age RealSrcSpan - -> DynFlags - -> ClientCapabilities - -> Uri - -> Tracked age (Annotated ParsedSource) - -> RunTacticResults - -> Either UserFacingMessage WorkspaceEdit -mkTacticResultEdits (unTrack -> span) dflags ccs uri (unTrack -> pm) rtr = do - for_ (rtr_other_solns rtr) $ \soln -> do - traceMX "other solution" $ syn_val soln - traceMX "with score" $ scoreSolution soln (rtr_jdg rtr) [] - traceMX "solution" $ rtr_extract rtr - mkWorkspaceEdits dflags ccs uri pm $ graftHole (RealSrcSpan span) rtr - - ------------------------------------------------------------------------------- --- | Graft a 'RunTacticResults' into the correct place in an AST. Correctly --- deals with top-level holes, in which we might need to fiddle with the --- 'Match's that bind variables. -graftHole - :: SrcSpan - -> RunTacticResults - -> Graft (Either String) ParsedSource -graftHole span rtr - | _jIsTopHole (rtr_jdg rtr) - = genericGraftWithSmallestM - (Proxy @(Located [LMatch GhcPs (LHsExpr GhcPs)])) span - $ \dflags matches -> - everywhereM' - $ mkBindListT $ \ix -> - graftDecl dflags span ix $ \name pats -> - splitToDecl - (case not $ jNeedsToBindArgs (rtr_jdg rtr) of - -- If the user has explicitly bound arguments, use the - -- fixity they wrote. - True -> matchContextFixity . m_ctxt . unLoc - =<< listToMaybe matches - -- Otherwise, choose based on the name of the function. - False -> Nothing - ) - (occName name) - $ iterateSplit - $ mkFirstAgda (fmap unXPat pats) - $ unLoc - $ rtr_extract rtr -graftHole span rtr - = graft span - $ rtr_extract rtr - - -matchContextFixity :: HsMatchContext p -> Maybe LexicalFixity -matchContextFixity (FunRhs _ l _) = Just l -matchContextFixity _ = Nothing - - ------------------------------------------------------------------------------- --- | Helper function to route 'mergeFunBindMatches' into the right place in an --- AST --- correctly dealing with inserting into instance declarations. -graftDecl - :: DynFlags - -> SrcSpan - -> Int - -> (RdrName -> [Pat GhcPs] -> LHsDecl GhcPs) - -> LMatch GhcPs (LHsExpr GhcPs) - -> TransformT (Either String) [LMatch GhcPs (LHsExpr GhcPs)] -graftDecl dflags dst ix make_decl (L src (AMatch (FunRhs (L _ name) _ _) pats _)) - | dst `isSubspanOf` src = do - L _ dec <- annotateDecl dflags $ make_decl name pats - case dec of - ValD _ (FunBind { fun_matches = MG { mg_alts = L _ alts@(first_match : _)} - }) -> do - -- For whatever reason, ExactPrint annotates newlines to the ends of - -- case matches and type signatures, but only allows us to insert - -- them at the beginning of those things. Thus, we need want to - -- insert a preceeding newline (done in 'annotateDecl') on all - -- matches, except for the first one --- since it gets its newline - -- from the line above. - when (ix == 0) $ - setPrecedingLinesT first_match 0 0 - pure alts - _ -> lift $ Left "annotateDecl didn't produce a funbind" -graftDecl _ _ _ _ x = pure $ pure x + , pluginModifyDynflags = staticPlugin + }