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
Copy file name to clipboardExpand all lines: docs/contributing/plugin-tutorial.md
+26-12Lines changed: 26 additions & 12 deletions
Original file line number
Diff line number
Diff line change
@@ -1,7 +1,7 @@
1
1
# Let’s write a Haskell Language Server plugin
2
2
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 to create a Haskell IDE.
4
+
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.
5
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.
6
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.
7
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.
@@ -87,33 +87,47 @@ This way you can simply test your changes by reloading your editor after rebuild
87
87
88
88
[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.
89
89
90
+
## Digression about the Language Server Protocol
91
+
92
+
There are two main types of communication in the Language Server Protocol:
93
+
- A **request-response interaction** type where one party sends a message that requires a response from the other party.
94
+
- A **notification** is a one-way interaction where one party sends a message without expecting any response.
95
+
96
+
> **Note**: The LSP client and server can both send requests or notifications to the other party.
97
+
90
98
## Anatomy of a plugin
91
99
92
100
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, command handlers, request handlers, notification handlers and rules:
104
110
105
-
*Request handlers respond to requests from an LSP client.They must fulfill these requests as quickly as possible.
106
-
*Notification handlers receive notifications from code not directly triggered by a user or client.
107
-
*Rules add new targets to the Shake build graph.Most plugins donot need to define new rules.
108
-
*Commands are an LSP abstraction for user-initiated actions that the server handles.These actions can be long-running and involve multiple modules.
111
+
### Request-response interaction
112
+
113
+
The`pluginHandlers` handle LSP client requests and provide responses to the client.They must fulfill these requests as quickly as possible.
114
+
-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
+
116
+
### Notification
117
+
118
+
The`pluginNotificationHandlers` handle notifications sent by the client to the server that are not explicitly triggered by a user.
119
+
-Example:Whenever you modify a Haskell file, the client sends a notification informing HLS about the changes to the file.
120
+
121
+
The`pluginCommands` are special types of user-initiated notifications sent to
122
+
the server.These actions can be long-running and involve multiple modules.
109
123
110
124
## The explicit imports plugin
111
125
112
126
To achieve our plugin goals, we need to define:
113
127
- a command handler (`importLensCommand`),
114
128
- a code lens request handler (`lensProvider`).
115
129
116
-
These will be assembled together in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above.
130
+
These will be assembled in the `descriptor` function of the plugin, which contains all the information wrapped in the `PluginDescriptor` datatype mentioned above.
117
131
118
132
Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts:
119
133
@@ -135,9 +149,9 @@ We'll start with the command, since it's the simplest of the two.
135
149
136
150
### The command handler
137
151
138
-
In short, commands work like this:
152
+
In short, LSP commands work like this:
139
153
- The LSP server (HLS) initially sends a command descriptor to the client, in this case as part of a code lens.
140
-
-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.
154
+
-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.
141
155
142
156
> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work.
143
157
@@ -181,7 +195,7 @@ type CommandFunction ideState a
181
195
```
182
196
183
197
184
-
`CommandFunction` takes an `ideState`and a JSON-encodable argument.
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`.
185
199
186
200
Our handler will ignore the state argument and only use the `WorkspaceEdit` argument.
187
201
@@ -318,7 +332,7 @@ There's only one Haskell code change left to do at this point: "link" the plugin
318
332
319
333
Integrating the plugin into HLS itself requires changes to several configuration files.
320
334
321
-
A good approach is to use the ID of an existing plugin (e.g., `hls-class-plugin`) as a guide:
335
+
A good approach is to search for the ID of an existing plugin (e.g., `hls-class-plugin`):
322
336
-`./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field.
323
337
-`./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency.
324
338
-`./.github/workflows/test.yml`: Add a block to run the plugin's test suite.
0 commit comments