|
7 | 7 | [lsp4clj.lsp.requests :as lsp.requests] |
8 | 8 | [lsp4clj.lsp.responses :as lsp.responses] |
9 | 9 | [lsp4clj.protocols.endpoint :as protocols.endpoint] |
10 | | - [lsp4clj.trace :as trace])) |
| 10 | + [lsp4clj.trace :as trace] |
| 11 | + [promesa.core :as p]) |
| 12 | + (:import (java.util.concurrent CancellationException))) |
11 | 13 |
|
12 | 14 | (set! *warn-on-reflection* true) |
13 | 15 |
|
|
102 | 104 | (format "%s: %s (%s)" description message code))) |
103 | 105 |
|
104 | 106 | (defn ^:private log-error-receiving [server e message] |
105 | | - (protocols.endpoint/log server :error e |
106 | | - (str (format-error-code "Error receiving message" :internal-error) "\n" |
107 | | - (select-keys message [:id :method])))) |
| 107 | + (let [message-details (select-keys message [:id :method]) |
| 108 | + log-title (format-error-code "Error receiving message" :internal-error)] |
| 109 | + (protocols.endpoint/log server :error e (str log-title "\n" message-details)))) |
108 | 110 |
|
109 | | -(defn ^:private start-pipeline [input-ch output-ch server context] |
110 | | - ;; Fork the input off to two streams of work, the input initiated by the |
111 | | - ;; client (the client's requests and notifications) and the input initiated by |
112 | | - ;; the server (the client's responses). Process each stream one message at a |
113 | | - ;; time, but independently. The streams must be processed indepedently so that |
114 | | - ;; while receiving a request, the server can send a request and receive the |
115 | | - ;; response before sending its response to the original request. This happens, |
116 | | - ;; for example, when servers send showMessageRequest while processing a |
117 | | - ;; request they have received. |
118 | | - (let [server-initiated-ch (async/chan 1) |
119 | | - client-initiated-ch (async/chan 1) |
120 | | - pipeline |
121 | | - (async/go-loop [] |
122 | | - (if-let [message (async/<! input-ch)] |
123 | | - (let [message-type (coercer/input-message-type message)] |
124 | | - (case message-type |
125 | | - (:parse-error :invalid-request) |
126 | | - (protocols.endpoint/log server :error (format-error-code "Error reading message" message-type)) |
127 | | - (:request :notification) |
128 | | - (async/>! client-initiated-ch [message-type message]) |
129 | | - (:response.result :response.error) |
130 | | - (async/>! server-initiated-ch message)) |
131 | | - (recur)) |
132 | | - (do |
133 | | - (async/close! client-initiated-ch) |
134 | | - (async/close! server-initiated-ch) |
135 | | - (async/close! output-ch))))] |
136 | | - ;; a thread so server can use >!! and so that we can use (>!! output-ch) to |
137 | | - ;; respect back pressure from clients that are slow to read. |
138 | | - (async/thread |
| 111 | +(defn ^:private receive-message |
| 112 | + [server context message] |
| 113 | + (let [message-type (coercer/input-message-type message)] |
| 114 | + (try |
139 | 115 | (discarding-stdout |
140 | | - (loop [] |
141 | | - (when-let [[message-type message] (async/<!! client-initiated-ch)] |
142 | | - ;; TODO: return error until initialize response is sent? |
143 | | - ;; https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize |
144 | | - (case message-type |
145 | | - :request |
146 | | - (async/>!! output-ch |
147 | | - (try |
148 | | - (protocols.endpoint/receive-request server context message) |
149 | | - (catch Throwable e |
150 | | - (log-error-receiving server e message) |
151 | | - (-> (lsp.responses/response (:id message)) |
152 | | - (lsp.responses/error (lsp.errors/internal-error (select-keys message [:id :method]))))))) |
153 | | - :notification |
154 | | - (try |
155 | | - (protocols.endpoint/receive-notification server context message) |
156 | | - (catch Throwable e |
157 | | - (log-error-receiving server e message)))) |
158 | | - (recur))))) |
159 | | - (async/go-loop [] |
160 | | - (when-let [message (async/<! server-initiated-ch)] |
161 | | - (protocols.endpoint/receive-response server message) |
162 | | - (recur))) |
163 | | - pipeline)) |
| 116 | + (case message-type |
| 117 | + (:parse-error :invalid-request) |
| 118 | + (protocols.endpoint/log server :error (format-error-code "Error reading message" message-type)) |
| 119 | + :request |
| 120 | + (protocols.endpoint/receive-request server context message) |
| 121 | + (:response.result :response.error) |
| 122 | + (protocols.endpoint/receive-response server message) |
| 123 | + :notification |
| 124 | + (protocols.endpoint/receive-notification server context message))) |
| 125 | + (catch Throwable e ;; exceptions thrown by receive-response or receive-notification (receive-request catches its own exceptions) |
| 126 | + (log-error-receiving server e message))))) |
164 | 127 |
|
165 | 128 | ;; Expose endpoint methods to language servers |
166 | 129 |
|
|
178 | 141 |
|
179 | 142 | (defmethod receive-request :default [_method _context _params] ::method-not-found) |
180 | 143 | (defmethod receive-notification :default [_method _context _params] ::method-not-found) |
181 | | -;; Servers can't implement cancellation of inbound requests themselves, because |
182 | | -;; lsp4clj manages request ids. Until lsp4clj adds support, ignore cancellation |
183 | | -;; requests. |
184 | | -(defmethod receive-notification "$/cancelRequest" [_ _ _]) |
185 | 144 |
|
| 145 | +(defn ^:private internal-error-response [resp req] |
| 146 | + (let [error-body (lsp.errors/internal-error (select-keys req [:id :method]))] |
| 147 | + (lsp.responses/error resp error-body))) |
| 148 | + |
| 149 | +(defn ^:private cancellation-response [resp req] |
| 150 | + (let [message-details (select-keys req [:id :method]) |
| 151 | + error-body (lsp.errors/body :request-cancelled |
| 152 | + (format "The request %s has been cancelled." |
| 153 | + (pr-str message-details)) |
| 154 | + message-details)] |
| 155 | + (lsp.responses/error resp error-body))) |
| 156 | + |
| 157 | +;; TODO: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize |
| 158 | +;; * receive-request should return error until initialize request is received |
| 159 | +;; * receive-notification should drop until initialize request is received, with the exception of exit |
| 160 | +;; * send-request should do nothing until initialize response is sent, with the exception of window/showMessageRequest |
| 161 | +;; * send-notification should do nothing until initialize response is sent, with the exception of window/showMessage, window/logMessage, telemetry/event, and $/progress |
186 | 162 | (defrecord ChanServer [input-ch |
187 | 163 | output-ch |
188 | 164 | trace-ch |
189 | 165 | log-ch |
190 | 166 | ^java.time.Clock clock |
191 | 167 | on-close |
192 | 168 | request-id* |
193 | | - pending-requests* |
| 169 | + pending-sent-requests* |
| 170 | + pending-received-requests* |
194 | 171 | join] |
195 | 172 | protocols.endpoint/IEndpoint |
196 | 173 | (start [this context] |
197 | | - (let [pipeline (start-pipeline input-ch output-ch this context)] |
| 174 | + (let [;; a thread so language server can use >!! and so that receive-message |
| 175 | + ;; can use (>!! output-ch) to respect back pressure from clients that |
| 176 | + ;; are slow to read. |
| 177 | + pipeline (async/thread |
| 178 | + (loop [] |
| 179 | + (if-let [message (async/<!! input-ch)] |
| 180 | + (do |
| 181 | + (receive-message this context message) |
| 182 | + (recur)) |
| 183 | + (async/close! output-ch))))] |
198 | 184 | (async/go |
199 | 185 | ;; Wait for pipeline to close. This indicates input-ch was closed and |
200 | 186 | ;; that now output-ch is closed. |
|
224 | 210 | (some-> trace-ch (async/put! (trace/sending-request req now))) |
225 | 211 | ;; Important: record request before sending it, so it is sure to be |
226 | 212 | ;; available during receive-response. |
227 | | - (swap! pending-requests* assoc id pending-request) |
| 213 | + (swap! pending-sent-requests* assoc id pending-request) |
228 | 214 | ;; respect back pressure from clients that are slow to read; (go (>!)) will not suffice |
229 | 215 | (async/>!! output-ch req) |
230 | 216 | pending-request)) |
|
236 | 222 | (async/>!! output-ch notif))) |
237 | 223 | (receive-response [_this {:keys [id error result] :as resp}] |
238 | 224 | (let [now (.instant clock) |
239 | | - [pending-requests _] (swap-vals! pending-requests* dissoc id)] |
| 225 | + [pending-requests _] (swap-vals! pending-sent-requests* dissoc id)] |
240 | 226 | (if-let [{:keys [p started] :as req} (get pending-requests id)] |
241 | 227 | (do |
242 | 228 | (some-> trace-ch (async/put! (trace/received-response req resp started now))) |
243 | 229 | (deliver p (if error resp result))) |
244 | 230 | (some-> trace-ch (async/put! (trace/received-unmatched-response resp now)))))) |
245 | 231 | (receive-request [this context {:keys [id method params] :as req}] |
246 | | - (let [started (.instant clock)] |
247 | | - (some-> trace-ch (async/put! (trace/received-request req started))) |
248 | | - (let [result (receive-request method context params) |
249 | | - resp (lsp.responses/response id) |
250 | | - resp (if (identical? ::method-not-found result) |
| 232 | + (let [started (.instant clock) |
| 233 | + resp (lsp.responses/response id)] |
| 234 | + (try |
| 235 | + (some-> trace-ch (async/put! (trace/received-request req started))) |
| 236 | + ;; coerce result/error to promise |
| 237 | + (let [result-promise (p/promise (receive-request method context params))] |
| 238 | + (swap! pending-received-requests* assoc id result-promise) |
| 239 | + (-> result-promise |
| 240 | + ;; convert result/error to response |
| 241 | + (p/then |
| 242 | + (fn [result] |
| 243 | + (if (identical? ::method-not-found result) |
| 244 | + (do |
| 245 | + (protocols.endpoint/log this :warn "received unexpected request" method) |
| 246 | + (lsp.responses/error resp (lsp.errors/not-found method))) |
| 247 | + (lsp.responses/infer resp result)))) |
| 248 | + ;; Handle |
| 249 | + ;; 1. Exceptions thrown within p/future created by receive-request. |
| 250 | + ;; 2. Cancelled requests. |
| 251 | + (p/catch |
| 252 | + (fn [e] |
| 253 | + (if (instance? CancellationException e) |
| 254 | + (cancellation-response resp req) |
251 | 255 | (do |
252 | | - (protocols.endpoint/log this :warn "received unexpected request" method) |
253 | | - (lsp.responses/error resp (lsp.errors/not-found method))) |
254 | | - (lsp.responses/infer resp result)) |
255 | | - finished (.instant clock)] |
256 | | - (some-> trace-ch (async/put! (trace/sending-response req resp started finished))) |
257 | | - resp))) |
| 256 | + (log-error-receiving this e req) |
| 257 | + (internal-error-response resp req))))) |
| 258 | + (p/finally |
| 259 | + (fn [resp _error] |
| 260 | + (swap! pending-received-requests* dissoc id) |
| 261 | + (some-> trace-ch (async/put! (trace/sending-response req resp started (.instant clock)))) |
| 262 | + (async/>!! output-ch resp))))) |
| 263 | + (catch Throwable e ;; exceptions thrown by receive-request |
| 264 | + (log-error-receiving this e req) |
| 265 | + (async/>!! output-ch (internal-error-response resp req)))))) |
258 | 266 | (receive-notification [this context {:keys [method params] :as notif}] |
259 | | - (some-> trace-ch (async/put! (trace/received-notification notif (.instant clock)))) |
260 | | - (let [result (receive-notification method context params)] |
261 | | - (when (identical? ::method-not-found result) |
262 | | - (protocols.endpoint/log this :warn "received unexpected notification" method))))) |
| 267 | + (let [now (.instant clock)] |
| 268 | + (if (= method "$/cancelRequest") |
| 269 | + (if-let [result-promise (get @pending-received-requests* (:id params))] |
| 270 | + (p/cancel! result-promise) |
| 271 | + (some-> trace-ch (async/put! (trace/received-unmatched-cancellation-notification notif now)))) |
| 272 | + (do |
| 273 | + (some-> trace-ch (async/put! (trace/received-notification notif now))) |
| 274 | + (let [result (receive-notification method context params)] |
| 275 | + (when (identical? ::method-not-found result) |
| 276 | + (protocols.endpoint/log this :warn "received unexpected notification" method)))))))) |
263 | 277 |
|
264 | 278 | (defn chan-server |
265 | 279 | [{:keys [output-ch input-ch log-ch trace? trace-ch clock on-close] |
|
273 | 287 | :clock clock |
274 | 288 | :on-close on-close |
275 | 289 | :request-id* (atom 0) |
276 | | - :pending-requests* (atom {}) |
| 290 | + :pending-sent-requests* (atom {}) |
| 291 | + :pending-received-requests* (atom {}) |
277 | 292 | :join (promise)})) |
0 commit comments