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
Originally written by Pepe Iborra, maintained by the Haskell community.
3
4
4
5
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
22
23
23
24
The plugins also make these tools much more accessible to all users of HLS.
24
25
26
+
## Preamble
27
+
28
+
```haskell
29
+
{-# LANGUAGE OverloadedStrings #-}
30
+
{-# LANGUAGE DerivingStrategies #-}
31
+
{-# LANGUAGE ScopedTypeVariables #-}
32
+
{-# LANGUAGE RecordWildCards #-}
33
+
{-# LANGUAGE NamedFieldPuns #-}
34
+
{-# LANGUAGE ViewPatterns #-}
35
+
{-# LANGUAGE DeriveGeneric #-}
36
+
{-# LANGUAGE DataKinds #-}
37
+
{-# LANGUAGE DeriveAnyClass #-}
38
+
39
+
importIde.Types
40
+
importIde.Logger
41
+
importIde.Plugin.Error
42
+
43
+
importDevelopment.IDE.Core.RuleTypes
44
+
importDevelopment.IDE.Core.Servicehiding (Log)
45
+
importDevelopment.IDE.Core.Shakehiding (Log)
46
+
importDevelopment.IDE.GHC.Compat
47
+
importDevelopment.IDE.GHC.Compat.Core
48
+
importDevelopment.IDE.GHC.Error
49
+
importDevelopment.IDE.Types.HscEnvEq
50
+
importDevelopment.IDE.Core.PluginUtils
51
+
52
+
importqualifiedLanguage.LSP.ServerasLSP
53
+
importLanguage.LSP.Protocol.TypesasJL
54
+
importLanguage.LSP.Protocol.Message
55
+
56
+
importData.AesonasAeson
57
+
importData.Map (Map)
58
+
importData.IORef
59
+
importData.Maybe (fromMaybe, catMaybes)
60
+
importqualifiedData.MapasMap
61
+
importqualifiedData.HashMap.StrictasHashMap
62
+
importqualifiedData.TextasT
63
+
importControl.Monad (forM)
64
+
importControl.Monad.IO.Class (liftIO)
65
+
importControl.Monad.Trans.Class
66
+
importGHC.Generics (Generic)
67
+
```
68
+
25
69
## Plugins in the HLS codebase
26
70
27
71
TheHLS 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
37
81
-Folders containing the plugin follow the `hls-pluginname-plugin` naming convention
38
82
-Plugins are "linked"in `src/HlsPlugins.hs#idePlugins`.New plugin descriptors
39
83
must be added there.
40
-
```haskell -- src/HlsPlugins.hs
84
+
85
+
```haskell ignore
86
+
-- Defined in src/HlsPlugins.**hs**
41
87
42
88
idePlugins = pluginDescToIdePlugins allPlugins
43
89
where
@@ -53,6 +99,7 @@ I recommend looking at the existing plugins for inspiration and reference. A few
53
99
, NewPlugin.descriptor "new-plugin"-- Add new plugins here.
54
100
]
55
101
```
102
+
56
103
To add a new plugin, extend the list of `allPlugins` and rebuild.
57
104
58
105
## 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
80
127
This way you can simply test your changes by reloading your editor after rebuilding the binary.
81
128
82
129
> **Note:** In VSCode, edit the "Haskell Server Executable Path" setting.
83
-
130
+
>
84
131
> **Note:** In Emacs, edit the `lsp-haskell-server-path` variable.
85
132
86
133

@@ -90,6 +137,7 @@ This way you can simply test your changes by reloading your editor after rebuild
90
137
## Digression about the Language Server Protocol
91
138
92
139
There are two main types of communication in the Language Server Protocol:
140
+
93
141
- A **request-response interaction** type where one party sends a message that requires a response from the other party.
94
142
- A **notification** is a one-way interaction where one party sends a message without expecting any response.
95
143
@@ -98,24 +146,27 @@ There are two main types of communication in the Language Server Protocol:
98
146
## Anatomy of a plugin
99
147
100
148
HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as:
The`pluginHandlers` handle LSP client requests and provide responses to the client.They must fulfill these requests as quickly as possible.
163
+
114
164
-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.
115
165
116
166
### Notification
117
167
118
168
The`pluginNotificationHandlers` handle notifications sent by the client to the server that are not explicitly triggered by a user.
169
+
119
170
-Example:Whenever you modify a Haskell file, the client sends a notification informing HLS about the changes to the file.
120
171
121
172
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.
124
175
## The explicit imports plugin
125
176
126
177
To achieve our plugin goals, we need to define:
178
+
127
179
- a command handler (`importLensCommand`),
128
180
- a code lens request handler (`lensProvider`).
129
181
@@ -134,13 +186,15 @@ Using the convenience `defaultPluginDescriptor` function, we can bootstrap the p
@@ -150,14 +204,15 @@ We'll start with the command, since it's the simplest of the two.
150
204
### The command handler
151
205
152
206
In short, LSP commands work like this:
207
+
153
208
- The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens.
154
209
- 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.
155
210
156
211
> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work.
157
212
158
213
The command handler will be called `importLensCommand` and have the `PluginCommand` type, a type defined in `Ide.Types` as:
159
214
160
-
```haskell
215
+
```haskell ignore
161
216
-- hls-plugin-api/src/Ide/Types.hs
162
217
163
218
dataPluginCommandideState=foralla. (FromJSONa) =>
@@ -174,18 +229,23 @@ Let's start by creating an unfinished command handler. We'll give it an ID and a
174
229
importLensCommand::PluginCommandIdeState
175
230
importLensCommand =
176
231
PluginCommand
177
-
{ commandId ="ImportLensCommand"
232
+
{ commandId =importCommandId
178
233
, commandDesc ="Explicit import command"
179
234
, commandFunc = runImportCommand
180
235
}
181
236
237
+
importCommandId::CommandId
238
+
importCommandId ="ImportLensCommand"
239
+
```
240
+
241
+
```haskell ignore
182
242
--| Not implemented yet.
183
243
runImportCommand =undefined
184
244
```
185
245
186
246
The most important (and still `undefined`) field is `commandFunc :: CommandFunction`, a type synonym from `LSP.Types`:
187
247
188
-
```haskell
248
+
```haskell ignore
189
249
-- hls-plugin-api/src/Ide/Types.hs
190
250
191
251
typeCommandFunctionideStatea
@@ -194,8 +254,7 @@ type CommandFunction ideState a
194
254
->LspMConfig (EitherResponseErrorValue)
195
255
```
196
256
197
-
198
-
`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`.
257
+
`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`.
199
258
200
259
Our handler will ignore the state argument and only use the `WorkspaceEdit` argument.
`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
219
278
220
279
The code lens provider implements all the steps of the algorithm described earlier:
221
280
222
-
> 1. Request the type checking artifacts.
223
-
> 2. Extract the actual import lists from the type-checked AST.
224
-
> 3. Ask GHC to produce the minimal import lists for this AST.
225
-
> 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.
281
+
> 1. Request the type checking artifacts.
282
+
> 2. Extract the actual import lists from the type-checked AST.
283
+
> 3. Ask GHC to produce the minimal import lists for this AST.
284
+
> 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.
226
285
227
286
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.
228
287
229
288
```haskell
230
-
provider::CodeLensProvider
231
-
provider _lspFuncs -- LSP functions, not used
232
-
state -- ghcide state, used to retrieve typechecking artifacts
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
292
349
--| Given an import declaration, generate a code lens unless it has an explicit import list
293
350
generateLens::PluginId
294
351
->Uri
295
-
->MapSrcLoc (ImportDeclGhcRn)
352
+
->MapPosition (ImportDeclGhcRn)
296
353
->LImportDeclGhcRn
297
354
->IO (MaybeCodeLens)
298
355
generateLens pId uri minImports (L src imp)
299
356
-- Explicit import list case
300
-
|ImportDecl{ideclHiding=Just(False,_)} <- imp
357
+
|ImportDecl{ideclImportList=Just_} <- imp
301
358
=returnNothing
302
359
-- No explicit import list
303
-
|RealSrcSpan l <- src
304
-
, Just explicit <-Map.lookup (srcSpanStart src) minImports
360
+
|RealSrcSpan l _ <- locA src
361
+
, let position = realSrcLocToPosition $ realSrcSpanStart l
362
+
, Just explicit <-Map.lookup position minImports
305
363
, L _ mn <- ideclName imp
306
364
-- (Almost) no one wants to see an explicit import list for Prelude
307
365
, mn /= moduleName pRELUDE
308
366
=do
309
367
-- The title of the command is just the minimal explicit import decl
310
-
let title =T.pack $prettyPrint explicit
368
+
let title =T.pack $printWithoutUniques explicit
311
369
-- The range of the code lens is the span of the original import decl
312
370
_range ::Range= realSrcSpanToRange l
313
371
-- The code lens has no extra data
314
372
_xdata =Nothing
315
373
-- An edit that replaces the whole declaration with the explicit one
_command <-Just<$> mkLspCommand pId importCommandId title _arguments
379
+
_data_ =Nothing
380
+
-- Create the command
381
+
_command =Just$ mkLspCommand pId importCommandId title _arguments
323
382
-- Create and return the code lens
324
383
return$JustCodeLens{..}
325
384
|otherwise
@@ -333,6 +392,7 @@ There's only one Haskell code change left to do at this point: "link" the plugin
333
392
Integrating the plugin into HLS itself requires changes to several configuration files.
334
393
335
394
A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`):
395
+
336
396
-`./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field.
337
397
-`./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency.
338
398
-`./.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-
342
402
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).
343
403
344
404
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).
0 commit comments