From 4faad0d73df451a52bb6b3896f27ddd3c9c1cd42 Mon Sep 17 00:00:00 2001 From: Christian Georgii Date: Sun, 11 Jun 2023 21:11:56 +0200 Subject: [PATCH 1/2] Rewrite plugin tutorial --- docs/contributing/plugin-tutorial.md | 417 ++++++++++-------- hls-plugin-api/src/Ide/Types.hs | 5 +- .../src/Ide/Plugin/ExplicitImports.hs | 57 +-- 3 files changed, 257 insertions(+), 222 deletions(-) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 81608122ed..13b60de094 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -4,22 +4,55 @@ Haskell Language Server is an LSP server for the Haskell programming language. I to create a Haskell IDE, you can find many more details on the history and architecture in the [IDE 2020](https://mpickering.github.io/ide/index.html) community page. In this article we are going to cover the creation of an HLS plugin from scratch: a code lens to display explicit import lists. -Along the way we will learn about HLS, its plugin model, and the relationship with ghcide and LSP. +Along the way we will learn about HLS, its plugin model, and the relationship with [ghcide](https://github.com/haskell/haskell-language-server/tree/master/ghcide) and LSP. ## Introduction Writing plugins for HLS is a joy. Personally, I enjoy the ability to tap into the gigantic bag of goodies that is GHC, as well as the IDE integration thanks to LSP. -In the last couple of months I have written various HLS (and ghcide) plugins for things like: +In the last couple of months I have written various HLS plugins for things like: 1. Suggest imports for variables not in scope, 2. Remove redundant imports, 2. Evaluate code in comments (a la doctest), -3. Integrate the retrie refactoring library. +3. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library. -These plugins are small but meaningful steps towards a more polished IDE experience, and in writing them I didn't have to worry about performance, UI, distribution, or even think for the most part, since it's always another tool (usually GHC) doing all the heavy lifting. The plugins also make these tools much more accessible to all the users of HLS. +These plugins are small but meaningful steps towards a more polished IDE experience, and when writing them I didn't have to worry about performance, UI, distribution, or even think for the most part, since it's always another tool (usually GHC) doing all the heavy lifting. The plugins also make these tools much more accessible to all the users of HLS. -## The task +## Plugins in the HLS codebase + +The `haskell-language-server` codebase includes several plugins (found in `./plugins`). Notable examples include: + +- The `ormolu`, `fourmolu`, `floskell` and `stylish-haskell` plugins +- The `eval` plugin, a code lens provider to evaluate code in comments +- The `retrie` plugin, a code actions provider to execute retrie commands + +I would recommend looking at the existing plugins for inspiration and reference. A few conventions shared by all plugins: + +- Plugins are located in the `./plugins` folder +- Plugins implement their code under the `Ide.Plugin.*` namespace +- Folders containing the plugin follow the `hls-pluginname-plugin` naming convention +- Plugins are "linked" in `src/HlsPlugins.hs#idePlugins`, so new plugin descriptors must be added there: + ```haskell + -- src/HlsPlugins.hs + + idePlugins = pluginDescToIdePlugins allPlugins + where + allPlugins = + [ GhcIde.descriptor "ghcide" + , Pragmas.descriptor "pragmas" + , Floskell.descriptor "floskell" + , Fourmolu.descriptor "fourmolu" + , Ormolu.descriptor "ormolu" + , StylishHaskell.descriptor "stylish-haskell" + , Retrie.descriptor "retrie" + , Eval.descriptor "eval" + , NewPlugin.descriptor "new-plugin" -- New plugin added here + ] + ``` +To add a new plugin, extend the list of `allPlugins` and rebuild. + +## The goal of the plugin Here is a visual statement of what we want to accomplish: @@ -27,245 +60,143 @@ Here is a visual statement of what we want to accomplish: And here is the gist of the algorithm: -1. Request the type checking artefacts from the ghcide subsystem +1. Request the type checking artefacts from the `ghcide` subsystem 2. Extract the actual import lists from the type checked AST, 3. Ask GHC to produce the minimal import lists for this AST, -4. For every import statement without a explicit import list, find out the minimal import list, and produce a code lens to display it together with a command to graft it on. +4. For every import statement without a explicit import list: + - find out the minimal import list + - produce a code lens to display it and a command to apply it ## Setup -To get started, let’s fetch the HLS repo and build it. You need at least GHC 8.10 for this: +To get started, let’s fetch the HLS repo and build it by following the [installation instructions](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#building) in the Contributing section of this documentation. -``` -git clone --recursive http://github.com/haskell/haskell-language-server hls -cd hls -cabal update -cabal build -``` +If you run into any issues trying to build the binaries, you can get in touch with the HLS team using one of the [contact channels](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#how-to-contact-the-haskell-ide-team) or [open an issue](https://github.com/haskell/haskell-language-server/issues/new?assignees=&labels=status%3A+needs+triage%2C+type%3A+support&projects=&template=support.md&title=) in the HLS repository. + +Once the build is done, you can find the location of the `haskell-language-server` binary with ` cabal list-bin exe:haskell-language-server` and point your LSP client to it: -If you run into any issues trying to build the binaries, the #haskell-language-server IRC chat room in -[Libera Chat](https://libera.chat/) is always a good place to ask for help. +```sh +cabal list-bin exe:haskell-language-server +path/to/hls/dist-newstyle/build/x86_64-linux/ghc-9.2.7/haskell-language-server-2.0.0.0/x/haskell-language-server/build/haskell-language-server/haskell-language-server +``` -Once cabal is done take a note of the location of the `haskell-language-server` binary and point your LSP client to it. In VSCode this is done by editing the "Haskell Server Executable Path" setting. This way you can simply test your changes by reloading your editor after rebuilding the binary. +> **Note:** In VSCode this is done by editing the "Haskell Server Executable Path" setting. This way you can simply test your changes by reloading your editor after rebuilding the binary. ![Settings](settings-vscode.png) ## Anatomy of a plugin -HLS plugins are values of the `Plugin` datatype, which is defined in `Ide.Plugin` as: +HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as: ```haskell -data PluginDescriptor = - PluginDescriptor { pluginId :: !PluginId - , pluginRules :: !(Rules ()) - , pluginCommands :: ![PluginCommand] - , pluginCodeActionProvider :: !(Maybe CodeActionProvider) - , pluginCodeLensProvider :: !(Maybe CodeLensProvider) - , pluginHoverProvider :: !(Maybe HoverProvider) - , pluginSymbolsProvider :: !(Maybe SymbolsProvider) - , pluginFormattingProvider :: !(Maybe (FormattingProvider IO)) - , pluginCompletionProvider :: !(Maybe CompletionProvider) - , pluginRenameProvider :: !(Maybe RenameProvider) +data PluginDescriptor (ideState :: *) = + PluginDescriptor { pluginId :: PluginId + , pluginRules :: Rules () + , pluginCommands :: [PluginCommand ideState] + , pluginHandlers :: PluginHandlers ideState + , pluginNotificationHandlers :: PluginNotificationHandlers ideState + , [...] -- Other fields omitted for brevity. } ``` -A plugin has a unique id, a set of rules, a set of command handlers, and a set of "providers": +A plugin has a unique id, command handlers, request handlers, notification handlers and rules: -* Rules add new targets to the Shake build graph defined in ghcide. 99% of plugins need not define any new rules. -* Commands are an LSP abstraction for actions initiated by the user which are handled in the server. These actions can be long running and involve multiple modules. Many plugins define command handlers. -* Providers are a query-like abstraction where the LSP client asks the server for information. These queries must be fulfilled as quickly as possible. +* Commands are an LSP abstraction for actions initiated by the user which are handled in the server. These actions can be long running and involve multiple modules. +* Request handlers are called when an LSP client asks the server for information. These queries must be fulfilled as quickly as possible. +* Notification handlers are called by code that was not directly triggerd by an user/client. +* Rules add new targets to the Shake build graph defined in ghcide. Most plugins do not need to define new rules. -The HLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are: +## The explicit imports plugin -- The ghcide plugin, which embeds ghcide as a plugin (ghcide is also the engine under HLS). -- The example and example2 plugins, offering a dubious welcome to new contributors -- The ormolu, fourmolu, floskell and stylish-haskell plugins, a testament to the code formatting wars of our community. -- The eval plugin, a code lens provider to evaluate code in comments -- The retrie plugin, a code actions provider to execute retrie commands +To achieve the plugin goals, we will have to define: +- a command handler (`importLensCommand`) +- a code lens request handler (`lensProvider`) -I would recommend looking at the existing plugins for inspiration and reference. +These will be assembled together in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above. -Plugins are "linked" in the `HlsPlugins` module, so we will need to add our plugin there once we have defined it: +Using the convenience `defaultPluginDescriptor` function, the plugin can be bootstrapped with the required parts: ```haskell -idePlugins = pluginDescToIdePlugins allPlugins - where - allPlugins = - [ GhcIde.descriptor "ghcide" - , Pragmas.descriptor "pragmas" - , Floskell.descriptor "floskell" - , Fourmolu.descriptor "fourmolu" - , Ormolu.descriptor "ormolu" - , StylishHaskell.descriptor "stylish-haskell" - , Retrie.descriptor "retrie" - , Eval.descriptor "eval" - ] +-- plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs + +-- | The "main" function of a plugin +descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState +descriptor recorder plId = + (defaultPluginDescriptor plId) + { pluginCommands = [importLensCommand], -- The plugin provides a command handler + pluginHandlers = mconcat -- The plugin provides request handlers + [ lensProvider + ] + } ``` -To add a new plugin, simply extend the list of `allPlugins` and rebuild. - -## Providers - -99% of plugins will want to define at least one type of provider. But what is a provider? Let's take a look at some types: -```haskell -type CodeActionProvider = LSP.LspFuncs Config - -> IdeState - -> PluginId - -> TextDocumentIdentifier - -> Range - -> CodeActionContext - -> IO (Either ResponseError (List CAResult)) -type CompletionProvider = LSP.LspFuncs Config - -> IdeState - -> CompletionParams - -> IO (Either ResponseError CompletionResponseResult) +We'll start with the command, since it's the simplest of the two. -type CodeLensProvider = LSP.LspFuncs Config - -> IdeState - -> PluginId - -> CodeLensParams - -> IO (Either ResponseError (List CodeLens)) +### The command handler -type RenameProvider = LSP.LspFuncs Config - -> IdeState - -> RenameParams - -> IO (Either ResponseError WorkspaceEdit) -``` +In short, commands works like this: +- The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens. +- Whenever the client decides to execute the command on behalf of a user action (in this case a click on the code lens), it sends this same descriptor back to the LSP server which then proceeds to handle and execute the command. The latter part is implemented by the `commandFunc` field of our `PluginCommand` value. -Providers are functions that receive some inputs and produce an IO computation that returns either an error or some result. +> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work. -All providers receive an `LSP.LspFuncs` value, which is a record of functions to perform LSP actions. Most providers can safely ignore this argument, since the LSP interaction is automatically managed by HLS. -Some of its capabilities are: -- Querying the LSP client capabilities -- Manual progress reporting and cancellation, for plugins that provide long running commands (like the Retrie plugin), -- Custom user interactions via [message dialogs](https://microsoft.github.io/language-server-protocol/specification#window_showMessage). For instance, the Retrie plugin uses this to report skipped modules. +The command handler will be called `importLensCommand` and have the `PluginCommand` type, which is a type synonym defined in `Ide.Types` as: -The second argument plugins receive is `IdeState`, which encapsulates all the ghcide state including the build graph. This allows to request ghcide rule results, which leverages Shake to parallelize and reuse previous results as appropriate. Rule types are instances of the `RuleResult` type family, and -most of them are defined in `Development.IDE.Core.RuleTypes`. Some relevant rule types are: ```haskell --- | The parse tree for the file using GetFileContents -type instance RuleResult GetParsedModule = ParsedModule +-- hls-plugin-api/src/Ide/Types.hs --- | The type checked version of this file -type instance RuleResult TypeCheck = TcModuleResult - --- | A GHC session that we reuse. -type instance RuleResult GhcSession = HscEnvEq - --- | A GHC session preloaded with all the dependencies -type instance RuleResult GhcSessionDeps = HscEnvEq - --- | A ModSummary that has enough information to be used to get .hi and .hie files. -type instance RuleResult GetModSummary = ModSummary -``` - -The `use` family of combinators allow to request rule results. For example, the following code is used in the Eval plugin to request a GHC session and a module summary (for the imports) in order to set up an interactive evaluation environment -```haskell - let nfp = toNormalizedFilePath' fp - session <- runAction "runEvalCmd.ghcSession" state $ use_ GhcSessionDeps nfp - ms <- runAction "runEvalCmd.getModSummary" state $ use_ GetModSummary nfp -``` - -There are three flavours of `use` combinators: - -1. `use*` combinators block and propagate errors, -2. `useWithStale*` combinators block and switch to stale data in case of error, -3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise. - -## LSP abstractions - -If you have used VSCode or any other LSP editor you are probably already familiar with the capabilities afforded by LSP. If not, check the [specification](https://microsoft.github.io/language-server-protocol/specification) for the full details. -Another good source of information is the [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types) package, which contains a Haskell encoding of the protocol. - -The [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types-0.22.0.0/docs/Language-Haskell-LSP-Types.html#t:CodeLens) package encodes code lenses in Haskell as: -```haskell -data CodeLens = - CodeLens - { _range :: Range - , _command :: Maybe Command - , _xdata :: Maybe A.Value - } deriving (Read,Show,Eq) -``` -That is, a code lens is a triple of a source range, maybe a command, and optionally some extra data. The [specification](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens) clarifies the optionality: -``` -/** - * A code lens represents a command that should be shown along with - * source text, like the number of references, a way to run tests, etc. - * - * A code lens is _unresolved_ when no command is associated to it. For performance - * reasons the creation of a code lens and resolving should be done in two stages. - */ +data PluginCommand ideState = forall a. (FromJSON a) => + PluginCommand { commandId :: CommandId + , commandDesc :: T.Text + , commandFunc :: CommandFunction ideState a + } ``` -To keep things simple our plugin won't make use of the unresolved facility, embedding the command directly in the code lens. - -## The explicit imports plugin - -To provide code lenses, our plugin must define a code lens provider as well as a Command handler. -The code at `Ide.Plugin.Example` shows how the convenience `defaultPluginDescriptor` function is used -to bootstrap the plugin and how to add the desired providers: +Let's start by creating an unfinished command handler. We'll give it an id and a description for now: ```haskell -descriptor :: PluginId -> PluginDescriptor -descriptor plId = (defaultPluginDescriptor plId) { - -- This plugin provides code lenses - pluginCodeLensProvider = Just provider, - -- This plugin provides a command handler - pluginCommands = [ importLensCommand ] -} +-- | The command handler +importLensCommand :: PluginCommand IdeState +importLensCommand = + PluginCommand + { commandId = "ImportLensCommand" + , commandDesc = "Explicit import command" + , commandFunc = runImportCommand + } + +-- | Not implemented yet. +runImportCommand = undefined ``` -### The command handler - -Our plugin provider has two components that need to be fleshed out. Let's start with the command provider, since it's the simplest of the two. +The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, another type synonym from `LSP.Types`: ```haskell -importLensCommand :: PluginCommand -``` +-- hls-plugin-api/src/Ide/Types.hs -`PluginCommand` is a type synonym defined in `LSP.Types` as: - -```haskell -data PluginCommand = forall a. (FromJSON a) => - PluginCommand { commandId :: CommandId - , commandDesc :: T.Text - , commandFunc :: CommandFunction a - } -``` - -The meat is in the `commandFunc` field, which is of type `CommandFunction`, another type synonym from `LSP.Types`: -```haskell -type CommandFunction a = - LSP.LspFuncs Config - -> IdeState +type CommandFunction ideState a + = ideState -> a - -> IO (Either ResponseError Value, Maybe (ServerMethod, ApplyWorkspaceEditParams)) + -> LspM Config (Either ResponseError Value) ``` -`CommandFunction` takes in the familiar `LspFuncs` and `IdeState` arguments, together with a JSON encoded argument. -I recommend checking the LSP spec in order to understand how commands work, but briefly the LSP server (us) initially sends a command descriptor to the client, in this case as part of a code lens. When the client decides to execute the command on behalf of a user action (in this case a click on the code lens), the client sends this descriptor back to the LSP server which then proceeds to handle and execute the command. The latter part is implemented by the `commandFunc` field of our `PluginCommand` value. - -For our command, we are going to have a very simple handler that receives a diff (`WorkspaceEdit`) and returns it to the client. The diff will be generated by our code lens provider and sent as part -of the code lens to the LSP client, who will send it back to our command handler when the user activates -the code lens: -```haskell -importCommandId :: CommandId -importCommandId = "ImportLensCommand" +`CommandFunction` takes in an `ideState` and a JSON-encodable argument. -importLensCommand :: PluginCommand -importLensCommand = - PluginCommand importCommandId "Explicit import command" runImportCommand +Our handler will ignore the state argument and only really use the `WorkspaceEdit` argument. +```haskell -- | The type of the parameters accepted by our command -data ImportCommandParams = ImportCommandParams WorkspaceEdit - deriving Generic +newtype ImportCommandParams = ImportCommandParams WorkspaceEdit + deriving (Generic) deriving anyclass (FromJSON, ToJSON) -- | The actual command handler -runImportCommand :: CommandFunction ImportCommandParams -runImportCommand _lspFuncs _state (ImportCommandParams edit) = do - return (Right Null, Just (WorkspaceApplyEdit, ApplyWorkspaceEditParams edit)) - +runImportCommand :: CommandFunction IdeState ImportCommandParams +runImportCommand _ (ImportCommandParams edit) = do + -- This command simply triggers a workspace edit! + _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + return (Right Null) ``` +It [sends a request](https://hackage.haskell.org/package/lsp-1.6.0.0/docs/Language-LSP-Server.html#v:sendRequest) with the method `SWorkspaceApplyEdit` to the server with the `ApplyWorkspaceEditParams Nothing edit` parameters and a response handler (that does nothing). It then returns `Right Null`, an empty `Aeson.Value` wrapped in `Right`. + ### The code lens provider The code lens provider implements all the steps of the algorithm described earlier: @@ -392,3 +323,105 @@ The full code as used in this tutorial, including imports, can be found in [this I hope this has given you a taste of how easy and joyful it is to write plugins for HLS. If you are looking for ideas for contributing, here are some cool ones found in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues?q=is%3Aopen+is%3Aissue+label%3A%22type%3A+possible+new+plugin%22). + +TODO: Figure out what to do with the following sections: + +## Further info + +### LSP abstractions + +If you have used VSCode or any other LSP editor you are probably already familiar with the capabilities afforded by LSP. If not, check the [specification](https://microsoft.github.io/language-server-protocol/specification) for the full details. +Another good source of information is the [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types) package, which contains a Haskell encoding of the protocol. + +The [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types-0.22.0.0/docs/Language-Haskell-LSP-Types.html#t:CodeLens) package encodes code lenses in Haskell as: +```haskell +data CodeLens = + CodeLens + { _range :: Range + , _command :: Maybe Command + , _xdata :: Maybe A.Value + } deriving (Read,Show,Eq) +``` +That is, a code lens is a triple of a source range, maybe a command, and optionally some extra data. The [specification](https://microsoft.github.io/language-server-protocol/specification#textDocument_codeLens) clarifies the optionality: +``` +/** + * A code lens represents a command that should be shown along with + * source text, like the number of references, a way to run tests, etc. + * + * A code lens is _unresolved_ when no command is associated to it. For performance + * reasons the creation of a code lens and resolving should be done in two stages. + */ +``` + +To keep things simple our plugin won't make use of the unresolved facility, embedding the command directly in the code lens. + +### Providers +**DEPRECATED**: _Providers were split into request handlers and notification handlers. The following section might contain outdated information._ + +99% of plugins will want to define at least one type of provider. But what is a provider? Let's take a look at some types: +```haskell +type CodeActionProvider = LSP.LspFuncs Config + -> IdeState + -> PluginId + -> TextDocumentIdentifier + -> Range + -> CodeActionContext + -> IO (Either ResponseError (List CAResult)) + +type CompletionProvider = LSP.LspFuncs Config + -> IdeState + -> CompletionParams + -> IO (Either ResponseError CompletionResponseResult) + +type CodeLensProvider = LSP.LspFuncs Config + -> IdeState + -> PluginId + -> CodeLensParams + -> IO (Either ResponseError (List CodeLens)) + +type RenameProvider = LSP.LspFuncs Config + -> IdeState + -> RenameParams + -> IO (Either ResponseError WorkspaceEdit) +``` + +Providers are functions that receive some inputs and produce an IO computation that returns either an error or some result. + +All providers receive an `LSP.LspFuncs` value, which is a record of functions to perform LSP actions. Most providers can safely ignore this argument, since the LSP interaction is automatically managed by HLS. +Some of its capabilities are: +- Querying the LSP client capabilities +- Manual progress reporting and cancellation, for plugins that provide long running commands (like the Retrie plugin), +- Custom user interactions via [message dialogs](https://microsoft.github.io/language-server-protocol/specification#window_showMessage). For instance, the Retrie plugin uses this to report skipped modules. + +The second argument plugins receive is `IdeState`, which encapsulates all the ghcide state including the build graph. This allows to request ghcide rule results, which leverages Shake to parallelize and reuse previous results as appropriate. Rule types are instances of the `RuleResult` type family, and +most of them are defined in `Development.IDE.Core.RuleTypes`. Some relevant rule types are: +```haskell +-- | The parse tree for the file using GetFileContents +type instance RuleResult GetParsedModule = ParsedModule + +-- | The type checked version of this file +type instance RuleResult TypeCheck = TcModuleResult + +-- | A GHC session that we reuse. +type instance RuleResult GhcSession = HscEnvEq + +-- | A GHC session preloaded with all the dependencies +type instance RuleResult GhcSessionDeps = HscEnvEq + +-- | A ModSummary that has enough information to be used to get .hi and .hie files. +type instance RuleResult GetModSummary = ModSummary +``` + +The `use` family of combinators allow to request rule results. For example, the following code is used in the Eval plugin to request a GHC session and a module summary (for the imports) in order to set up an interactive evaluation environment +```haskell + let nfp = toNormalizedFilePath' fp + session <- runAction "runEvalCmd.ghcSession" state $ use_ GhcSessionDeps nfp + ms <- runAction "runEvalCmd.getModSummary" state $ use_ GetModSummary nfp +``` + +There are three flavours of `use` combinators: + +1. `use*` combinators block and propagate errors, +2. `useWithStale*` combinators block and switch to stale data in case of error, +3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise. + diff --git a/hls-plugin-api/src/Ide/Types.hs b/hls-plugin-api/src/Ide/Types.hs index 7647d085b7..ff47fbd150 100644 --- a/hls-plugin-api/src/Ide/Types.hs +++ b/hls-plugin-api/src/Ide/Types.hs @@ -69,6 +69,7 @@ import Data.GADT.Compare 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 @@ -269,7 +270,7 @@ instance ToJSON PluginConfig where -- --------------------------------------------------------------------- -data PluginDescriptor (ideState :: *) = +data PluginDescriptor (ideState :: Type) = PluginDescriptor { pluginId :: !PluginId -- ^ Unique identifier of the plugin. , pluginPriority :: Natural @@ -499,7 +500,7 @@ instance PluginMethod Request TextDocumentDocumentSymbol where uri = msgParams ^. J.textDocument . J.uri instance PluginMethod Request CompletionItemResolve where - pluginEnabled _ msgParams pluginDesc config = pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) + pluginEnabled _ _ pluginDesc config = pluginEnabledConfig plcCompletionOn (configForPlugin config pluginDesc) instance PluginMethod Request TextDocumentCompletion where pluginEnabled _ msgParams pluginDesc config = pluginResponsible uri pluginDesc diff --git a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs index 331eb72d91..e106cf61c2 100644 --- a/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs +++ b/plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs @@ -11,7 +11,6 @@ module Ide.Plugin.ExplicitImports ( descriptor - , descriptorForModules , extractMinimalImports , within , abbreviateImportTitle @@ -43,9 +42,6 @@ import Ide.Types import Language.LSP.Server import Language.LSP.Types -importCommandId :: CommandId -importCommandId = "ImportLensCommand" - newtype Log = LogShake Shake.Log deriving Show @@ -56,35 +52,37 @@ instance Pretty Log where -- | The "main" function of a plugin descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState -descriptor recorder = - -- (almost) no one wants to see an explicit import list for Prelude - descriptorForModules recorder (/= moduleName pRELUDE) - -descriptorForModules - :: Recorder (WithPriority Log) - -> (ModuleName -> Bool) - -- ^ Predicate to select modules that will be annotated - -> PluginId - -> PluginDescriptor IdeState -descriptorForModules recorder pred plId = +descriptor recorder plId = (defaultPluginDescriptor plId) - { - -- This plugin provides a command handler + { -- This plugin provides a command handler pluginCommands = [importLensCommand], -- This plugin defines a new rule pluginRules = minimalImportsRule recorder, pluginHandlers = mconcat [ -- This plugin provides code lenses - mkPluginHandler STextDocumentCodeLens $ lensProvider pred + lensProvider -- This plugin provides code actions - , mkPluginHandler STextDocumentCodeAction $ codeActionProvider pred + , codeActionProvider ] } +codeActionProvider :: PluginHandlers IdeState +codeActionProvider = mkPluginHandler STextDocumentCodeAction $ codeActionProvider' + +lensProvider :: PluginHandlers IdeState +lensProvider = mkPluginHandler STextDocumentCodeLens $ lensProvider' + +importCommandId :: CommandId +importCommandId = "ImportLensCommand" + -- | The command descriptor importLensCommand :: PluginCommand IdeState importLensCommand = - PluginCommand importCommandId "Explicit import command" runImportCommand + PluginCommand + { commandId = importCommandId + , commandDesc = "Explicit import command" + , commandFunc = runImportCommand + } -- | The type of the parameters accepted by our command newtype ImportCommandParams = ImportCommandParams WorkspaceEdit @@ -93,7 +91,7 @@ newtype ImportCommandParams = ImportCommandParams WorkspaceEdit -- | The actual command handler runImportCommand :: CommandFunction IdeState ImportCommandParams -runImportCommand _state (ImportCommandParams edit) = do +runImportCommand _ (ImportCommandParams edit) = do -- This command simply triggers a workspace edit! _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) return (Right Null) @@ -108,9 +106,8 @@ runImportCommand _state (ImportCommandParams edit) = do -- the provider should produce one code lens associated to the import statement: -- -- > import Data.List (intercalate, sortBy) -lensProvider :: (ModuleName -> Bool) -> PluginMethodHandler IdeState TextDocumentCodeLens -lensProvider - pred +lensProvider' :: PluginMethodHandler IdeState TextDocumentCodeLens +lensProvider' state -- ghcide state, used to retrieve typechecking artifacts pId -- plugin Id CodeLensParams {_textDocument = TextDocumentIdentifier {_uri}} @@ -127,7 +124,7 @@ lensProvider sequence [ generateLens pId _uri edit | (imp, Just minImport) <- minImports, - Just edit <- [mkExplicitEdit pred posMapping imp minImport] + Just edit <- [mkExplicitEdit predImport posMapping imp minImport] ] return $ Right (List $ catMaybes commands) _ -> @@ -137,8 +134,8 @@ lensProvider -- | If there are any implicit imports, provide one code action to turn them all -- into explicit imports. -codeActionProvider :: (ModuleName -> Bool) -> PluginMethodHandler IdeState TextDocumentCodeAction -codeActionProvider pred ideState _pId (CodeActionParams _ _ docId range _context) +codeActionProvider' :: PluginMethodHandler IdeState TextDocumentCodeAction +codeActionProvider' ideState _pId (CodeActionParams _ _ docId range _context) | TextDocumentIdentifier {_uri} <- docId, Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri = liftIO $ do @@ -157,7 +154,7 @@ codeActionProvider pred ideState _pId (CodeActionParams _ _ docId range _context [ e | (imp, Just explicit) <- maybe [] getMinimalImportsResult minImports, - Just e <- [mkExplicitEdit pred zeroMapping imp explicit] + Just e <- [mkExplicitEdit predImport zeroMapping imp explicit] ] caExplicitImports = InR CodeAction {..} _title = "Make all imports explicit" @@ -350,3 +347,7 @@ runIde = runAction "importLens" within :: Range -> SrcSpan -> Bool within (Range start end) span = isInsideSrcSpan start span || isInsideSrcSpan end span + +-- | (almost) no one wants to see an explicit import list for Prelude +predImport :: ModuleName -> Bool +predImport = (/= moduleName pRELUDE) From e3155c6ad4ff8f044426761cf6f32241b9fe9e7d Mon Sep 17 00:00:00 2001 From: Christian Georgii Date: Thu, 15 Jun 2023 11:49:26 +0200 Subject: [PATCH 2/2] Code review changes --- docs/contributing/plugin-tutorial.md | 33 ++++++++++------------------ 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 13b60de094..7b8d8b3b21 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -60,7 +60,7 @@ Here is a visual statement of what we want to accomplish: And here is the gist of the algorithm: -1. Request the type checking artefacts from the `ghcide` subsystem +1. Request the type checking artefacts 2. Extract the actual import lists from the type checked AST, 3. Ask GHC to produce the minimal import lists for this AST, 4. For every import statement without a explicit import list: @@ -69,20 +69,11 @@ And here is the gist of the algorithm: ## Setup -To get started, let’s fetch the HLS repo and build it by following the [installation instructions](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#building) in the Contributing section of this documentation. +To get started, let’s fetch the HLS repo and build it by following the [setup instructions](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#building) in the Contributing section of this documentation. -If you run into any issues trying to build the binaries, you can get in touch with the HLS team using one of the [contact channels](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#how-to-contact-the-haskell-ide-team) or [open an issue](https://github.com/haskell/haskell-language-server/issues/new?assignees=&labels=status%3A+needs+triage%2C+type%3A+support&projects=&template=support.md&title=) in the HLS repository. +If you run into any issues trying to build the binaries, you can get in touch with the HLS team using one of the [contact channels](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#how-to-contact-the-haskell-ide-team) or [open an issue](https://github.com/haskell/haskell-language-server/issues) in the HLS repository. -Once the build is done, you can find the location of the `haskell-language-server` binary with ` cabal list-bin exe:haskell-language-server` and point your LSP client to it: - -```sh -cabal list-bin exe:haskell-language-server -path/to/hls/dist-newstyle/build/x86_64-linux/ghc-9.2.7/haskell-language-server-2.0.0.0/x/haskell-language-server/build/haskell-language-server/haskell-language-server -``` - -> **Note:** In VSCode this is done by editing the "Haskell Server Executable Path" setting. This way you can simply test your changes by reloading your editor after rebuilding the binary. - -![Settings](settings-vscode.png) +Make sure you use the HLS package you just built by following [this section](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#manually-testing-your-hacked-hls) of the "Contributing" guide. ## Anatomy of a plugin @@ -99,10 +90,10 @@ data PluginDescriptor (ideState :: *) = ``` A plugin has a unique id, command handlers, request handlers, notification handlers and rules: -* Commands are an LSP abstraction for actions initiated by the user which are handled in the server. These actions can be long running and involve multiple modules. * Request handlers are called when an LSP client asks the server for information. These queries must be fulfilled as quickly as possible. * Notification handlers are called by code that was not directly triggerd by an user/client. -* Rules add new targets to the Shake build graph defined in ghcide. Most plugins do not need to define new rules. +* Rules add new targets to the Shake build graph. Most plugins do not need to define new rules. +* Commands are an LSP abstraction for actions initiated by the user which are handled in the server. These actions can be long running and involve multiple modules. ## The explicit imports plugin @@ -134,11 +125,11 @@ We'll start with the command, since it's the simplest of the two. In short, commands works like this: - The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens. -- Whenever the client decides to execute the command on behalf of a user action (in this case a click on the code lens), it sends this same descriptor back to the LSP server which then proceeds to handle and execute the command. The latter part is implemented by the `commandFunc` field of our `PluginCommand` value. +- Whenever the client decides to execute the command on behalf of a user (in this case a click on the code lens), it sends this same descriptor back to the LSP server which then proceeds to handle and execute the command. The latter part is implemented by the `commandFunc` field of our `PluginCommand` value. > **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work. -The command handler will be called `importLensCommand` and have the `PluginCommand` type, which is a type synonym defined in `Ide.Types` as: +The command handler will be called `importLensCommand` and have the `PluginCommand` type, which is a type defined in `Ide.Types` as: ```haskell -- hls-plugin-api/src/Ide/Types.hs @@ -166,7 +157,7 @@ importLensCommand = runImportCommand = undefined ``` -The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, another type synonym from `LSP.Types`: +The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, a type synonym from `LSP.Types`: ```haskell -- hls-plugin-api/src/Ide/Types.hs @@ -195,13 +186,13 @@ runImportCommand _ (ImportCommandParams edit) = do return (Right Null) ``` -It [sends a request](https://hackage.haskell.org/package/lsp-1.6.0.0/docs/Language-LSP-Server.html#v:sendRequest) with the method `SWorkspaceApplyEdit` to the server with the `ApplyWorkspaceEditParams Nothing edit` parameters and a response handler (that does nothing). It then returns `Right Null`, an empty `Aeson.Value` wrapped in `Right`. +It [sends a request](https://hackage.haskell.org/package/lsp-1.6.0.0/docs/Language-LSP-Server.html#v:sendRequest) with the method `SWorkspaceApplyEdit` to the client with the `ApplyWorkspaceEditParams Nothing edit` parameters and a response handler (that does nothing). It then returns `Right Null`, an empty `Aeson.Value` wrapped in `Right`. ### The code lens provider The code lens provider implements all the steps of the algorithm described earlier: -> 1. Request the type checking artefacts from the ghcide subsystem +> 1. Request the type checking artefacts > 2. Extract the actual import lists from the type checked AST, > 3. Ask GHC to produce the minimal import lists for this AST, > 4. For every import statement without a explicit import list, find out what's the minimal import list, and produce a code lens to display it together with a diff to graft the import list in. @@ -333,7 +324,7 @@ TODO: Figure out what to do with the following sections: If you have used VSCode or any other LSP editor you are probably already familiar with the capabilities afforded by LSP. If not, check the [specification](https://microsoft.github.io/language-server-protocol/specification) for the full details. Another good source of information is the [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types) package, which contains a Haskell encoding of the protocol. -The [haskell-lsp-types](https://hackage.haskell.org/package/haskell-lsp-types-0.22.0.0/docs/Language-Haskell-LSP-Types.html#t:CodeLens) package encodes code lenses in Haskell as: +The [haskell-lsp-types](https://hackage.haskell.org/package/lsp-types) package encodes code lenses in Haskell as: ```haskell data CodeLens = CodeLens