hls-plugin-api 2.0.0.1 → 2.1.0.0
raw patch · 9 files changed
+1237/−406 lines, 9 filesdep +co-log-coredep +mtldep +prettyprinterdep ~hls-graphdep ~lspPVP ok
version bump matches the API change (PVP)
Dependencies added: co-log-core, mtl, prettyprinter, row-types, stm, time, unliftio
Dependency ranges changed: hls-graph, lsp
API changes (from Hackage documentation)
- Ide.PluginUtils: extractRange :: Range -> Text -> Text
- Ide.PluginUtils: getNormalizedFilePath :: Monad m => Uri -> ExceptT String m NormalizedFilePath
- Ide.PluginUtils: handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b
- Ide.PluginUtils: handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b
- Ide.PluginUtils: pluginResponse :: Monad m => ExceptT String m a -> m (Either ResponseError a)
- Ide.PluginUtils: responseError :: Text -> ResponseError
- Ide.PluginUtils: throwPluginError :: Monad m => String -> ExceptT String m b
- Ide.Types: instance (Language.LSP.Types.Lens.HasTextDocument a doc, Language.LSP.Types.Lens.HasUri doc Language.LSP.Types.Uri.Uri) => Ide.Types.HasTracing a
- Ide.Types: instance Ide.Types.HasTracing (GHC.Maybe.Maybe Language.LSP.Types.Initialize.InitializedParams)
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.CallHierarchy.CallHierarchyIncomingCallsParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.CallHierarchy.CallHierarchyOutgoingCallsParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.Command.ExecuteCommandParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.Completion.CompletionItem
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.Configuration.DidChangeConfigurationParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.Initialize.InitializeParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.WatchedFiles.DidChangeWatchedFilesParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.WorkspaceFolders.DidChangeWorkspaceFoldersParams
- Ide.Types: instance Ide.Types.HasTracing Language.LSP.Types.WorkspaceSymbol.WorkspaceSymbolParams
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.Initialized
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.TextDocumentDidChange
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.TextDocumentDidClose
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.TextDocumentDidOpen
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.TextDocumentDidSave
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.WorkspaceDidChangeConfiguration
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.WorkspaceDidChangeWatchedFiles
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Notification 'Language.LSP.Types.Method.WorkspaceDidChangeWorkspaceFolders
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.CallHierarchyIncomingCalls
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.CallHierarchyOutgoingCalls
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.CompletionItemResolve
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.CustomMethod
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentCodeAction
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentCodeLens
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentCompletion
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentDefinition
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentDocumentHighlight
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentDocumentSymbol
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentFoldingRange
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentFormatting
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentHover
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentPrepareCallHierarchy
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentRangeFormatting
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentReferences
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentRename
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentSelectionRange
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.TextDocumentTypeDefinition
- Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Types.Method.Request 'Language.LSP.Types.Method.WorkspaceSymbol
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.Initialized
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.TextDocumentDidChange
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.TextDocumentDidClose
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.TextDocumentDidOpen
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.TextDocumentDidSave
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.WorkspaceDidChangeConfiguration
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.WorkspaceDidChangeWatchedFiles
- Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Types.Method.WorkspaceDidChangeWorkspaceFolders
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.CallHierarchyIncomingCalls
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.CallHierarchyOutgoingCalls
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.CompletionItemResolve
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.CustomMethod
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentCodeAction
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentCodeLens
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentCompletion
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentDefinition
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentDocumentHighlight
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentDocumentSymbol
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentFoldingRange
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentFormatting
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentHover
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentPrepareCallHierarchy
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentRangeFormatting
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentReferences
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentRename
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentSelectionRange
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.TextDocumentTypeDefinition
- Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Types.Method.WorkspaceSymbol
- Ide.Types: responseError :: Text -> ResponseError
+ Ide.Logger: DataColumn :: LoggingColumn
+ Ide.Logger: Debug :: Priority
+ Ide.Logger: Error :: Priority
+ Ide.Logger: Info :: Priority
+ Ide.Logger: Logger :: (Priority -> Text -> IO ()) -> Logger
+ Ide.Logger: PriorityColumn :: LoggingColumn
+ Ide.Logger: Recorder :: (forall m. MonadIO m => msg -> m ()) -> Recorder msg
+ Ide.Logger: SourceLocColumn :: LoggingColumn
+ Ide.Logger: ThreadIdColumn :: LoggingColumn
+ Ide.Logger: TimeColumn :: LoggingColumn
+ Ide.Logger: Warning :: Priority
+ Ide.Logger: WithPriority :: Priority -> CallStack -> a -> WithPriority a
+ Ide.Logger: [callStack_] :: WithPriority a -> CallStack
+ Ide.Logger: [logPriority] :: Logger -> Priority -> Text -> IO ()
+ Ide.Logger: [logger_] :: Recorder msg -> forall m. MonadIO m => msg -> m ()
+ Ide.Logger: [payload] :: WithPriority a -> a
+ Ide.Logger: [priority] :: WithPriority a -> Priority
+ Ide.Logger: cfilter :: (a -> Bool) -> Recorder a -> Recorder a
+ Ide.Logger: cmap :: (a -> b) -> Recorder b -> Recorder a
+ Ide.Logger: cmapIO :: (a -> IO b) -> Recorder b -> Recorder a
+ Ide.Logger: cmapWithPrio :: (a -> b) -> Recorder (WithPriority b) -> Recorder (WithPriority a)
+ Ide.Logger: data LoggingColumn
+ Ide.Logger: data Priority
+ Ide.Logger: data WithPriority a
+ Ide.Logger: instance Data.Functor.Contravariant.Contravariant Ide.Logger.Recorder
+ Ide.Logger: instance GHC.Base.Functor Ide.Logger.WithPriority
+ Ide.Logger: instance GHC.Base.Monoid (Ide.Logger.Recorder msg)
+ Ide.Logger: instance GHC.Base.Monoid Ide.Logger.Logger
+ Ide.Logger: instance GHC.Base.Semigroup (Ide.Logger.Recorder msg)
+ Ide.Logger: instance GHC.Base.Semigroup Ide.Logger.Logger
+ Ide.Logger: instance GHC.Classes.Eq Ide.Logger.Priority
+ Ide.Logger: instance GHC.Classes.Ord Ide.Logger.Priority
+ Ide.Logger: instance GHC.Enum.Bounded Ide.Logger.Priority
+ Ide.Logger: instance GHC.Enum.Enum Ide.Logger.Priority
+ Ide.Logger: instance GHC.Read.Read Ide.Logger.Priority
+ Ide.Logger: instance GHC.Show.Show Ide.Logger.Priority
+ Ide.Logger: logDebug :: Logger -> Text -> IO ()
+ Ide.Logger: logError :: Logger -> Text -> IO ()
+ Ide.Logger: logInfo :: Logger -> Text -> IO ()
+ Ide.Logger: logWarning :: Logger -> Text -> IO ()
+ Ide.Logger: logWith :: (HasCallStack, MonadIO m) => Recorder (WithPriority msg) -> Priority -> msg -> m ()
+ Ide.Logger: lspClientLogRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
+ Ide.Logger: lspClientMessageRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)
+ Ide.Logger: makeDefaultHandleRecorder :: MonadIO m => Maybe [LoggingColumn] -> Lock -> Handle -> m (Recorder (WithPriority (Doc a)))
+ Ide.Logger: makeDefaultStderrRecorder :: MonadIO m => Maybe [LoggingColumn] -> m (Recorder (WithPriority (Doc a)))
+ Ide.Logger: newtype Logger
+ Ide.Logger: newtype Recorder msg
+ Ide.Logger: noLogging :: Logger
+ Ide.Logger: renderStrict :: SimpleDocStream ann -> Text
+ Ide.Logger: toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)
+ Ide.Logger: withBacklog :: (v -> Recorder a) -> IO (Recorder a, v -> IO ())
+ Ide.Logger: withFileRecorder :: MonadUnliftIO m => FilePath -> Maybe [LoggingColumn] -> (Either IOException (Recorder (WithPriority (Doc d))) -> m a) -> m a
+ Ide.Plugin.Error: PluginInternalError :: Text -> PluginError
+ Ide.Plugin.Error: PluginInvalidParams :: Text -> PluginError
+ Ide.Plugin.Error: PluginInvalidUserState :: Text -> PluginError
+ Ide.Plugin.Error: PluginRequestRefused :: Text -> PluginError
+ Ide.Plugin.Error: PluginRuleFailed :: Text -> PluginError
+ Ide.Plugin.Error: PluginStaleResolve :: PluginError
+ Ide.Plugin.Error: data PluginError
+ Ide.Plugin.Error: getNormalizedFilePathE :: Monad m => Uri -> ExceptT PluginError m NormalizedFilePath
+ Ide.Plugin.Error: handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b
+ Ide.Plugin.Error: handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b
+ Ide.Plugin.Error: instance Prettyprinter.Internal.Pretty Ide.Plugin.Error.PluginError
+ Ide.Plugin.Error: toErrorCode :: PluginError -> LSPErrorCodes |? ErrorCodes
+ Ide.Plugin.Error: toPriority :: PluginError -> Priority
+ Ide.Plugin.Resolve: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Plugin.Resolve.WithURI
+ Ide.Plugin.Resolve: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Plugin.Resolve.WithURI
+ Ide.Plugin.Resolve: instance GHC.Generics.Generic Ide.Plugin.Resolve.WithURI
+ Ide.Plugin.Resolve: instance GHC.Show.Show Ide.Plugin.Resolve.WithURI
+ Ide.Plugin.Resolve: instance Prettyprinter.Internal.Pretty Ide.Plugin.Resolve.Log
+ Ide.Plugin.Resolve: mkCodeActionHandlerWithResolve :: forall ideState a. FromJSON a => Recorder (WithPriority Log) -> PluginMethodHandler ideState 'Method_TextDocumentCodeAction -> ResolveFunction ideState a 'Method_CodeActionResolve -> PluginHandlers ideState
+ Ide.Plugin.Resolve: mkCodeActionWithResolveAndCommand :: forall ideState a. FromJSON a => Recorder (WithPriority Log) -> PluginId -> PluginMethodHandler ideState 'Method_TextDocumentCodeAction -> ResolveFunction ideState a 'Method_CodeActionResolve -> ([PluginCommand ideState], PluginHandlers ideState)
+ Ide.PluginUtils: extendToFullLines :: Range -> Range
+ Ide.PluginUtils: extractTextInRange :: Range -> Text -> Text
+ Ide.Types: instance (Language.LSP.Protocol.Internal.Lens.HasTextDocument a doc, Language.LSP.Protocol.Internal.Lens.HasUri doc Language.LSP.Protocol.Types.Uri.Uri) => Ide.Types.HasTracing a
+ Ide.Types: instance Data.Aeson.Types.FromJSON.FromJSON Ide.Types.PluginResolveData
+ Ide.Types: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Types.PluginId
+ Ide.Types: instance Data.Aeson.Types.ToJSON.ToJSON Ide.Types.PluginResolveData
+ Ide.Types: instance GHC.Generics.Generic Ide.Types.PluginResolveData
+ Ide.Types: instance GHC.Show.Show Ide.Types.PluginResolveData
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.CallHierarchyIncomingCallsParams.CallHierarchyIncomingCallsParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.CallHierarchyOutgoingCallsParams.CallHierarchyOutgoingCallsParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.CodeAction.CodeAction
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.CodeLens.CodeLens
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.CompletionItem.CompletionItem
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.DidChangeConfigurationParams.DidChangeConfigurationParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.DidChangeWatchedFilesParams.DidChangeWatchedFilesParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.DidChangeWorkspaceFoldersParams.DidChangeWorkspaceFoldersParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.DocumentLink.DocumentLink
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.ExecuteCommandParams.ExecuteCommandParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.InitializeParams.InitializeParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.InitializedParams.InitializedParams
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.InlayHint.InlayHint
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.WorkspaceSymbol.WorkspaceSymbol
+ Ide.Types: instance Ide.Types.HasTracing Language.LSP.Protocol.Internal.Types.WorkspaceSymbolParams.WorkspaceSymbolParams
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_Initialized
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidChange
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidClose
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidOpen
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidSave
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeConfiguration
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeWatchedFiles
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Notification 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeWorkspaceFolders
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_CallHierarchyIncomingCalls
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_CallHierarchyOutgoingCalls
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_CodeActionResolve
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_CodeLensResolve
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_CompletionItemResolve
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCodeAction
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCodeLens
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCompletion
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDefinition
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDocumentHighlight
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDocumentSymbol
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentFoldingRange
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentFormatting
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentHover
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentPrepareCallHierarchy
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentRangeFormatting
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentReferences
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentRename
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentSelectionRange
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentTypeDefinition
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceSymbol
+ Ide.Types: instance Ide.Types.PluginMethod 'Language.LSP.Protocol.Message.Meta.Request ('Language.LSP.Protocol.Internal.Method.Method_CustomMethod m)
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_Initialized
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidChange
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidClose
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidOpen
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDidSave
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeConfiguration
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeWatchedFiles
+ Ide.Types: instance Ide.Types.PluginNotificationMethod 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceDidChangeWorkspaceFolders
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_CallHierarchyIncomingCalls
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_CallHierarchyOutgoingCalls
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_CodeActionResolve
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_CodeLensResolve
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_CompletionItemResolve
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCodeAction
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCodeLens
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentCompletion
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDefinition
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDocumentHighlight
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentDocumentSymbol
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentFoldingRange
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentFormatting
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentHover
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentPrepareCallHierarchy
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentRangeFormatting
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentReferences
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentRename
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentSelectionRange
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_TextDocumentTypeDefinition
+ Ide.Types: instance Ide.Types.PluginRequestMethod 'Language.LSP.Protocol.Internal.Method.Method_WorkspaceSymbol
+ Ide.Types: instance Ide.Types.PluginRequestMethod ('Language.LSP.Protocol.Internal.Method.Method_CustomMethod m)
+ Ide.Types: mkResolveHandler :: forall ideState a m. (FromJSON a, PluginRequestMethod m, HasData_ (MessageParams m) (Maybe Value)) => SClientMethod m -> ResolveFunction ideState a m -> PluginHandlers ideState
+ Ide.Types: type ResolveFunction ideState a (m :: Method ClientToServer Request) = ideState -> PluginId -> MessageParams m -> Uri -> a -> ExceptT PluginError (LspM Config) (MessageResult m)
- Ide.PluginUtils: diffText :: ClientCapabilities -> (Uri, Text) -> Text -> WithDeletions -> WorkspaceEdit
+ Ide.PluginUtils: diffText :: ClientCapabilities -> (VersionedTextDocumentIdentifier, Text) -> Text -> WithDeletions -> WorkspaceEdit
- Ide.PluginUtils: diffText' :: Bool -> (Uri, Text) -> Text -> WithDeletions -> WorkspaceEdit
+ Ide.PluginUtils: diffText' :: Bool -> (VersionedTextDocumentIdentifier, Text) -> Text -> WithDeletions -> WorkspaceEdit
- Ide.PluginUtils: makeDiffTextEdit :: Text -> Text -> List TextEdit
+ Ide.PluginUtils: makeDiffTextEdit :: Text -> Text -> [TextEdit]
- Ide.PluginUtils: makeDiffTextEditAdditive :: Text -> Text -> List TextEdit
+ Ide.PluginUtils: makeDiffTextEditAdditive :: Text -> Text -> [TextEdit]
- Ide.Types: IdeMethod :: SMethod m -> IdeMethod (m :: Method FromClient Request)
+ Ide.Types: IdeMethod :: SMethod m -> IdeMethod (m :: Method ClientToServer Request)
- Ide.Types: IdeNotification :: SMethod m -> IdeNotification (m :: Method FromClient Notification)
+ Ide.Types: IdeNotification :: SMethod m -> IdeNotification (m :: Method ClientToServer Notification)
- Ide.Types: PluginDescriptor :: !PluginId -> Natural -> !Rules () -> ![PluginCommand ideState] -> PluginHandlers ideState -> ConfigDescriptor -> PluginNotificationHandlers ideState -> DynFlagsModifications -> Maybe (ParserInfo (IdeCommand ideState)) -> [Text] -> PluginDescriptor (ideState :: *)
+ Ide.Types: PluginDescriptor :: !PluginId -> Natural -> !Rules () -> ![PluginCommand ideState] -> PluginHandlers ideState -> ConfigDescriptor -> PluginNotificationHandlers ideState -> DynFlagsModifications -> Maybe (ParserInfo (IdeCommand ideState)) -> [Text] -> PluginDescriptor (ideState :: Type)
- Ide.Types: PluginHandler :: (PluginId -> a -> MessageParams m -> LspM Config (NonEmpty (Either ResponseError (ResponseResult m)))) -> PluginHandler a (m :: Method FromClient Request)
+ Ide.Types: PluginHandler :: (PluginId -> a -> MessageParams m -> LspM Config (NonEmpty (Either PluginError (MessageResult m)))) -> PluginHandler a (m :: Method ClientToServer Request)
- Ide.Types: PluginNotificationHandler :: (PluginId -> a -> VFS -> MessageParams m -> LspM Config ()) -> PluginNotificationHandler a (m :: Method FromClient Notification)
+ Ide.Types: PluginNotificationHandler :: (PluginId -> a -> VFS -> MessageParams m -> LspM Config ()) -> PluginNotificationHandler a (m :: Method ClientToServer Notification)
- Ide.Types: [pluginCli] :: PluginDescriptor (ideState :: *) -> Maybe (ParserInfo (IdeCommand ideState))
+ Ide.Types: [pluginCli] :: PluginDescriptor (ideState :: Type) -> Maybe (ParserInfo (IdeCommand ideState))
- Ide.Types: [pluginCommands] :: PluginDescriptor (ideState :: *) -> ![PluginCommand ideState]
+ Ide.Types: [pluginCommands] :: PluginDescriptor (ideState :: Type) -> ![PluginCommand ideState]
- Ide.Types: [pluginConfigDescriptor] :: PluginDescriptor (ideState :: *) -> ConfigDescriptor
+ Ide.Types: [pluginConfigDescriptor] :: PluginDescriptor (ideState :: Type) -> ConfigDescriptor
- Ide.Types: [pluginFileType] :: PluginDescriptor (ideState :: *) -> [Text]
+ Ide.Types: [pluginFileType] :: PluginDescriptor (ideState :: Type) -> [Text]
- Ide.Types: [pluginHandlers] :: PluginDescriptor (ideState :: *) -> PluginHandlers ideState
+ Ide.Types: [pluginHandlers] :: PluginDescriptor (ideState :: Type) -> PluginHandlers ideState
- Ide.Types: [pluginId] :: PluginDescriptor (ideState :: *) -> !PluginId
+ Ide.Types: [pluginId] :: PluginDescriptor (ideState :: Type) -> !PluginId
- Ide.Types: [pluginModifyDynflags] :: PluginDescriptor (ideState :: *) -> DynFlagsModifications
+ Ide.Types: [pluginModifyDynflags] :: PluginDescriptor (ideState :: Type) -> DynFlagsModifications
- Ide.Types: [pluginNotificationHandlers] :: PluginDescriptor (ideState :: *) -> PluginNotificationHandlers ideState
+ Ide.Types: [pluginNotificationHandlers] :: PluginDescriptor (ideState :: Type) -> PluginNotificationHandlers ideState
- Ide.Types: [pluginPriority] :: PluginDescriptor (ideState :: *) -> Natural
+ Ide.Types: [pluginPriority] :: PluginDescriptor (ideState :: Type) -> Natural
- Ide.Types: [pluginRules] :: PluginDescriptor (ideState :: *) -> !Rules ()
+ Ide.Types: [pluginRules] :: PluginDescriptor (ideState :: Type) -> !Rules ()
- Ide.Types: class HasTracing (MessageParams m) => PluginMethod (k :: MethodType) (m :: Method FromClient k)
+ Ide.Types: class HasTracing (MessageParams m) => PluginMethod (k :: MessageKind) (m :: Method ClientToServer k)
- Ide.Types: class PluginMethod Request m => PluginRequestMethod (m :: Method FromClient Request)
+ Ide.Types: class PluginMethod Request m => PluginRequestMethod (m :: Method ClientToServer Request)
- Ide.Types: combineResponses :: (PluginRequestMethod m, Semigroup (ResponseResult m)) => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (ResponseResult m) -> ResponseResult m
+ Ide.Types: combineResponses :: (PluginRequestMethod m, Semigroup (MessageResult m)) => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (MessageResult m) -> MessageResult m
- Ide.Types: data IdeMethod (m :: Method FromClient Request)
+ Ide.Types: data IdeMethod (m :: Method ClientToServer Request)
- Ide.Types: data IdeNotification (m :: Method FromClient Notification)
+ Ide.Types: data IdeNotification (m :: Method ClientToServer Notification)
- Ide.Types: data PluginDescriptor (ideState :: *)
+ Ide.Types: data PluginDescriptor (ideState :: Type)
- Ide.Types: mkPluginHandler :: PluginRequestMethod m => SClientMethod m -> PluginMethodHandler ideState m -> PluginHandlers ideState
+ Ide.Types: mkPluginHandler :: forall ideState m. PluginRequestMethod m => SClientMethod m -> PluginMethodHandler ideState m -> PluginHandlers ideState
- Ide.Types: mkPluginNotificationHandler :: PluginNotificationMethod m => SClientMethod (m :: Method FromClient Notification) -> PluginNotificationMethodHandler ideState m -> PluginNotificationHandlers ideState
+ Ide.Types: mkPluginNotificationHandler :: PluginNotificationMethod m => SClientMethod (m :: Method ClientToServer Notification) -> PluginNotificationMethodHandler ideState m -> PluginNotificationHandlers ideState
- Ide.Types: newtype PluginHandler a (m :: Method FromClient Request)
+ Ide.Types: newtype PluginHandler a (m :: Method ClientToServer Request)
- Ide.Types: newtype PluginNotificationHandler a (m :: Method FromClient Notification)
+ Ide.Types: newtype PluginNotificationHandler a (m :: Method ClientToServer Notification)
- Ide.Types: type CommandFunction ideState a = ideState -> a -> LspM Config (Either ResponseError Value)
+ Ide.Types: type CommandFunction ideState a = ideState -> a -> ExceptT PluginError (LspM Config) (Value |? Null)
- Ide.Types: type FormattingHandler a = a -> FormattingType -> Text -> NormalizedFilePath -> FormattingOptions -> LspM Config (Either ResponseError (List TextEdit))
+ Ide.Types: type FormattingHandler a = a -> FormattingType -> Text -> NormalizedFilePath -> FormattingOptions -> ExceptT PluginError (LspM Config) ([TextEdit] |? Null)
- Ide.Types: type FormattingMethod m = (HasOptions (MessageParams m) FormattingOptions, HasTextDocument (MessageParams m) TextDocumentIdentifier, ResponseResult m ~ List TextEdit)
+ Ide.Types: type FormattingMethod m = (HasOptions (MessageParams m) FormattingOptions, HasTextDocument (MessageParams m) TextDocumentIdentifier, MessageResult m ~ ([TextEdit] |? Null))
- Ide.Types: type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> LspM Config (Either ResponseError (ResponseResult m))
+ Ide.Types: type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> ExceptT PluginError (LspM Config) (MessageResult m)
Files
- hls-plugin-api.cabal +13/−3
- src/Ide/Logger.hs +316/−0
- src/Ide/Plugin/ConfigUtils.hs +26/−25
- src/Ide/Plugin/Error.hs +147/−0
- src/Ide/Plugin/RangeMap.hs +2/−3
- src/Ide/Plugin/Resolve.hs +210/−0
- src/Ide/PluginUtils.hs +111/−119
- src/Ide/Types.hs +405/−249
- test/Ide/PluginUtilsTest.hs +7/−7
hls-plugin-api.cabal view
@@ -1,6 +1,6 @@ cabal-version: 2.4 name: hls-plugin-api-version: 2.0.0.1+version: 2.1.0.0 synopsis: Haskell Language Server API for plugin communication description: Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>@@ -34,17 +34,21 @@ library exposed-modules:+ Ide.Plugin.Error Ide.Plugin.Config Ide.Plugin.ConfigUtils Ide.Plugin.Properties Ide.Plugin.RangeMap+ Ide.Plugin.Resolve Ide.PluginUtils Ide.Types+ Ide.Logger hs-source-dirs: src build-depends: , aeson , base >=4.12 && <5+ , co-log-core , containers , data-default , dependent-map@@ -55,15 +59,21 @@ , filepath , ghc , hashable- , hls-graph == 2.0.0.1+ , hls-graph == 2.1.0.0 , lens , lens-aeson- , lsp ^>=1.6.0.0+ , lsp ^>=2.1.0.0+ , mtl , opentelemetry >=0.4 , optparse-applicative+ , prettyprinter , regex-tdfa >=1.3.1.0+ , row-types+ , stm , text+ , time , transformers+ , unliftio , unordered-containers , megaparsec > 9
+ src/Ide/Logger.hs view
@@ -0,0 +1,316 @@+-- Copyright (c) 2019 The DAML Authors. All rights reserved.+-- SPDX-License-Identifier: Apache-2.0++{-# LANGUAGE DeriveFunctor #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- | This is a compatibility module that abstracts over the+-- concrete choice of logging framework so users can plug in whatever+-- framework they want to.+module Ide.Logger+ ( Priority(..)+ , Logger(..)+ , Recorder(..)+ , logError, logWarning, logInfo, logDebug+ , noLogging+ , WithPriority(..)+ , logWith+ , cmap+ , cmapIO+ , cfilter+ , withFileRecorder+ , makeDefaultStderrRecorder+ , makeDefaultHandleRecorder+ , LoggingColumn(..)+ , cmapWithPrio+ , withBacklog+ , lspClientMessageRecorder+ , lspClientLogRecorder+ , module PrettyPrinterModule+ , renderStrict+ , toCologActionWithPrio+ ) where++import Colog.Core (LogAction (..), Severity,+ WithSeverity (..))+import qualified Colog.Core as Colog+import Control.Concurrent (myThreadId)+import Control.Concurrent.Extra (Lock, newLock, withLock)+import Control.Concurrent.STM (atomically, flushTBQueue,+ isFullTBQueue, newTBQueueIO,+ newTVarIO, readTVarIO,+ writeTBQueue, writeTVar)+import Control.Exception (IOException)+import Control.Monad (unless, when, (>=>))+import Control.Monad.IO.Class (MonadIO (liftIO))+import Data.Foldable (for_)+import Data.Functor.Contravariant (Contravariant (contramap))+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text as Text+import qualified Data.Text.IO as Text+import Data.Time (defaultTimeLocale, formatTime,+ getCurrentTime)+import GHC.Stack (CallStack, HasCallStack,+ SrcLoc (SrcLoc, srcLocModule, srcLocStartCol, srcLocStartLine),+ callStack, getCallStack,+ withFrozenCallStack)+import Language.LSP.Protocol.Message (SMethod (SMethod_WindowLogMessage, SMethod_WindowShowMessage))+import Language.LSP.Protocol.Types (LogMessageParams (..),+ MessageType (..),+ ShowMessageParams (..))+import Language.LSP.Server+import qualified Language.LSP.Server as LSP+import Prettyprinter as PrettyPrinterModule+import Prettyprinter.Render.Text (renderStrict)+import System.IO (Handle, IOMode (AppendMode),+ hClose, hFlush, openFile,+ stderr)+import UnliftIO (MonadUnliftIO, finally, try)++data Priority+-- Don't change the ordering of this type or you will mess up the Ord+-- instance+ = Debug -- ^ Verbose debug logging.+ | Info -- ^ Useful information in case an error has to be understood.+ | Warning+ -- ^ These error messages should not occur in a expected usage, and+ -- should be investigated.+ | Error -- ^ Such log messages must never occur in expected usage.+ deriving (Eq, Show, Read, Ord, Enum, Bounded)++-- | Note that this is logging actions _of the program_, not of the user.+-- You shouldn't call warning/error if the user has caused an error, only+-- if our code has gone wrong and is itself erroneous (e.g. we threw an exception).+newtype Logger = Logger {logPriority :: Priority -> T.Text -> IO ()}++instance Semigroup Logger where+ l1 <> l2 = Logger $ \p t -> logPriority l1 p t >> logPriority l2 p t++instance Monoid Logger where+ mempty = Logger $ \_ _ -> pure ()++logError :: Logger -> T.Text -> IO ()+logError x = logPriority x Error++logWarning :: Logger -> T.Text -> IO ()+logWarning x = logPriority x Warning++logInfo :: Logger -> T.Text -> IO ()+logInfo x = logPriority x Info++logDebug :: Logger -> T.Text -> IO ()+logDebug x = logPriority x Debug++noLogging :: Logger+noLogging = Logger $ \_ _ -> return ()++data WithPriority a = WithPriority { priority :: Priority, callStack_ :: CallStack, payload :: a } deriving Functor++-- | Note that this is logging actions _of the program_, not of the user.+-- You shouldn't call warning/error if the user has caused an error, only+-- if our code has gone wrong and is itself erroneous (e.g. we threw an exception).+newtype Recorder msg = Recorder+ { logger_ :: forall m. (MonadIO m) => msg -> m () }++logWith :: (HasCallStack, MonadIO m) => Recorder (WithPriority msg) -> Priority -> msg -> m ()+logWith recorder priority msg = withFrozenCallStack $ logger_ recorder (WithPriority priority callStack msg)++instance Semigroup (Recorder msg) where+ (<>) Recorder{ logger_ = logger_1 } Recorder{ logger_ = logger_2 } =+ Recorder+ { logger_ = \msg -> logger_1 msg >> logger_2 msg }++instance Monoid (Recorder msg) where+ mempty =+ Recorder+ { logger_ = \_ -> pure () }++instance Contravariant Recorder where+ contramap f Recorder{ logger_ } =+ Recorder+ { logger_ = logger_ . f }++cmap :: (a -> b) -> Recorder b -> Recorder a+cmap = contramap++cmapWithPrio :: (a -> b) -> Recorder (WithPriority b) -> Recorder (WithPriority a)+cmapWithPrio f = cmap (fmap f)++cmapIO :: (a -> IO b) -> Recorder b -> Recorder a+cmapIO f Recorder{ logger_ } =+ Recorder+ { logger_ = (liftIO . f) >=> logger_ }++cfilter :: (a -> Bool) -> Recorder a -> Recorder a+cfilter p Recorder{ logger_ } =+ Recorder+ { logger_ = \msg -> when (p msg) (logger_ msg) }++textHandleRecorder :: Handle -> Recorder Text+textHandleRecorder handle =+ Recorder+ { logger_ = \text -> liftIO $ Text.hPutStrLn handle text *> hFlush handle }++makeDefaultStderrRecorder :: MonadIO m => Maybe [LoggingColumn] -> m (Recorder (WithPriority (Doc a)))+makeDefaultStderrRecorder columns = do+ lock <- liftIO newLock+ makeDefaultHandleRecorder columns lock stderr++withFileRecorder+ :: MonadUnliftIO m+ => FilePath+ -- ^ Log file path.+ -> Maybe [LoggingColumn]+ -- ^ logging columns to display. `Nothing` uses `defaultLoggingColumns`+ -> (Either IOException (Recorder (WithPriority (Doc d))) -> m a)+ -- ^ action given a recorder, or the exception if we failed to open the file+ -> m a+withFileRecorder path columns action = do+ lock <- liftIO newLock+ let makeHandleRecorder = makeDefaultHandleRecorder columns lock+ fileHandle :: Either IOException Handle <- liftIO $ try (openFile path AppendMode)+ case fileHandle of+ Left e -> action $ Left e+ Right fileHandle -> finally ((Right <$> makeHandleRecorder fileHandle) >>= action) (liftIO $ hClose fileHandle)++makeDefaultHandleRecorder+ :: MonadIO m+ => Maybe [LoggingColumn]+ -- ^ built-in logging columns to display. Nothing uses the default+ -> Lock+ -- ^ lock to take when outputting to handle+ -> Handle+ -- ^ handle to output to+ -> m (Recorder (WithPriority (Doc a)))+makeDefaultHandleRecorder columns lock handle = do+ let Recorder{ logger_ } = textHandleRecorder handle+ let threadSafeRecorder = Recorder { logger_ = \msg -> liftIO $ withLock lock (logger_ msg) }+ let loggingColumns = fromMaybe defaultLoggingColumns columns+ let textWithPriorityRecorder = cmapIO (textWithPriorityToText loggingColumns) threadSafeRecorder+ pure (cmap docToText textWithPriorityRecorder)+ where+ docToText = fmap (renderStrict . layoutPretty defaultLayoutOptions)++data LoggingColumn+ = TimeColumn+ | ThreadIdColumn+ | PriorityColumn+ | DataColumn+ | SourceLocColumn++defaultLoggingColumns :: [LoggingColumn]+defaultLoggingColumns = [TimeColumn, PriorityColumn, DataColumn]++textWithPriorityToText :: [LoggingColumn] -> WithPriority Text -> IO Text+textWithPriorityToText columns WithPriority{ priority, callStack_, payload } = do+ textColumns <- mapM loggingColumnToText columns+ pure $ Text.intercalate " | " textColumns+ where+ showAsText :: Show a => a -> Text+ showAsText = Text.pack . show++ utcTimeToText utcTime = Text.pack $ formatTime defaultTimeLocale "%Y-%m-%dT%H:%M:%S%6QZ" utcTime++ priorityToText :: Priority -> Text+ priorityToText = showAsText++ threadIdToText = showAsText++ callStackToSrcLoc :: CallStack -> Maybe SrcLoc+ callStackToSrcLoc callStack =+ case getCallStack callStack of+ (_, srcLoc) : _ -> Just srcLoc+ _ -> Nothing++ srcLocToText = \case+ Nothing -> "<unknown>"+ Just SrcLoc{ srcLocModule, srcLocStartLine, srcLocStartCol } ->+ Text.pack srcLocModule <> "#" <> showAsText srcLocStartLine <> ":" <> showAsText srcLocStartCol++ loggingColumnToText :: LoggingColumn -> IO Text+ loggingColumnToText = \case+ TimeColumn -> do+ utcTime <- getCurrentTime+ pure (utcTimeToText utcTime)+ SourceLocColumn -> pure $ (srcLocToText . callStackToSrcLoc) callStack_+ ThreadIdColumn -> do+ threadId <- myThreadId+ pure (threadIdToText threadId)+ PriorityColumn -> pure (priorityToText priority)+ DataColumn -> pure payload++-- | Given a 'Recorder' that requires an argument, produces a 'Recorder'+-- that queues up messages until the argument is provided using the callback, at which+-- point it sends the backlog and begins functioning normally.+withBacklog :: (v -> Recorder a) -> IO (Recorder a, v -> IO ())+withBacklog recFun = do+ -- Arbitrary backlog capacity+ backlog <- newTBQueueIO 100+ let backlogRecorder = Recorder $ \it -> liftIO $ atomically $ do+ -- If the queue is full just drop the message on the floor. This is most likely+ -- to happen if the callback is just never going to be called; in which case+ -- we want neither to build up an unbounded backlog in memory, nor block waiting+ -- for space!+ full <- isFullTBQueue backlog+ unless full $ writeTBQueue backlog it++ -- The variable holding the recorder starts out holding the recorder that writes+ -- to the backlog.+ recVar <- newTVarIO backlogRecorder+ -- The callback atomically swaps out the recorder for the final one, and flushes+ -- the backlog to it.+ let cb arg = do+ let recorder = recFun arg+ toRecord <- atomically $ writeTVar recVar recorder >> flushTBQueue backlog+ for_ toRecord (logger_ recorder)++ -- The recorder we actually return looks in the variable and uses whatever is there.+ let varRecorder = Recorder $ \it -> do+ r <- liftIO $ readTVarIO recVar+ logger_ r it++ pure (varRecorder, cb)++-- | Creates a recorder that sends logs to the LSP client via @window/showMessage@ notifications.+lspClientMessageRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)+lspClientMessageRecorder env = Recorder $ \WithPriority {..} ->+ liftIO $ LSP.runLspT env $ LSP.sendNotification SMethod_WindowShowMessage+ ShowMessageParams+ { _type_ = priorityToLsp priority,+ _message = payload+ }++-- | Creates a recorder that sends logs to the LSP client via @window/logMessage@ notifications.+lspClientLogRecorder :: LanguageContextEnv config -> Recorder (WithPriority Text)+lspClientLogRecorder env = Recorder $ \WithPriority {..} ->+ liftIO $ LSP.runLspT env $ LSP.sendNotification SMethod_WindowLogMessage+ LogMessageParams+ { _type_ = priorityToLsp priority,+ _message = payload+ }++priorityToLsp :: Priority -> MessageType+priorityToLsp =+ \case+ Debug -> MessageType_Log+ Info -> MessageType_Info+ Warning -> MessageType_Warning+ Error -> MessageType_Error++toCologActionWithPrio :: (MonadIO m, HasCallStack) => Recorder (WithPriority msg) -> LogAction m (WithSeverity msg)+toCologActionWithPrio (Recorder _logger) = LogAction $ \WithSeverity{..} -> do+ let priority = severityToPriority getSeverity+ _logger $ WithPriority priority callStack getMsg+ where+ severityToPriority :: Severity -> Priority+ severityToPriority Colog.Debug = Debug+ severityToPriority Colog.Info = Info+ severityToPriority Colog.Warning = Warning+ severityToPriority Colog.Error = Error
src/Ide/Plugin/ConfigUtils.hs view
@@ -5,20 +5,21 @@ module Ide.Plugin.ConfigUtils where -import Control.Lens (at, ix, (&), (?~))-import qualified Data.Aeson as A-import Data.Aeson.Lens (_Object)-import qualified Data.Aeson.Types as A+import Control.Lens (at, ix, (&), (?~))+import qualified Data.Aeson as A+import Data.Aeson.Lens (_Object)+import qualified Data.Aeson.Types as A import Data.Default-import qualified Data.Dependent.Map as DMap-import qualified Data.Dependent.Sum as DSum-import Data.List.Extra (nubOrd)-import Data.String (IsString (fromString))-import qualified Data.Text as T+import qualified Data.Dependent.Map as DMap+import qualified Data.Dependent.Sum as DSum+import Data.List.Extra (nubOrd)+import Data.String (IsString (fromString))+import qualified Data.Text as T import Ide.Plugin.Config-import Ide.Plugin.Properties (toDefaultJSON, toVSCodeExtensionSchema)+import Ide.Plugin.Properties (toDefaultJSON,+ toVSCodeExtensionSchema) import Ide.Types-import Language.LSP.Types+import Language.LSP.Protocol.Message -- Attention: -- 'diagnosticsOn' will never be added into the default config or the schema,@@ -86,13 +87,13 @@ -- This function captures ide methods registered by the plugin, and then converts it to kv pairs handlersToGenericDefaultConfig :: PluginConfig -> DSum.DSum IdeMethod f -> [A.Pair] handlersToGenericDefaultConfig PluginConfig{..} (IdeMethod m DSum.:=> _) = case m of- STextDocumentCodeAction -> ["codeActionsOn" A..= plcCodeActionsOn]- STextDocumentCodeLens -> ["codeLensOn" A..= plcCodeLensOn]- STextDocumentRename -> ["renameOn" A..= plcRenameOn]- STextDocumentHover -> ["hoverOn" A..= plcHoverOn]- STextDocumentDocumentSymbol -> ["symbolsOn" A..= plcSymbolsOn]- STextDocumentCompletion -> ["completionOn" A..= plcCompletionOn]- STextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= plcCallHierarchyOn]+ SMethod_TextDocumentCodeAction -> ["codeActionsOn" A..= plcCodeActionsOn]+ SMethod_TextDocumentCodeLens -> ["codeLensOn" A..= plcCodeLensOn]+ SMethod_TextDocumentRename -> ["renameOn" A..= plcRenameOn]+ SMethod_TextDocumentHover -> ["hoverOn" A..= plcHoverOn]+ SMethod_TextDocumentDocumentSymbol -> ["symbolsOn" A..= plcSymbolsOn]+ SMethod_TextDocumentCompletion -> ["completionOn" A..= plcCompletionOn]+ SMethod_TextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= plcCallHierarchyOn] _ -> [] -- | Generates json schema used in haskell vscode extension@@ -116,13 +117,13 @@ _ -> x dedicatedSchema = customConfigToDedicatedSchema configCustomConfig handlersToGenericSchema (IdeMethod m DSum.:=> _) = case m of- STextDocumentCodeAction -> [toKey' "codeActionsOn" A..= schemaEntry "code actions"]- STextDocumentCodeLens -> [toKey' "codeLensOn" A..= schemaEntry "code lenses"]- STextDocumentRename -> [toKey' "renameOn" A..= schemaEntry "rename"]- STextDocumentHover -> [toKey' "hoverOn" A..= schemaEntry "hover"]- STextDocumentDocumentSymbol -> [toKey' "symbolsOn" A..= schemaEntry "symbols"]- STextDocumentCompletion -> [toKey' "completionOn" A..= schemaEntry "completions"]- STextDocumentPrepareCallHierarchy -> [toKey' "callHierarchyOn" A..= schemaEntry "call hierarchy"]+ SMethod_TextDocumentCodeAction -> [toKey' "codeActionsOn" A..= schemaEntry "code actions"]+ SMethod_TextDocumentCodeLens -> [toKey' "codeLensOn" A..= schemaEntry "code lenses"]+ SMethod_TextDocumentRename -> [toKey' "renameOn" A..= schemaEntry "rename"]+ SMethod_TextDocumentHover -> [toKey' "hoverOn" A..= schemaEntry "hover"]+ SMethod_TextDocumentDocumentSymbol -> [toKey' "symbolsOn" A..= schemaEntry "symbols"]+ SMethod_TextDocumentCompletion -> [toKey' "completionOn" A..= schemaEntry "completions"]+ SMethod_TextDocumentPrepareCallHierarchy -> [toKey' "callHierarchyOn" A..= schemaEntry "call hierarchy"] _ -> [] schemaEntry desc = A.object
+ src/Ide/Plugin/Error.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+module Ide.Plugin.Error (+ -- * Plugin Error Handling API+ PluginError(..),+ toErrorCode,+ toPriority,+ handleMaybe,+ handleMaybeM,+ getNormalizedFilePathE,+) where++import Control.Monad.Extra (maybeM)+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except (ExceptT (..), throwE)+import qualified Data.Text as T+import Ide.Logger+import Language.LSP.Protocol.Types++-- ----------------------------------------------------------------------------+-- Plugin Error wrapping+-- ----------------------------------------------------------------------------++-- |Each PluginError corresponds to either a specific ResponseError we want to+-- return or a specific way we want to log the error. If the currently present+-- ones are insufficient for the needs of your plugin, please feel free to add+-- a new one.+--+-- Currently the PluginErrors we provide can be broken up into several groups.+-- First is PluginInternalError, which is the most serious of the errors, and+-- also the "default" error that is used for things such as uncaught exceptions.+-- Then we have PluginInvalidParams, which along with PluginInternalError map+-- to a corresponding ResponseError.+--+-- Next we have PluginRuleFailed and PluginInvalidUserState, with the only+-- difference being PluginRuleFailed is specific to Shake rules and+-- PluginInvalidUserState can be used for everything else. Both of these are+-- "non-errors", and happen whenever the user's code is in a state where the+-- plugin is unable to provide a answer to the users request. PluginStaleResolve+-- is similar to the above two Error types, but is specific to resolve plugins,+-- and is used only when the data provided by the resolve request is stale,+-- preventing the proper resolution of it.+--+-- Finally we have the outlier, PluginRequestRefused, where we allow a handler+-- to preform "pluginEnabled" checks inside the handler, and reject the request+-- after viewing it. The behavior of only one handler passing `pluginEnabled`+-- and then returning PluginRequestRefused should be the same as if no plugins+-- passed the `pluginEnabled` stage.+data PluginError+ = -- |PluginInternalError should be used if an error has occurred. This+ -- should only rarely be returned. As it's logged with Error, it will be+ -- shown by the client to the user via `showWindow`. All uncaught exceptions+ -- will be caught and converted to this error.+ --+ -- This error will be be converted into an InternalError response code. It+ -- will be logged with Error and takes the highest precedence (1) in being+ -- returned as a response to the client.+ PluginInternalError T.Text+ -- |PluginInvalidParams should be used if the parameters of the request are+ -- invalid. This error means that there is a bug in the client's code+ -- (otherwise they wouldn't be sending you requests with invalid+ -- parameters).+ --+ -- This error will be will be converted into a InvalidParams response code.+ -- It will be logged with Warning and takes medium precedence (2) in being+ -- returned as a response to the client.+ | PluginInvalidParams T.Text+ -- |PluginInvalidUserState should be thrown when a function that your plugin+ -- depends on fails. This should only be used when the function fails+ -- because the user's code is in an invalid state.+ --+ -- This error takes the name of the function that failed. Prefer to catch+ -- this error as close to the source as possible.+ --+ -- This error will be logged with Debug, and will be converted into a+ -- RequestFailed response. It takes a low precedence (3) in being returned+ -- as a response to the client.+ | PluginInvalidUserState T.Text+ -- |PluginRequestRefused allows your handler to inspect a request before+ -- rejecting it. In effect it allows your plugin to act make a secondary+ -- `pluginEnabled` decision after receiving the request. This should only be+ -- used if the decision to accept the request can not be made in+ -- `pluginEnabled`.+ --+ -- This error will be with Debug. If it's the only response to a request,+ -- HLS will respond as if no plugins passed the `pluginEnabled` stage.+ | PluginRequestRefused T.Text+ -- |PluginRuleFailed should be thrown when a Rule your response depends on+ -- fails.+ --+ -- This error takes the name of the Rule that failed.+ --+ -- This error will be logged with Debug, and will be converted into a+ -- RequestFailed response code. It takes a low precedence (3) in being+ -- returned as a response to the client.+ | PluginRuleFailed T.Text+ -- |PluginStaleResolve should be thrown when your resolve request is+ -- provided with data it can no longer resolve.+ --+ -- This error will be logged with Debug, and will be converted into a+ -- ContentModified response. It takes a low precedence (3) in being returned+ -- as a response to the client.+ | PluginStaleResolve++instance Pretty PluginError where+ pretty = \case+ PluginInternalError msg -> "Internal Error:" <+> pretty msg+ PluginStaleResolve -> "Stale Resolve"+ PluginRuleFailed rule -> "Rule Failed:" <+> pretty rule+ PluginInvalidParams text -> "Invalid Params:" <+> pretty text+ PluginInvalidUserState text -> "Invalid User State:" <+> pretty text+ PluginRequestRefused msg -> "Request Refused: " <+> pretty msg++-- |Converts to ErrorCode used in LSP ResponseErrors+toErrorCode :: PluginError -> (LSPErrorCodes |? ErrorCodes)+toErrorCode (PluginInternalError _) = InR ErrorCodes_InternalError+toErrorCode (PluginInvalidParams _) = InR ErrorCodes_InvalidParams+toErrorCode (PluginInvalidUserState _) = InL LSPErrorCodes_RequestFailed+-- PluginRequestRefused should never be a argument to `toResponseError`, as+-- it should be dealt with in `extensiblePlugins`, but this is here to make+-- this function complete+toErrorCode (PluginRequestRefused _) = InR ErrorCodes_MethodNotFound+toErrorCode (PluginRuleFailed _) = InL LSPErrorCodes_RequestFailed+toErrorCode PluginStaleResolve = InL LSPErrorCodes_ContentModified++-- |Converts to a logging priority. In addition to being used by the logger,+-- `combineResponses` currently uses this to choose which response to return,+-- so care should be taken in changing it.+toPriority :: PluginError -> Priority+toPriority (PluginInternalError _) = Error+toPriority (PluginInvalidParams _) = Warning+toPriority (PluginInvalidUserState _) = Debug+toPriority (PluginRequestRefused _) = Debug+toPriority (PluginRuleFailed _) = Debug+toPriority PluginStaleResolve = Debug++handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b+handleMaybe msg = maybe (throwE msg) return++handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b+handleMaybeM msg act = maybeM (throwE msg) return $ lift act++getNormalizedFilePathE :: Monad m => Uri -> ExceptT PluginError m NormalizedFilePath+getNormalizedFilePathE uri = handleMaybe (PluginInvalidParams (T.pack $ "uriToNormalizedFile failed. Uri:" <> show uri))+ $ uriToNormalizedFilePath+ $ toNormalizedUri uri
src/Ide/Plugin/RangeMap.hs view
@@ -22,9 +22,8 @@ import Data.Bifunctor (first) import Data.Foldable (foldl') import Development.IDE.Graph.Classes (NFData)-import Language.LSP.Types (Position,- Range (Range),- isSubrangeOf)+import Language.LSP.Protocol.Types (Position,+ Range (Range)) #ifdef USE_FINGERTREE import qualified HaskellWorks.Data.IntervalMap.FingerTree as IM #endif
+ src/Ide/Plugin/Resolve.hs view
@@ -0,0 +1,210 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DisambiguateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedLabels #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE ScopedTypeVariables #-}+module Ide.Plugin.Resolve+(mkCodeActionHandlerWithResolve,+mkCodeActionWithResolveAndCommand) where++import Control.Lens (_Just, (&), (.~), (?~), (^.),+ (^?))+import Control.Monad.Error.Class (MonadError (throwError))+import Control.Monad.Trans.Class (lift)+import Control.Monad.Trans.Except (ExceptT (..), runExceptT)++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.Logger+import Ide.Plugin.Error+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 (LspT,+ ProgressCancellable (Cancellable),+ getClientCapabilities,+ sendRequest,+ withIndefiniteProgress)++data Log+ = DoesNotSupportResolve T.Text+ | ApplyWorkspaceEditFailed ResponseError+instance Pretty Log where+ pretty = \case+ DoesNotSupportResolve fallback->+ "Client does not support resolve," <+> pretty fallback+ ApplyWorkspaceEditFailed err ->+ "ApplyWorkspaceEditFailed:" <+> viaShow err++-- |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) =>+ Recorder (WithPriority Log)+ -> PluginMethodHandler ideState 'Method_TextDocumentCodeAction+ -> ResolveFunction ideState a 'Method_CodeActionResolve+ -> PluginHandlers ideState+mkCodeActionHandlerWithResolve recorder codeActionMethod codeResolveMethod =+ let newCodeActionMethod ideState pid params =+ do codeActionReturn <- codeActionMethod ideState pid params+ caps <- lift getClientCapabilities+ case codeActionReturn of+ r@(InR Null) -> pure r+ (InL ls) | -- We don't need to do anything if the client supports+ -- resolve+ supportsCodeActionResolve caps -> pure $ InL ls+ --This is the actual part where we call resolveCodeAction which fills in the edit data for the client+ | otherwise -> do+ logWith recorder Debug (DoesNotSupportResolve "filling in the code action")+ 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 PluginError (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 -> throwError $ parseError (Just value) (T.pack err)+ A.Success innerValueDecoded -> do+ resolveResult <- codeResolveMethod ideState pid codeAction uri innerValueDecoded+ case resolveResult of+ CodeAction {_edit = Just _ } -> do+ pure $ InR $ dropData resolveResult+ _ -> throwError $ invalidParamsError "Returned CodeAction has no data field"+ resolveCodeAction _ _ _ (InR CodeAction{_data_=Nothing}) = throwError $ 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) =>+ Recorder (WithPriority Log)+ -> PluginId+ -> PluginMethodHandler ideState 'Method_TextDocumentCodeAction+ -> ResolveFunction ideState a 'Method_CodeActionResolve+ -> ([PluginCommand ideState], PluginHandlers ideState)+mkCodeActionWithResolveAndCommand recorder plId codeActionMethod codeResolveMethod =+ let newCodeActionMethod ideState pid params =+ do codeActionReturn <- codeActionMethod ideState pid params+ caps <- lift getClientCapabilities+ case codeActionReturn of+ r@(InR Null) -> pure r+ (InL ls) | -- We don't need to do anything if the client supports+ -- resolve+ 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 -> do+ logWith recorder Debug (DoesNotSupportResolve "rewriting the code action to use commands")+ 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 :: ResolveFunction ideState a 'Method_CodeActionResolve -> CommandFunction ideState CodeAction+ executeResolveCmd resolveProvider ideState ca@CodeAction{_data_=Just value} = do+ ExceptT $ withIndefiniteProgress "Applying edits for code action..." Cancellable $ runExceptT $ do+ case A.fromJSON value of+ A.Error err -> throwError $ parseError (Just value) (T.pack err)+ A.Success (WithURI uri innerValue) -> do+ case A.fromJSON innerValue of+ A.Error err -> throwError $ parseError (Just value) (T.pack err)+ A.Success innerValueDecoded -> do+ resolveResult <- resolveProvider ideState plId ca uri innerValueDecoded+ case resolveResult of+ ca2@CodeAction {_edit = Just wedits } | diffCodeActions ca ca2 == ["edit"] -> do+ _ <- ExceptT $ Right <$> sendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wedits) handleWEditCallback+ pure $ InR Null+ ca2@CodeAction {_edit = Just _ } ->+ throwError $ internalError $+ "The resolve provider unexpectedly returned a code action with the following differing fields: "+ <> (T.pack $ show $ diffCodeActions ca ca2)+ _ -> throwError $ internalError "The resolve provider unexpectedly returned a result with no data field"+ executeResolveCmd _ _ CodeAction{_data_= value} = throwError $ invalidParamsError ("The code action to resolve has an illegal data field: " <> (T.pack $ show value))+ handleWEditCallback (Left err ) = do+ logWith recorder Warning (ApplyWorkspaceEditFailed err)+ pure ()+ handleWEditCallback _ = pure ()++-- 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 -> PluginError+internalError msg = PluginInternalError ("Ide.Plugin.Resolve: " <> msg)++invalidParamsError :: T.Text -> PluginError+invalidParamsError msg = PluginInvalidParams ("Ide.Plugin.Resolve: : " <> msg)++parseError :: Maybe A.Value -> T.Text -> PluginError+parseError value errMsg = PluginInternalError ("Ide.Plugin.Resolve: Error parsing value:"<> (T.pack $ show value) <> " Error: "<> errMsg)++{- 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!+ -}
src/Ide/PluginUtils.hs view
@@ -1,11 +1,14 @@ {-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeFamilies #-}+ module Ide.PluginUtils ( -- * LSP Range manipulation functions normalize, extendNextLine, extendLineStart,+ extendToFullLines, WithDeletions(..), getProcessID, makeDiffTextEdit,@@ -14,12 +17,11 @@ diffText', pluginDescToIdePlugins, idePluginsToPluginDesc,- responseError, getClientConfig, getPluginConfig, configForPlugin, pluginEnabled,- extractRange,+ extractTextInRange, fullRange, mkLspCommand, mkLspCmdId,@@ -30,42 +32,29 @@ subRange, positionInRange, usePropertyLsp,- getNormalizedFilePath,- pluginResponse,- handleMaybe,- handleMaybeM,- throwPluginError,+ -- * Escape unescape,- )+ ) where --import Control.Arrow ((&&&))-import Control.Monad.Extra (maybeM)-import Control.Monad.Trans.Class (lift)-import Control.Monad.Trans.Except (ExceptT, runExceptT, throwE)+import Control.Arrow ((&&&))+import Control.Lens (_head, _last, re, (%~), (^.)) import Data.Algorithm.Diff import Data.Algorithm.DiffOutput-import Data.Bifunctor (Bifunctor (first))-import Data.Char (isPrint, showLitChar)-import Data.Functor (void)-import qualified Data.HashMap.Strict as H-import Data.String (IsString (fromString))-import qualified Data.Text as T-import Data.Void (Void)+import Data.Char (isPrint, showLitChar)+import Data.Functor (void)+import qualified Data.Map as M+import qualified Data.Text as T+import Data.Void (Void) import Ide.Plugin.Config import Ide.Plugin.Properties import Ide.Types+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Types import Language.LSP.Server-import Language.LSP.Types hiding- (SemanticTokenAbsolute (length, line),- SemanticTokenRelative (length),- SemanticTokensEdit (_start))-import qualified Language.LSP.Types as J-import Language.LSP.Types.Capabilities-import qualified Text.Megaparsec as P-import qualified Text.Megaparsec.Char as P-import qualified Text.Megaparsec.Char.Lexer as P+import qualified Text.Megaparsec as P+import qualified Text.Megaparsec.Char as P+import qualified Text.Megaparsec.Char.Lexer as P -- --------------------------------------------------------------------- @@ -92,39 +81,56 @@ extendLineStart (Range (Position sl _) e) = Range (Position sl 0) e +-- | Extend 'Range' to include the start of the first line and start of the next line of the last line.+--+-- Caveat: It always extend the last line to the beginning of next line, even when the last position is at column 0.+-- This is to keep the compatibility with the implementation of old function @extractRange@.+--+-- >>> extendToFullLines (Range (Position 5 5) (Position 5 10))+-- Range (Position 5 0) (Position 6 0)+--+-- >>> extendToFullLines (Range (Position 5 5) (Position 7 2))+-- Range (Position 5 0) (Position 8 0)+--+-- >>> extendToFullLines (Range (Position 5 5) (Position 7 0))+-- Range (Position 5 0) (Position 8 0)+extendToFullLines :: Range -> Range+extendToFullLines = extendLineStart . extendNextLine++ -- --------------------------------------------------------------------- data WithDeletions = IncludeDeletions | SkipDeletions- deriving Eq+ deriving (Eq) -- | Generate a 'WorkspaceEdit' value from a pair of source Text-diffText :: ClientCapabilities -> (Uri,T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit+diffText :: ClientCapabilities -> (VersionedTextDocumentIdentifier, T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit diffText clientCaps old new withDeletions =- let- supports = clientSupportsDocumentChanges clientCaps- in diffText' supports old new withDeletions+ let supports = clientSupportsDocumentChanges clientCaps+ in diffText' supports old new withDeletions -makeDiffTextEdit :: T.Text -> T.Text -> List TextEdit+makeDiffTextEdit :: T.Text -> T.Text -> [TextEdit] makeDiffTextEdit f1 f2 = diffTextEdit f1 f2 IncludeDeletions -makeDiffTextEditAdditive :: T.Text -> T.Text -> List TextEdit+makeDiffTextEditAdditive :: T.Text -> T.Text -> [TextEdit] makeDiffTextEditAdditive f1 f2 = diffTextEdit f1 f2 SkipDeletions -diffTextEdit :: T.Text -> T.Text -> WithDeletions -> List TextEdit-diffTextEdit fText f2Text withDeletions = J.List r+diffTextEdit :: T.Text -> T.Text -> WithDeletions -> [TextEdit]+diffTextEdit fText f2Text withDeletions = r where r = map diffOperationToTextEdit diffOps d = getGroupedDiff (lines $ T.unpack fText) (lines $ T.unpack f2Text) - diffOps = filter (\x -> (withDeletions == IncludeDeletions) || not (isDeletion x))- (diffToLineRanges d)+ diffOps =+ filter+ (\x -> (withDeletions == IncludeDeletions) || not (isDeletion x))+ (diffToLineRanges d) isDeletion (Deletion _ _) = True isDeletion _ = False -- diffOperationToTextEdit :: DiffOperation LineRange -> J.TextEdit- diffOperationToTextEdit (Change fm to) = J.TextEdit range nt+ diffOperationToTextEdit :: DiffOperation LineRange -> TextEdit+ diffOperationToTextEdit (Change fm to) = TextEdit range nt where range = calcRange fm nt = T.pack $ init $ unlines $ lrContents to@@ -136,53 +142,54 @@ the line ending character(s) then use an end position denoting the start of the next line" -}- diffOperationToTextEdit (Deletion (LineRange (sl, el) _) _) = J.TextEdit range ""+ diffOperationToTextEdit (Deletion (LineRange (sl, el) _) _) = TextEdit range "" where- range = J.Range (J.Position (fromIntegral $ sl - 1) 0)- (J.Position (fromIntegral el) 0)-- diffOperationToTextEdit (Addition fm l) = J.TextEdit range nt- -- fm has a range wrt to the changed file, which starts in the current file at l + 1- -- So the range has to be shifted to start at l + 1+ range =+ Range+ (Position (fromIntegral $ sl - 1) 0)+ (Position (fromIntegral el) 0)+ diffOperationToTextEdit (Addition fm l) = TextEdit range nt where- range = J.Range (J.Position (fromIntegral l) 0)- (J.Position (fromIntegral l) 0)- nt = T.pack $ unlines $ lrContents fm+ -- fm has a range wrt to the changed file, which starts in the current file at l + 1+ -- So the range has to be shifted to start at l + 1 + range =+ Range+ (Position (fromIntegral l) 0)+ (Position (fromIntegral l) 0)+ nt = T.pack $ unlines $ lrContents fm - calcRange fm = J.Range s e+ calcRange fm = Range s e where sl = fst $ lrNumbers fm sc = 0- s = J.Position (fromIntegral $ sl - 1) sc -- Note: zero-based lines+ s = Position (fromIntegral $ sl - 1) sc -- Note: zero-based lines el = snd $ lrNumbers fm ec = fromIntegral $ length $ last $ lrContents fm- e = J.Position (fromIntegral $ el - 1) ec -- Note: zero-based lines-+ e = Position (fromIntegral $ el - 1) ec -- Note: zero-based lines -- | A pure version of 'diffText' for testing-diffText' :: Bool -> (Uri,T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit-diffText' supports (f,fText) f2Text withDeletions =+diffText' :: Bool -> (VersionedTextDocumentIdentifier, T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit+diffText' supports (verTxtDocId, fText) f2Text withDeletions = if supports then WorkspaceEdit Nothing (Just docChanges) Nothing else WorkspaceEdit (Just h) Nothing Nothing where diff = diffTextEdit fText f2Text withDeletions- h = H.singleton f diff- docChanges = J.List [InL docEdit]- docEdit = J.TextDocumentEdit (J.VersionedTextDocumentIdentifier f (Just 0)) $ fmap InL diff+ h = M.singleton (verTxtDocId ^. L.uri) diff+ docChanges = [InL docEdit]+ docEdit = TextDocumentEdit (verTxtDocId ^. re _versionedTextDocumentIdentifier) $ fmap InL diff -- --------------------------------------------------------------------- clientSupportsDocumentChanges :: ClientCapabilities -> Bool clientSupportsDocumentChanges caps =- let ClientCapabilities mwCaps _ _ _ _ = caps+ let ClientCapabilities mwCaps _ _ _ _ _ = caps supports = do wCaps <- mwCaps WorkspaceEditClientCapabilities mDc _ _ _ _ <- _workspaceEdit wCaps mDc- in- Just True == supports+ in Just True == supports -- --------------------------------------------------------------------- @@ -193,11 +200,11 @@ idePluginsToPluginDesc (IdePlugins pp) = pp -- ---------------------------------------------------------------------+ -- | Returns the current client configuration. It is not wise to permanently -- cache the returned value of this function, as clients can at runtime change -- their configuration.----getClientConfig :: MonadLsp Config m => m Config+getClientConfig :: (MonadLsp Config m) => m Config getClientConfig = getConfig -- ---------------------------------------------------------------------@@ -205,10 +212,10 @@ -- | Returns the current plugin configuration. It is not wise to permanently -- cache the returned value of this function, as clients can change their -- configuration at runtime.-getPluginConfig :: MonadLsp Config m => PluginDescriptor c -> m PluginConfig+getPluginConfig :: (MonadLsp Config m) => PluginDescriptor c -> m PluginConfig getPluginConfig plugin = do- config <- getClientConfig- return $ configForPlugin config plugin+ config <- getClientConfig+ return $ configForPlugin config plugin -- --------------------------------------------------------------------- @@ -225,24 +232,33 @@ -- --------------------------------------------------------------------- -extractRange :: Range -> T.Text -> T.Text-extractRange (Range (Position sl _) (Position el _)) s = newS- where focusLines = take (fromIntegral $ el-sl+1) $ drop (fromIntegral sl) $ T.lines s- newS = T.unlines focusLines+-- | Extracts exact matching text in the range.+extractTextInRange :: Range -> T.Text -> T.Text+extractTextInRange (Range (Position sl sc) (Position el ec)) s = newS+ where+ focusLines = take (fromIntegral $ el - sl + 1) $ drop (fromIntegral sl) $ T.lines s+ -- NOTE: We have to trim the last line first to handle the single-line case+ newS =+ focusLines+ & _last %~ T.take (fromIntegral ec)+ & _head %~ T.drop (fromIntegral sc)+ -- NOTE: We cannot use unlines here, because we don't want to add trailing newline!+ & T.intercalate "\n" -- | Gets the range that covers the entire text fullRange :: T.Text -> Range fullRange s = Range startPos endPos- where startPos = Position 0 0- endPos = Position lastLine 0- {-- In order to replace everything including newline characters,- the end range should extend below the last line. From the specification:- "If you want to specify a range that contains a line including- the line ending character(s) then use an end position denoting- the start of the next line"- -}- lastLine = fromIntegral $ length $ T.lines s+ where+ startPos = Position 0 0+ endPos = Position lastLine 0+ {-+ In order to replace everything including newline characters,+ the end range should extend below the last line. From the specification:+ "If you want to specify a range that contains a line including+ the line ending character(s) then use an end position denoting+ the start of the next line"+ -}+ lastLine = fromIntegral $ length $ T.lines s subRange :: Range -> Range -> Bool subRange = isSubrangeOf@@ -251,40 +267,16 @@ allLspCmdIds' :: T.Text -> IdePlugins ideState -> [T.Text] allLspCmdIds' pid (IdePlugins ls) =- allLspCmdIds pid $ map (pluginId &&& pluginCommands) ls+ allLspCmdIds pid $ map (pluginId &&& pluginCommands) ls allLspCmdIds :: T.Text -> [(PluginId, [PluginCommand ideState])] -> [T.Text] allLspCmdIds pid commands = concatMap go commands where go (plid, cmds) = map (mkLspCmdId pid plid . commandId) cmds - -- --------------------------------------------------------------------- -getNormalizedFilePath :: Monad m => Uri -> ExceptT String m NormalizedFilePath-getNormalizedFilePath uri = handleMaybe errMsg- $ uriToNormalizedFilePath- $ toNormalizedUri uri- where- errMsg = T.unpack $ "Failed converting " <> getUri uri <> " to NormalizedFilePath" --- ----------------------------------------------------------------------throwPluginError :: Monad m => String -> ExceptT String m b-throwPluginError = throwE--handleMaybe :: Monad m => e -> Maybe b -> ExceptT e m b-handleMaybe msg = maybe (throwE msg) return--handleMaybeM :: Monad m => e -> m (Maybe b) -> ExceptT e m b-handleMaybeM msg act = maybeM (throwE msg) return $ lift act--pluginResponse :: Monad m => ExceptT String m a -> m (Either ResponseError a)-pluginResponse =- fmap (first (\msg -> ResponseError InternalError (fromString msg) Nothing))- . runExceptT---- ---------------------------------------------------------------------- type TextParser = P.Parsec Void T.Text -- | Unescape printable escape sequences within double quotes.@@ -292,9 +284,9 @@ -- display as is. unescape :: T.Text -> T.Text unescape input =- case P.runParser escapedTextParser "inline" input of- Left _ -> input- Right strs -> T.pack strs+ case P.runParser escapedTextParser "inline" input of+ Left _ -> input+ Right strs -> T.pack strs -- | Parser for a string that contains double quotes. Returns unescaped string. escapedTextParser :: TextParser String@@ -305,11 +297,11 @@ stringLiteral :: TextParser String stringLiteral = do- inside <- P.char '"' >> P.manyTill P.charLiteral (P.char '"')- let f '"' = "\\\"" -- double quote should still be escaped- -- Despite the docs, 'showLitChar' and 'showLitString' from 'Data.Char' DOES ESCAPE unicode printable- -- characters. So we need to call 'isPrint' from 'Data.Char' manually.- f ch = if isPrint ch then [ch] else showLitChar ch ""- inside' = concatMap f inside+ inside <- P.char '"' >> P.manyTill P.charLiteral (P.char '"')+ let f '"' = "\\\"" -- double quote should still be escaped+ -- Despite the docs, 'showLitChar' and 'showLitString' from 'Data.Char' DOES ESCAPE unicode printable+ -- characters. So we need to call 'isPrint' from 'Data.Char' manually.+ f ch = if isPrint ch then [ch] else showLitChar ch ""+ inside' = concatMap f inside - pure $ "\"" <> inside' <> "\""+ pure $ "\"" <> inside' <> "\""
src/Ide/Types.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE BangPatterns #-}+{-# LANGUAGE BlockArguments #-} {-# LANGUAGE CPP #-} {-# LANGUAGE ConstraintKinds #-} {-# LANGUAGE DefaultSignatures #-}@@ -9,8 +10,10 @@ {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE GADTs #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MonadComprehensions #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE OverloadedLabels #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE PolyKinds #-}@@ -19,7 +22,6 @@ {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE UndecidableInstances #-} {-# LANGUAGE ViewPatterns #-}- module Ide.Types ( PluginDescriptor(..), defaultPluginDescriptor, defaultCabalPluginDescriptor , defaultPluginPriority@@ -45,71 +47,65 @@ , PluginRequestMethod(..) , getProcessID, getPid , installSigUsr1Handler-, responseError , lookupCommandProvider+, ResolveFunction+, mkResolveHandler ) where #ifdef mingw32_HOST_OS-import qualified System.Win32.Process as P (getCurrentProcessId)++import qualified System.Win32.Process as P (getCurrentProcessId)+ #else-import Control.Monad (void)-import qualified System.Posix.Process as P (getProcessID)++import qualified System.Posix.Process as P (getProcessID) import System.Posix.Signals+ #endif-import Control.Applicative ((<|>))-import Control.Arrow ((&&&))-import Control.Lens ((.~), (^.))-import Data.Aeson hiding (defaultOptions)++import Control.Applicative ((<|>))+import Control.Arrow ((&&&))+import Control.Lens (_Just, (.~), (?~), (^.), (^?))+import Control.Monad (void)+import Control.Monad.Error.Class (MonadError (throwError))+import Control.Monad.Trans.Class (MonadTrans (lift))+import Control.Monad.Trans.Except (ExceptT, runExceptT)+import Data.Aeson hiding (Null, defaultOptions) import Data.Default-import Data.Dependent.Map (DMap)-import qualified Data.Dependent.Map as DMap-import qualified Data.DList as DList+import Data.Dependent.Map (DMap)+import qualified Data.Dependent.Map as DMap+import qualified Data.DList as DList import Data.GADT.Compare-import Data.Hashable (Hashable)-import Data.HashMap.Strict (HashMap)-import qualified Data.HashMap.Strict as HashMap-import Data.List.Extra (find, sortOn)-import Data.List.NonEmpty (NonEmpty (..), toList)-import qualified Data.Map as Map+import Data.Hashable (Hashable)+import Data.HashMap.Strict (HashMap)+import qualified Data.HashMap.Strict as HashMap+import Data.Kind (Type)+import Data.List.Extra (find, sortOn)+import Data.List.NonEmpty (NonEmpty (..), toList)+import qualified Data.Map as Map import Data.Maybe import Data.Ord import Data.Semigroup import Data.String-import qualified Data.Text as T-import Data.Text.Encoding (encodeUtf8)+import qualified Data.Text as T+import Data.Text.Encoding (encodeUtf8) import Development.IDE.Graph-import GHC (DynFlags)+import GHC (DynFlags) import GHC.Generics+import Ide.Plugin.Error import Ide.Plugin.Properties-import Language.LSP.Server (LspM, getVirtualFile)-import Language.LSP.Types hiding- (SemanticTokenAbsolute (length, line),- SemanticTokenRelative (length),- SemanticTokensEdit (_start))-import Language.LSP.Types.Capabilities (ClientCapabilities (ClientCapabilities),- TextDocumentClientCapabilities (_codeAction, _documentSymbol))-import Language.LSP.Types.Lens as J (HasChildren (children),- HasCommand (command),- HasContents (contents),- HasDeprecated (deprecated),- HasEdit (edit),- HasKind (kind),- HasName (name),- HasOptions (..),- HasRange (range),- HasTextDocument (..),- HasTitle (title),- HasUri (..))-import qualified Language.LSP.Types.Lens as J+import qualified Language.LSP.Protocol.Lens as L+import Language.LSP.Protocol.Message+import Language.LSP.Protocol.Types+import Language.LSP.Server (LspM, LspT, getVirtualFile) import Language.LSP.VFS import Numeric.Natural import OpenTelemetry.Eventlog-import Options.Applicative (ParserInfo)+import Options.Applicative (ParserInfo) import System.FilePath import System.IO.Unsafe-import Text.Regex.TDFA.Text ()-+import Text.Regex.TDFA.Text () -- --------------------------------------------------------------------- data IdePlugins ideState = IdePlugins_@@ -170,7 +166,7 @@ -- | We (initially anyway) mirror the hie configuration, so that existing -- clients can simply switch executable and not have any nasty surprises. There--- will be surprises relating to config options being ignored, initially though.+-- will initially be surprises relating to config options being ignored though. data Config = Config { checkParents :: CheckParents@@ -200,6 +196,7 @@ -- , formattingProvider = "floskell" -- , formattingProvider = "stylish-haskell" , cabalFormattingProvider = "cabal-fmt"+ -- this string value needs to kept in sync with the value provided in HlsPlugins , maxCompletions = 40 , plugins = mempty }@@ -268,7 +265,7 @@ -- --------------------------------------------------------------------- -data PluginDescriptor (ideState :: *) =+data PluginDescriptor (ideState :: Type) = PluginDescriptor { pluginId :: !PluginId -- ^ Unique identifier of the plugin. , pluginPriority :: Natural@@ -282,7 +279,7 @@ , pluginCli :: Maybe (ParserInfo (IdeCommand ideState)) , pluginFileType :: [T.Text] -- ^ File extension of the files the plugin is responsible for.- -- The plugin is only allowed to handle files with these extensions+ -- The plugin is only allowed to handle files with these extensions. -- When writing handlers, etc. for this plugin it can be assumed that all handled files are of this type. -- The file extension must have a leading '.'. }@@ -301,8 +298,8 @@ -- | An existential wrapper of 'Properties' data CustomConfig = forall r. CustomConfig (Properties r) --- | Describes the configuration a plugin.--- A plugin may be configurable in such form:+-- | Describes the configuration of a plugin.+-- A plugin may be configurable as can be seen below: -- -- @ -- {@@ -317,7 +314,7 @@ -- } -- @ ----- @globalOn@, @codeActionsOn@, and @codeLensOn@ etc. are called generic configs,+-- @globalOn@, @codeActionsOn@, and @codeLensOn@ etc. are called generic configs -- which can be inferred from handlers registered by the plugin. -- @config@ is called custom config, which is defined using 'Properties'. data ConfigDescriptor = ConfigDescriptor {@@ -341,393 +338,431 @@ -- | Methods that can be handled by plugins. -- 'ExtraParams' captures any extra data the IDE passes to the handlers for this method -- Only methods for which we know how to combine responses can be instances of 'PluginMethod'-class HasTracing (MessageParams m) => PluginMethod (k :: MethodType) (m :: Method FromClient k) where+class HasTracing (MessageParams m) => PluginMethod (k :: MessageKind) (m :: Method ClientToServer k) where -- | Parse the configuration to check if this plugin is enabled.- -- Perform sanity checks on the message to see whether plugin is enabled+ -- Perform sanity checks on the message to see whether the plugin is enabled -- for this message in particular.- -- If a plugin is not enabled, its handlers, commands, etc... will not be+ -- If a plugin is not enabled, its handlers, commands, etc. will not be -- run for the given message. --- -- Semantically, this method described whether a Plugin is enabled configuration wise+ -- Semantically, this method describes whether a plugin is enabled configuration wise -- and is allowed to respond to the message. This might depend on the URI that is- -- associated to the Message Parameters, but doesn't have to. There are requests- -- with no associated URI that, consequentially, can't inspect the URI.+ -- associated to the Message Parameters. There are requests+ -- with no associated URI that, consequentially, cannot inspect the URI. --- -- Common reason why a plugin might not be allowed to respond although it is enabled:- -- * Plugin can not handle requests associated to the specific URI+ -- A common reason why a plugin might not be allowed to respond although it is enabled:+ -- * The plugin cannot handle requests associated with the specific URI -- * Since the implementation of [cabal plugins](https://github.com/haskell/haskell-language-server/issues/2940)- -- HLS knows plugins specific for Haskell and specific for [Cabal file descriptions](https://cabal.readthedocs.io/en/3.6/cabal-package.html)+ -- HLS knows plugins specific to Haskell and specific to [Cabal file descriptions](https://cabal.readthedocs.io/en/3.6/cabal-package.html) -- -- Strictly speaking, we are conflating two concepts here:- -- * Dynamically enabled (e.g. enabled on a per-message basis)+ -- * Dynamically enabled (e.g. on a per-message basis) -- * Statically enabled (e.g. by configuration in the lsp-client) -- * Strictly speaking, this might also change dynamically --- -- But there is no use to split it up currently into two different methods for now.+ -- But there is no use to split it up into two different methods for now. pluginEnabled :: SMethod m -- ^ Method type. -> MessageParams m -- ^ Whether a plugin is enabled might depend on the message parameters- -- eg 'pluginFileType' specifies what file extension a plugin is allowed to handle+ -- e.g. 'pluginFileType' specifies which file extensions a plugin is allowed to handle -> PluginDescriptor c- -- ^ Contains meta information such as PluginId and what file types this+ -- ^ Contains meta information such as PluginId and which file types this -- plugin is able to handle. -> Config- -- ^ Generic config description, expected to hold 'PluginConfig' configuration+ -- ^ Generic config description, expected to contain 'PluginConfig' configuration -- for this plugin -> Bool -- ^ Is this plugin enabled and allowed to respond to the given request -- with the given parameters? - default pluginEnabled :: (HasTextDocument (MessageParams m) doc, HasUri doc Uri)+ default pluginEnabled :: (L.HasTextDocument (MessageParams m) doc, L.HasUri doc Uri) => SMethod m -> MessageParams m -> PluginDescriptor c -> Config -> Bool pluginEnabled _ params desc conf = pluginResponsible uri desc && plcGlobalOn (configForPlugin conf desc) where- uri = params ^. J.textDocument . J.uri+ uri = params ^. L.textDocument . L.uri -- --------------------------------------------------------------------- -- Plugin Requests -- --------------------------------------------------------------------- -class PluginMethod Request m => PluginRequestMethod (m :: Method FromClient Request) where+class PluginMethod Request m => PluginRequestMethod (m :: Method ClientToServer Request) where -- | How to combine responses from different plugins. -- -- For example, for Hover requests, we might have multiple producers of- -- Hover information, we do not want to decide which one to display to the user- -- but allow here to define how to merge two hover request responses into one+ -- Hover information. We do not want to decide which one to display to the user+ -- but instead allow to define how to merge two hover request responses into one -- glorious hover box. --- -- However, sometimes only one handler of a request can realistically exist,- -- such as TextDocumentFormatting, it is safe to just unconditionally report+ -- However, as sometimes only one handler of a request can realistically exist+ -- (such as TextDocumentFormatting), it is safe to just unconditionally report -- back one arbitrary result (arbitrary since it should only be one anyway). combineResponses :: SMethod m -> Config -- ^ IDE Configuration -> ClientCapabilities -> MessageParams m- -> NonEmpty (ResponseResult m) -> ResponseResult m+ -> NonEmpty (MessageResult m) -> MessageResult m - default combineResponses :: Semigroup (ResponseResult m)- => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (ResponseResult m) -> ResponseResult m+ default combineResponses :: Semigroup (MessageResult m)+ => SMethod m -> Config -> ClientCapabilities -> MessageParams m -> NonEmpty (MessageResult m) -> MessageResult m combineResponses _method _config _caps _params = sconcat -instance PluginMethod Request TextDocumentCodeAction where+instance PluginMethod Request Method_TextDocumentCodeAction where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcCodeActionsOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri--instance PluginRequestMethod TextDocumentCodeAction where- combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _) (CodeActionParams _ _ _ _ context) resps =- fmap compat $ List $ filter wasRequested $ (\(List x) -> x) $ sconcat resps- where- compat :: (Command |? CodeAction) -> (Command |? CodeAction)- compat x@(InL _) = x- compat x@(InR action)- | Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport- = x- | otherwise = InL cmd- where- cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. title) (Just cmdParams)- cmdParams = [toJSON (FallbackCodeActionParams (action ^. edit) (action ^. command))]+ uri = msgParams ^. L.textDocument . L.uri - wasRequested :: (Command |? CodeAction) -> Bool- wasRequested (InL _) = True- wasRequested (InR ca)- | Nothing <- _only context = True- | Just (List allowed) <- _only context- -- See https://github.com/microsoft/language-server-protocol/issues/970- -- This is somewhat vague, but due to the hierarchical nature of action kinds, we- -- should check whether the requested kind is a *prefix* of the action kind.- -- That means, for example, we will return actions with kinds `quickfix.import` and- -- `quickfix.somethingElse` if the requested kind is `quickfix`.- , Just caKind <- ca ^. kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed- | otherwise = False+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) -instance PluginMethod Request TextDocumentDefinition where+instance PluginMethod Request Method_TextDocumentDefinition where pluginEnabled _ msgParams pluginDesc _ = pluginResponsible uri pluginDesc where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentTypeDefinition where+instance PluginMethod Request Method_TextDocumentTypeDefinition where pluginEnabled _ msgParams pluginDesc _ = pluginResponsible uri pluginDesc where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentDocumentHighlight where+instance PluginMethod Request Method_TextDocumentDocumentHighlight where pluginEnabled _ msgParams pluginDesc _ = pluginResponsible uri pluginDesc where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentReferences where+instance PluginMethod Request Method_TextDocumentReferences where pluginEnabled _ msgParams pluginDesc _ = pluginResponsible uri pluginDesc where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request WorkspaceSymbol where+instance PluginMethod Request Method_WorkspaceSymbol where -- Unconditionally enabled, but should it really be? pluginEnabled _ _ _ _ = True -instance PluginMethod Request TextDocumentCodeLens where+instance PluginMethod Request Method_TextDocumentCodeLens where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcCodeLensOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentRename where+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)++instance PluginMethod Request Method_TextDocumentRename where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcRenameOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri-instance PluginMethod Request TextDocumentHover where+ uri = msgParams ^. L.textDocument . L.uri+instance PluginMethod Request Method_TextDocumentHover where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcHoverOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentDocumentSymbol where+instance PluginMethod Request Method_TextDocumentDocumentSymbol where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcSymbolsOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request CompletionItemResolve where- pluginEnabled _ msgParams pluginDesc config = pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc)+instance PluginMethod Request Method_CompletionItemResolve where+ -- See Note [Resolve in PluginHandlers]+ pluginEnabled _ msgParams pluginDesc config = pluginResolverResponsible (msgParams ^. L.data_) pluginDesc+ && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) -instance PluginMethod Request TextDocumentCompletion where+instance PluginMethod Request Method_TextDocumentCompletion where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc && pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentFormatting where- pluginEnabled STextDocumentFormatting msgParams pluginDesc conf =+instance PluginMethod Request Method_TextDocumentFormatting where+ pluginEnabled SMethod_TextDocumentFormatting msgParams pluginDesc conf = pluginResponsible uri pluginDesc && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri pid = pluginId pluginDesc -instance PluginMethod Request TextDocumentRangeFormatting where+instance PluginMethod Request Method_TextDocumentRangeFormatting where pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc && (PluginId (formattingProvider conf) == pid || PluginId (cabalFormattingProvider conf) == pid) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri pid = pluginId pluginDesc -instance PluginMethod Request TextDocumentPrepareCallHierarchy where+instance PluginMethod Request Method_TextDocumentPrepareCallHierarchy where pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc && pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentSelectionRange where+instance PluginMethod Request Method_TextDocumentSelectionRange where pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc && pluginEnabledConfig plcSelectionRangeOn (configForPlugin conf pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request TextDocumentFoldingRange where+instance PluginMethod Request Method_TextDocumentFoldingRange where pluginEnabled _ msgParams pluginDesc conf = pluginResponsible uri pluginDesc && pluginEnabledConfig plcFoldingRangeOn (configForPlugin conf pluginDesc) where- uri = msgParams ^. J.textDocument . J.uri+ uri = msgParams ^. L.textDocument . L.uri -instance PluginMethod Request CallHierarchyIncomingCalls where+instance PluginMethod Request Method_CallHierarchyIncomingCalls where -- This method has no URI parameter, thus no call to 'pluginResponsible' pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) -instance PluginMethod Request CallHierarchyOutgoingCalls where+instance PluginMethod Request Method_CallHierarchyOutgoingCalls where -- This method has no URI parameter, thus no call to 'pluginResponsible' pluginEnabled _ _ pluginDesc conf = pluginEnabledConfig plcCallHierarchyOn (configForPlugin conf pluginDesc) -instance PluginMethod Request CustomMethod where+instance PluginMethod Request (Method_CustomMethod m) where pluginEnabled _ _ _ _ = True ----instance PluginRequestMethod TextDocumentDefinition where+instance PluginRequestMethod Method_TextDocumentCodeAction where+ combineResponses _method _config (ClientCapabilities _ textDocCaps _ _ _ _) (CodeActionParams _ _ _ _ context) resps =+ InL $ fmap compat $ filter wasRequested $ concat $ mapMaybe nullToMaybe $ toList resps+ where+ compat :: (Command |? CodeAction) -> (Command |? CodeAction)+ compat x@(InL _) = x+ compat x@(InR action)+ | Just _ <- textDocCaps >>= _codeAction >>= _codeActionLiteralSupport+ = x+ | otherwise = InL cmd+ where+ cmd = mkLspCommand "hls" "fallbackCodeAction" (action ^. L.title) (Just cmdParams)+ cmdParams = [toJSON (FallbackCodeActionParams (action ^. L.edit) (action ^. L.command))]++ wasRequested :: (Command |? CodeAction) -> Bool+ wasRequested (InL _) = True+ wasRequested (InR ca)+ | Nothing <- _only context = True+ | Just allowed <- _only context+ -- See https://github.com/microsoft/language-server-protocol/issues/970+ -- This is somewhat vague, but due to the hierarchical nature of action kinds, we+ -- should check whether the requested kind is a *prefix* of the action kind.+ -- That means, for example, we will return actions with kinds `quickfix.import` and+ -- `quickfix.somethingElse` if the requested kind is `quickfix`.+ , Just caKind <- ca ^. L.kind = any (\k -> k `codeActionKindSubsumes` caKind) allowed+ | otherwise = False++instance PluginRequestMethod Method_CodeActionResolve where+ -- A resolve request should only have one response.+ -- See Note [Resolve in PluginHandlers].+ combineResponses _ _ _ _ (x :| _) = x++instance PluginRequestMethod Method_TextDocumentDefinition where combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentTypeDefinition where+instance PluginRequestMethod Method_TextDocumentTypeDefinition where combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentDocumentHighlight where+instance PluginRequestMethod Method_TextDocumentDocumentHighlight where -instance PluginRequestMethod TextDocumentReferences where+instance PluginRequestMethod Method_TextDocumentReferences where -instance PluginRequestMethod WorkspaceSymbol where+instance PluginRequestMethod Method_WorkspaceSymbol where+ -- TODO: combine WorkspaceSymbol. Currently all WorkspaceSymbols are dumped+ -- as it is new of lsp-types 2.0.0.0+ combineResponses _ _ _ _ xs = InL $ mconcat $ takeLefts $ toList xs -instance PluginRequestMethod TextDocumentCodeLens where+instance PluginRequestMethod Method_TextDocumentCodeLens where -instance PluginRequestMethod TextDocumentRename where+instance PluginRequestMethod Method_CodeLensResolve where+ -- A resolve request should only ever get one response.+ -- See note Note [Resolve in PluginHandlers]+ combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentHover where- combineResponses _ _ _ _ (catMaybes . toList -> hs) = h+instance PluginRequestMethod Method_TextDocumentRename where++instance PluginRequestMethod Method_TextDocumentHover where+ combineResponses _ _ _ _ (mapMaybe nullToMaybe . toList -> hs :: [Hover]) =+ if null hs+ then InR Null+ else InL $ Hover (InL mcontent) r where- r = listToMaybe $ mapMaybe (^. range) hs- h = case foldMap (^. contents) hs of- HoverContentsMS (List []) -> Nothing- hh -> Just $ Hover hh r+ r = listToMaybe $ mapMaybe (^. L.range) hs+ -- We are only taking MarkupContent here, because MarkedStrings have been+ -- deprecated for a while and don't occur in the hls codebase+ mcontent :: MarkupContent+ mcontent = mconcat $ takeLefts $ map (^. L.contents) hs -instance PluginRequestMethod TextDocumentDocumentSymbol where- combineResponses _ _ (ClientCapabilities _ tdc _ _ _) params xs = res+instance PluginRequestMethod Method_TextDocumentDocumentSymbol where+ combineResponses _ _ (ClientCapabilities _ tdc _ _ _ _) params xs = res where- uri' = params ^. textDocument . uri+ uri' = params ^. L.textDocument . L.uri supportsHierarchy = Just True == (tdc >>= _documentSymbol >>= _hierarchicalDocumentSymbolSupport)- dsOrSi = fmap toEither xs+ dsOrSi :: [Either [SymbolInformation] [DocumentSymbol]]+ dsOrSi = toEither <$> mapMaybe nullToMaybe' (toList xs)+ res :: [SymbolInformation] |? ([DocumentSymbol] |? Null) res- | supportsHierarchy = InL $ sconcat $ fmap (either id (fmap siToDs)) dsOrSi- | otherwise = InR $ sconcat $ fmap (either (List . concatMap dsToSi) id) dsOrSi- siToDs (SymbolInformation name kind _tags dep (Location _uri range) cont)+ | supportsHierarchy = InR $ InL $ concatMap (either (fmap siToDs) id) dsOrSi+ | otherwise = InL $ concatMap (either id ( concatMap dsToSi)) dsOrSi+ -- Is this actually a good conversion? It's what there was before, but some+ -- things such as tags are getting lost+ siToDs :: SymbolInformation -> DocumentSymbol+ siToDs (SymbolInformation name kind _tags cont dep (Location _uri range) ) = DocumentSymbol name cont kind Nothing dep range range Nothing dsToSi = go Nothing go :: Maybe T.Text -> DocumentSymbol -> [SymbolInformation] go parent ds = let children' :: [SymbolInformation]- children' = concatMap (go (Just name')) (fromMaybe mempty (ds ^. children))- loc = Location uri' (ds ^. range)- name' = ds ^. name- si = SymbolInformation name' (ds ^. kind) Nothing (ds ^. deprecated) loc parent+ children' = concatMap (go (Just name')) (fromMaybe mempty (ds ^. L.children))+ loc = Location uri' (ds ^. L.range)+ name' = ds ^. L.name+ si = SymbolInformation name' (ds ^. L.kind) Nothing parent (ds ^. L.deprecated) loc in [si] <> children' -instance PluginRequestMethod 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- & J.detail .~ comp1 ^. J.detail <> comp2 ^. J.detail- & J.documentation .~ ((comp1 ^. J.documentation) <|> (comp2 ^. J.documentation)) -- difficult to write generic concatentation for docs- & J.additionalTextEdits .~ comp1 ^. J.additionalTextEdits <> comp2 ^. J.additionalTextEdits)- xs+instance PluginRequestMethod Method_CompletionItemResolve where+ -- A resolve request should only have one response.+ -- See Note [Resolve in PluginHandlers]+ combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentCompletion where+instance PluginRequestMethod Method_TextDocumentCompletion where combineResponses _ conf _ _ (toList -> xs) = snd $ consumeCompletionResponse limit $ combine xs where limit = maxCompletions conf- combine :: [List CompletionItem |? CompletionList] -> (List CompletionItem |? CompletionList)+ combine :: [[CompletionItem] |? (CompletionList |? Null)] -> ([CompletionItem] |? (CompletionList |? Null)) combine cs = go True mempty cs + go :: Bool -> DList.DList CompletionItem -> [[CompletionItem] |? (CompletionList |? Null)] -> ([CompletionItem] |? (CompletionList |? Null)) go !comp acc [] =- InR (CompletionList comp (List $ DList.toList acc))- go comp acc (InL (List ls) : rest) =+ InR (InL (CompletionList comp Nothing ( DList.toList acc)))+ go comp acc ((InL ls) : rest) = go comp (acc <> DList.fromList ls) rest- go comp acc (InR (CompletionList comp' (List ls)) : rest) =+ go comp acc ( (InR (InL (CompletionList comp' _ ls))) : rest) = go (comp && comp') (acc <> DList.fromList ls) rest-+ go comp acc ( (InR (InR Null)) : rest) =+ go comp acc rest -- boolean disambiguators isCompleteResponse, isIncompleteResponse :: Bool isIncompleteResponse = True isCompleteResponse = False-- consumeCompletionResponse limit it@(InR (CompletionList _ (List xx))) =+ consumeCompletionResponse :: Int -> ([CompletionItem] |? (CompletionList |? Null)) -> (Int, [CompletionItem] |? (CompletionList |? Null))+ consumeCompletionResponse limit it@(InR (InL (CompletionList _ _ xx))) = case splitAt limit xx of -- consumed all the items, return the result as is (_, []) -> (limit - length xx, it) -- need to crop the response, set the 'isIncomplete' flag- (xx', _) -> (0, InR (CompletionList isIncompleteResponse (List xx')))- consumeCompletionResponse n (InL (List xx)) =- consumeCompletionResponse n (InR (CompletionList isCompleteResponse (List xx)))--instance PluginRequestMethod TextDocumentFormatting where+ (xx', _) -> (0, InR (InL (CompletionList isIncompleteResponse Nothing xx')))+ consumeCompletionResponse n (InL xx) =+ consumeCompletionResponse n (InR (InL (CompletionList isCompleteResponse Nothing xx)))+ consumeCompletionResponse n (InR (InR Null)) = (n, InR (InR Null))+instance PluginRequestMethod Method_TextDocumentFormatting where combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentRangeFormatting where+instance PluginRequestMethod Method_TextDocumentRangeFormatting where combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentPrepareCallHierarchy where+instance PluginRequestMethod Method_TextDocumentPrepareCallHierarchy where -instance PluginRequestMethod TextDocumentSelectionRange where+instance PluginRequestMethod Method_TextDocumentSelectionRange where combineResponses _ _ _ _ (x :| _) = x -instance PluginRequestMethod TextDocumentFoldingRange where+instance PluginRequestMethod Method_TextDocumentFoldingRange where combineResponses _ _ _ _ x = sconcat x -instance PluginRequestMethod CallHierarchyIncomingCalls where+instance PluginRequestMethod Method_CallHierarchyIncomingCalls where -instance PluginRequestMethod CallHierarchyOutgoingCalls where+instance PluginRequestMethod Method_CallHierarchyOutgoingCalls where -instance PluginRequestMethod CustomMethod where+instance PluginRequestMethod (Method_CustomMethod m) where combineResponses _ _ _ _ (x :| _) = x +takeLefts :: [a |? b] -> [a]+takeLefts = mapMaybe (\x -> [res | (InL res) <- Just x])++nullToMaybe' :: (a |? (b |? Null)) -> Maybe (a |? b)+nullToMaybe' (InL x) = Just $ InL x+nullToMaybe' (InR (InL x)) = Just $ InR x+nullToMaybe' (InR (InR _)) = Nothing -- --------------------------------------------------------------------- -- Plugin Notifications -- --------------------------------------------------------------------- -- | Plugin Notification methods. No specific methods at the moment, but -- might contain more in the future.-class PluginMethod Notification m => PluginNotificationMethod (m :: Method FromClient Notification) where+class PluginMethod Notification m => PluginNotificationMethod (m :: Method ClientToServer Notification) where -instance PluginMethod Notification TextDocumentDidOpen where+instance PluginMethod Notification Method_TextDocumentDidOpen where -instance PluginMethod Notification TextDocumentDidChange where+instance PluginMethod Notification Method_TextDocumentDidChange where -instance PluginMethod Notification TextDocumentDidSave where+instance PluginMethod Notification Method_TextDocumentDidSave where -instance PluginMethod Notification TextDocumentDidClose where+instance PluginMethod Notification Method_TextDocumentDidClose where -instance PluginMethod Notification WorkspaceDidChangeWatchedFiles where+instance PluginMethod Notification Method_WorkspaceDidChangeWatchedFiles where -- This method has no URI parameter, thus no call to 'pluginResponsible'. pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc -instance PluginMethod Notification WorkspaceDidChangeWorkspaceFolders where+instance PluginMethod Notification Method_WorkspaceDidChangeWorkspaceFolders where -- This method has no URI parameter, thus no call to 'pluginResponsible'. pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc -instance PluginMethod Notification WorkspaceDidChangeConfiguration where+instance PluginMethod Notification Method_WorkspaceDidChangeConfiguration where -- This method has no URI parameter, thus no call to 'pluginResponsible'. pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc -instance PluginMethod Notification Initialized where+instance PluginMethod Notification Method_Initialized where -- This method has no URI parameter, thus no call to 'pluginResponsible'. pluginEnabled _ _ desc conf = plcGlobalOn $ configForPlugin conf desc -instance PluginNotificationMethod TextDocumentDidOpen where+instance PluginNotificationMethod Method_TextDocumentDidOpen where -instance PluginNotificationMethod TextDocumentDidChange where+instance PluginNotificationMethod Method_TextDocumentDidChange where -instance PluginNotificationMethod TextDocumentDidSave where+instance PluginNotificationMethod Method_TextDocumentDidSave where -instance PluginNotificationMethod TextDocumentDidClose where+instance PluginNotificationMethod Method_TextDocumentDidClose where -instance PluginNotificationMethod WorkspaceDidChangeWatchedFiles where+instance PluginNotificationMethod Method_WorkspaceDidChangeWatchedFiles where -instance PluginNotificationMethod WorkspaceDidChangeWorkspaceFolders where+instance PluginNotificationMethod Method_WorkspaceDidChangeWorkspaceFolders where -instance PluginNotificationMethod WorkspaceDidChangeConfiguration where+instance PluginNotificationMethod Method_WorkspaceDidChangeConfiguration where -instance PluginNotificationMethod Initialized where+instance PluginNotificationMethod Method_Initialized where -- --------------------------------------------------------------------- -- | Methods which have a PluginMethod instance-data IdeMethod (m :: Method FromClient Request) = PluginRequestMethod m => IdeMethod (SMethod m)+data IdeMethod (m :: Method ClientToServer Request) = PluginRequestMethod m => IdeMethod (SMethod m) instance GEq IdeMethod where geq (IdeMethod a) (IdeMethod b) = geq a b instance GCompare IdeMethod where gcompare (IdeMethod a) (IdeMethod b) = gcompare a b -- | Methods which have a PluginMethod instance-data IdeNotification (m :: Method FromClient Notification) = PluginNotificationMethod m => IdeNotification (SMethod m)+data IdeNotification (m :: Method ClientToServer Notification) = PluginNotificationMethod m => IdeNotification (SMethod m) instance GEq IdeNotification where geq (IdeNotification a) (IdeNotification b) = geq a b instance GCompare IdeNotification where gcompare (IdeNotification a) (IdeNotification b) = gcompare a b -- | Combine handlers for the-newtype PluginHandler a (m :: Method FromClient Request)- = PluginHandler (PluginId -> a -> MessageParams m -> LspM Config (NonEmpty (Either ResponseError (ResponseResult m))))+newtype PluginHandler a (m :: Method ClientToServer Request)+ = PluginHandler (PluginId -> a -> MessageParams m -> LspM Config (NonEmpty (Either PluginError (MessageResult m)))) -newtype PluginNotificationHandler a (m :: Method FromClient Notification)+newtype PluginNotificationHandler a (m :: Method ClientToServer Notification) = PluginNotificationHandler (PluginId -> a -> VFS -> MessageParams m -> LspM Config ()) newtype PluginHandlers a = PluginHandlers (DMap IdeMethod (PluginHandler a))@@ -750,24 +785,51 @@ instance Monoid (PluginNotificationHandlers a) where mempty = PluginNotificationHandlers mempty -type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> LspM Config (Either ResponseError (ResponseResult m))+type PluginMethodHandler a m = a -> PluginId -> MessageParams m -> ExceptT PluginError (LspM Config) (MessageResult m) 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 PluginError (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) <$> runExceptT (f ide pid params)+ f' SMethod_TextDocumentCodeLens pid ide params@CodeLensParams{_textDocument=TextDocumentIdentifier {_uri}} =+ pure . fmap (wrapCodeLenses pid _uri) <$> runExceptT (f ide pid params)+ f' SMethod_TextDocumentCompletion pid ide params@CompletionParams{_textDocument=TextDocumentIdentifier {_uri}} =+ pure . fmap (wrapCompletions pid _uri) <$> runExceptT (f ide pid params) + -- This is the default case for all other methods+ f' _ pid ide params = pure <$> runExceptT (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 :: PluginNotificationMethod m- => SClientMethod (m :: Method FromClient Notification)+ => SClientMethod (m :: Method ClientToServer Notification) -> PluginNotificationMethodHandler ideState m -> PluginNotificationHandlers ideState mkPluginNotificationHandler m f@@ -779,7 +841,7 @@ defaultPluginPriority = 1000 -- | Set up a plugin descriptor, initialized with default values.--- This is plugin descriptor is prepared for @haskell@ files, such as+-- This plugin descriptor is prepared for @haskell@ files, such as -- -- * @.hs@ -- * @.lhs@@@ -802,7 +864,7 @@ [".hs", ".lhs", ".hs-boot"] -- | Set up a plugin descriptor, initialized with default values.--- This is plugin descriptor is prepared for @.cabal@ files and as such,+-- This plugin descriptor is prepared for @.cabal@ files and as such, -- will only respond / run when @.cabal@ files are currently in scope. -- -- Handles files with the following extensions:@@ -837,13 +899,69 @@ type CommandFunction ideState a = ideState -> a- -> LspM Config (Either ResponseError Value)+ -> ExceptT PluginError (LspM Config) (Value |? Null) -- --------------------------------------------------------------------- +type ResolveFunction ideState a (m :: Method ClientToServer Request) =+ ideState+ -> PluginId+ -> MessageParams m+ -> Uri+ -> a+ -> ExceptT PluginError (LspM Config) (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+ -> ResolveFunction ideState a 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 msg ->+ -- We are assuming that if we can't decode the data, that this+ -- request belongs to another resolve handler for this plugin.+ throwError (PluginRequestRefused (T.pack ("Unable to decode payload for handler, assuming that it's for a different handler" <> msg)))+ -- If we are getting an owner that isn't us, this means that there is an+ -- error, as we filter these our in `pluginEnabled`+ else throwError $ PluginInternalError invalidRequest+ -- If we are getting params without a decodable data field, this means that+ -- there is an error, as we filter these our in `pluginEnabled`+ (Just (Error err)) -> throwError $ PluginInternalError (parseError (params ^. L.data_) err)+ -- If there are no params at all, this also means that there is an error,+ -- as this is filtered out in `pluginEnabled`+ _ -> throwError $ PluginInternalError invalidRequest+ 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 (FromJSON, Hashable)+ deriving newtype (ToJSON, FromJSON, Hashable) instance IsString PluginId where fromString = PluginId . T.pack@@ -869,9 +987,9 @@ type FormattingMethod m =- ( J.HasOptions (MessageParams m) FormattingOptions- , J.HasTextDocument (MessageParams m) TextDocumentIdentifier- , ResponseResult m ~ List TextEdit+ ( L.HasOptions (MessageParams m) FormattingOptions+ , L.HasTextDocument (MessageParams m) TextDocumentIdentifier+ , MessageResult m ~ ([TextEdit] |? Null) ) type FormattingHandler a@@ -880,37 +998,33 @@ -> T.Text -> NormalizedFilePath -> FormattingOptions- -> LspM Config (Either ResponseError (List TextEdit))+ -> ExceptT PluginError (LspM Config) ([TextEdit] |? Null) mkFormattingHandlers :: forall a. FormattingHandler a -> PluginHandlers a-mkFormattingHandlers f = mkPluginHandler STextDocumentFormatting (provider STextDocumentFormatting)- <> mkPluginHandler STextDocumentRangeFormatting (provider STextDocumentRangeFormatting)+mkFormattingHandlers f = mkPluginHandler SMethod_TextDocumentFormatting ( provider SMethod_TextDocumentFormatting)+ <> mkPluginHandler SMethod_TextDocumentRangeFormatting (provider SMethod_TextDocumentRangeFormatting) where provider :: forall m. FormattingMethod m => SMethod m -> PluginMethodHandler a m provider m ide _pid params | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri uri = do- mf <- getVirtualFile $ toNormalizedUri uri+ mf <- lift $ getVirtualFile $ toNormalizedUri uri case mf of Just vf -> do let typ = case m of- STextDocumentFormatting -> FormatText- STextDocumentRangeFormatting -> FormatRange (params ^. J.range)- _ -> error "mkFormattingHandlers: impossible"+ SMethod_TextDocumentFormatting -> FormatText+ SMethod_TextDocumentRangeFormatting -> FormatRange (params ^. L.range)+ _ -> Prelude.error "mkFormattingHandlers: impossible" f ide typ (virtualFileText vf) nfp opts- Nothing -> pure $ Left $ responseError $ T.pack $ "Formatter plugin: could not get file contents for " ++ show uri+ Nothing -> throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: could not get file contents for " ++ show uri - | otherwise = pure $ Left $ responseError $ T.pack $ "Formatter plugin: uriToFilePath failed for: " ++ show uri+ | otherwise = throwError $ PluginInvalidParams $ T.pack $ "Formatter plugin: uriToFilePath failed for: " ++ show uri where- uri = params ^. J.textDocument . J.uri- opts = params ^. J.options+ uri = params ^. L.textDocument . L.uri+ opts = params ^. L.options -- --------------------------------------------------------------------- -responseError :: T.Text -> ResponseError-responseError txt = ResponseError InvalidParams txt Nothing --- ---------------------------------------------------------------------- data FallbackCodeActionParams = FallbackCodeActionParams { fallbackWorkspaceEdit :: Maybe WorkspaceEdit@@ -927,8 +1041,8 @@ traceWithSpan :: SpanInFlight -> a -> IO () traceWithSpan _ _ = pure () -instance {-# OVERLAPPABLE #-} (HasTextDocument a doc, HasUri doc Uri) => HasTracing a where- traceWithSpan sp a = otSetUri sp (a ^. J.textDocument . J.uri)+instance {-# OVERLAPPABLE #-} (L.HasTextDocument a doc, L.HasUri doc Uri) => HasTracing a where+ traceWithSpan sp a = otSetUri sp (a ^. L.textDocument . L.uri) instance HasTracing Value instance HasTracing ExecuteCommandParams@@ -938,24 +1052,29 @@ instance HasTracing DidChangeWorkspaceFoldersParams instance HasTracing DidChangeConfigurationParams instance HasTracing InitializeParams-instance HasTracing (Maybe InitializedParams)+instance HasTracing InitializedParams 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 mkLspCommand :: PluginId -> CommandId -> T.Text -> Maybe [Value] -> Command-mkLspCommand plid cn title args' = Command title cmdId args+mkLspCommand plid cn title args = Command title cmdId args where cmdId = mkLspCmdId pROCESS_ID plid cn- args = List <$> args' mkLspCmdId :: T.Text -> PluginId -> CommandId -> T.Text mkLspCmdId pid (PluginId plid) (CommandId cid)@@ -979,3 +1098,40 @@ installSigUsr1Handler h = void $ installHandler sigUSR1 (Catch h) Nothing #endif++-- |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 (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.+ -}
test/Ide/PluginUtilsTest.hs view
@@ -5,13 +5,13 @@ ( tests ) where -import Data.Char (isPrint)-import qualified Data.Set as Set-import qualified Data.Text as T-import qualified Ide.Plugin.RangeMap as RangeMap-import Ide.PluginUtils (positionInRange, unescape)-import Language.LSP.Types (Position (..), Range (Range), UInt,- isSubrangeOf)+import Data.Char (isPrint)+import qualified Data.Set as Set+import qualified Data.Text as T+import qualified Ide.Plugin.RangeMap as RangeMap+import Ide.PluginUtils (positionInRange, unescape)+import Language.LSP.Protocol.Types (Position (..), Range (Range),+ UInt, isSubrangeOf) import Test.Tasty import Test.Tasty.HUnit import Test.Tasty.QuickCheck