From 8e027800f47cba2ec4e54d437e69eeecb878e772 Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Wed, 23 Apr 2025 21:04:50 +0200 Subject: [PATCH 1/8] Plugin tutorial, more changes Some changes are mine, but many have been cherry picked and amended from PR #3655 by Christian Georgii . --- docs/contributing/plugin-tutorial.md | 362 +++++++++++---------------- 1 file changed, 149 insertions(+), 213 deletions(-) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index c952ef9eb2..0c3424ab7f 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -1,26 +1,61 @@ # Let’s write a Haskell Language Server plugin Originally written by Pepe Iborra, maintained by the Haskell community. -Haskell Language Server (HLS) is an LSP server for the Haskell programming language. It builds on several previous efforts -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. - +Haskell Language Server (HLS) is an LSP server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE. +You can find many more details on the history and architecture on 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, including: 1. Suggest imports for variables not in scope, 2. Remove redundant imports, -2. Evaluate code in comments (à la [doctest](https://docs.python.org/3/library/doctest.html)), -3. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library. +3. Evaluate code in comments (à la [doctest](https://docs.python.org/3/library/doctest.html)), +4. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library. + +These plugins are small but meaningful steps towards a more polished IDE experience. +While writing them, I didn't have to worry about performance, UI, or distribution; another tool (usually GHC) always did the heavy lifting. + +The plugins also make these tools much more accessible to all users of HLS. + +## Plugins in the HLS codebase + +The HLS codebase includes several plugins (found in `./plugins`). For example: + +- The `ormolu`, `fourmolu`, `floskell` and `stylish-haskell` plugins used to format code +- The `eval` plugin, a code lens provider to evaluate code in comments +- The `retrie` plugin, a code action provider to execute retrie commands + +I recommend looking at the existing plugins for inspiration and reference. A few conventions shared by all plugins are: -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 users of HLS. +- Plugins are 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`. New plugin descriptors + must be added there. + ```haskell -- src/HlsPlugins.hs -## The task + 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" -- Add new plugins here. + ] + ``` +To add a new plugin, extend the list of `allPlugins` and rebuild. + +## The goal of the plugin we will write Here is a visual statement of what we want to accomplish: @@ -29,254 +64,153 @@ Here is a visual statement of what we want to accomplish: And here is the gist of the algorithm: 1. Request the type checking artifacts 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 an 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. +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 an explicit import list: + - Determine 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 repository and build it. You need at least GHC 9.0 for this: +To get started, fetch the HLS repository and build it by following the [installation instructions](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#building). -``` -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) in the HLS repository. + +Once the build is done, you can find the location of the HLS binary with `cabal list-bin exe:haskell-language-server` and point your LSP client to it. +This way you can simply test your changes by reloading your editor after rebuilding the binary. -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. +> **Note:** In VSCode, edit the "Haskell Server Executable Path" setting. -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 Emacs, edit the `lsp-haskell-server-path` variable. ![Settings](settings-vscode.png) +[Manually test your hacked HLS](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#manually-testing-your-hacked-hls) to ensure you use the HLS package you just built. + ## 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 :: Type) = + 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": - -* 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. - -The HLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are: +A plugin has a unique ID, command handlers, request handlers, notification handlers and rules: -- The `ghcide` plugin, which embeds `ghcide` as a plugin (`ghcide` is also the engine under HLS), -- 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. +* Request handlers respond to requests from an LSP client. They must fulfill these requests as quickly as possible. +* Notification handlers receive notifications from code not directly triggered by a user or client. +* Rules add new targets to the Shake build graph. Most plugins do not need to define new rules. +* Commands are an LSP abstraction for user-initiated actions that the server handles. These actions can be long-running and involve multiple modules. -I would recommend looking at the existing plugins for inspiration and reference. - -Plugins are "linked" in the `HlsPlugins` module, so we will need to add our plugin there once we have defined it: - -```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" - ] -``` -To add a new plugin, simply extend the list of `allPlugins` and rebuild. - -## Providers +## The explicit imports plugin -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) -``` +To achieve our plugin goals, we need to define: +- a command handler (`importLensCommand`), +- a code lens request handler (`lensProvider`). -Providers are functions that receive some inputs and produce an IO computation that returns either an error or some result. +These will be assembled together in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above. -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. +Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts: -The second argument, which plugins receive, is `IdeState`. `IdeState` 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 +-- 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 + ] + } ``` -The `use` family of combinators allows 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 -``` +We'll start with the command, since it's the simplest of the two. -There are three flavours of `use` combinators: +### The command handler -1. `use*` combinators block and propagate errors, -2. `useWithStale*` combinators block and switch to stale data in case of an error, -3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise. +In short, commands work 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 (in this case a click on the code lens), it sends this same descriptor back to the LSP server. The server then handles and executes the command; this latter part is implemented by the `commandFunc` field of our `PluginCommand` value. -## LSP abstractions +> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work. -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 command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as: -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. - -## The explicit imports plugin +-- hls-plugin-api/src/Ide/Types.hs -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: - -```haskell -descriptor :: PluginId -> PluginDescriptor -descriptor plId = (defaultPluginDescriptor plId) { - -- This plugin provides code lenses - pluginCodeLensProvider = Just provider, - -- This plugin provides a command handler - pluginCommands = [ importLensCommand ] -} +data PluginCommand ideState = forall a. (FromJSON a) => + PluginCommand { commandId :: CommandId + , commandDesc :: T.Text + , commandFunc :: CommandFunction ideState a + } ``` -### 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. +Let's start by creating an unfinished command handler. We'll give it an ID and a description for now: ```haskell -importLensCommand :: PluginCommand +-- | The command handler. +importLensCommand :: PluginCommand IdeState +importLensCommand = + PluginCommand + { commandId = "ImportLensCommand" + , commandDesc = "Explicit import command" + , commandFunc = runImportCommand + } + +-- | Not implemented yet. +runImportCommand = undefined ``` -`PluginCommand` is a data type defined in `LSP.Types` as: +The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, a type synonym from `LSP.Types`: ```haskell -data PluginCommand = forall a. (FromJSON a) => - PluginCommand { commandId :: CommandId - , commandDesc :: T.Text - , commandFunc :: CommandFunction a - } -``` +-- hls-plugin-api/src/Ide/Types.hs -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 specifications 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 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 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) ``` +`runImportCommand` [sends a request](https://hackage.haskell.org/package/lsp/docs/Language-LSP-Server.html#v:sendRequest) to the client using the method `SWorkspaceApplyEdit` and the parameters `ApplyWorkspaceEditParams Nothing edit`, providing a response handler that does nothing. It then returns `Right Null`, which is 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 -> 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 an 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. +> 1. Request the type checking artifacts. +> 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 each import statement lacking an explicit list, determine its minimal import list and generate a code lens displaying this list along with a command to insert it. -The provider takes the usual `LspFuncs` and `IdeState` argument, as well as a `CodeLensParams` value containing the URI -for a file, and returns an IO action producing either an error or a list of code lenses for that file. +The provider takes the usual `LspFuncs` and `IdeState` arguments, as well as a `CodeLensParams` value containing a file URI. It returns an IO action that produces either an error or a list of code lenses for that file. ```haskell provider :: CodeLensProvider @@ -309,9 +243,10 @@ provider _lspFuncs -- LSP functions, not used = return $ Right (List []) ``` -Note how simple it is to retrieve the type checking artifacts for the module as well as a fully setup GHC session via the `ghcide` rules. +Note the simplicity of retrieving the type checking artifacts for the module, as well as a fully set up GHC session, via the `ghcide` rules. The function `extractMinimalImports` extracts the import statements from the AST and generates the minimal import lists, implementing steps 2 and 3 of the algorithm. + The details of the GHC API are not relevant to this tutorial, but the code is terse and easy to read: ```haskell @@ -337,8 +272,7 @@ extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = extractMinimalImports _ _ = return ([], Nothing) ``` -The function `generateLens` implements step 4 of the algorithm, producing a code lens for an import statement that lacks an import list. Note how the code lens includes an `ImportCommandParams` value -that contains a workspace edit that rewrites the import statement, as expected by our command provider. +The function `generateLens` implements step 4 of the algorithm, producing a code lens for an import statement that lacks an import list. The code lens includes an `ImportCommandParams` value containing a workspace edit that rewrites the import statement, as our command provider expects. ```haskell -- | Given an import declaration, generate a code lens unless it has an explicit import list @@ -381,14 +315,16 @@ generateLens pId uri minImports (L src imp) ## Wrapping up There's only one Haskell code change left to do at this point: "link" the plugin in the `HlsPlugins` HLS module. -However integrating the plugin in HLS itself will need some changes in configuration files. The best way is looking for the ID (f.e. `hls-class-plugin`) of an existing plugin: -- `./cabal*.project` and `./stack*.yaml`: add the plugin package in the `packages` field, -- `./haskell-language-server.cabal`: add a conditional block with the plugin package dependency, -- `./.github/workflows/test.yml`: add a block to run the test suite of the plugin, -- `./.github/workflows/hackage.yml`: add the plugin to the component list to release the plugin package to Hackage, -- `./*.nix`: add the plugin to Nix builds. -The full code as used in this tutorial, including imports, can be found in [this Gist](https://gist.github.com/pepeiborra/49b872b2e9ad112f61a3220cdb7db967) as well as in this [branch](https://github.com/pepeiborra/ide/blob/imports-lens/src/Ide/Plugin/ImportLens.hs) +Integrating the plugin into HLS itself requires changes to several configuration files. + +A good approach is to use the ID of an existing plugin (e.g., `hls-class-plugin`) as a guide: +- `./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field. +- `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency. +- `./.github/workflows/test.yml`: Add a block to run the plugin's test suite. +- `./.github/workflows/hackage.yml`: Add the plugin to the component list for releasing the plugin package to Hackage. +- `./*.nix`: Add the plugin to Nix builds. + +The full code used in this tutorial, including imports, is available in [this Gist](https://gist.github.com/pepeiborra/49b872b2e9ad112f61a3220cdb7db967) and in this [branch](https://github.com/pepeiborra/ide/blob/imports-lens/src/Ide/Plugin/ImportLens.hs). -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). +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 contribution ideas, here are some good ones listed in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues). From f2cd8978db6ce075f06efc928f0438dd349062e8 Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Sun, 18 May 2025 12:55:50 +0200 Subject: [PATCH 2/8] Update Nix Flake lock (this makes GHC 9.8 the default) --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 3fb48889a5..4efe1416b6 100644 --- a/flake.lock +++ b/flake.lock @@ -3,11 +3,11 @@ "flake-compat": { "flake": false, "locked": { - "lastModified": 1733328505, - "narHash": "sha256-NeCCThCEP3eCl2l/+27kNNK7QrwZB1IJCrXfrbv5oqU=", + "lastModified": 1747046372, + "narHash": "sha256-CIVLLkVgvHYbgI2UpXvIIBJ12HWgX+fjA8Xf8PUmqCY=", "owner": "edolstra", "repo": "flake-compat", - "rev": "ff81ac966bb2cae68946d5ed5fc4994f96d0ffec", + "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885", "type": "github" }, "original": { @@ -36,11 +36,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739019272, - "narHash": "sha256-7Fu7oazPoYCbDzb9k8D/DdbKrC3aU1zlnc39Y8jy/s8=", + "lastModified": 1747467164, + "narHash": "sha256-JBXbjJ0t6T6BbVc9iPVquQI9XSXCGQJD8c8SgnUquus=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fa35a3c8e17a3de613240fea68f876e5b4896aec", + "rev": "3fcbdcfc707e0aa42c541b7743e05820472bdaec", "type": "github" }, "original": { From 985f7262d0957f717b5e8b4d25822571c5833452 Mon Sep 17 00:00:00 2001 From: Dominik Schrempf Date: Sun, 18 May 2025 13:18:16 +0200 Subject: [PATCH 3/8] Review comments and other improvments, thanks @VeryMilkyJoe. --- docs/contributing/plugin-tutorial.md | 38 +++++++++++++++++++--------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 0c3424ab7f..9d2f8129b9 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -1,7 +1,7 @@ # Let’s write a Haskell Language Server plugin Originally written by Pepe Iborra, maintained by the Haskell community. -Haskell Language Server (HLS) is an LSP server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE. +Haskell Language Server (HLS) is a Language Server Protocol (LSP) server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE. You can find many more details on the history and architecture on 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](https://github.com/haskell/haskell-language-server/tree/master/ghcide) and LSP. @@ -87,25 +87,39 @@ This way you can simply test your changes by reloading your editor after rebuild [Manually test your hacked HLS](https://haskell-language-server.readthedocs.io/en/latest/contributing/contributing.html#manually-testing-your-hacked-hls) to ensure you use the HLS package you just built. +## Digression about the Language Server Protocol + +There are two main types of communication in the Language Server Protocol: +- A **request-response interaction** type where one party sends a message that requires a response from the other party. +- A **notification** is a one-way interaction where one party sends a message without expecting any response. + +> **Note**: The LSP client and server can both send requests or notifications to the other party. + ## Anatomy of a plugin HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as: ```haskell data PluginDescriptor (ideState :: Type) = 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, command handlers, request handlers, notification handlers and rules: -* Request handlers respond to requests from an LSP client. They must fulfill these requests as quickly as possible. -* Notification handlers receive notifications from code not directly triggered by a user or client. -* Rules add new targets to the Shake build graph. Most plugins do not need to define new rules. -* Commands are an LSP abstraction for user-initiated actions that the server handles. These actions can be long-running and involve multiple modules. +### Request-response interaction + +The `pluginHandlers` handle LSP client requests and provide responses to the client. They must fulfill these requests as quickly as possible. +- Example: When you want to format a file, the client sends the [`textDocument/formatting`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting) request to the server. The server formats the file and responds with the formatted content. + +### Notification + +The `pluginNotificationHandlers` handle notifications sent by the client to the server that are not explicitly triggered by a user. +- Example: Whenever you modify a Haskell file, the client sends a notification informing HLS about the changes to the file. + +The `pluginCommands` are special types of user-initiated notifications sent to +the server. These actions can be long-running and involve multiple modules. ## The explicit imports plugin @@ -113,7 +127,7 @@ To achieve our plugin goals, we need to define: - a command handler (`importLensCommand`), - a code lens request handler (`lensProvider`). -These will be assembled together in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above. +These will be assembled in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above. Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts: @@ -135,9 +149,9 @@ We'll start with the command, since it's the simplest of the two. ### The command handler -In short, commands work like this: +In short, LSP commands work 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 (in this case a click on the code lens), it sends this same descriptor back to the LSP server. The server then handles and executes the command; this latter part is implemented by the `commandFunc` field of our `PluginCommand` value. +- When the user clicks on the code lens, the client asks HLS to execute the command with the given descriptor. The server then handles and executes the command; this 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. @@ -181,7 +195,7 @@ type CommandFunction ideState a ``` -`CommandFunction` takes an `ideState` and a JSON-encodable argument. +`CommandFunction` takes an `ideState` and a JSON-encodable argument. `LspM` is a monad transformer with access to IO, and having access to a language context environment `Config`. The action evaluates to an `Either` value. `Left` indicates failure with a `ResponseError`, `Right` indicates success with a `Value`. Our handler will ignore the state argument and only use the `WorkspaceEdit` argument. @@ -318,7 +332,7 @@ There's only one Haskell code change left to do at this point: "link" the plugin Integrating the plugin into HLS itself requires changes to several configuration files. -A good approach is to use the ID of an existing plugin (e.g., `hls-class-plugin`) as a guide: +A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`): - `./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field. - `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency. - `./.github/workflows/test.yml`: Add a block to run the plugin's test suite. From db128ecdee32b55d76b739f81231ad9e34092c42 Mon Sep 17 00:00:00 2001 From: fendor Date: Mon, 19 May 2025 17:54:39 +0200 Subject: [PATCH 4/8] Compile plugin-tutorial using markdown-unlit Makes sure the plugin-tutorial can never be out-of-date again. --- docs/contributing/plugin-tutorial.lhs | 1 + docs/contributing/plugin-tutorial.md | 183 ++++++++++++------ ghcide/src/Development/IDE/GHC/Compat/Core.hs | 1 + haskell-language-server.cabal | 19 ++ .../src/Development/IDE/Plugin/CodeAction.hs | 2 - 5 files changed, 145 insertions(+), 61 deletions(-) create mode 120000 docs/contributing/plugin-tutorial.lhs diff --git a/docs/contributing/plugin-tutorial.lhs b/docs/contributing/plugin-tutorial.lhs new file mode 120000 index 0000000000..e1837100c2 --- /dev/null +++ b/docs/contributing/plugin-tutorial.lhs @@ -0,0 +1 @@ +plugin-tutorial.md \ No newline at end of file diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 9d2f8129b9..5b6cf733f9 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -1,4 +1,5 @@ # Let’s write a Haskell Language Server plugin + Originally written by Pepe Iborra, maintained by the Haskell community. Haskell Language Server (HLS) is a Language Server Protocol (LSP) server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE. @@ -22,6 +23,49 @@ While writing them, I didn't have to worry about performance, UI, or distributio The plugins also make these tools much more accessible to all users of HLS. +## Preamble + +```haskell +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE ScopedTypeVariables #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE ViewPatterns #-} +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DeriveAnyClass #-} + +import Ide.Types +import Ide.Logger +import Ide.Plugin.Error + +import Development.IDE.Core.RuleTypes +import Development.IDE.Core.Service hiding (Log) +import Development.IDE.Core.Shake hiding (Log) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Core +import Development.IDE.GHC.Error +import Development.IDE.Types.HscEnvEq +import Development.IDE.Core.PluginUtils + +import qualified Language.LSP.Server as LSP +import Language.LSP.Protocol.Types as JL +import Language.LSP.Protocol.Message + +import Data.Aeson as Aeson +import Data.Map (Map) +import Data.IORef +import Data.Maybe (fromMaybe, catMaybes) +import qualified Data.Map as Map +import qualified Data.HashMap.Strict as HashMap +import qualified Data.Text as T +import Control.Monad (forM) +import Control.Monad.IO.Class (liftIO) +import Control.Monad.Trans.Class +import GHC.Generics (Generic) +``` + ## Plugins in the HLS codebase The HLS codebase includes several plugins (found in `./plugins`). For example: @@ -37,7 +81,9 @@ I recommend looking at the existing plugins for inspiration and reference. A few - Folders containing the plugin follow the `hls-pluginname-plugin` naming convention - Plugins are "linked" in `src/HlsPlugins.hs#idePlugins`. New plugin descriptors must be added there. - ```haskell -- src/HlsPlugins.hs + + ```haskell ignore + -- Defined in src/HlsPlugins.**hs** idePlugins = pluginDescToIdePlugins allPlugins where @@ -53,6 +99,7 @@ I recommend looking at the existing plugins for inspiration and reference. A few , NewPlugin.descriptor "new-plugin" -- Add new plugins here. ] ``` + To add a new plugin, extend the list of `allPlugins` and rebuild. ## The goal of the plugin we will write @@ -80,7 +127,7 @@ Once the build is done, you can find the location of the HLS binary with `cabal This way you can simply test your changes by reloading your editor after rebuilding the binary. > **Note:** In VSCode, edit the "Haskell Server Executable Path" setting. - +> > **Note:** In Emacs, edit the `lsp-haskell-server-path` variable. ![Settings](settings-vscode.png) @@ -90,6 +137,7 @@ This way you can simply test your changes by reloading your editor after rebuild ## Digression about the Language Server Protocol There are two main types of communication in the Language Server Protocol: + - A **request-response interaction** type where one party sends a message that requires a response from the other party. - A **notification** is a one-way interaction where one party sends a message without expecting any response. @@ -98,24 +146,27 @@ There are two main types of communication in the Language Server Protocol: ## Anatomy of a plugin HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as: -```haskell + +```haskell ignore data PluginDescriptor (ideState :: Type) = PluginDescriptor { pluginId :: !PluginId , pluginCommands :: ![PluginCommand ideState] , pluginHandlers :: PluginHandlers ideState , pluginNotificationHandlers :: PluginNotificationHandlers ideState - , [...] -- Other fields omitted for brevity. +-- , [...] -- Other fields omitted for brevity. } ``` ### Request-response interaction The `pluginHandlers` handle LSP client requests and provide responses to the client. They must fulfill these requests as quickly as possible. + - Example: When you want to format a file, the client sends the [`textDocument/formatting`](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting) request to the server. The server formats the file and responds with the formatted content. ### Notification The `pluginNotificationHandlers` handle notifications sent by the client to the server that are not explicitly triggered by a user. + - Example: Whenever you modify a Haskell file, the client sends a notification informing HLS about the changes to the file. The `pluginCommands` are special types of user-initiated notifications sent to @@ -124,6 +175,7 @@ the server. These actions can be long-running and involve multiple modules. ## The explicit imports plugin To achieve our plugin goals, we need to define: + - a command handler (`importLensCommand`), - a code lens request handler (`lensProvider`). @@ -134,13 +186,15 @@ Using the convenience `defaultPluginDescriptor` function, we can bootstrap the p ```haskell -- plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs +data Log + -- | The "main" function of a plugin. descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = - (defaultPluginDescriptor plId) + (defaultPluginDescriptor plId "A plugin for generating the minimal imports") { pluginCommands = [importLensCommand], -- The plugin provides a command handler pluginHandlers = mconcat -- The plugin provides request handlers - [ lensProvider + [ mkPluginHandler SMethod_TextDocumentCodeLens provider ] } ``` @@ -150,6 +204,7 @@ We'll start with the command, since it's the simplest of the two. ### The command handler In short, LSP commands work like this: + - The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens. - When the user clicks on the code lens, the client asks HLS to execute the command with the given descriptor. The server then handles and executes the command; this latter part is implemented by the `commandFunc` field of our `PluginCommand` value. @@ -157,7 +212,7 @@ In short, LSP commands work like this: The command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as: -```haskell +```haskell ignore -- hls-plugin-api/src/Ide/Types.hs data PluginCommand ideState = forall a. (FromJSON a) => @@ -174,18 +229,23 @@ Let's start by creating an unfinished command handler. We'll give it an ID and a importLensCommand :: PluginCommand IdeState importLensCommand = PluginCommand - { commandId = "ImportLensCommand" + { commandId = importCommandId , commandDesc = "Explicit import command" , commandFunc = runImportCommand } +importCommandId :: CommandId +importCommandId = "ImportLensCommand" +``` + +```haskell ignore -- | Not implemented yet. runImportCommand = undefined ``` The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, a type synonym from `LSP.Types`: -```haskell +```haskell ignore -- hls-plugin-api/src/Ide/Types.hs type CommandFunction ideState a @@ -194,8 +254,7 @@ type CommandFunction ideState a -> LspM Config (Either ResponseError Value) ``` - -`CommandFunction` takes an `ideState` and a JSON-encodable argument. `LspM` is a monad transformer with access to IO, and having access to a language context environment `Config`. The action evaluates to an `Either` value. `Left` indicates failure with a `ResponseError`, `Right` indicates success with a `Value`. +`CommandFunction` takes an `ideState` and a JSON-encodable argument. `LspM` is a monad transformer with access to IO, and having access to a language context environment `Config`. The action evaluates to an `Either` value. `Left` indicates failure with a `ResponseError`, `Right` indicates sucess with a `Value`. Our handler will ignore the state argument and only use the `WorkspaceEdit` argument. @@ -207,10 +266,10 @@ newtype ImportCommandParams = ImportCommandParams WorkspaceEdit -- | The actual command handler runImportCommand :: CommandFunction IdeState ImportCommandParams -runImportCommand _ (ImportCommandParams edit) = do +runImportCommand _ _ (ImportCommandParams edit) = do -- This command simply triggers a workspace edit! - _ <- sendRequest SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) - return (Right Null) + _ <- lift $ pluginSendRequest SMethod_WorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing edit) (\_ -> pure ()) + return $ InR JL.Null ``` `runImportCommand` [sends a request](https://hackage.haskell.org/package/lsp/docs/Language-LSP-Server.html#v:sendRequest) to the client using the method `SWorkspaceApplyEdit` and the parameters `ApplyWorkspaceEditParams Nothing edit`, providing a response handler that does nothing. It then returns `Right Null`, which is an empty `Aeson.Value` wrapped in `Right`. @@ -219,42 +278,41 @@ runImportCommand _ (ImportCommandParams edit) = do The code lens provider implements all the steps of the algorithm described earlier: -> 1. Request the type checking artifacts. -> 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 each import statement lacking an explicit list, determine its minimal import list and generate a code lens displaying this list along with a command to insert it. +> 1. Request the type checking artifacts. +> 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 each import statement lacking an explicit list, determine its minimal import list and generate a code lens displaying this list along with a command to insert it. The provider takes the usual `LspFuncs` and `IdeState` arguments, as well as a `CodeLensParams` value containing a file URI. It returns an IO action that produces either an error or a list of code lenses for that file. ```haskell -provider :: CodeLensProvider -provider _lspFuncs -- LSP functions, not used - state -- ghcide state, used to retrieve typechecking artifacts +provider :: PluginMethodHandler IdeState Method_TextDocumentCodeLens +provider state -- ghcide state, used to retrieve typechecking artifacts pId -- Plugin ID - CodeLensParams{_textDocument = TextDocumentIdentifier{_uri}} + CodeLensParams{_textDocument = TextDocumentIdentifier{_uri}} = do -- VSCode uses URIs instead of file paths -- haskell-lsp provides conversion functions - | Just nfp <- uriToNormalizedFilePath $ toNormalizedUri _uri - = do - -- Get the typechecking artifacts from the module - tmr <- runAction "importLens" state $ use TypeCheck nfp - -- We also need a GHC session with all the dependencies - hsc <- runAction "importLens" state $ use GhcSessionDeps nfp - -- Use the GHC API to extract the "minimal" imports - (imports, mbMinImports) <- extractMinimalImports hsc tmr - - case mbMinImports of - Just minImports -> do - let minImportsMap = - Map.fromList [ (srcSpanStart l, i) | L l i <- minImports ] - lenses <- forM imports $ - -- for every import, maybe generate a code lens - generateLens pId _uri minImportsMap - return $ Right (List $ catMaybes lenses) - _ -> - return $ Right (List []) - | otherwise - = return $ Right (List []) + nfp <- getNormalizedFilePathE _uri + -- Get the typechecking artifacts from the module + tmr <- runActionE "importLens" state $ useE TypeCheck nfp + -- We also need a GHC session with all the dependencies + hsc <- runActionE "importLens" state $ useE GhcSessionDeps nfp + -- Use the GHC API to extract the "minimal" imports + (imports, mbMinImports) <- liftIO $ extractMinimalImports hsc tmr + + case mbMinImports of + Just minImports -> do + let minImportsMap = + Map.fromList [ (realSrcLocToPosition loc, i) + | L l i <- minImports + , let RealSrcLoc loc _ = srcSpanStart (locA l) + ] + lenses <- forM imports $ \imp -> + -- for every import, maybe generate a code lens + liftIO (generateLens pId _uri minImportsMap imp) + return $ InL (catMaybes lenses) + _ -> + return $ InL [] ``` Note the simplicity of retrieving the type checking artifacts for the module, as well as a fully set up GHC session, via the `ghcide` rules. @@ -265,14 +323,14 @@ The details of the GHC API are not relevant to this tutorial, but the code is te ```haskell extractMinimalImports - :: Maybe HscEnvEq - -> Maybe TcModuleResult + :: HscEnvEq + -> TcModuleResult -> IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn]) -extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = do +extractMinimalImports hsc TcModuleResult{..} = do -- Extract the original imports and the typechecking environment - let (tcEnv,_) = tm_internals_ - Just (_, imports, _, _) = tm_renamed_source - ParsedModule{ pm_parsed_source = L loc _} = tm_parsed_module + let tcEnv = tmrTypechecked + (_, imports, _, _) = tmrRenamed + ParsedModule{ pm_parsed_source = L loc _} = tmrParsed span = fromMaybe (error "expected real") $ realSpan loc -- GHC is secretly full of mutable state @@ -283,7 +341,6 @@ extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = -- getMinimalImports computes the minimal explicit import lists initTcWithGbl (hscEnv hsc) tcEnv span $ getMinimalImports usage return (imports, minimalImports) -extractMinimalImports _ _ = return ([], Nothing) ``` The function `generateLens` implements step 4 of the algorithm, producing a code lens for an import statement that lacks an import list. The code lens includes an `ImportCommandParams` value containing a workspace edit that rewrites the import statement, as our command provider expects. @@ -292,34 +349,36 @@ The function `generateLens` implements step 4 of the algorithm, producing a code -- | Given an import declaration, generate a code lens unless it has an explicit import list generateLens :: PluginId -> Uri - -> Map SrcLoc (ImportDecl GhcRn) + -> Map Position (ImportDecl GhcRn) -> LImportDecl GhcRn -> IO (Maybe CodeLens) generateLens pId uri minImports (L src imp) -- Explicit import list case - | ImportDecl{ideclHiding = Just (False,_)} <- imp + | ImportDecl{ideclImportList = Just _} <- imp = return Nothing -- No explicit import list - | RealSrcSpan l <- src - , Just explicit <- Map.lookup (srcSpanStart src) minImports + | RealSrcSpan l _ <- locA src + , let position = realSrcLocToPosition $ realSrcSpanStart l + , Just explicit <- Map.lookup position minImports , L _ mn <- ideclName imp -- (Almost) no one wants to see an explicit import list for Prelude , mn /= moduleName pRELUDE = do -- The title of the command is just the minimal explicit import decl - let title = T.pack $ prettyPrint explicit + let title = T.pack $ printWithoutUniques explicit -- The range of the code lens is the span of the original import decl _range :: Range = realSrcSpanToRange l -- The code lens has no extra data _xdata = Nothing -- An edit that replaces the whole declaration with the explicit one - edit = WorkspaceEdit (Just editsMap) Nothing - editsMap = HashMap.fromList [(uri, List [importEdit])] + edit = WorkspaceEdit (Just editsMap) Nothing Nothing + editsMap = Map.fromList [(uri, [importEdit])] importEdit = TextEdit _range title -- The command argument is simply the edit _arguments = Just [toJSON $ ImportCommandParams edit] - -- Create the command - _command <- Just <$> mkLspCommand pId importCommandId title _arguments + _data_ = Nothing + -- Create the command + _command = Just $ mkLspCommand pId importCommandId title _arguments -- Create and return the code lens return $ Just CodeLens{..} | otherwise @@ -333,6 +392,7 @@ There's only one Haskell code change left to do at this point: "link" the plugin Integrating the plugin into HLS itself requires changes to several configuration files. A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`): + - `./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field. - `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency. - `./.github/workflows/test.yml`: Add a block to run the plugin's test suite. @@ -342,3 +402,8 @@ A good approach is to search for the ID of an existing plugin (e.g., `hls-class- The full code used in this tutorial, including imports, is available in [this Gist](https://gist.github.com/pepeiborra/49b872b2e9ad112f61a3220cdb7db967) and in this [branch](https://github.com/pepeiborra/ide/blob/imports-lens/src/Ide/Plugin/ImportLens.hs). 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 contribution ideas, here are some good ones listed in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues). + +```haskell +main :: IO () +main = putStrLn "Just here to silence the error!" +``` diff --git a/ghcide/src/Development/IDE/GHC/Compat/Core.hs b/ghcide/src/Development/IDE/GHC/Compat/Core.hs index 3f19cd7489..ebd1fe0b9e 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Core.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Core.hs @@ -225,6 +225,7 @@ module Development.IDE.GHC.Compat.Core ( SrcLoc.noSrcSpan, SrcLoc.noSrcLoc, SrcLoc.noLoc, + SrcLoc.srcSpanToRealSrcSpan, mapLoc, -- * Finder FindResult(..), diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 9e1b1d4251..4bf02f371d 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -2265,3 +2265,22 @@ test-suite ghcide-bench-test OverloadedStrings RecordWildCards ViewPatterns + +executable plugin-tutorial + import: defaults + ghc-options: -pgmL markdown-unlit + main-is: docs/contributing/plugin-tutorial.lhs + build-tool-depends: markdown-unlit:markdown-unlit + build-depends: + base, + ghcide, + hls-plugin-api, + aeson, + lsp, + lsp-types, + markdown-unlit, + text, + unordered-containers, + containers, + transformers, + ghc, diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs index 0f41f988e8..2303ce97d7 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs @@ -121,7 +121,6 @@ import GHC (AddEpAnn (Ad EpaLocation, EpaLocation' (..), HasLoc (..)) -import GHC.Types.SrcLoc (srcSpanToRealSrcSpan) #endif #if MIN_VERSION_ghc(9,11,0) import GHC (EpaLocation, @@ -129,7 +128,6 @@ import GHC (EpaLocation, EpaLocation' (..), HasLoc (..), EpToken (..)) -import GHC.Types.SrcLoc (srcSpanToRealSrcSpan) #endif From 8ed60a016fc9ed6a2d3b111588de1815166cce4f Mon Sep 17 00:00:00 2001 From: fendor Date: Mon, 19 May 2025 17:59:04 +0200 Subject: [PATCH 5/8] Remove out-of-date references --- docs/contributing/plugin-tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 5b6cf733f9..33e06a36e1 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -393,13 +393,13 @@ Integrating the plugin into HLS itself requires changes to several configuration A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`): -- `./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field. - `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency. - `./.github/workflows/test.yml`: Add a block to run the plugin's test suite. - `./.github/workflows/hackage.yml`: Add the plugin to the component list for releasing the plugin package to Hackage. - `./*.nix`: Add the plugin to Nix builds. -The full code used in this tutorial, including imports, is available in [this Gist](https://gist.github.com/pepeiborra/49b872b2e9ad112f61a3220cdb7db967) and in this [branch](https://github.com/pepeiborra/ide/blob/imports-lens/src/Ide/Plugin/ImportLens.hs). +This plugin tutorial re-implements parts of the [`hls-explicit-imports-plugin`] which is part of HLS. +The plugin code additionally contains advanced concepts, such as `Rules`. 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 contribution ideas, here are some good ones listed in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues). From 21f5f623db85a80c8fa5b3b108122f59870e8f35 Mon Sep 17 00:00:00 2001 From: fendor Date: Mon, 19 May 2025 18:07:52 +0200 Subject: [PATCH 6/8] Add plugin-tutorial to CI --- .github/workflows/test.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 71a9e85443..ccc20fb7d7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,6 +255,10 @@ jobs: name: Test hls-notes-plugin test suite run: cabal test hls-notes-plugin-tests || cabal test hls-notes-plugin-tests + - if: matrix.test + name: Compile the plugin-tutorial + run: cabal build plugin-tutorial + test_post_job: if: always() runs-on: ubuntu-latest From 4aff2f3660c1d20a83885b0a7964d5b91a785311 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 20 May 2025 11:05:31 +0200 Subject: [PATCH 7/8] Only build the plugin-tutorial with GHC 9.6 and 9.8 --- .github/workflows/test.yml | 4 +++- haskell-language-server.cabal | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ccc20fb7d7..984758a310 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -255,7 +255,9 @@ jobs: name: Test hls-notes-plugin test suite run: cabal test hls-notes-plugin-tests || cabal test hls-notes-plugin-tests - - if: matrix.test + # The plugin tutorial is only compatible with 9.6 and 9.8. + # No particular reason, just to avoid excessive CPP. + - if: matrix.test && matrix.ghc != '9.4' && matrix.ghc != '9.10' && matrix.ghc != '9.12' name: Compile the plugin-tutorial run: cabal build plugin-tutorial diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index 4bf02f371d..e7583c9829 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -2268,6 +2268,12 @@ test-suite ghcide-bench-test executable plugin-tutorial import: defaults + -- The plugin tutorial is only compatible with 9.6 and 9.8. + -- No particular reason, just to avoid excessive CPP. + if (impl(ghc >= 9.6) && impl(ghc < 9.10)) + buildable: True + else + buildable: False ghc-options: -pgmL markdown-unlit main-is: docs/contributing/plugin-tutorial.lhs build-tool-depends: markdown-unlit:markdown-unlit From 6c22e2c618efa2f4ed1c4e69c6d295775b091f09 Mon Sep 17 00:00:00 2001 From: fendor Date: Tue, 20 May 2025 11:17:16 +0200 Subject: [PATCH 8/8] Add explanation for preamble --- docs/contributing/plugin-tutorial.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/contributing/plugin-tutorial.md b/docs/contributing/plugin-tutorial.md index 33e06a36e1..d9ca59c0ad 100644 --- a/docs/contributing/plugin-tutorial.md +++ b/docs/contributing/plugin-tutorial.md @@ -25,6 +25,11 @@ The plugins also make these tools much more accessible to all users of HLS. ## Preamble +This tutorial is a literate Haskell file that can be compiled. +As such, we list the imports, extensions etc... necessary for compilation. + +Please just skip over this `import` section, if you are only interested in the tutorial! + ```haskell {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE DerivingStrategies #-} @@ -403,7 +408,11 @@ The plugin code additionally contains advanced concepts, such as `Rules`. 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 contribution ideas, here are some good ones listed in the HLS [issue tracker](https://github.com/haskell/haskell-language-server/issues). +
+ Placeholder Main, unused + ```haskell main :: IO () main = putStrLn "Just here to silence the error!" ``` +