Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 4b40384

Browse files
committedAug 8, 2024
Support custom handler fns
- allows multiple server instances that don't compete on implementing the multi-methods - allows ring-style middleware
1 parent 3e108c7 commit 4b40384

File tree

2 files changed

+93
-10
lines changed

2 files changed

+93
-10
lines changed
 

‎src/lsp4clj/server.clj

+58-10
Original file line numberDiff line numberDiff line change
@@ -174,13 +174,13 @@
174174
(-cancelled? [_]
175175
@cancelled?))
176176

177-
(defn pending-received-request [method context params]
177+
(defn ^:private pending-received-request [handler method context params]
178178
(let [cancelled? (atom false)
179179
;; coerce result/error to promise
180180
result-promise (p/promise
181-
(receive-request method
182-
(assoc context ::req-cancelled? cancelled?)
183-
params))]
181+
(handler method
182+
(assoc context ::req-cancelled? cancelled?)
183+
params))]
184184
(map->PendingReceivedRequest
185185
{:result-promise result-promise
186186
:cancelled? cancelled?})))
@@ -192,6 +192,8 @@
192192
;; * send-notification should do nothing until initialize response is sent, with the exception of window/showMessage, window/logMessage, telemetry/event, and $/progress
193193
(defrecord ChanServer [input-ch
194194
output-ch
195+
request-handler
196+
notification-handler
195197
log-ch
196198
trace-ch
197199
tracer*
@@ -362,7 +364,7 @@
362364
resp (lsp.responses/response id)]
363365
(try
364366
(trace this trace/received-request req started)
365-
(let [pending-req (pending-received-request method context params)]
367+
(let [pending-req (pending-received-request request-handler method context params)]
366368
(swap! pending-received-requests* assoc id pending-req)
367369
(-> pending-req
368370
:result-promise
@@ -375,7 +377,7 @@
375377
(lsp.responses/error resp (lsp.errors/not-found method)))
376378
(lsp.responses/infer resp result))))
377379
;; Handle
378-
;; 1. Exceptions thrown within p/future created by receive-request.
380+
;; 1. Exceptions thrown within promise returned by request-handler.
379381
;; 2. Cancelled requests.
380382
(p/catch
381383
(fn [e]
@@ -389,7 +391,7 @@
389391
(swap! pending-received-requests* dissoc id)
390392
(trace this trace/sending-response req resp started (.instant clock))
391393
(async/>!! output-ch resp)))))
392-
(catch Throwable e ;; exceptions thrown by receive-request
394+
(catch Throwable e ;; exceptions thrown by request-handler
393395
(log-error-receiving this e req)
394396
(async/>!! output-ch (internal-error-response resp req))))))
395397
(receive-notification [this context {:keys [method params] :as notif}]
@@ -400,7 +402,7 @@
400402
(if-let [pending-req (get @pending-received-requests* (:id params))]
401403
(p/cancel! pending-req)
402404
(trace this trace/received-unmatched-cancellation-notification notif now))
403-
(let [result (receive-notification method context params)]
405+
(let [result (notification-handler method context params)]
404406
(when (identical? ::method-not-found result)
405407
(protocols.endpoint/log this :warn "received unexpected notification" method)))))
406408
(catch Throwable e
@@ -410,8 +412,52 @@
410412
(update server :tracer* reset! (trace/tracer-for-level trace-level)))
411413

412414
(defn chan-server
413-
[{:keys [output-ch input-ch log-ch trace? trace-level trace-ch clock on-close]
414-
:or {clock (java.time.Clock/systemDefaultZone)
415+
"Creates a channel-based Language Server.
416+
417+
The returned server will be in unstarted state. Pass it to `start` to
418+
start it.
419+
420+
Required options:
421+
422+
- `output-ch` is a core.async channel that the server puts messages to the
423+
client onto.
424+
- `input-ch` is a core.async channel that the server takes messages from the
425+
client from.
426+
427+
Handler functions:
428+
429+
- `request-handler` is a 3-arg fn `[message context params] => response`
430+
to handle incoming client requests. The response can be a response map
431+
or a promise resolving to a response map. Defaults to the `receive-request`
432+
multi-method.
433+
- `notification-handler` is a 3-arg fn `[message context params]` to handle
434+
incoming client notifications. Its return value is ignored. Defaults to
435+
the `receive-notification` multi-method.
436+
437+
Options for logging and tracing:
438+
439+
- `log-ch` is an optional core.async channel that the server will put log
440+
messages onto. If none is specified, a default one will be created.
441+
- `trace-ch` is an optional core.async channel that the server will put
442+
trace events onto.
443+
- `trace-level` is a string that determines the verbosity of trace messages,
444+
can be \"verbose\", \"messages\", or \"off\".
445+
- `trace?` is a short-hand for `:trace-level \"verbose\"` and the default
446+
when a `trace-ch` is specified.
447+
448+
Other options:
449+
450+
- `clock` is a `java.time.Clock` that provides the current time for trace
451+
messages.
452+
- `on-close` is a 0-arg fn that the server will call after it has shut down."
453+
[{:keys [output-ch input-ch
454+
request-handler notification-handler
455+
log-ch
456+
trace? trace-level trace-ch
457+
clock on-close]
458+
:or {request-handler #'receive-request
459+
notification-handler #'receive-notification
460+
clock (java.time.Clock/systemDefaultZone)
415461
on-close (constantly nil)}}]
416462
(let [;; before defaulting trace-ch, so that default is "off"
417463
tracer (trace/tracer-for-level (or trace-level
@@ -422,6 +468,8 @@
422468
(map->ChanServer
423469
{:output-ch output-ch
424470
:input-ch input-ch
471+
:request-handler request-handler
472+
:notification-handler notification-handler
425473
:log-ch log-ch
426474
:trace-ch trace-ch
427475
:tracer* (atom tracer)

‎test/lsp4clj/server_test.clj

+35
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,41 @@
99
[lsp4clj.test-helper :as h]
1010
[promesa.core :as p]))
1111

12+
(deftest should-pass-requests-to-handler
13+
(let [input-ch (async/chan 3)
14+
output-ch (async/chan 3)
15+
requests* (atom [])
16+
server (server/chan-server {:output-ch output-ch
17+
:input-ch input-ch
18+
:request-handler (fn [& args]
19+
(swap! requests* conj args)
20+
::server/method-not-found)})]
21+
(server/start server {:context :some-value})
22+
(async/put! input-ch (lsp.requests/request 1 "foo" {:param 42}))
23+
(h/assert-take output-ch)
24+
(is (= 1 (count @requests*)))
25+
(let [args (first @requests*)]
26+
(is (= "foo" (first args)))
27+
(is (= :some-value (:context (second args))))
28+
(is (= 42 (:param (nth args 2)))))
29+
(server/shutdown server)))
30+
31+
(deftest should-pass-notifications-to-handler
32+
(let [input-ch (async/chan 3)
33+
output-ch (async/chan 3)
34+
notification (promise)
35+
server (server/chan-server {:output-ch output-ch
36+
:input-ch input-ch
37+
:notification-handler (fn [& args]
38+
(deliver notification args))})]
39+
(server/start server {:context :some-value})
40+
(async/put! input-ch (lsp.requests/notification "foo" {:param 42}))
41+
(let [args (deref notification 100 nil)]
42+
(is (= "foo" (first args)))
43+
(is (= :some-value (:context (second args))))
44+
(is (= 42 (:param (nth args 2)))))
45+
(server/shutdown server)))
46+
1247
(deftest should-process-messages-received-before-start
1348
(let [input-ch (async/chan 3)
1449
output-ch (async/chan 3)

0 commit comments

Comments
 (0)
Please sign in to comment.