monaco-lsp-bridge 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,129 @@
1
+ import {
2
+ DidChangeTextDocumentNotification,
3
+ DidChangeTextDocumentParams,
4
+ DidCloseTextDocumentParams,
5
+ DidCloseTextDocumentNotification,
6
+ DidOpenTextDocumentNotification,
7
+ DidOpenTextDocumentParams,
8
+ CompletionRequest,
9
+ CompletionParams,
10
+ InitializeRequest,
11
+ InitializeParams,
12
+ InitializeResult,
13
+ CompletionList,
14
+ CompletionItem,
15
+ CompletionResolveRequest,
16
+ ProgressToken,
17
+ PublishDiagnosticsNotification,
18
+ PublishDiagnosticsParams,
19
+ LogMessageNotification,
20
+ LogMessageParams,
21
+ DocumentFormattingRequest,
22
+ DocumentFormattingParams,
23
+ DocumentRangeFormattingRequest,
24
+ DocumentRangeFormattingParams,
25
+ DocumentOnTypeFormattingRequest,
26
+ DocumentOnTypeFormattingParams,
27
+ TextEdit,
28
+ HoverRequest,
29
+ HoverParams,
30
+ Hover,
31
+ } from 'vscode-languageserver-protocol'
32
+
33
+ import { LspErrorShape } from './error.js'
34
+
35
+ export type Id = number | string
36
+ export const isId = (id: unknown): id is Id => typeof id === 'number' || typeof id === 'string'
37
+
38
+ // Requests (bidirectional, have responses)
39
+ export interface RequestMap {
40
+ [InitializeRequest.method]: [InitializeParams, InitializeResult]
41
+ [CompletionResolveRequest.method]: [CompletionItem, CompletionItem]
42
+ [CompletionRequest.method]: [CompletionParams, CompletionList | CompletionItem[] | null]
43
+ [DocumentFormattingRequest.method]: [DocumentFormattingParams, TextEdit[] | null]
44
+ [DocumentRangeFormattingRequest.method]: [DocumentRangeFormattingParams, TextEdit[] | null]
45
+ [DocumentOnTypeFormattingRequest.method]: [DocumentOnTypeFormattingParams, TextEdit[] | null]
46
+ [HoverRequest.method]: [HoverParams, Hover | null]
47
+ shutdown: [null, null]
48
+ }
49
+
50
+ // Client-to-server notifications (no response expected)
51
+ export interface ClientNotifMap {
52
+ [DidOpenTextDocumentNotification.method]: [DidOpenTextDocumentParams, void]
53
+ [DidChangeTextDocumentNotification.method]: [DidChangeTextDocumentParams, void]
54
+ [DidCloseTextDocumentNotification.method]: [DidCloseTextDocumentParams, void]
55
+ '$/cancelRequest': [{ id: Id }, void]
56
+ exit: [null, void]
57
+ }
58
+
59
+ // Server-to-client notifications (no response expected)
60
+ export interface ServerNotifMap {
61
+ '$/progress': [{ token: ProgressToken; value: any }, void]
62
+ [PublishDiagnosticsNotification.method]: [PublishDiagnosticsParams, void]
63
+ [LogMessageNotification.method]: [LogMessageParams, void]
64
+ }
65
+
66
+ export const JSONRPC_VERSION = '2.0'
67
+ type JSONRPC = typeof JSONRPC_VERSION
68
+
69
+ export type JsonRpcRequest<K, T> = {
70
+ jsonrpc: JSONRPC
71
+ id: Id
72
+ params: T
73
+ method: K
74
+ }
75
+
76
+ export type JsonRpcResponse<T> =
77
+ | { jsonrpc: JSONRPC; id: Id; result: T; error?: undefined }
78
+ | { jsonrpc: JSONRPC; id: Id; result?: undefined; error: LspErrorShape }
79
+
80
+ export type JsonRpcNotification<K, T> = {
81
+ jsonrpc: JSONRPC
82
+ method: K
83
+ params: T
84
+ }
85
+
86
+ export const makeRequest = <K extends keyof RequestMap>(
87
+ id: Id,
88
+ method: K,
89
+ params: RequestMap[K][0],
90
+ ): JsonRpcRequest<K, RequestMap[K][0]> => ({
91
+ jsonrpc: JSONRPC_VERSION,
92
+ id,
93
+ method,
94
+ params,
95
+ })
96
+
97
+ export const makeNotification = <K extends keyof ClientNotifMap>(
98
+ method: K,
99
+ params: ClientNotifMap[K][0],
100
+ ): JsonRpcNotification<K, ClientNotifMap[K][0]> => ({
101
+ jsonrpc: JSONRPC_VERSION,
102
+ method,
103
+ params,
104
+ })
105
+
106
+ export type ProgressPayload<T = any> = {
107
+ token: ProgressToken
108
+ value: T
109
+ }
110
+
111
+ export type WorkDoneProgressBegin = {
112
+ kind: 'begin'
113
+ title: string
114
+ cancellable?: boolean
115
+ message?: string
116
+ percentage?: number
117
+ }
118
+
119
+ export type WorkDoneProgressReport = {
120
+ kind: 'report'
121
+ cancellable?: boolean
122
+ message?: string
123
+ percentage?: number
124
+ }
125
+
126
+ export type WorkDoneProgressEnd = {
127
+ kind: 'end'
128
+ message?: string
129
+ }
@@ -0,0 +1,337 @@
1
+ import type { ProgressToken } from 'vscode-languageserver-protocol'
2
+ import type * as monaco from 'monaco-editor'
3
+
4
+ import {
5
+ JSONRPC_VERSION,
6
+ makeRequest,
7
+ makeNotification,
8
+ JsonRpcRequest,
9
+ RequestMap,
10
+ ClientNotifMap,
11
+ ServerNotifMap,
12
+ JsonRpcResponse,
13
+ JsonRpcNotification,
14
+ ProgressPayload,
15
+ Id,
16
+ isId,
17
+ } from './protocol.js'
18
+ import { LspError, LspErrorCode, toLspError, isCancellationError } from './error.js'
19
+
20
+ export type Transport = PostMessagePort | LspTransport
21
+
22
+ export interface PostMessagePort {
23
+ postMessage(message: any): void
24
+ addEventListener(type: 'message', listener: (event: { data: any }) => void): void
25
+ removeEventListener(type: 'message', listener: (event: { data: any }) => void): void
26
+ }
27
+
28
+ type Disposable = () => void
29
+
30
+ export type Cancelled = {
31
+ cancelled: true
32
+ code: LspErrorCode.RequestCancelled | LspErrorCode.ServerCancelled
33
+ }
34
+
35
+ export type MaybeCancelled<T> = { cancelled: false; result: T } | Cancelled
36
+
37
+ export type ProgressHandler<T = any> = (value: T) => void
38
+ export type PartialResultHandler<T = any> = (partialResult: T) => void
39
+
40
+ export class LspTransport {
41
+ private nextId = 1
42
+ private nextTokenId = 1
43
+ private disposables: Set<Disposable> = new Set()
44
+
45
+ // Fire-and-forget requests handled by withHandlers()
46
+ private pending = new Map<Id, JsonRpcRequest<keyof RequestMap, any>>()
47
+
48
+ // Async bookkeeping
49
+ private timeouts = new Map<Id, ReturnType<typeof setTimeout>>()
50
+ private rejectors = new Map<Id, (err: LspError) => void>()
51
+
52
+ // Central dispatcher: subscribers + per-id waiters (for sendAsync)
53
+ private subs = new Set<(message: any) => void>()
54
+ private waiters = new Map<Id, (res: JsonRpcResponse<any>) => void>()
55
+
56
+ // Progress tracking (both work done progress and partial results use $/progress)
57
+ private progressCallbacks = new Map<ProgressToken, ProgressHandler>()
58
+
59
+ /** Construct a binding over a sender/receiver transport */
60
+ constructor(
61
+ private readonly sender: (
62
+ message: JsonRpcRequest<keyof RequestMap, any> | JsonRpcNotification<any, any> | Array<any>,
63
+ ) => void,
64
+ private readonly receiver: (cb: (message: any) => void) => Disposable,
65
+ private readonly defaultTimeoutMs: number = 15_000,
66
+ ) {
67
+ // One receiver to rule them all — handles single or batch frames.
68
+ const unbind = this.receiver((raw) => this.ingest(raw))
69
+ this.disposables.add(unbind)
70
+ }
71
+
72
+ // ---------------- Constructors ----------------
73
+ /** Create a binding from a Worker-like endpoint */
74
+ static fromWorker(worker: PostMessagePort): LspTransport {
75
+ const sender = (message: any) => worker.postMessage(message)
76
+ const receiver = (cb: (message: any) => void) => {
77
+ const handler = (e: { data: any }) => cb(e.data)
78
+ worker.addEventListener('message', handler)
79
+ return () => worker.removeEventListener('message', handler)
80
+ }
81
+ return new LspTransport(sender, receiver)
82
+ }
83
+
84
+ /** Infer a binding from either a worker or an existing binding */
85
+ static infer(endpoint: Transport): LspTransport {
86
+ return endpoint instanceof LspTransport ? endpoint : LspTransport.fromWorker(endpoint)
87
+ }
88
+
89
+ // -------- Helpers
90
+ /** Track a Monaco disposable for cleanup */
91
+ addDisposable(d: monaco.IDisposable) {
92
+ this.disposables.add(() => d.dispose())
93
+ return this
94
+ }
95
+
96
+ /** Listen for error responses and forward normalized errors */
97
+ onError(cb: (error: LspError) => void) {
98
+ const onMessage = (res: JsonRpcResponse<any>) => {
99
+ if (res?.jsonrpc !== JSONRPC_VERSION) return
100
+ if (res?.error) cb(toLspError(res.error))
101
+ }
102
+ this.disposables.add(this.subscribe(onMessage))
103
+ return this
104
+ }
105
+
106
+ /** Register a progress callback for a specific token */
107
+ onProgress(token: ProgressToken, callback: ProgressHandler): Disposable {
108
+ this.progressCallbacks.set(token, callback)
109
+ return () => this.progressCallbacks.delete(token)
110
+ }
111
+
112
+ /** Register a callback for all $/progress notifications */
113
+ onProgressNotification(callback: (params: ProgressPayload) => void) {
114
+ const onMessage = (msg: any) => {
115
+ if (msg?.jsonrpc !== JSONRPC_VERSION || msg?.method !== '$/progress') return
116
+ if (msg.params) callback(msg.params)
117
+ }
118
+ this.disposables.add(this.subscribe(onMessage))
119
+ return this
120
+ }
121
+
122
+ /** Listen for a specific server notification method */
123
+ onServerNotification<K extends keyof ServerNotifMap>(method: K, cb: (params: ServerNotifMap[K][0]) => void) {
124
+ const onMessage = (msg: any) => {
125
+ if (msg?.jsonrpc !== JSONRPC_VERSION || msg?.method !== method) return
126
+ cb(msg.params as ServerNotifMap[K][0])
127
+ }
128
+ this.disposables.add(this.subscribe(onMessage))
129
+ return this
130
+ }
131
+
132
+ /** Dispose all resources and reject in-flight requests */
133
+ dispose() {
134
+ for (const rej of this.rejectors.values()) {
135
+ try {
136
+ rej(new LspError({ code: LspErrorCode.Disposed, message: 'Binding disposed before response' }))
137
+ } catch {}
138
+ }
139
+ this.rejectors.clear()
140
+
141
+ for (const t of this.timeouts.values()) clearTimeout(t)
142
+ this.timeouts.clear()
143
+
144
+ this.disposables.forEach((d) => d())
145
+ this.disposables.clear()
146
+ this.pending.clear()
147
+ this.waiters.clear()
148
+ this.subs.clear()
149
+ this.progressCallbacks.clear()
150
+ }
151
+
152
+ // -------- Core API
153
+
154
+ /** Send a client-to-server notification (no response expected) */
155
+ sendNotification<K extends keyof ClientNotifMap>(method: K, params: ClientNotifMap[K][0]) {
156
+ this.sender(makeNotification(method, params))
157
+ return this
158
+ }
159
+
160
+ /** Send an async request with timeout, cancellation and progress support */
161
+ sendRequest<K extends keyof RequestMap>(
162
+ method: K,
163
+ params: RequestMap[K][0],
164
+ opts?: {
165
+ timeoutMs?: number
166
+ signal?: AbortSignal
167
+ workDoneToken?: ProgressToken
168
+ partialResultToken?: ProgressToken
169
+ onProgress?: ProgressHandler
170
+ onPartialResult?: PartialResultHandler
171
+ },
172
+ ): Promise<MaybeCancelled<RequestMap[K][1]>> {
173
+ const id = this.nextId++
174
+
175
+ // Generate tokens if callbacks provided but tokens not specified
176
+ const workDoneToken = opts?.workDoneToken ?? (opts?.onProgress ? `work-${this.nextTokenId++}` : undefined)
177
+ const partialResultToken =
178
+ opts?.partialResultToken ?? (opts?.onPartialResult ? `partial-${this.nextTokenId++}` : undefined)
179
+
180
+ // Augment params with tokens if provided
181
+ const augmentedParams: any = { ...params }
182
+ if (workDoneToken !== undefined) {
183
+ augmentedParams.workDoneToken = workDoneToken
184
+ }
185
+ if (partialResultToken !== undefined) {
186
+ augmentedParams.partialResultToken = partialResultToken
187
+ }
188
+
189
+ const m = makeRequest(id, method, augmentedParams)
190
+
191
+ if (opts?.signal?.aborted) {
192
+ return Promise.resolve({ cancelled: true as const, code: LspErrorCode.RequestCancelled })
193
+ }
194
+
195
+ // Register progress callbacks if provided
196
+ // Both work done progress and partial results use $/progress notifications
197
+ if (workDoneToken !== undefined && opts?.onProgress) {
198
+ this.progressCallbacks.set(workDoneToken, opts.onProgress)
199
+ }
200
+ if (partialResultToken !== undefined && opts?.onPartialResult) {
201
+ this.progressCallbacks.set(partialResultToken, opts.onPartialResult)
202
+ }
203
+
204
+ this.sender(m)
205
+
206
+ return new Promise<MaybeCancelled<RequestMap[K][1]>>((resolve, reject) => {
207
+ const resolveCancelled = (code: LspErrorCode.RequestCancelled | LspErrorCode.ServerCancelled) => {
208
+ cleanup()
209
+ resolve({ cancelled: true, code })
210
+ }
211
+ const rejectAs = (err: LspError) => {
212
+ cleanup()
213
+ reject(err)
214
+ }
215
+
216
+ const onResponse = (res: JsonRpcResponse<any>) => {
217
+ if (res?.id !== id) return
218
+ if (res.error) {
219
+ const err = toLspError(res.error)
220
+ if (isCancellationError(err)) return resolveCancelled(err.code as any)
221
+ return rejectAs(err)
222
+ }
223
+ cleanup()
224
+ resolve({ cancelled: false, result: res.result })
225
+ }
226
+
227
+ // route by id via central dispatcher
228
+ this.waiters.set(id, onResponse)
229
+
230
+ const cleanup = () => {
231
+ this.waiters.delete(id)
232
+ const t = this.timeouts.get(id)
233
+ if (t) clearTimeout(t)
234
+ this.timeouts.delete(id)
235
+ this.rejectors.delete(id)
236
+ // Clean up progress callbacks (on completion, cancellation, or error)
237
+ // Per LSP spec: workDoneToken is only valid until response/cancellation
238
+ if (workDoneToken !== undefined) {
239
+ this.progressCallbacks.delete(workDoneToken)
240
+ }
241
+ if (partialResultToken !== undefined) {
242
+ this.progressCallbacks.delete(partialResultToken)
243
+ }
244
+ if (abortCleanup) opts?.signal?.removeEventListener('abort', abortCleanup)
245
+ }
246
+
247
+ // Timeout
248
+ const timeoutMs = opts?.timeoutMs ?? this.defaultTimeoutMs
249
+ if (timeoutMs > 0 && Number.isFinite(timeoutMs)) {
250
+ const t = setTimeout(() => {
251
+ rejectAs(
252
+ new LspError({
253
+ code: LspErrorCode.Timeout,
254
+ message: `Request timed out: ${String(method)} (${id}) after ${timeoutMs}ms`,
255
+ }),
256
+ )
257
+ }, timeoutMs)
258
+ this.timeouts.set(id, t)
259
+ }
260
+
261
+ // Allow dispose() to reject in-flight requests
262
+ this.rejectors.set(id, rejectAs)
263
+
264
+ // AbortSignal → send $/cancelRequest and resolve immediately (eager cancel)
265
+ // Don't wait for server response - some servers don't echo promptly
266
+ let abortCleanup: (() => void) | undefined
267
+ if (opts?.signal) {
268
+ const onAbort = () => {
269
+ // Notify server of cancellation (best effort)
270
+ this.sendNotification('$/cancelRequest', { id })
271
+ // Resolve immediately for better UX (don't wait for server acknowledgement)
272
+ resolveCancelled(LspErrorCode.RequestCancelled)
273
+ }
274
+ opts.signal.addEventListener('abort', onAbort, { once: true })
275
+ abortCleanup = () => opts.signal?.removeEventListener('abort', onAbort)
276
+ }
277
+ })
278
+ }
279
+
280
+ /** Subscribe to all inbound JSON-RPC messages */
281
+ private subscribe(cb: (message: any) => void): Disposable {
282
+ this.subs.add(cb)
283
+ return () => this.subs.delete(cb)
284
+ }
285
+
286
+ /** Ingest an incoming frame and dispatch to listeners/waiters */
287
+ private ingest = (raw: any) => {
288
+ // Batch: process each frame independently
289
+ if (Array.isArray(raw)) {
290
+ for (const frame of raw) this.ingest(frame)
291
+ return
292
+ }
293
+
294
+ // Early drop anything that isn't a proper JSON-RPC 2.0 message
295
+ if (!raw || typeof raw !== 'object' || raw.jsonrpc !== JSONRPC_VERSION) return
296
+
297
+ // Handle $/progress notifications
298
+ if (raw.method === '$/progress' && raw.params) {
299
+ const params = raw.params as ProgressPayload
300
+ const callback = this.progressCallbacks.get(params.token)
301
+ if (callback) {
302
+ try {
303
+ callback(params.value)
304
+ } catch {}
305
+
306
+ // Auto-remove workDoneToken callbacks when receiving WorkDoneProgressEnd
307
+ if (params.value && typeof params.value === 'object' && params.value.kind === 'end') {
308
+ this.progressCallbacks.delete(params.token)
309
+ }
310
+ }
311
+ // Still fan out to subscribers for withProgress handlers
312
+ }
313
+
314
+ // If it's a response with an id and a waiting promise, resolve that first
315
+ if (Object.prototype.hasOwnProperty.call(raw, 'id')) {
316
+ const id = raw.id as Id
317
+ if (isId(id)) {
318
+ const waiter = this.waiters.get(id)
319
+ if (waiter) {
320
+ // Claim and stop propagation to generic handlers
321
+ this.waiters.delete(id)
322
+ try {
323
+ waiter(raw)
324
+ } catch {}
325
+ return
326
+ }
327
+ }
328
+ }
329
+
330
+ // Fan-out to generic subscribers (e.g. withHandlers / withError / withProgress)
331
+ for (const cb of Array.from(this.subs)) {
332
+ try {
333
+ cb(raw)
334
+ } catch {}
335
+ }
336
+ }
337
+ }