-
Notifications
You must be signed in to change notification settings - Fork 7
Remove lsp4j #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Remove lsp4j #13
Changes from 67 commits
607a2fe
1dc0dac
7101970
548fd5e
f51c577
229def7
82b34b0
c6cbc76
f94eceb
cbf1610
4d34f0f
4b122fe
b20d0a5
6521c7f
ac6579b
59a9787
9567155
a037776
dcd6e42
6d8929a
e9d18c0
170a651
ce960f9
63a8ce0
be9d336
bd18fcf
1eb7279
b2c3399
01b25d1
61a2dc7
88a8200
b828d68
32fd7cd
c81ca1c
9b46f29
f0f0b58
bef2b0f
0534c09
4f1d4a4
bfe7cc1
7f2be73
c206597
255f1d2
6996fdb
9865b1b
ac2712d
e48f55d
0d97376
38779d3
5d1c339
772f669
786837e
423a8a4
9aefcf6
0826be1
68a5862
baa344f
8b3731a
a3595e1
eed89cd
e58eda1
20c4cbd
597e91e
150115d
48a15ca
f4e2cae
6d02043
f57870f
0d5a9c5
0c5d06b
9f631f2
5dc8183
534d4d6
d93fac0
b1a28ea
117347e
33401b1
d73a556
b36af24
43294db
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
((clojure-mode | ||
(cider-clojure-cli-aliases . "test"))) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,18 +2,126 @@ | |
|
||
A [Language Server Protocol](https://microsoft.github.io/language-server-protocol/) base for developing any LSP implementation in Clojure. | ||
|
||
## Server | ||
[](https://clojars.org/com.github.clojure-lsp/lsp4clj) | ||
|
||
[](https://clojars.org/com.github.clojure-lsp/lsp4clj-server) | ||
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. | ||
|
||
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. | ||
## Usage | ||
|
||
## Protocols | ||
### Create a server | ||
|
||
[](https://clojars.org/com.github.clojure-lsp/lsp4clj-protocols) | ||
To initialize a server that will read from stdin and write to stdout: | ||
|
||
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. | ||
```clojure | ||
(lsp4clj.server/stdio-server {:in System/in, :out System/out}) | ||
``` | ||
|
||
## Known LSPs users | ||
The returned server will have a core.async `:log-ch`, from which you can read server logs (vectors beginning with a log level). | ||
|
||
```clojure | ||
(async/go-loop [] | ||
(when-let [[level & args] (async/<! (:log-ch server))] | ||
(apply logger/warn level args) | ||
(recur))) | ||
``` | ||
|
||
### Receive messages | ||
|
||
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. | ||
|
||
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.) | ||
|
||
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. | ||
Comment on lines
+32
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing I think we lost using multimethods is that we can't require that clients implement specific methods like we had on protocol, the LSP spec has some methods that all server should provide, they are required methods, like initialize, diagnostics, definition etc. I'm ok for now with this, we can think on how to make required some methods in the future There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To be pedantic, you're not required to implement every method on a protocol, the docs just say that you should. If you don't, you don't get any compile time errors, though you do get a runtime error if you try to call the method you didn't define. But I understand what you're saying—a protocol is a stronger indication of which methods you're expected to implement. This is just one of the drawbacks to the multimethods. They also don't give you anywhere to hang extra data about handlers. I can imagine wanting to flag certain messages as cancellable, give them validators to apply to their input or output, or more generally give them middleware style wrappers, etc. I think the best way to think about this is that one of the typical responsibilities of a server is to route requests. The multimethods work for that, but reitit, Compojure, et al. address the fact that there's more you might want to do with routing. On the other hand, I haven't run into anything on the clojure-lsp side yet where I really regret the simplicity of the multimethods. So, I guess my feeling is that multimethods might not be the best design long-term, but I'm not sure I have enough information to commit to another design either. I've vaguely considered expressing the "router" as a hashmap, keyed by method name, which you pass in to the server when you create it. The server could validate that certain keys were present. That would also give us lots of flexibility about what the values of the hashmap are. They could be functions to call as the handler (much like a multimethod, which maps dispatch values to functions). Or they could be other hashmaps, with a handler function in each. We could add other data over time. If you feel strongly that this is a better solution, I can work on it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
yeah, the issue is that most clients expect that some methods are always available like diagnostics, but I agree it's best to be a little bit more loose than strict for now.
I think it would work well, but I'm ok trying the multimethods for now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm also split on this. On the one hand, multimethods bring great simplicity, but our tooling for them is a lot worse. Previously I could find the implementation of the specific message handler with a "find reference", that's gone with multimethods. Additionally, there's no indication of all possible methods I could implement. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Cyrik we improved a lot defmethods support on clojure-lsp, doesn't find implementations work for that case? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The thing I'm missing from defmethods is a way of seeing references to a specific method. I know that this isn't possible in the general case, which is the whole point of defmethods, but often enough there are hardcoded calls to a specific method that could be shown as a direct reference. Something like But in I also fully agree with @mainej that we should leave this as is for now since these are very minor gripes with the system. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Cyrik let me see if I follow... Suppose we have this multimethod: (defmulti caps identity)
(defmethod caps "a" [_] "A")
(defmethod caps "b" [_] "B") At call sites like the following, you know for certain that (caps "a") In theory Of course this isn't possible when it's ambiguous which defmethod is being called: (let [lower (rand-nth ["a" "b"])]
(caps lower)) In these situations Assuming I'm following, I agree, that makes it harder to understand how multimethods, or really any kind of polymorphism, is being used. Polymorphism limits tooling, and that's true of all editors and languages. Maybe the next generation of tooling will offer more insight into polymorphic calls! For these particular I think there's a stronger argument that the other main entrypoints into There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @mainej Yeah that's the exact case I'm talking about. In my experience, most multimethods are built in this way (using the first param for dispatch). But all of this is very much unrelated so we should probably stop the topic 😄 To your second point: you're correct that this happens more often, I just didn't notice the change yet. Previously it was easy to find references on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we could open a new discussion on lsp4clj to see if we could provide something that help you like a different project using lsp4clj under the hood, I think discussing the problem a little bit more would make us understand better the problem There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
||
```clojure | ||
;; a notification; return value is ignored | ||
(defmethod lsp4clj.server/receive-notification "textDocument/didOpen" | ||
[_ context {:keys [text-document]}] | ||
(handler/did-open context (:uri text-document) (:text text-document)) | ||
|
||
;; a request; return value is converted to a response | ||
(defmethod lsp4clj.server/receive-request "textDocument/definition" | ||
[_ context params] | ||
(->> params | ||
(handler/definition context) | ||
(conform-or-log ::coercer/location))) | ||
``` | ||
|
||
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.) | ||
|
||
### Send messages | ||
|
||
Servers also initiate their own requests and notifications to a client. To send a notification, call `lsp4clj.server/send-notification`. | ||
|
||
```clojure | ||
(->> {:message message | ||
:type type | ||
:extra extra} | ||
(conform-or-log ::coercer/show-message) | ||
mainej marked this conversation as resolved.
Show resolved
Hide resolved
|
||
(lsp4clj.server/send-notification server "window/showMessage")) | ||
``` | ||
|
||
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. | ||
|
||
```clojure | ||
(let [request (->> {:edit edit} | ||
(conform-or-log ::coercer/workspace-edit-params) | ||
(lsp4clj.server/send-request server "workspace/applyEdit")) | ||
response (lsp4clj.server/deref-or-cancel request 10e3 ::timeout)] | ||
(if (= ::timeout response) | ||
(logger/error "No reponse from client after 10 seconds.") | ||
response)) | ||
``` | ||
|
||
Otherwise, the request object presents the same interface as `future`. Responds to `future-cancel` (which sends `$/cancelRequest`), `realized?`, `future?`, `future-done?` and `future-cancelled?`. | ||
|
||
If the request is cancelled, future invocations of `deref` will return `:lsp4clj.server/cancelled`. | ||
|
||
Sends `$/cancelRequest` only once, though `lsp4clj.server/deref-or-cancel` or `future-cancel` can be called multiple times. | ||
|
||
### Start and stop a server | ||
|
||
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". | ||
|
||
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/...`. | ||
mainej marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
```clojure | ||
(lsp4clj.server/start server {:server server, :logger logger}) | ||
mainej marked this conversation as resolved.
Show resolved
Hide resolved
|
||
``` | ||
|
||
The return of `start` is a promise that will resolve to `:done` when the server shuts down, which can happen in a few ways. | ||
|
||
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. | ||
|
||
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. | ||
|
||
## Development details | ||
|
||
### Tracing | ||
|
||
As you are implementing, you may want to trace incoming and outgoing messages. Initialize the server with `:trace? true` and then read traces (strings) off its `:trace-ch`. | ||
|
||
```clojure | ||
(let [server (lsp4clj.server/stdio-server {:trace? true | ||
:in System/in | ||
:out System/out})] | ||
(async/go-loop [] | ||
(when-let [trace (async/<! (:trace-ch server))] | ||
(logger/debug trace) | ||
(recur))) | ||
(lsp4clj.server/start server context)) | ||
``` | ||
|
||
### Testing | ||
|
||
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. | ||
|
||
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. | ||
|
||
## Caveats | ||
|
||
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. | ||
|
||
## Known lsp4clj users | ||
|
||
- [clojure-lsp](https://clojure-lsp.io/): A Clojure LSP server implementation. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,13 @@ | ||
{:paths [] | ||
:aliases {:dev {:extra-paths ["protocols/src" | ||
"server/src"] | ||
:extra-deps {lsp4clj/server {:local/root "server"} | ||
lsp4clj/protocols {:local/root "protocols"} | ||
io.github.clojure/tools.build {:git/url "https://github.com/clojure/tools.build.git" | ||
:tag "v0.8.1" | ||
:sha "7d40500"}}} | ||
:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.64.1010"}} | ||
:extra-paths ["server/test"] | ||
:main-opts ["-m" "kaocha.runner"]}}} | ||
{:deps {org.clojure/clojure {:mvn/version "1.11.1"} | ||
org.clojure/core.async {:mvn/version "1.5.648"} | ||
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.3"} | ||
cheshire/cheshire {:mvn/version "5.11.0"} } | ||
:paths ["src" "resources"] | ||
:aliases {:test {:extra-deps {lambdaisland/kaocha {:mvn/version "1.64.1010"}} | ||
:extra-paths ["test"] | ||
:main-opts ["-m" "kaocha.runner"]} | ||
:build {:extra-paths ["resources"] | ||
:deps {io.github.clojure/tools.build {:git/tag "v0.8.1" | ||
:git/sha "7d40500"} | ||
slipset/deps-deploy {:mvn/version "0.2.0"}} | ||
:ns-default build}}} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
# Generated by org.clojure/tools.build | ||
# Thu Jul 14 13:48:53 PDT 2022 | ||
version=1.0.0 | ||
groupId=com.github.clojure-lsp | ||
artifactId=lsp4clj |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.