{-# 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_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) ------------------------------------------------------------------------------ -- | 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) getNfp :: Applicative m => Uri -> MaybeT m NormalizedFilePath getNfp = MaybeT . pure . uriToNormalizedFilePath . toNormalizedUri instance IsTarget HoleTarget where type TargetArgs HoleTarget = HoleJudgment fetchTargetArgs LspEnv{..} = do let FileContext{..} = le_fileContext range <- MaybeT $ pure fc_range nfp <- getNfp fc_uri mapMaybeT liftIO $ judgementForHole le_ideState nfp range le_config