|
2 | 2 |
|
3 | 3 | A [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) base for developing any LSP implementation in Clojure.
|
4 | 4 |
|
5 |
| -## Server |
| 5 | +[](https://clojars.org/com.github.clojure-lsp/lsp4clj) |
6 | 6 |
|
7 |
| -[](https://clojars.org/com.github.clojure-lsp/lsp4clj-server) |
| 7 | +lsp4clj reads and writes from stdio, parsing JSON-RPC according to the LSP spec. It provides tools to allow server implementors to receive, process, and respond to any of the methods defined in the LSP spec, and to send their own requests and notifications to clients. |
8 | 8 |
|
9 |
| -The lsp4clj-server has the necessary integration with Input/Output, LSP-JSON parsing, allowing for users of this lib to just code the entrypoints of each LSP method. |
| 9 | +## Usage |
10 | 10 |
|
11 |
| -## Protocols |
| 11 | +### Create a server |
12 | 12 |
|
13 |
| -[](https://clojars.org/com.github.clojure-lsp/lsp4clj-protocols) |
| 13 | +To initialize a server that will read from stdin and write to stdout: |
14 | 14 |
|
15 |
| -The lsp4clj-protocols contains only the protocols/interfaces for servers that want to extend the official LSP protocol to provide more features, also, it is used by lsp4clj-server itself. |
| 15 | +```clojure |
| 16 | +(lsp4clj.server/stdio-server {:in System/in, :out System/out}) |
| 17 | +``` |
16 | 18 |
|
17 |
| -## Known LSPs users |
| 19 | +The returned server will have a core.async `:log-ch`, from which you can read server logs (vectors beginning with a log level). |
| 20 | + |
| 21 | +```clojure |
| 22 | +(async/go-loop [] |
| 23 | + (when-let [[level & args] (async/<! (:log-ch server))] |
| 24 | + (apply logger/log level args) |
| 25 | + (recur))) |
| 26 | +``` |
| 27 | + |
| 28 | +### Receive messages |
| 29 | + |
| 30 | +To receive messages from a client, lsp4clj defines a pair of multimethods, `lsp4clj.server/receive-notification` and `lsp4clj.server/receive-request` that dispatch on the method name (as defined by the LSP spec) of an incoming JSON-RPC message. |
| 31 | + |
| 32 | +Server implementors should create `defmethod`s for the messages they want to process. (Other methods will be logged and responded to with a generic "Method not found" response.) |
| 33 | + |
| 34 | +These `defmethod`s receive 3 arguments, the method name, a "context", and the `params` of the [JSON-RPC request or notification object](https://www.jsonrpc.org/specification#request_object). The keys of the params will have been converted (recursively) to kebab-case keywords. Read on for an explanation of what a "context" is and how to set it. |
| 35 | + |
| 36 | +```clojure |
| 37 | +;; a notification; return value is ignored |
| 38 | +(defmethod lsp4clj.server/receive-notification "textDocument/didOpen" |
| 39 | + [_ context {:keys [text-document]}] |
| 40 | + (handler/did-open context (:uri text-document) (:text text-document)) |
| 41 | + |
| 42 | +;; a request; return value is converted to a response |
| 43 | +(defmethod lsp4clj.server/receive-request "textDocument/definition" |
| 44 | + [_ context params] |
| 45 | + (->> params |
| 46 | + (handler/definition context) |
| 47 | + (conform-or-log ::coercer/location))) |
| 48 | +``` |
| 49 | + |
| 50 | +The return value of requests will be converted to camelCase json and returned to the client. If the return value looks like `{:error ...}`, it is assumed to indicate an error response, and the `...` part will be set as the `error` of a [JSON-RPC error object](https://www.jsonrpc.org/specification#error_object). It is up to you to conform the `...` object (by giving it a `code`, `message`, and `data`.) Otherwise, the entire return value will be set as the `result` of a [JSON-RPC response object](https://www.jsonrpc.org/specification#response_object). (Message ids are handled internally by lsp4clj.) |
| 51 | + |
| 52 | +### Send messages |
| 53 | + |
| 54 | +Servers also initiate their own requests and notifications to a client. To send a notification, call `lsp4clj.server/send-notification`. |
| 55 | + |
| 56 | +```clojure |
| 57 | +(->> {:message message |
| 58 | + :type type |
| 59 | + :extra extra} |
| 60 | + (conform-or-log ::coercer/show-message) |
| 61 | + (lsp4clj.server/send-notification server "window/showMessage")) |
| 62 | +``` |
| 63 | + |
| 64 | +Sending a request is similar, with `lsp4clj.server/send-request`. This method returns a request object which may be dereffed to get the client's response. Most of the time you will want to call `lsp4clj.server/deref-or-cancel`, which will send a `$/cancelRequest` to the client if a timeout is reached before the client responds. |
| 65 | + |
| 66 | +```clojure |
| 67 | +(let [request (->> {:edit edit} |
| 68 | + (conform-or-log ::coercer/workspace-edit-params) |
| 69 | + (lsp4clj.server/send-request server "workspace/applyEdit")) |
| 70 | + response (lsp4clj.server/deref-or-cancel request 10e3 ::timeout)] |
| 71 | + (if (= ::timeout response) |
| 72 | + (logger/error "No reponse from client after 10 seconds.") |
| 73 | + response)) |
| 74 | +``` |
| 75 | + |
| 76 | +Otherwise, the request object presents the same interface as `future`. Responds to `future-cancel` (which sends `$/cancelRequest`), `realized?`, `future?`, `future-done?` and `future-cancelled?`. |
| 77 | + |
| 78 | +If the request is cancelled, future invocations of `deref` will return `:lsp4clj.server/cancelled`. |
| 79 | + |
| 80 | +Sends `$/cancelRequest` only once, though `lsp4clj.server/deref-or-cancel` or `future-cancel` can be called multiple times. |
| 81 | + |
| 82 | +### Start and stop a server |
| 83 | + |
| 84 | +The last step is to start the server you created earlier. Use `lsp4clj.server/start`. This method accepts two arguments, the server and a "context". |
| 85 | + |
| 86 | +The context should be `associative?`. Whatever you provide in the context will be passed as the second argument to the notification and request `defmethod`s you defined earlier. This is a convenient way to make components of your system available to those methods without definining global constants. Often the context will include the server itself so that you can initiate outbound requests and notifications in reaction to inbound messages. lsp4clj reserves the right to add its own data to the context, using keys namespaced with `:lsp4clj.server/...`. |
| 87 | + |
| 88 | +```clojure |
| 89 | +(lsp4clj.server/start server {:custom-settings custom-settings, :logger logger}) |
| 90 | +``` |
| 91 | + |
| 92 | +The return of `start` is a promise that will resolve to `:done` when the server shuts down, which can happen in a few ways. |
| 93 | + |
| 94 | +First, if the server's input is closed, it will shut down too. Second, if you call `lsp4clj.server/shutdown` on it, it will shut down. |
| 95 | + |
| 96 | +When a server shuts down it stops reading input, finishes processing the messages it has in flight, and then closes is output. (If it is shut down with `lsp4cl.server/shutdown` it also closes its `:log-ch` and `:trace-ch`.) As such, it should probably not be shut down until the LSP `exit` notification (as opposed to the `shutdown` request) to ensure all messages are received. `lsp4clj.server/shutdown` will not return until all messages have been processed, or until 10 seconds have passed, whichever happens sooner. It will return `:done` in the first case and `:timeout` in the second. |
| 97 | + |
| 98 | +## Development details |
| 99 | + |
| 100 | +### Tracing |
| 101 | + |
| 102 | +As you are implementing, you may want to trace incoming and outgoing messages. Initialize the server with `:trace? true` and then read traces (two element vectors, beginning with the log level `:debug` and ending with a string, the trace itself) off its `:trace-ch`. |
| 103 | + |
| 104 | +```clojure |
| 105 | +(let [server (lsp4clj.server/stdio-server {:trace? true |
| 106 | + :in System/in |
| 107 | + :out System/out})] |
| 108 | + (async/go-loop [] |
| 109 | + (when-let [[level trace] (async/<! (:trace-ch server))] |
| 110 | + (logger/log level trace) |
| 111 | + (recur))) |
| 112 | + (lsp4clj.server/start server context)) |
| 113 | +``` |
| 114 | + |
| 115 | +### Testing |
| 116 | + |
| 117 | +A client is in many ways like a server—it also sends requests and notifications and receives responses. That is, LSP's flavor of JSON-RPC is bi-directional. As such, you may be able to use some of lsp4clj's tools to build a mock client for testing. See `integration.client` in `clojure-lsp` for one such example. |
| 118 | + |
| 119 | +You may also find `lsp4clj.server/chan-server` a useful alternative to `stdio-server`. This server reads and writes off channels, instead of stdio streams. See `lsp4clj.server-test` for many examples of interacting with such a server. |
| 120 | + |
| 121 | +## Caveats |
| 122 | + |
| 123 | +You must not print to stdout while a `stdio-server` is running. This will corrupt its output stream and clients will receive malformed messages. To protect a block of code from writing to stdout, wrap it with `lsp4clj.server/discarding-stdout`. The `receive-notification` and `receive-request` multimethods are already protected this way, but tasks started outside of these multimethods need this protection added. See https://github.com/clojure-lsp/lsp4clj/issues/1 for future work on avoiding this problem. |
| 124 | + |
| 125 | +## Known lsp4clj users |
18 | 126 |
|
19 | 127 | - [clojure-lsp](https://clojure-lsp.io/): A Clojure LSP server implementation.
|
0 commit comments