@adecore/lsp 0.0.1 → 0.17.0-beta.2

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.
Files changed (48) hide show
  1. package/LICENSE +110 -0
  2. package/README.md +47 -0
  3. package/dist/connection.d.ts +50 -0
  4. package/dist/connection.js +290 -0
  5. package/dist/document.d.ts +60 -0
  6. package/dist/document.js +282 -0
  7. package/dist/edits.d.ts +18 -0
  8. package/dist/edits.js +161 -0
  9. package/dist/file-operations.d.ts +5 -0
  10. package/dist/file-operations.js +23 -0
  11. package/dist/glob.d.ts +1 -0
  12. package/dist/glob.js +67 -0
  13. package/dist/index.d.ts +12 -0
  14. package/dist/index.js +10 -0
  15. package/dist/protocol.d.ts +470 -0
  16. package/dist/protocol.js +1 -0
  17. package/dist/service.d.ts +53 -0
  18. package/dist/service.js +1 -0
  19. package/dist/session.d.ts +78 -0
  20. package/dist/session.js +528 -0
  21. package/dist/test-transport.d.ts +33 -0
  22. package/dist/test-transport.js +72 -0
  23. package/dist/testing.d.ts +33 -0
  24. package/dist/testing.js +72 -0
  25. package/dist/transport.d.ts +27 -0
  26. package/dist/transport.js +258 -0
  27. package/dist/uris.d.ts +2 -0
  28. package/dist/uris.js +28 -0
  29. package/dist/vue.d.ts +6 -0
  30. package/dist/vue.js +86 -0
  31. package/dist/watched-files.d.ts +3 -0
  32. package/dist/watched-files.js +41 -0
  33. package/package.json +36 -3
  34. package/src/connection.ts +310 -0
  35. package/src/document.ts +357 -0
  36. package/src/edits.ts +182 -0
  37. package/src/file-operations.ts +30 -0
  38. package/src/glob.ts +60 -0
  39. package/src/index.ts +30 -0
  40. package/src/protocol.ts +479 -0
  41. package/src/service.ts +109 -0
  42. package/src/session.ts +613 -0
  43. package/src/test-transport.ts +92 -0
  44. package/src/testing.ts +105 -0
  45. package/src/transport.ts +290 -0
  46. package/src/uris.ts +29 -0
  47. package/src/vue.ts +93 -0
  48. package/src/watched-files.ts +49 -0
@@ -0,0 +1,310 @@
1
+ import type { Disposable, LspTransport, RequestId, RequestOptions, RpcMessage, RpcRequest } from './protocol.ts';
2
+
3
+ /* The JSON-RPC and LSP codes this package answers with. */
4
+ export const ErrorCodes = {
5
+ InvalidRequest: -32600,
6
+ MethodNotFound: -32601,
7
+ InternalError: -32603,
8
+ ServerNotInitialized: -32002,
9
+ RequestCancelled: -32800,
10
+ ContentModified: -32801
11
+ } as const;
12
+
13
+ export class LspError extends Error {
14
+ readonly code: number;
15
+ readonly data?: unknown;
16
+
17
+ constructor(message: string, code: number = ErrorCodes.InternalError, data?: unknown) {
18
+ super(message);
19
+ this.name = 'LspError';
20
+ this.code = code;
21
+ this.data = data;
22
+ }
23
+ }
24
+
25
+ /* The document changed or closed before the answer arrived, so the answer describes a text that is gone. */
26
+ export class StaleResultError extends LspError {
27
+ readonly uri: string;
28
+
29
+ constructor(uri: string) {
30
+ super(`The document changed before its language result arrived: ${uri}`, ErrorCodes.ContentModified);
31
+ this.name = 'StaleResultError';
32
+ this.uri = uri;
33
+ }
34
+ }
35
+
36
+ export type RequestHandler = (params: unknown, signal: AbortSignal) => unknown | Promise<unknown>;
37
+ type NotificationHandler = (params: unknown) => void | Promise<void>;
38
+
39
+ interface PendingRequest {
40
+ resolve(value: unknown): void;
41
+ reject(error: unknown): void;
42
+ cleanup(): void;
43
+ }
44
+
45
+ function asError(error: unknown): Error {
46
+ return error instanceof Error ? error : new Error(String(error));
47
+ }
48
+
49
+ /* JSON-RPC 2.0 over an `LspTransport`: correlation, cancellation, and the requests a server sends back. */
50
+ export class JsonRpcConnection {
51
+ readonly transport: LspTransport;
52
+ readonly timeoutMs: number;
53
+ private nextId = 0;
54
+ private closed = false;
55
+ private sendQueue: Promise<void> = Promise.resolve();
56
+ private readonly pending = new Map<RequestId, PendingRequest>();
57
+ private readonly incoming = new Map<RequestId, AbortController>();
58
+ private readonly handlers = new Map<string, RequestHandler>();
59
+ private readonly notifications = new Map<string, Set<NotificationHandler>>();
60
+ private readonly errors = new Set<(error: Error) => void>();
61
+ private readonly closes = new Set<(error?: Error) => void>();
62
+ private readonly subscriptions: Disposable[];
63
+
64
+ /* A `timeoutMs` of 0 turns the deadline off; a request then ends by its reply or its signal. */
65
+ constructor(transport: LspTransport, timeoutMs = 30_000) {
66
+ this.transport = transport;
67
+ this.timeoutMs = timeoutMs;
68
+ this.subscriptions = [
69
+ transport.onMessage((message) => {
70
+ void this.receive(message).catch((error) => this.reportError(error));
71
+ }),
72
+ transport.onClose((error) => this.finish(error))
73
+ ];
74
+ }
75
+
76
+ get isClosed(): boolean {
77
+ return this.closed;
78
+ }
79
+
80
+ request<T = unknown>(method: string, params?: unknown, options: RequestOptions = {}): Promise<T> {
81
+ if (this.closed) {
82
+ return Promise.reject(new LspError('LSP connection is closed'));
83
+ }
84
+ if (options.signal?.aborted) {
85
+ return Promise.reject(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
86
+ }
87
+ const id = ++this.nextId;
88
+ return new Promise<T>((resolve, reject) => {
89
+ let timer: ReturnType<typeof setTimeout> | undefined;
90
+ const cancel = (error: LspError): void => {
91
+ if (!this.pending.has(id)) {
92
+ return;
93
+ }
94
+ this.settle(id, undefined, error);
95
+ void this.notify('$/cancelRequest', { id }).catch((failure) => this.reportError(failure));
96
+ };
97
+ const abort = (): void => cancel(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
98
+ this.pending.set(id, {
99
+ resolve: (value) => resolve(value as T),
100
+ reject,
101
+ cleanup: () => {
102
+ clearTimeout(timer);
103
+ options.signal?.removeEventListener('abort', abort);
104
+ }
105
+ });
106
+ options.signal?.addEventListener('abort', abort, { once: true });
107
+ const timeout = options.timeoutMs ?? this.timeoutMs;
108
+ if (timeout > 0) {
109
+ timer = setTimeout(() => cancel(new LspError(`LSP request timed out: ${method}`, ErrorCodes.RequestCancelled)), timeout);
110
+ }
111
+ void this.send({ jsonrpc: '2.0', id, method, params }).catch((error) => this.settle(id, undefined, error));
112
+ });
113
+ }
114
+
115
+ notify(method: string, params?: unknown): Promise<void> {
116
+ return this.send({ jsonrpc: '2.0', method, params });
117
+ }
118
+
119
+ onRequest(method: string, handler: RequestHandler): Disposable {
120
+ if (this.handlers.has(method)) {
121
+ throw new Error(`LSP request handler already registered: ${method}`);
122
+ }
123
+ this.handlers.set(method, handler);
124
+ return {
125
+ dispose: () => {
126
+ if (this.handlers.get(method) === handler) {
127
+ this.handlers.delete(method);
128
+ }
129
+ }
130
+ };
131
+ }
132
+
133
+ onNotification(method: string, listener: NotificationHandler): Disposable {
134
+ let listeners = this.notifications.get(method);
135
+ if (!listeners) {
136
+ listeners = new Set();
137
+ this.notifications.set(method, listeners);
138
+ }
139
+ const registered = listeners;
140
+ registered.add(listener);
141
+ return {
142
+ dispose: () => {
143
+ registered.delete(listener);
144
+ if (registered.size === 0) {
145
+ this.notifications.delete(method);
146
+ }
147
+ }
148
+ };
149
+ }
150
+
151
+ onError(listener: (error: Error) => void): Disposable {
152
+ this.errors.add(listener);
153
+ return {
154
+ dispose: () => {
155
+ this.errors.delete(listener);
156
+ }
157
+ };
158
+ }
159
+
160
+ onClose(listener: (error?: Error) => void): Disposable {
161
+ this.closes.add(listener);
162
+ if (this.closed) {
163
+ listener();
164
+ }
165
+ return {
166
+ dispose: () => {
167
+ this.closes.delete(listener);
168
+ }
169
+ };
170
+ }
171
+
172
+ reportError(error: unknown): void {
173
+ const normalized = asError(error);
174
+ for (const listener of this.errors) {
175
+ try {
176
+ listener(normalized);
177
+ } catch {
178
+ // An error observer must not break protocol dispatch.
179
+ }
180
+ }
181
+ }
182
+
183
+ async close(): Promise<void> {
184
+ this.finish();
185
+ await this.transport.close();
186
+ }
187
+
188
+ private send(message: RpcMessage): Promise<void> {
189
+ const sent = this.sendQueue.then(async () => {
190
+ if (this.closed) {
191
+ throw new LspError('LSP connection is closed');
192
+ }
193
+ await this.transport.send(message);
194
+ });
195
+ this.sendQueue = sent.catch((error) => this.finish(asError(error)));
196
+ return sent;
197
+ }
198
+
199
+ private settle(id: RequestId, result?: unknown, error?: unknown): void {
200
+ const request = this.pending.get(id);
201
+ if (!request) {
202
+ return;
203
+ }
204
+ this.pending.delete(id);
205
+ request.cleanup();
206
+ if (error !== undefined) {
207
+ request.reject(error);
208
+ } else {
209
+ request.resolve(result);
210
+ }
211
+ }
212
+
213
+ private async receive(value: unknown): Promise<void> {
214
+ if (this.closed) {
215
+ return;
216
+ }
217
+ if (!value || typeof value !== 'object' || !('jsonrpc' in value) || value.jsonrpc !== '2.0') {
218
+ throw new LspError('Invalid JSON-RPC message', ErrorCodes.InvalidRequest);
219
+ }
220
+ const message = value as RpcMessage;
221
+ if ('method' in message) {
222
+ if (typeof message.method !== 'string') {
223
+ throw new LspError('Invalid JSON-RPC method', ErrorCodes.InvalidRequest);
224
+ }
225
+ if ('id' in message) {
226
+ if (typeof message.id !== 'number' && typeof message.id !== 'string') {
227
+ throw new LspError('Invalid request id', ErrorCodes.InvalidRequest);
228
+ }
229
+ await this.handleRequest(message);
230
+ } else if (message.method === '$/cancelRequest') {
231
+ const id = (message.params as { id?: RequestId } | undefined)?.id;
232
+ if (id !== undefined) {
233
+ this.incoming.get(id)?.abort();
234
+ }
235
+ } else {
236
+ for (const listener of this.notifications.get(message.method) ?? []) {
237
+ try {
238
+ await listener(message.params);
239
+ } catch (error) {
240
+ this.reportError(error);
241
+ }
242
+ }
243
+ }
244
+ } else if ('id' in message && message.id !== null) {
245
+ if (message.error) {
246
+ this.settle(message.id, undefined, new LspError(message.error.message, message.error.code, message.error.data));
247
+ } else if ('result' in message) {
248
+ this.settle(message.id, message.result);
249
+ } else {
250
+ this.settle(message.id, undefined, new LspError('Invalid JSON-RPC response', ErrorCodes.InvalidRequest));
251
+ }
252
+ } else {
253
+ throw new LspError('Invalid JSON-RPC response', ErrorCodes.InvalidRequest);
254
+ }
255
+ }
256
+
257
+ private async handleRequest(message: RpcRequest): Promise<void> {
258
+ const controller = new AbortController();
259
+ this.incoming.set(message.id, controller);
260
+ try {
261
+ const handler = this.handlers.get(message.method);
262
+ if (!handler) {
263
+ throw new LspError(`Unsupported server request: ${message.method}`, ErrorCodes.MethodNotFound);
264
+ }
265
+ const result = await handler(message.params, controller.signal);
266
+ if (controller.signal.aborted) {
267
+ throw new LspError('Request cancelled', ErrorCodes.RequestCancelled);
268
+ }
269
+ await this.send({ jsonrpc: '2.0', id: message.id, result: result ?? null });
270
+ } catch (error) {
271
+ const failure = error instanceof LspError ? error : new LspError(asError(error).message);
272
+ if (!this.closed) {
273
+ await this.send({ jsonrpc: '2.0', id: message.id, error: { code: failure.code, message: failure.message, data: failure.data } });
274
+ }
275
+ } finally {
276
+ this.incoming.delete(message.id);
277
+ }
278
+ }
279
+
280
+ private finish(error?: Error): void {
281
+ if (this.closed) {
282
+ return;
283
+ }
284
+ this.closed = true;
285
+ for (const id of [...this.pending.keys()]) {
286
+ this.settle(id, undefined, error ?? new LspError('LSP connection closed'));
287
+ }
288
+ for (const controller of this.incoming.values()) {
289
+ controller.abort();
290
+ }
291
+ this.incoming.clear();
292
+ // A transport that is closed already calls back from inside the constructor, before the subscriptions exist.
293
+ for (const subscription of this.subscriptions ?? []) {
294
+ subscription.dispose();
295
+ }
296
+ for (const listener of this.closes) {
297
+ try {
298
+ listener(error);
299
+ } catch (failure) {
300
+ this.reportError(failure);
301
+ }
302
+ }
303
+ this.handlers.clear();
304
+ this.notifications.clear();
305
+ this.closes.clear();
306
+ if (error) {
307
+ this.reportError(error);
308
+ }
309
+ }
310
+ }
@@ -0,0 +1,357 @@
1
+ import { ErrorCodes, LspError, StaleResultError } from './connection.ts';
2
+ import { applyContentChanges, minimalChange } from './edits.ts';
3
+ import type { LspSession } from './session.ts';
4
+ import type {
5
+ CodeAction,
6
+ CodeActionContext,
7
+ CodeLens,
8
+ Command,
9
+ CompletionContext,
10
+ CompletionItem,
11
+ CompletionResult,
12
+ ContentChange,
13
+ DocumentDiagnosticReport,
14
+ DocumentHighlight,
15
+ DocumentItem,
16
+ DocumentRequestOptions,
17
+ DocumentSymbolResult,
18
+ FoldingRange,
19
+ FormattingOptions,
20
+ Hover,
21
+ InlayHint,
22
+ Location,
23
+ NavigationResult,
24
+ Position,
25
+ PrepareRenameResult,
26
+ Range,
27
+ SelectionRange,
28
+ SemanticTokens,
29
+ SemanticTokensDelta,
30
+ SignatureHelp,
31
+ SignatureHelpContext,
32
+ TextEdit,
33
+ WorkspaceEdit
34
+ } from './protocol.ts';
35
+
36
+ /*
37
+ * One open text document in one session. It owns the version: every change raises it by one, and a
38
+ * result that arrives for an older version is refused, so nothing computed for a text that is gone
39
+ * reaches the caller.
40
+ */
41
+ export class LspDocument {
42
+ readonly session: LspSession;
43
+ readonly uri: string;
44
+ readonly languageId: string;
45
+ readonly ready: Promise<void>;
46
+ private revision: number;
47
+ private content: string;
48
+ private closed = false;
49
+ private synced: Promise<void>;
50
+ private closePromise?: Promise<void>;
51
+ private readonly inFlight = new Set<AbortController>();
52
+ private readonly latest = new Map<string, AbortController>();
53
+ private readonly resultVersions = new WeakMap<object, number>();
54
+
55
+ constructor(session: LspSession, item: DocumentItem) {
56
+ // A uri that does not parse is the caller's bug, and fails here instead of inside a server.
57
+ new URL(item.uri);
58
+ this.session = session;
59
+ this.uri = item.uri;
60
+ this.languageId = item.languageId;
61
+ this.content = item.text;
62
+ this.revision = item.version ?? 1;
63
+ if (!Number.isSafeInteger(this.revision) || this.revision < 0) {
64
+ throw new LspError('Invalid document version');
65
+ }
66
+ this.ready = session.syncOptions.openClose
67
+ ? session.notify('textDocument/didOpen', { textDocument: { ...item, version: this.revision } })
68
+ : Promise.resolve();
69
+ this.synced = this.ready;
70
+ void this.ready.catch((error) => session.connection.reportError(error));
71
+ }
72
+
73
+ get version(): number {
74
+ return this.revision;
75
+ }
76
+
77
+ get text(): string {
78
+ return this.content;
79
+ }
80
+
81
+ get isClosed(): boolean {
82
+ return this.closed;
83
+ }
84
+
85
+ /* Sends the difference to the text as the one range that changed, or the whole text to a server that wants no ranges. */
86
+ updateText(text: string): Promise<void> {
87
+ this.assertOpen();
88
+ if (text === this.content) {
89
+ return this.synced;
90
+ }
91
+ return this.applyChanges([minimalChange(this.content, text)]);
92
+ }
93
+
94
+ /*
95
+ * Applies `didChange` entries in order, which a server that negotiated incremental sync gets as
96
+ * they are. The version goes up by one even when the text ends up the same.
97
+ */
98
+ applyChanges(changes: readonly ContentChange[]): Promise<void> {
99
+ this.assertOpen();
100
+ const kind = this.session.syncOptions.change ?? 0;
101
+ if (!kind) {
102
+ return Promise.reject(new LspError('Server did not negotiate document changes', ErrorCodes.MethodNotFound));
103
+ }
104
+ let next: string;
105
+ try {
106
+ next = applyContentChanges(this.content, changes);
107
+ } catch (error) {
108
+ return Promise.reject(error);
109
+ }
110
+ this.content = next;
111
+ const version = ++this.revision;
112
+ this.cancelRequests();
113
+ const contentChanges = kind === 1 ? [{ text: next }] : changes.map((change) => ({ ...change }));
114
+ this.synced = this.synced.then(() =>
115
+ this.session.connection.notify('textDocument/didChange', { textDocument: { uri: this.uri, version }, contentChanges })
116
+ );
117
+ void this.synced.catch((error) => this.session.connection.reportError(error));
118
+ return this.synced;
119
+ }
120
+
121
+ /* Call it once the file is on disk; it sends `didSave` when the server asked for it. */
122
+ async save(): Promise<void> {
123
+ this.assertOpen();
124
+ const version = this.version;
125
+ const text = this.text;
126
+ await this.synced;
127
+ this.assertCurrent(version);
128
+ const save = this.session.syncOptions.save;
129
+ if (save) {
130
+ await this.session.notify('textDocument/didSave', {
131
+ textDocument: { uri: this.uri },
132
+ text: typeof save === 'object' && save.includeText ? text : undefined
133
+ });
134
+ }
135
+ }
136
+
137
+ completion(position: Position, context?: CompletionContext, options?: DocumentRequestOptions): Promise<CompletionResult> {
138
+ return this.feature('textDocument/completion', { position, context }, options);
139
+ }
140
+
141
+ resolveCompletion(item: CompletionItem, options?: DocumentRequestOptions): Promise<CompletionItem> {
142
+ return this.resolve('completionItem/resolve', item, options);
143
+ }
144
+
145
+ hover(position: Position, options?: DocumentRequestOptions): Promise<Hover | null> {
146
+ return this.feature('textDocument/hover', { position }, options);
147
+ }
148
+
149
+ signatureHelp(position: Position, context?: SignatureHelpContext, options?: DocumentRequestOptions): Promise<SignatureHelp | null> {
150
+ return this.feature('textDocument/signatureHelp', { position, context }, options);
151
+ }
152
+
153
+ definition(position: Position, options?: DocumentRequestOptions): Promise<NavigationResult> {
154
+ return this.feature('textDocument/definition', { position }, options);
155
+ }
156
+
157
+ declaration(position: Position, options?: DocumentRequestOptions): Promise<NavigationResult> {
158
+ return this.feature('textDocument/declaration', { position }, options);
159
+ }
160
+
161
+ typeDefinition(position: Position, options?: DocumentRequestOptions): Promise<NavigationResult> {
162
+ return this.feature('textDocument/typeDefinition', { position }, options);
163
+ }
164
+
165
+ implementation(position: Position, options?: DocumentRequestOptions): Promise<NavigationResult> {
166
+ return this.feature('textDocument/implementation', { position }, options);
167
+ }
168
+
169
+ references(position: Position, includeDeclaration = true, options?: DocumentRequestOptions): Promise<Location[] | null> {
170
+ return this.feature('textDocument/references', { position, context: { includeDeclaration } }, options);
171
+ }
172
+
173
+ documentHighlights(position: Position, options?: DocumentRequestOptions): Promise<DocumentHighlight[] | null> {
174
+ return this.feature('textDocument/documentHighlight', { position }, options);
175
+ }
176
+
177
+ documentSymbols(options?: DocumentRequestOptions): Promise<DocumentSymbolResult> {
178
+ return this.feature('textDocument/documentSymbol', {}, options);
179
+ }
180
+
181
+ prepareRename(position: Position, options?: DocumentRequestOptions): Promise<PrepareRenameResult> {
182
+ return this.feature('textDocument/prepareRename', { position }, options);
183
+ }
184
+
185
+ rename(position: Position, newName: string, options?: DocumentRequestOptions): Promise<WorkspaceEdit | null> {
186
+ return this.feature('textDocument/rename', { position, newName }, options);
187
+ }
188
+
189
+ codeActions(range: Range, context: CodeActionContext = { diagnostics: [] }, options?: DocumentRequestOptions): Promise<(CodeAction | Command)[] | null> {
190
+ return this.feature('textDocument/codeAction', { range, context }, options);
191
+ }
192
+
193
+ resolveCodeAction(action: CodeAction, options?: DocumentRequestOptions): Promise<CodeAction> {
194
+ return this.resolve('codeAction/resolve', action, options);
195
+ }
196
+
197
+ codeLenses(options?: DocumentRequestOptions): Promise<CodeLens[] | null> {
198
+ return this.feature('textDocument/codeLens', {}, options);
199
+ }
200
+
201
+ resolveCodeLens(lens: CodeLens, options?: DocumentRequestOptions): Promise<CodeLens> {
202
+ return this.resolve('codeLens/resolve', lens, options);
203
+ }
204
+
205
+ formatting(formatting: FormattingOptions, options?: DocumentRequestOptions): Promise<TextEdit[] | null> {
206
+ return this.feature('textDocument/formatting', { options: formatting }, options);
207
+ }
208
+
209
+ rangeFormatting(range: Range, formatting: FormattingOptions, options?: DocumentRequestOptions): Promise<TextEdit[] | null> {
210
+ return this.feature('textDocument/rangeFormatting', { range, options: formatting }, options);
211
+ }
212
+
213
+ foldingRanges(options?: DocumentRequestOptions): Promise<FoldingRange[] | null> {
214
+ return this.feature('textDocument/foldingRange', {}, options);
215
+ }
216
+
217
+ selectionRanges(positions: readonly Position[], options?: DocumentRequestOptions): Promise<SelectionRange[] | null> {
218
+ return this.feature('textDocument/selectionRange', { positions }, options);
219
+ }
220
+
221
+ semanticTokens(options?: DocumentRequestOptions): Promise<SemanticTokens | null> {
222
+ return this.feature('textDocument/semanticTokens/full', {}, options);
223
+ }
224
+
225
+ semanticTokensDelta(previousResultId: string, options?: DocumentRequestOptions): Promise<SemanticTokens | SemanticTokensDelta | null> {
226
+ return this.feature('textDocument/semanticTokens/full/delta', { previousResultId }, options);
227
+ }
228
+
229
+ semanticTokensRange(range: Range, options?: DocumentRequestOptions): Promise<SemanticTokens | null> {
230
+ return this.feature('textDocument/semanticTokens/range', { range }, options);
231
+ }
232
+
233
+ inlayHints(range: Range, options?: DocumentRequestOptions): Promise<InlayHint[] | null> {
234
+ return this.feature('textDocument/inlayHint', { range }, options);
235
+ }
236
+
237
+ resolveInlayHint(hint: InlayHint, options?: DocumentRequestOptions): Promise<InlayHint> {
238
+ return this.resolve('inlayHint/resolve', hint, options);
239
+ }
240
+
241
+ diagnostics(previousResultId?: string, identifier?: string, options?: DocumentRequestOptions): Promise<DocumentDiagnosticReport> {
242
+ return this.feature('textDocument/diagnostic', { previousResultId, identifier }, options);
243
+ }
244
+
245
+ /* Any feature by its LSP method, for a caller that gets the method from elsewhere. `params` leave out `textDocument`; a resolve method takes the item. */
246
+ request<T = unknown>(method: string, params: object, options?: DocumentRequestOptions): Promise<T> {
247
+ return method.endsWith('/resolve') ? (this.resolve(method, params, options) as Promise<T>) : this.feature<T>(method, params, options);
248
+ }
249
+
250
+ /* Sends `didClose` after every change queued before it. The uri stays taken until then, so a reopen cannot overtake it. */
251
+ close(): Promise<void> {
252
+ if (this.closePromise) {
253
+ return this.closePromise;
254
+ }
255
+ if (this.closed) {
256
+ return Promise.resolve();
257
+ }
258
+ this.closed = true;
259
+ this.cancelRequests();
260
+ this.closePromise = this.synced
261
+ .then(async () => {
262
+ if (!this.session.connection.isClosed && this.session.syncOptions.openClose) {
263
+ await this.session.connection.notify('textDocument/didClose', { textDocument: { uri: this.uri } });
264
+ }
265
+ })
266
+ .finally(() => this.session.forgetDocument(this));
267
+ return this.closePromise;
268
+ }
269
+
270
+ /* The connection is gone, so there is nobody to tell. */
271
+ disconnect(): void {
272
+ this.closed = true;
273
+ this.cancelRequests();
274
+ this.session.forgetDocument(this);
275
+ }
276
+
277
+ private feature<T>(method: string, params: object, options?: DocumentRequestOptions): Promise<T> {
278
+ return this.perform(method, { ...params, textDocument: { uri: this.uri } }, options);
279
+ }
280
+
281
+ private resolve<T extends object>(method: string, item: T, options?: DocumentRequestOptions): Promise<T> {
282
+ const version = this.resultVersions.get(item);
283
+ if (version !== undefined && version !== this.version) {
284
+ return Promise.reject(new StaleResultError(this.uri));
285
+ }
286
+ return this.perform(method, item, options);
287
+ }
288
+
289
+ private async perform<T>(method: string, params: unknown, options: DocumentRequestOptions = {}): Promise<T> {
290
+ this.assertOpen();
291
+ if (!this.session.supports(method, this)) {
292
+ throw new LspError(`Server does not support ${method}`, ErrorCodes.MethodNotFound);
293
+ }
294
+ const version = this.version;
295
+ const controller = new AbortController();
296
+ if (options.cancelPrevious !== false) {
297
+ this.latest.get(method)?.abort();
298
+ }
299
+ this.latest.set(method, controller);
300
+ this.inFlight.add(controller);
301
+ const abort = (): void => controller.abort();
302
+ options.signal?.addEventListener('abort', abort, { once: true });
303
+ if (options.signal?.aborted) {
304
+ abort();
305
+ }
306
+ try {
307
+ await this.synced;
308
+ this.assertCurrent(version);
309
+ const result = await this.session.request<T>(method, params, { signal: controller.signal, timeoutMs: options.timeoutMs });
310
+ this.assertCurrent(version);
311
+ this.remember(result, version);
312
+ return result;
313
+ } catch (error) {
314
+ this.assertCurrent(version);
315
+ throw error;
316
+ } finally {
317
+ options.signal?.removeEventListener('abort', abort);
318
+ this.inFlight.delete(controller);
319
+ if (this.latest.get(method) === controller) {
320
+ this.latest.delete(method);
321
+ }
322
+ }
323
+ }
324
+
325
+ /* A completion item, code action or hint resolves only against the version it was produced for. */
326
+ private remember(result: unknown, version: number): void {
327
+ if (!result || typeof result !== 'object') {
328
+ return;
329
+ }
330
+ this.resultVersions.set(result, version);
331
+ if (Array.isArray(result)) {
332
+ for (const item of result) {
333
+ this.remember(item, version);
334
+ }
335
+ } else if ('items' in result) {
336
+ this.remember(result.items, version);
337
+ }
338
+ }
339
+
340
+ private assertOpen(): void {
341
+ if (this.closed) {
342
+ throw new LspError(`Document is closed: ${this.uri}`);
343
+ }
344
+ }
345
+
346
+ private assertCurrent(version: number): void {
347
+ if (this.closed || this.version !== version) {
348
+ throw new StaleResultError(this.uri);
349
+ }
350
+ }
351
+
352
+ private cancelRequests(): void {
353
+ for (const controller of this.inFlight) {
354
+ controller.abort();
355
+ }
356
+ }
357
+ }