Skip to content

Commit 348678c

Browse files
committed
Documentation: HLS plugin tutorial improvements
1 parent 31b8787 commit 348678c

File tree

1 file changed

+50
-49
lines changed

1 file changed

+50
-49
lines changed

docs/contributing/plugin-tutorial.md

+50-49
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,24 @@
11
# Let’s write a Haskell Language Server plugin
2+
Originally written by Pepe Iborra, maintained by the Haskell community.
23

34
Haskell Language Server is an LSP server for the Haskell programming language. It builds on several previous efforts
4-
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.
5+
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.
56

67
In this article we are going to cover the creation of an HLS plugin from scratch: a code lens to display explicit import lists.
7-
Along the way we will learn about HLS, its plugin model, and the relationship with ghcide and LSP.
8+
Along the way we will learn about HLS, its plugin model, and the relationship with `ghcide` and LSP.
89

910
## Introduction
1011

1112
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.
1213

13-
In the last couple of months I have written various HLS (and ghcide) plugins for things like:
14+
In the last couple of months I have written various HLS (and `ghcide`) plugins for things like:
1415

1516
1. Suggest imports for variables not in scope,
1617
2. Remove redundant imports,
17-
2. Evaluate code in comments (a la doctest),
18-
3. Integrate the retrie refactoring library.
18+
2. Evaluate code in comments (à la [doctest](https://docs.python.org/3/library/doctest.html)),
19+
3. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library.
1920

20-
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.
21+
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.
2122

2223
## The task
2324

@@ -27,14 +28,14 @@ Here is a visual statement of what we want to accomplish:
2728

2829
And here is the gist of the algorithm:
2930

30-
1. Request the type checking artefacts from the ghcide subsystem
31-
2. Extract the actual import lists from the type checked AST,
31+
1. Request the type checking artifacts from the `ghcide` subsystem
32+
2. Extract the actual import lists from the type-checked AST,
3233
3. Ask GHC to produce the minimal import lists for this AST,
33-
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.
34+
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.
3435

3536
## Setup
3637

37-
To get started, let’s fetch the HLS repo and build it. You need at least GHC 9.0 for this:
38+
To get started, let’s fetch the HLS repository and build it. You need at least GHC 9.0 for this:
3839

3940
```
4041
git clone --recursive http://github.com/haskell/haskell-language-server hls
@@ -67,19 +68,19 @@ data PluginDescriptor =
6768
, pluginRenameProvider :: !(Maybe RenameProvider)
6869
}
6970
```
70-
A plugin has a unique id, a set of rules, a set of command handlers, and a set of "providers":
71+
A plugin has a unique ID, a set of rules, a set of command handlers, and a set of "providers":
7172

72-
* Rules add new targets to the Shake build graph defined in ghcide. 99% of plugins need not define any new rules.
73+
* Rules add new targets to the Shake build graph defined in `ghcide`. 99% of plugins need not define any new rules.
7374
* 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.
7475
* Providers are a query-like abstraction where the LSP client asks the server for information. These queries must be fulfilled as quickly as possible.
7576

7677
The HLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are:
7778

78-
- The ghcide plugin, which embeds ghcide as a plugin (ghcide is also the engine under HLS).
79-
- The example and example2 plugins, offering a dubious welcome to new contributors
80-
- The ormolu, fourmolu, floskell and stylish-haskell plugins, a testament to the code formatting wars of our community.
81-
- The eval plugin, a code lens provider to evaluate code in comments
82-
- The retrie plugin, a code actions provider to execute retrie commands
79+
- The `ghcide` plugin, which embeds `ghcide` as a plugin (`ghcide` is also the engine under HLS),
80+
- The `example` and `example2` plugins, offering a dubious welcome to new contributors,
81+
- The `ormolu`, `fourmolu`, `floskell` and `stylish-haskell` plugins, a testament to the code formatting wars of our community,
82+
- The `eval` plugin, a code lens provider to evaluate code in comments,
83+
- The `retrie` plugin, a code actions provider to execute retrie commands.
8384

8485
I would recommend looking at the existing plugins for inspiration and reference.
8586

@@ -134,11 +135,11 @@ Providers are functions that receive some inputs and produce an IO computation t
134135

135136
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.
136137
Some of its capabilities are:
137-
- Querying the LSP client capabilities
138-
- Manual progress reporting and cancellation, for plugins that provide long running commands (like the Retrie plugin),
139-
- 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.
138+
- Querying the LSP client capabilities,
139+
- Manual progress reporting and cancellation, for plugins that provide long running commands (like the `retrie` plugin),
140+
- 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.
140141

141-
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
142+
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
142143
most of them are defined in `Development.IDE.Core.RuleTypes`. Some relevant rule types are:
143144
```haskell
144145
-- | The parse tree for the file using GetFileContents
@@ -157,7 +158,7 @@ type instance RuleResult GhcSessionDeps = HscEnvEq
157158
type instance RuleResult GetModSummary = ModSummary
158159
```
159160

160-
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
161+
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
161162
```haskell
162163
let nfp = toNormalizedFilePath' fp
163164
session <- runAction "runEvalCmd.ghcSession" state $ use_ GhcSessionDeps nfp
@@ -167,7 +168,7 @@ The `use` family of combinators allow to request rule results. For example, the
167168
There are three flavours of `use` combinators:
168169

169170
1. `use*` combinators block and propagate errors,
170-
2. `useWithStale*` combinators block and switch to stale data in case of error,
171+
2. `useWithStale*` combinators block and switch to stale data in case of an error,
171172
3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise.
172173

173174
## LSP abstractions
@@ -199,7 +200,7 @@ To keep things simple our plugin won't make use of the unresolved facility, embe
199200
200201
## The explicit imports plugin
201202
202-
To provide code lenses, our plugin must define a code lens provider as well as a Command handler.
203+
To provide code lenses, our plugin must define a code lens provider as well as a command handler.
203204
The code at `Ide.Plugin.Example` shows how the convenience `defaultPluginDescriptor` function is used
204205
to bootstrap the plugin and how to add the desired providers:
205206
@@ -221,7 +222,7 @@ Our plugin provider has two components that need to be fleshed out. Let's start
221222
importLensCommand :: PluginCommand
222223
```
223224

224-
`PluginCommand` is a type synonym defined in `LSP.Types` as:
225+
`PluginCommand` is a data type defined in `LSP.Types` as:
225226

226227
```haskell
227228
data PluginCommand = forall a. (FromJSON a) =>
@@ -241,7 +242,7 @@ type CommandFunction a =
241242
```
242243

243244
`CommandFunction` takes in the familiar `LspFuncs` and `IdeState` arguments, together with a JSON encoded argument.
244-
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.
245+
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.
245246

246247
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
247248
of the code lens to the LSP client, who will send it back to our command handler when the user activates
@@ -270,10 +271,10 @@ runImportCommand _lspFuncs _state (ImportCommandParams edit) = do
270271

271272
The code lens provider implements all the steps of the algorithm described earlier:
272273

273-
> 1. Request the type checking artefacts from the ghcide subsystem
274-
> 2. Extract the actual import lists from the type checked AST,
274+
> 1. Request the type checking artefacts from the `ghcide` subsystem
275+
> 2. Extract the actual import lists from the type-checked AST,
275276
> 3. Ask GHC to produce the minimal import lists for this AST,
276-
> 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.
277+
> 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.
277278
278279
The provider takes the usual `LspFuncs` and `IdeState` argument, as well as a `CodeLensParams` value containing the URI
279280
for a file, and returns an IO action producing either an error or a list of code lenses for that file.
@@ -282,7 +283,7 @@ for a file, and returns an IO action producing either an error or a list of code
282283
provider :: CodeLensProvider
283284
provider _lspFuncs -- LSP functions, not used
284285
state -- ghcide state, used to retrieve typechecking artifacts
285-
pId -- plugin Id
286+
pId -- Plugin ID
286287
CodeLensParams{_textDocument = TextDocumentIdentifier{_uri}}
287288
-- VSCode uses URIs instead of file paths
288289
-- haskell-lsp provides conversion functions
@@ -292,7 +293,7 @@ provider _lspFuncs -- LSP functions, not used
292293
tmr <- runAction "importLens" state $ use TypeCheck nfp
293294
-- We also need a GHC session with all the dependencies
294295
hsc <- runAction "importLens" state $ use GhcSessionDeps nfp
295-
-- Use the GHC api to extract the "minimal" imports
296+
-- Use the GHC API to extract the "minimal" imports
296297
(imports, mbMinImports) <- extractMinimalImports hsc tmr
297298

298299
case mbMinImports of
@@ -309,18 +310,18 @@ provider _lspFuncs -- LSP functions, not used
309310
= return $ Right (List [])
310311
```
311312

312-
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.
313+
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.
313314

314315
The function `extractMinimalImports` extracts the import statements from the AST and generates the minimal import lists, implementing steps 2 and 3 of the algorithm.
315-
The details of the GHC api are not relevant to this tutorial, but the code is terse and easy to read:
316+
The details of the GHC API are not relevant to this tutorial, but the code is terse and easy to read:
316317

317318
```haskell
318319
extractMinimalImports
319320
:: Maybe HscEnvEq
320321
-> Maybe TcModuleResult
321322
-> IO ([LImportDecl GhcRn], Maybe [LImportDecl GhcRn])
322323
extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) = do
323-
-- extract the original imports and the typechecking environment
324+
-- Extract the original imports and the typechecking environment
324325
let (tcEnv,_) = tm_internals_
325326
Just (_, imports, _, _) = tm_renamed_source
326327
ParsedModule{ pm_parsed_source = L loc _} = tm_parsed_module
@@ -337,7 +338,7 @@ extractMinimalImports (Just hsc)) (Just (tmrModule -> TypecheckedModule{..})) =
337338
extractMinimalImports _ _ = return ([], Nothing)
338339
```
339340

340-
The function `generateLens` implements the last piece of the algorithm, step 4, producing a code lens for an import statement that lacks an import list. Note how the code lens includes an `ImportCommandParams` value
341+
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
341342
that contains a workspace edit that rewrites the import statement, as expected by our command provider.
342343

343344
```haskell
@@ -355,38 +356,38 @@ generateLens pId uri minImports (L src imp)
355356
| RealSrcSpan l <- src
356357
, Just explicit <- Map.lookup (srcSpanStart src) minImports
357358
, L _ mn <- ideclName imp
358-
-- (almost) no one wants to see an explicit import list for Prelude
359+
-- (Almost) no one wants to see an explicit import list for Prelude
359360
, mn /= moduleName pRELUDE
360361
= do
361362
-- The title of the command is just the minimal explicit import decl
362363
let title = T.pack $ prettyPrint explicit
363-
-- the range of the code lens is the span of the original import decl
364+
-- The range of the code lens is the span of the original import decl
364365
_range :: Range = realSrcSpanToRange l
365-
-- the code lens has no extra data
366+
-- The code lens has no extra data
366367
_xdata = Nothing
367-
-- an edit that replaces the whole declaration with the explicit one
368+
-- An edit that replaces the whole declaration with the explicit one
368369
edit = WorkspaceEdit (Just editsMap) Nothing
369370
editsMap = HashMap.fromList [(uri, List [importEdit])]
370371
importEdit = TextEdit _range title
371-
-- the command argument is simply the edit
372+
-- The command argument is simply the edit
372373
_arguments = Just [toJSON $ ImportCommandParams edit]
373-
-- create the command
374+
-- Create the command
374375
_command <- Just <$> mkLspCommand pId importCommandId title _arguments
375-
-- create and return the code lens
376+
-- Create and return the code lens
376377
return $ Just CodeLens{..}
377378
| otherwise
378379
= return Nothing
379380
```
380381

381382
## Wrapping up
382383

383-
There's only one haskell code change left to do at this point: "link" the plugin in the `HlsPlugins` HLS module.
384-
However integrating the plugin in haskell-language-server itself will need some changes in config files. The best way is looking for the id (f.e. `hls-class-plugin`) of an existing plugin:
385-
- `./cabal*.project` and `./stack*.yaml`: add the plugin package in the `packages` field
386-
- `./haskell-language-server.cabal`: add a conditional block with the plugin package dependency
387-
- `./.github/workflows/test.yml`: add a block to run the test suite of the plugin
388-
- `./.github/workflows/hackage.yml`: add the plugin to the component list to release the plugin package to hackage
389-
- `./*.nix`: add the plugin to nix builds
384+
There's only one Haskell code change left to do at this point: "link" the plugin in the `HlsPlugins` HLS module.
385+
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:
386+
- `./cabal*.project` and `./stack*.yaml`: add the plugin package in the `packages` field,
387+
- `./haskell-language-server.cabal`: add a conditional block with the plugin package dependency,
388+
- `./.github/workflows/test.yml`: add a block to run the test suite of the plugin,
389+
- `./.github/workflows/hackage.yml`: add the plugin to the component list to release the plugin package to Hackage,
390+
- `./*.nix`: add the plugin to Nix builds.
390391

391392
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)
392393

0 commit comments

Comments
 (0)