Skip to content

Commit 776301a

Browse files
committed
Review comments and other improvments, thanks @VeryMilkyJoe.
1 parent 98e4bad commit 776301a

File tree

1 file changed

+26
-12
lines changed

1 file changed

+26
-12
lines changed

docs/contributing/plugin-tutorial.md

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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 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.
55
You can find many more details on the history and architecture on the [IDE 2020](https://mpickering.github.io/ide/index.html) community page.
66
In this article we are going to cover the creation of an HLS plugin from scratch: a code lens to display explicit import lists.
77
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
8787

8888
[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.
8989

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+
9098
## Anatomy of a plugin
9199

92100
HLS plugins are values of the `PluginDescriptor` datatype, which is defined in `hls-plugin-api/src/Ide/Types.hs` as:
93101
```haskell
94102
data PluginDescriptor (ideState :: Type) =
95103
PluginDescriptor { pluginId :: !PluginId
96-
, pluginRules :: !(Rules ())
97104
, pluginCommands :: ![PluginCommand ideState]
98105
, pluginHandlers :: PluginHandlers ideState
99106
, pluginNotificationHandlers :: PluginNotificationHandlers ideState
100107
, [...] -- Other fields omitted for brevity.
101108
}
102109
```
103-
A plugin has a unique ID, command handlers, request handlers, notification handlers and rules:
104110

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 do not 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.
109123

110124
## The explicit imports plugin
111125

112126
To achieve our plugin goals, we need to define:
113127
- a command handler (`importLensCommand`),
114128
- a code lens request handler (`lensProvider`).
115129

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

118132
Using the convenience `defaultPluginDescriptor` function, we can bootstrap the plugin with the required parts:
119133

@@ -135,9 +149,9 @@ We'll start with the command, since it's the simplest of the two.
135149

136150
### The command handler
137151

138-
In short, commands work like this:
152+
In short, LSP commands work like this:
139153
- 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.
141155

142156
> **Note**: Check the [LSP spec](https://microsoft.github.io/language-server-protocol/specification) for a deeper understanding of how commands work.
143157
@@ -181,7 +195,7 @@ type CommandFunction ideState a
181195
```
182196

183197

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`.
185199

186200
Our handler will ignore the state argument and only use the `WorkspaceEdit` argument.
187201

@@ -318,7 +332,7 @@ There's only one Haskell code change left to do at this point: "link" the plugin
318332

319333
Integrating the plugin into HLS itself requires changes to several configuration files.
320334

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`):
322336
- `./cabal*.project` and `./stack*.yaml`: Add the plugin package to the `packages` field.
323337
- `./haskell-language-server.cabal`: Add a conditional block with the plugin package dependency.
324338
- `./.github/workflows/test.yml`: Add a block to run the plugin's test suite.

0 commit comments

Comments
 (0)