hls-plugin-api-2.12.0.0: src/Ide/Plugin/ConfigUtils.hs
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ViewPatterns #-}
module Ide.Plugin.ConfigUtils (
pluginsToDefaultConfig,
pluginsToVSCodeExtensionSchema,
pluginsCustomConfigToMarkdownTables
) where
import Control.Lens (at, (&), (?~))
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 GHC.TypeLits (symbolVal)
import Ide.Plugin.Config
import Ide.Plugin.Properties (KeyNameProxy, MetaData (..),
PluginCustomConfig (..),
PluginCustomConfigParam (..),
Properties (..),
SPropertyKey (..),
SomePropertyKeyWithMetaData (..),
toDefaultJSON,
toVSCodeExtensionSchema)
import Ide.Types
import Language.LSP.Protocol.Message
-- Attention:
-- 'diagnosticsOn' will never be added into the default config or the schema,
-- since diagnostics emit in arbitrary shake rules -- we don't know
-- whether a plugin is capable of producing diagnostics.
-- | Generates a default 'Config', but remains only effective items
pluginsToDefaultConfig :: IdePlugins a -> A.Value
pluginsToDefaultConfig IdePlugins {..} =
-- Use '_Object' and 'at' to get at the "plugin" key
-- and actually set it.
A.toJSON defaultConfig & _Object . at "plugin" ?~ pluginSpecificDefaultConfigs
where
defaultConfig = def :: Config
pluginSpecificDefaultConfigs = A.object $ mconcat $ singlePlugin <$> ipMap
-- Splice genericDefaultConfig and dedicatedDefaultConfig
-- Example:
--
-- {
-- "plugin-id": {
-- "globalOn": true,
-- "codeActionsOn": true,
-- "codeLensOn": true,
-- "config": {
-- "property1": "foo"
-- }
-- }
-- }
singlePlugin :: PluginDescriptor ideState -> [A.Pair]
singlePlugin PluginDescriptor {pluginConfigDescriptor = ConfigDescriptor {..}, ..} =
let x = genericDefaultConfig <> dedicatedDefaultConfig
in [fromString (T.unpack pId) A..= A.object x | not $ null x]
where
(PluginHandlers (DMap.toList -> handlers)) = pluginHandlers
customConfigToDedicatedDefaultConfig (CustomConfig p) = toDefaultJSON p
-- Example:
--
-- {
-- "codeActionsOn": true,
-- "codeLensOn": true
-- }
--
genericDefaultConfig =
let x = ["diagnosticsOn" A..= True | configHasDiagnostics]
<> nubOrd (mconcat
(handlersToGenericDefaultConfig configInitialGenericConfig <$> handlers))
in case x of
-- If the plugin has only one capability, we produce globalOn instead of the specific one;
-- otherwise we omit globalOn
[_] -> ["globalOn" A..= plcGlobalOn configInitialGenericConfig]
_ -> x
-- Example:
--
-- {
-- "config": {
-- "property1": "foo"
-- }
--}
dedicatedDefaultConfig =
let x = customConfigToDedicatedDefaultConfig configCustomConfig
in ["config" A..= A.object x | not $ null x]
(PluginId pId) = pluginId
-- 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
SMethod_TextDocumentCodeAction -> ["codeActionsOn" A..= plcCodeActionsOn]
SMethod_TextDocumentCodeLens -> ["codeLensOn" A..= plcCodeLensOn]
SMethod_TextDocumentInlayHint -> ["inlayHintsOn" A..= plcInlayHintsOn]
SMethod_TextDocumentRename -> ["renameOn" A..= plcRenameOn]
SMethod_TextDocumentHover -> ["hoverOn" A..= plcHoverOn]
SMethod_TextDocumentDocumentSymbol -> ["symbolsOn" A..= plcSymbolsOn]
SMethod_TextDocumentSignatureHelp -> ["signatureHelpOn" A..= plcSignatureHelpOn]
SMethod_TextDocumentCompletion -> ["completionOn" A..= plcCompletionOn]
SMethod_TextDocumentPrepareCallHierarchy -> ["callHierarchyOn" A..= plcCallHierarchyOn]
SMethod_TextDocumentSemanticTokensFull -> ["semanticTokensOn" A..= plcSemanticTokensOn]
SMethod_TextDocumentSemanticTokensFullDelta -> ["semanticTokensOn" A..= plcSemanticTokensOn]
_ -> []
-- | Generates json schema used in haskell vscode extension
-- Similar to 'pluginsToDefaultConfig' but simpler, since schema has a flatten structure
pluginsToVSCodeExtensionSchema :: IdePlugins a -> A.Value
pluginsToVSCodeExtensionSchema IdePlugins {..} = A.object $ mconcat $ singlePlugin <$> ipMap
where
singlePlugin PluginDescriptor {pluginConfigDescriptor = ConfigDescriptor {..}, ..} = genericSchema <> dedicatedSchema
where
(PluginHandlers (DMap.toList -> handlers)) = pluginHandlers
customConfigToDedicatedSchema (CustomConfig p) = toVSCodeExtensionSchema (withIdPrefix "config.") p
(PluginId pId) = pluginId
genericSchema =
let x =
[toKey' "diagnosticsOn" A..= schemaEntry "diagnostics" True | configHasDiagnostics]
<> nubOrd (mconcat (handlersToGenericSchema configInitialGenericConfig <$> handlers))
in case x of
-- If the plugin has only one capability, we produce globalOn instead of the specific one;
-- otherwise we don't produce globalOn at all
[_] -> [toKey' "globalOn" A..= schemaEntry "plugin" (plcGlobalOn configInitialGenericConfig)]
_ -> x
dedicatedSchema = customConfigToDedicatedSchema configCustomConfig
handlersToGenericSchema PluginConfig{..} (IdeMethod m DSum.:=> _) = case m of
SMethod_TextDocumentCodeAction -> [toKey' "codeActionsOn" A..= schemaEntry "code actions" plcCodeActionsOn]
SMethod_TextDocumentCodeLens -> [toKey' "codeLensOn" A..= schemaEntry "code lenses" plcCodeLensOn]
SMethod_TextDocumentInlayHint -> [toKey' "inlayHintsOn" A..= schemaEntry "inlay hints" plcInlayHintsOn]
SMethod_TextDocumentRename -> [toKey' "renameOn" A..= schemaEntry "rename" plcRenameOn]
SMethod_TextDocumentHover -> [toKey' "hoverOn" A..= schemaEntry "hover" plcHoverOn]
SMethod_TextDocumentDocumentSymbol -> [toKey' "symbolsOn" A..= schemaEntry "symbols" plcSymbolsOn]
SMethod_TextDocumentSignatureHelp -> [toKey' "signatureHelpOn" A..= schemaEntry "signature help" plcSignatureHelpOn]
SMethod_TextDocumentCompletion -> [toKey' "completionOn" A..= schemaEntry "completions" plcCompletionOn]
SMethod_TextDocumentPrepareCallHierarchy -> [toKey' "callHierarchyOn" A..= schemaEntry "call hierarchy" plcCallHierarchyOn]
SMethod_TextDocumentSemanticTokensFull -> [toKey' "semanticTokensOn" A..= schemaEntry "semantic tokens" plcSemanticTokensOn]
SMethod_TextDocumentSemanticTokensFullDelta -> [toKey' "semanticTokensOn" A..= schemaEntry "semantic tokens" plcSemanticTokensOn]
_ -> []
schemaEntry desc defaultVal =
A.object
[ "scope" A..= A.String "resource",
"type" A..= A.String "boolean",
"default" A..= A.Bool defaultVal,
"description" A..= A.String ("Enables " <> pId <> " " <> desc)
]
withIdPrefix x = "haskell.plugin." <> pId <> "." <> x
toKey' = fromString . T.unpack . withIdPrefix
-- | Generates markdown tables for custom config
pluginsCustomConfigToMarkdownTables :: IdePlugins a -> T.Text
pluginsCustomConfigToMarkdownTables IdePlugins {..} = T.unlines
$ map renderCfg
$ filter (\(PluginCustomConfig _ params) -> not $ null params)
$ map toPluginCustomConfig ipMap
where
toPluginCustomConfig :: PluginDescriptor ideState -> PluginCustomConfig
toPluginCustomConfig PluginDescriptor {pluginConfigDescriptor = ConfigDescriptor {configCustomConfig = c}, pluginId = PluginId pId} =
PluginCustomConfig { pcc'Name = pId, pcc'Params = toPluginCustomConfigParams c}
toPluginCustomConfigParams :: CustomConfig -> [PluginCustomConfigParam]
toPluginCustomConfigParams (CustomConfig p) = toPluginCustomConfigParams' p
toPluginCustomConfigParams' :: Properties r -> [PluginCustomConfigParam]
toPluginCustomConfigParams' EmptyProperties = []
toPluginCustomConfigParams' (ConsProperties (keyNameProxy :: KeyNameProxy s) (k :: SPropertyKey k) (m :: MetaData t) xs) =
toEntry (SomePropertyKeyWithMetaData k m) : toPluginCustomConfigParams' xs
where
toEntry :: SomePropertyKeyWithMetaData -> PluginCustomConfigParam
toEntry (SomePropertyKeyWithMetaData SNumber MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData SInteger MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData SString MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData SBoolean MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData (SObject _) MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = "TODO: nested object", -- T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData (SArray _) MetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = "TODO: Array values", -- T.pack $ show defaultValue,
pccp'EnumValues = []
}
toEntry (SomePropertyKeyWithMetaData (SEnum _) EnumMetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = map (T.pack . show) enumValues
}
toEntry (SomePropertyKeyWithMetaData SProperties PropertiesMetaData {..}) =
PluginCustomConfigParam {
pccp'Name = T.pack $ symbolVal keyNameProxy,
pccp'Description = description,
pccp'Default = T.pack $ show defaultValue,
pccp'EnumValues = []
}
renderCfg :: PluginCustomConfig -> T.Text
renderCfg (PluginCustomConfig pId pccParams) =
T.unlines (pluginHeader : tableHeader : rows pccParams)
where
pluginHeader = "## " <> pId
tableHeader =
"| Property | Description | Default | Allowed values |" <> "\n" <>
"| --- | --- | --- | --- |"
rows = map renderRow
renderRow PluginCustomConfigParam {..} =
"| `" <> pccp'Name <> "` | " <> pccp'Description <> " | `" <> pccp'Default <> "` | " <> renderEnum pccp'EnumValues <> " |"
renderEnum [] = " " -- Placeholder to prevent missing cells
renderEnum vs = "<ul> " <> (T.intercalate " " $ map (\x -> "<li><code>" <> x <> "</code></li>") vs) <> " </ul>"