Skip to content

Commit 39a170a

Browse files
committed
Plugin tutorial, more changes
This includes changes up until (but excluding) the section "The command handler". Some changes are mine, but many have been cherry picked and amended from PR
1 parent 173b5a7 commit 39a170a

File tree

1 file changed

+114
-157
lines changed

1 file changed

+114
-157
lines changed

docs/contributing/plugin-tutorial.md

Lines changed: 114 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,61 @@
11
# Let’s write a Haskell Language Server plugin
22
Originally written by Pepe Iborra, maintained by the Haskell community.
33

4-
Haskell Language Server (HLS) is an LSP server for the Haskell programming language. It builds on several previous efforts
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.
6-
4+
Haskell Language Server (HLS) is an LSP server for the Haskell programming language. It builds on several previous efforts to create a Haskell IDE.
5+
You can find many more details on the history and architecture on the [IDE 2020](https://mpickering.github.io/ide/index.html) community page.
76
In this article we are going to cover the creation of an HLS plugin from scratch: a code lens to display explicit import lists.
8-
Along the way we will learn about HLS, its plugin model, and the relationship with `ghcide` and LSP.
7+
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.
98

109
## Introduction
1110

1211
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.
1312

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

1615
1. Suggest imports for variables not in scope,
1716
2. Remove redundant imports,
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.
17+
3. Evaluate code in comments (à la [doctest](https://docs.python.org/3/library/doctest.html)),
18+
4. Integrate the [retrie](https://github.com/facebookincubator/retrie) refactoring library.
19+
20+
These plugins are small but meaningful steps towards a more polished IDE experience.
21+
While writing them, I didn't have to worry about performance, UI, or distribution; another tool (usually GHC) always did the heavy lifting.
22+
23+
The plugins also make these tools much more accessible to all users of HLS.
24+
25+
## Plugins in the HLS codebase
26+
27+
The HLS codebase includes several plugins (found in `./plugins`). For example:
2028

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.
29+
- The `ormolu`, `fourmolu`, `floskell` and `stylish-haskell` plugins used to format code
30+
- The `eval` plugin, a code lens provider to evaluate code in comments
31+
- The `retrie` plugin, a code action provider to execute retrie commands
2232

23-
## The task
33+
I recommend looking at the existing plugins for inspiration and reference. A few conventions shared by all plugins are:
34+
35+
- Plugins are in the `./plugins` folder
36+
- Plugins implement their code under the `Ide.Plugin.*` namespace
37+
- Folders containing the plugin follow the `hls-pluginname-plugin` naming convention
38+
- Plugins are "linked" in `src/HlsPlugins.hs#idePlugins`. New plugin descriptors
39+
must be added there.
40+
```haskell -- src/HlsPlugins.hs
41+
42+
idePlugins = pluginDescToIdePlugins allPlugins
43+
where
44+
allPlugins =
45+
[ GhcIde.descriptor "ghcide"
46+
, Pragmas.descriptor "pragmas"
47+
, Floskell.descriptor "floskell"
48+
, Fourmolu.descriptor "fourmolu"
49+
, Ormolu.descriptor "ormolu"
50+
, StylishHaskell.descriptor "stylish-haskell"
51+
, Retrie.descriptor "retrie"
52+
, Eval.descriptor "eval"
53+
, NewPlugin.descriptor "new-plugin" -- Add new plugins here.
54+
]
55+
```
56+
To add a new plugin, extend the list of `allPlugins` and rebuild.
57+
58+
## The goal of the plugin we will write
2459

2560
Here is a visual statement of what we want to accomplish:
2661

@@ -29,190 +64,112 @@ Here is a visual statement of what we want to accomplish:
2964
And here is the gist of the algorithm:
3065

3166
1. Request the type checking artifacts from the `ghcide` subsystem
32-
2. Extract the actual import lists from the type-checked AST,
33-
3. Ask GHC to produce the minimal import lists for this AST,
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.
67+
2. Extract the actual import lists from the type-checked AST
68+
3. Ask GHC to produce the minimal import lists for this AST
69+
4. For every import statement without an explicit import list:
70+
- Determine the minimal import list
71+
- Produce a code lens to display it and a command to apply it
3572

3673
## Setup
3774

38-
To get started, let’s fetch the HLS repository and build it. You need at least GHC 9.0 for this:
75+
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).
3976

40-
```
41-
git clone --recursive http://github.com/haskell/haskell-language-server hls
42-
cd hls
43-
cabal update
44-
cabal build
45-
```
77+
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.
4678

47-
If you run into any issues trying to build the binaries, the `#haskell-language-server` IRC chat room in
48-
[Libera Chat](https://libera.chat/) is always a good place to ask for help.
79+
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.
80+
This way you can simply test your changes by reloading your editor after rebuilding the binary.
4981

50-
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.
82+
> **Note:** In VSCode, edit the "Haskell Server Executable Path" setting.
83+
> **Note:** In Emacs, edit the `lsp-haskell-server-path` variable.
5184
5285
![Settings](settings-vscode.png)
5386

87+
[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.
88+
5489
## Anatomy of a plugin
5590

56-
HLS plugins are values of the `Plugin` datatype, which is defined in `Ide.Plugin` as:
91+
HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as:
5792
```haskell
58-
data PluginDescriptor =
59-
PluginDescriptor { pluginId :: !PluginId
60-
, pluginRules :: !(Rules ())
61-
, pluginCommands :: ![PluginCommand]
62-
, pluginCodeActionProvider :: !(Maybe CodeActionProvider)
63-
, pluginCodeLensProvider :: !(Maybe CodeLensProvider)
64-
, pluginHoverProvider :: !(Maybe HoverProvider)
65-
, pluginSymbolsProvider :: !(Maybe SymbolsProvider)
66-
, pluginFormattingProvider :: !(Maybe (FormattingProvider IO))
67-
, pluginCompletionProvider :: !(Maybe CompletionProvider)
68-
, pluginRenameProvider :: !(Maybe RenameProvider)
93+
data PluginDescriptor (ideState :: Type) =
94+
PluginDescriptor { pluginId :: !PluginId
95+
, pluginRules :: !(Rules ())
96+
, pluginCommands :: ![PluginCommand ideState]
97+
, pluginHandlers :: PluginHandlers ideState
98+
, pluginNotificationHandlers :: PluginNotificationHandlers ideState
99+
, [...] -- Other fields omitted for brevity.
69100
}
70101
```
71-
A plugin has a unique ID, a set of rules, a set of command handlers, and a set of "providers":
102+
A plugin has a unique ID, command handlers, request handlers, notification handlers and rules:
72103

73-
* Rules add new targets to the Shake build graph defined in `ghcide`. 99% of plugins need not define any new rules.
74-
* 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.
75-
* Providers are a query-like abstraction where the LSP client asks the server for information. These queries must be fulfilled as quickly as possible.
104+
* Request handlers respond to requests from an LSP client. They must fulfill these requests as quickly as possible.
105+
* Notification handlers receive notifications from code not directly triggered by a user or client.
106+
* Rules add new targets to the Shake build graph. Most plugins do not need to define new rules.
107+
* Commands are an LSP abstraction for user-initiated actions that the server handles. These actions can be long-running and involve multiple modules.
76108

77-
The HLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are:
109+
## The explicit imports plugin
78110

79-
- The `ghcide` plugin, which embeds `ghcide` as a plugin (`ghcide` is also the engine under HLS),
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.
111+
To achieve our plugin goals, we need to define:
112+
- a command handler (`importLensCommand`),
113+
- a code lens request handler (`lensProvider`).
83114

84-
I would recommend looking at the existing plugins for inspiration and reference.
115+
These will be assembled together in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above.
85116

86-
Plugins are "linked" in the `HlsPlugins` module, so we will need to add our plugin there once we have defined it:
117+
Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts:
87118

88119
```haskell
89-
idePlugins = pluginDescToIdePlugins allPlugins
90-
where
91-
allPlugins =
92-
[ GhcIde.descriptor "ghcide"
93-
, Pragmas.descriptor "pragmas"
94-
, Floskell.descriptor "floskell"
95-
, Fourmolu.descriptor "fourmolu"
96-
, Ormolu.descriptor "ormolu"
97-
, StylishHaskell.descriptor "stylish-haskell"
98-
, Retrie.descriptor "retrie"
99-
, Eval.descriptor "eval"
100-
]
120+
-- plugins/hls-explicit-imports-plugin/src/Ide/Plugin/ExplicitImports.hs
121+
122+
-- | The "main" function of a plugin.
123+
descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState
124+
descriptor recorder plId =
125+
(defaultPluginDescriptor plId)
126+
{ pluginCommands = [importLensCommand], -- The plugin provides a command handler
127+
pluginHandlers = mconcat -- The plugin provides request handlers
128+
[ lensProvider
129+
]
130+
}
101131
```
102-
To add a new plugin, simply extend the list of `allPlugins` and rebuild.
103-
104-
## Providers
105132

106-
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:
107-
```haskell
108-
type CodeActionProvider = LSP.LspFuncs Config
109-
-> IdeState
110-
-> PluginId
111-
-> TextDocumentIdentifier
112-
-> Range
113-
-> CodeActionContext
114-
-> IO (Either ResponseError (List CAResult))
115-
116-
type CompletionProvider = LSP.LspFuncs Config
117-
-> IdeState
118-
-> CompletionParams
119-
-> IO (Either ResponseError CompletionResponseResult)
120-
121-
type CodeLensProvider = LSP.LspFuncs Config
122-
-> IdeState
123-
-> PluginId
124-
-> CodeLensParams
125-
-> IO (Either ResponseError (List CodeLens))
126-
127-
type RenameProvider = LSP.LspFuncs Config
128-
-> IdeState
129-
-> RenameParams
130-
-> IO (Either ResponseError WorkspaceEdit)
131-
```
132-
133-
Providers are functions that receive some inputs and produce an IO computation that returns either an error or some result.
134-
135-
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.
136-
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.
140-
141-
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
142-
most of them are defined in `Development.IDE.Core.RuleTypes`. Some relevant rule types are:
143-
```haskell
144-
-- | The parse tree for the file using GetFileContents
145-
type instance RuleResult GetParsedModule = ParsedModule
133+
We'll start with the command, since it's the simplest of the two.
146134

147-
-- | The type checked version of this file
148-
type instance RuleResult TypeCheck = TcModuleResult
135+
### The command handler
149136

150-
-- | A GHC session that we reuse.
151-
type instance RuleResult GhcSession = HscEnvEq
137+
In short, commands work like this:
138+
- The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens.
139+
- 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.
152140

153-
-- | A GHC session preloaded with all the dependencies
154-
type instance RuleResult GhcSessionDeps = HscEnvEq
141+
> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work.
155142
156-
-- | A ModSummary that has enough information to be used to get .hi and .hie files.
157-
type instance RuleResult GetModSummary = ModSummary
158-
```
143+
The command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as:
159144

160-
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
161145
```haskell
162-
let nfp = toNormalizedFilePath' fp
163-
session <- runAction "runEvalCmd.ghcSession" state $ use_ GhcSessionDeps nfp
164-
ms <- runAction "runEvalCmd.getModSummary" state $ use_ GetModSummary nfp
165-
```
166-
167-
There are three flavours of `use` combinators:
168-
169-
1. `use*` combinators block and propagate errors,
170-
2. `useWithStale*` combinators block and switch to stale data in case of an error,
171-
3. `useWithStaleFast*` combinators return immediately with stale data if any, or block otherwise.
146+
-- hls-plugin-api/src/Ide/Types.hs
172147

173-
## LSP abstractions
174-
175-
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.
176-
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.
177-
178-
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:
179-
```haskell
180-
data CodeLens =
181-
CodeLens
182-
{ _range :: Range
183-
, _command :: Maybe Command
184-
, _xdata :: Maybe A.Value
185-
} deriving (Read,Show,Eq)
186-
```
187-
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:
188-
```
189-
/**
190-
* A code lens represents a command that should be shown along with
191-
* source text, like the number of references, a way to run tests, etc.
192-
*
193-
* A code lens is _unresolved_ when no command is associated to it. For performance
194-
* reasons the creation of a code lens and resolving should be done in two stages.
195-
*/
148+
data PluginCommand ideState = forall a. (FromJSON a) =>
149+
PluginCommand { commandId :: CommandId
150+
, commandDesc :: T.Text
151+
, commandFunc :: CommandFunction ideState a
152+
}
196153
```
197154

198-
To keep things simple our plugin won't make use of the unresolved facility, embedding the command directly in the code lens.
199-
200-
## The explicit imports plugin
201-
202-
To provide code lenses, our plugin must define a code lens provider as well as a command handler.
203-
The code at `Ide.Plugin.Example` shows how the convenience `defaultPluginDescriptor` function is used
204-
to bootstrap the plugin and how to add the desired providers:
155+
Let's start by creating an unfinished command handler. We'll give it an ID and a description for now:
205156

206157
```haskell
207-
descriptor :: PluginId -> PluginDescriptor
208-
descriptor plId = (defaultPluginDescriptor plId) {
209-
-- This plugin provides code lenses
210-
pluginCodeLensProvider = Just provider,
211-
-- This plugin provides a command handler
212-
pluginCommands = [ importLensCommand ]
213-
}
158+
-- | The command handler.
159+
importLensCommand :: PluginCommand IdeState
160+
importLensCommand =
161+
PluginCommand
162+
{ commandId = "ImportLensCommand"
163+
, commandDesc = "Explicit import command"
164+
, commandFunc = runImportCommand
165+
}
166+
167+
-- | Not implemented yet.
168+
runImportCommand = undefined
214169
```
215170

171+
<!-- CONTINUE HERE. -->
172+
216173
### The command handler
217174

218175
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.

0 commit comments

Comments
 (0)