You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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
Originally written by Pepe Iborra, maintained by the Haskell community.
3
3
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.
7
6
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.
9
8
10
9
## Introduction
11
10
12
11
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.
13
12
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:
15
14
16
15
1. Suggest imports for variables not in scope,
17
16
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:
20
28
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
22
32
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
24
59
25
60
Here is a visual statement of what we want to accomplish:
26
61
@@ -29,190 +64,112 @@ Here is a visual statement of what we want to accomplish:
29
64
And here is the gist of the algorithm:
30
65
31
66
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
35
72
36
73
## Setup
37
74
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).
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.
46
78
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.
49
81
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.
51
84
52
85

53
86
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
+
54
89
## Anatomy of a plugin
55
90
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:
A plugin has a unique ID, a set of rules, a set of command handlers,anda set of"providers":
102
+
A plugin has a unique ID, command handlers, request handlers, notification handlers andrules:
72
103
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 donot 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.
76
108
77
-
TheHLS codebase includes several plugins under the namespace `Ide.Plugin.*`, the most relevant are:
109
+
##The explicit imports plugin
78
110
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`).
83
114
84
-
I would recommend looking at the existing plugins for inspiration and reference.
115
+
These will be assembled together inthe `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above.
85
116
86
-
Plugins are "linked"inthe `HlsPlugins` module, so we will need to add our plugin there once we have defined it:
117
+
Usingthe convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts:
Providers are functions that receive some inputs and produce an IO computation that returns either an erroror 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
-
Someof 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
We'll start with the command, since it's the simplest of the two.
146
134
147
-
--| The type checked version of this file
148
-
typeinstanceRuleResultTypeCheck=TcModuleResult
135
+
### The command handler
149
136
150
-
--| A GHC session that we reuse.
151
-
typeinstanceRuleResultGhcSession=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.
152
140
153
-
--| A GHC session preloaded with all the dependencies
154
-
typeinstanceRuleResultGhcSessionDeps=HscEnvEq
141
+
> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work.
155
142
156
-
--| A ModSummary that has enough information to be used to get .hi and .hie files.
157
-
typeinstanceRuleResultGetModSummary=ModSummary
158
-
```
143
+
The command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as:
159
144
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
161
145
```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
172
147
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
-
dataCodeLens=
181
-
CodeLens
182
-
{_range::Range
183
-
, _command::MaybeCommand
184
-
, _xdata::MaybeA.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
+
dataPluginCommandideState=foralla. (FromJSONa) =>
149
+
PluginCommand{commandId::CommandId
150
+
, commandDesc::T.Text
151
+
, commandFunc::CommandFunctionideStatea
152
+
}
196
153
```
197
154
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 IDand a description for now:
0 commit comments