Skip to content

Commit 749a724

Browse files
Support custom handler fns
- allows multiple server instances that don't compete on implementing the multi-methods - allows ring-style middleware
1 parent 899d588 commit 749a724

File tree

2 files changed

+93
-10
lines changed

2 files changed

+93
-10
lines changed

src/lsp4clj/server.clj

Lines changed: 58 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -175,13 +175,13 @@
175175
(-cancelled? [_]
176176
@cancelled?))
177177

178-
(defn pending-received-request [method context params]
178+
(defn ^:private pending-received-request [handler method context params]
179179
(let [cancelled? (atom false)
180180
;; coerce result/error to promise
181181
result-promise (p/promise
182-
(receive-request method
183-
(assoc context ::req-cancelled? cancelled?)
184-
params))]
182+
(handler method
183+
(assoc context ::req-cancelled? cancelled?)
184+
params))]
185185
(map->PendingReceivedRequest
186186
{:result-promise result-promise
187187
:cancelled? cancelled?})))
@@ -193,6 +193,8 @@
193193
;; * send-notification should do nothing until initialize response is sent, with the exception of window/showMessage, window/logMessage, telemetry/event, and $/progress
194194
(defrecord ChanServer [input-ch
195195
output-ch
196+
request-handler
197+
notification-handler
196198
log-ch
197199
trace-ch
198200
tracer*
@@ -374,7 +376,7 @@
374376
resp (lsp.responses/response id)]
375377
(try
376378
(trace this trace/received-request req started)
377-
(let [pending-req (pending-received-request method context params)]
379+
(let [pending-req (pending-received-request request-handler method context params)]
378380
(swap! pending-received-requests* assoc id pending-req)
379381
(-> pending-req
380382
:result-promise
@@ -387,7 +389,7 @@
387389
(lsp.responses/error resp (lsp.errors/not-found method)))
388390
(lsp.responses/infer resp result))))
389391
;; Handle
390-
;; 1. Exceptions thrown within p/future created by receive-request.
392+
;; 1. Exceptions thrown within promise returned by request-handler.
391393
;; 2. Cancelled requests.
392394
(p/catch
393395
(fn [e]
@@ -401,7 +403,7 @@
401403
(swap! pending-received-requests* dissoc id)
402404
(trace this trace/sending-response req resp started (.instant clock))
403405
(async/>!! output-ch resp)))))
404-
(catch Throwable e ;; exceptions thrown by receive-request
406+
(catch Throwable e ;; exceptions thrown by request-handler
405407
(log-error-receiving this e req)
406408
(async/>!! output-ch (internal-error-response resp req))))))
407409
(receive-notification [this context {:keys [method params] :as notif}]
@@ -412,7 +414,7 @@
412414
(if-let [pending-req (get @pending-received-requests* (:id params))]
413415
(p/cancel! pending-req)
414416
(trace this trace/received-unmatched-cancellation-notification notif now))
415-
(let [result (receive-notification method context params)]
417+
(let [result (notification-handler method context params)]
416418
(when (identical? ::method-not-found result)
417419
(protocols.endpoint/log this :warn "received unexpected notification" method)))))
418420
(catch Throwable e
@@ -422,8 +424,52 @@
422424
(update server :tracer* reset! (trace/tracer-for-level trace-level)))
423425

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

test/lsp4clj/server_test.clj

Lines changed: 35 additions & 0 deletions
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)