Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Regularize custom config of plugins #1576

Merged
merged 18 commits into from Mar 19, 2021

Conversation

berberman
Copy link
Collaborator

@berberman berberman commented Mar 15, 2021

With #691, each plugin is able to have a generic config:

{
   "pluginName":{
      "globalOn":false,
      "codeActionsOn":false,
      "codeLensOn":false,
      "diagnosticsOn":false,
      "hoverOn":false,
      "symbolsOn":false,
      "formattingOn":false,
      "completionOn":false,
      "renameOn":false,
      "config":{
         
      }
 }

where particularly, config is an arbitrary JSON object representing the dedicated, or the extra custom config, which can be parsed and used by the plugin. However, this is hard to be documented and used from the clinet side -- I don't know much about other lsp clients, but for vscode, we have to write the schema manually, like @jneira did in haskell/vscode-haskell#361, which is kind of cumbersome. So it would be better to automate this process.

This PR includes mainly two changes:

  • Defines a new data type Properties to let plugin type-safely describe what custom config it use
  • Adds --vscode-extension-schema to ghcide and hls executables, which prints the generated schema for vscode
  • Adds --generate-default-config to ghcide and hls executables, which prints the generated lsp json config with default values

Properties

In order to make plugins' custom config compatible with vscode, i.e. it's expressible in vscode settings UI, a property is defiend as a direct child of config JSON object, where the value has five potential types (taken from references of vscode):

  • Number
  • String
  • Boolean
  • Object
  • Array
  • Enum

Properties is a set of descriptions of properties used by the plugin. It has a phantom type r :: [*] to make sure the name, or the key of the property is unique, and we can derive the corresponding haskell type of the property once we have its name. Given a property name and Properties, we also can derive a JSON parser of this proeprty, which acts on the config object. Overall, Properties is used in two places: vscode schema generation and parser derivation at runtime. Currently type lenses plugin and tactics plugin have their own custom configs, and let's use type lenses plugin as an exmaple:

properties = emptyProperties
  & defineEnumProperty #mode "Control how type lenses are shown"
    [ ("always", "Always displays type lenses of global bindings")
    , ("exported", "Only display type lenses of exported global bindings")
    , ("diagnostics", "Follows error messages produced by GHC about missing signatures")
    ] "always"

where properties has type Properties '[ 'PropertyKey "mode" 'TEnum], inferred by GHC. We define a enum property calledmode, which have three valid values and a default value "always". And in plugin handler,

codeLensProvider ::
  IdeState ->
  PluginId ->
  CodeLensParams ->
  LSP.LspM Config (Either ResponseError (List CodeLens))
codeLensProvider ideState pId _ = do
  -- The type of mode is Text
  mode <- usePropertyLsp #mode pId properties
  -- ...

we can directly use properties defined in the set with the following function (ToHsType maps property types to corresponding haskell types):

usePropertyLsp ::
  (HasProperty s k t r, MonadLsp Config m) =>
  KeyNameProxy s ->
  PluginId ->
  Properties r ->
  m (ToHsType t)

It uses the parser derived from the property definition to parse the incoming Object from lsp monad, returning the default value as fallback if parse error occurs or the resource is unavailable. Plugin's Properties is also wrapped and passed to PluginDescriptor:

data CustomConfig = forall r. CustomConfig (Properties r)

data PluginDescriptor ideState =
  PluginDescriptor { pluginId           :: !PluginId
                   , pluginRules        :: !(Rules ())
                   , pluginCommands     :: ![PluginCommand ideState]
                   , pluginHandlers     :: PluginHandlers ideState
                   , pluginCustomConfig :: CustomConfig
                   }

where pluginCustomConfig is used only for schema generation, not in normal runtime.

Schema generation

Except diagnosticsOn (because diagnostics emit in shake rules), other generic config values can be captured if the plugin has registered the handler of the lsp capability. Combined with Properties, now we are able to generate the schema:

$ cabal run haskell-language-server -- --vscode-extension-schema

{
    "haskell.plugin.haddockComments.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables haddockComments code actions"
    },
    "haskell.plugin.class.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables class code actions"
    },
    "haskell.plugin.retrie.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables retrie code actions"
    },
    "haskell.plugin.tactic.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables tactic code actions"
    },
    "haskell.plugin.ghcide-type-lenses.config.mode": {
        "default": "always",
        "scope": "resource",
        "type": "string",
        "enum": [
            "always",
            "exported",
            "diagnostics"
        ],
        "enumDescriptions": [
            "Always displays type lenses of global bindings",
            "Only display type lenses of exported global bindings",
            "Follows error messages produced by GHC about missing signatures"
        ],
        "description": "Control how type lenses are shown"
    },
    "haskell.plugin.hlint.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables hlint code actions"
    },
    "haskell.plugin.pragmas.completionOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables pragmas completions"
    },
    "haskell.plugin.ghcide-hover-and-symbols.symbolsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables ghcide-hover-and-symbols symbols"
    },
    "haskell.plugin.moduleName.codeLensOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables moduleName code lenses"
    },
    "haskell.plugin.ghcide-code-actions.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables ghcide-code-actions code actions"
    },
    "haskell.plugin.pragmas.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables pragmas code actions"
    },
    "haskell.plugin.importLens.codeLensOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables importLens code lenses"
    },
    "haskell.plugin.splice.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables splice code actions"
    },
    "haskell.plugin.eval.codeLensOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables eval code lenses"
    },
    "haskell.plugin.ghcide-hover-and-symbols.hoverOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables ghcide-hover-and-symbols hover"
    },
    "haskell.plugin.ghcide-type-lenses.codeLensOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables ghcide-type-lenses code lenses"
    },
    "haskell.plugin.ghcide-completions.completionOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables ghcide-completions completions"
    },
    "haskell.plugin.importLens.codeActionsOn": {
        "default": true,
        "scope": "resource",
        "type": "boolean",
        "description": "Enables importLens code actions"
    }
}

@jneira jneira self-requested a review March 15, 2021 09:59
@jneira
Copy link
Member

jneira commented Mar 15, 2021

That really a nice feature. However i had to manually choose the set of configuration options to reduce them as much as possible without lose control. And finally i added only some of them. Having the complete list automatically to cut down it is great!

What do you think about another option, named --generate-defult-config f.e. that outputs the entire json configuration with default values, to be used in any editor simply creating a file with that json in the appropiate directory. The key is that fetaure could be useful for all editors out there. It would be used to get a look to the configuration (with values instead a schema but useful anyways)

It could be complementary with this one and be done in another pr, of course.

@berberman
Copy link
Collaborator Author

berberman commented Mar 16, 2021

$ cabal run haskell-language-server -- --generate-default-config

{
    "haskell": {
        "hlintOn": true,
        "formatOnImportOn": true,
        "checkParents": "CheckOnSaveAndClose",
        "liquidOn": false,
        "maxCompletions": 40,
        "checkProject": true,
        "diagnosticsOnChange": true,
        "completionSnippetsOn": true,
        "diagnosticsDebounceDuration": 350000,
        "formattingProvider": "ormolu",
        "plugin": {
            "hlint": {
                "globalOn": true,
                "codeActionsOn": true
            },
            "moduleName": {
                "globalOn": true,
                "codeLensOn": true
            },
            "splice": {
                "globalOn": true,
                "codeActionsOn": true
            },
            "pragmas": {
                "globalOn": true,
                "completionOn": true,
                "codeActionsOn": true
            },
            "eval": {
                "globalOn": true,
                "codeLensOn": true
            },
            "ghcide-type-lenses": {
                "globalOn": true,
                "config": {
                    "mode": "always"
                },
                "codeLensOn": true
            },
            "ghcide-completions": {
                "globalOn": true,
                "completionOn": true
            },
            "ghcide-hover-and-symbols": {
                "globalOn": true,
                "symbolsOn": true,
                "hoverOn": true
            },
            "importLens": {
                "globalOn": true,
                "codeLensOn": true,
                "codeActionsOn": true
            },
            "ghcide-code-actions": {
                "globalOn": true,
                "codeActionsOn": true
            },
            "retrie": {
                "globalOn": true,
                "codeActionsOn": true
            },
            "tactic": {
                "globalOn": true,
                "config": {
                    "max_use_ctor_actions": 5,
                    "features": ""
                },
                "codeActionsOn": true
            },
            "haddockComments": {
                "globalOn": true,
                "codeActionsOn": true
            },
            "class": {
                "globalOn": true,
                "codeActionsOn": true
            }
        }
    }
}

@michaelpj
Copy link
Collaborator

What do you think about another option, named --generate-defult-config f.e. that outputs the entire json configuration with default values, to be used in any editor simply creating a file with that json in the appropiate directory.

I mean, not any editor. This is the vscode editor config, the fact that other editors sometimes support it is a nice bonus. Let's not elevate it to universal status just yet :) Concretely, can we call it --generate-vscode-default-config or something?

@isovector
Copy link
Collaborator

I love the idea here. I haven't dug into the impl, but I'm happy to send a followup wingman PR migrating to the system.

@@ -8,26 +8,30 @@
module Utils where

import Control.Applicative.Combinators (skipManyTill)
import Control.Lens hiding (failing, (<.>))
import Control.Monad (unless)
import Control.Lens hiding (failing, (.=), (<.>))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please don't adjust the formatting of this file

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was done by the pre-commit hook. I will revert it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. @Ailrun, is there a way we can disable the pre-commit hook for Wingman files?

Copy link
Collaborator

@isovector isovector left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor nitpicks about wording, plugin-id and formatting, but otherwise looks fantastic!

@jneira
Copy link
Member

jneira commented Mar 17, 2021

@michaelpj

I mean, not any editor. This is the vscode editor config, the fact that other editors sometimes support it is a nice bonus. Let's not elevate it to universal status just yet :) Concretely, can we call it --generate-vscode-default-config or something?

I am a little bit confused, i thought the json config was inherent to lsp server and no belongs to any concrete editor. So all editors have to use the same json to send and configure the server.

@berberman
Copy link
Collaborator Author

I agree with @jneira. Although other lsp clients may not be able to use it directly whereas they have their own config ways, this json value still represents the superset of configuration acceptable by our server.

@michaelpj
Copy link
Collaborator

At okay, never mind, I misunderstood! :)

Copy link
Collaborator

@isovector isovector left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good. Left my stream-of-consciousness as comments. The Properties module is in desperate need of haddock and notes, but besides that I love it!

@berberman
Copy link
Collaborator Author

Now I think this PR is pretty much ready

--
-- "stylish-haskell": {
-- "globalOn": true
-- }
geenericDefaultConfig =
genericDefaultConfig =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, really nice

Copy link
Collaborator

@isovector isovector left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Thanks for the changes, and the fantastic work!

@berberman berberman added the merge me Label to trigger pull request merge label Mar 19, 2021
@berberman berberman removed the merge me Label to trigger pull request merge label Mar 19, 2021
@berberman berberman merged commit f135edb into haskell:master Mar 19, 2021
@berberman berberman deleted the declarative-custom-config branch March 19, 2021 05:30
@isovector
Copy link
Collaborator

@berberman is there anything I should be doing when I update my properties? The changes don't appear to show up in the VScode settings dialog.

@jneira
Copy link
Member

jneira commented Apr 7, 2021

The vscode extension itself should be updated "manually" although the new hls --vscode-extension-schema helps a lot to do it
See my last pr about: https://github.com/haskell/vscode-haskell/pull/361/files#diff-7ae45ad102eab3b6d7e7896acd08c427a9b25b346470d7bc6507b6481575d519R170

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants