@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,528 @@
1
+ import { ErrorCodes, JsonRpcConnection, LspError } from './connection.js';
2
+ import { LspDocument } from './document.js';
3
+ import { renamesTaken } from './file-operations.js';
4
+ import { globMatch } from './glob.js';
5
+ /* The capability that advertises each method statically. */
6
+ const PROVIDERS = {
7
+ 'textDocument/completion': 'completionProvider',
8
+ 'completionItem/resolve': 'completionProvider',
9
+ 'textDocument/hover': 'hoverProvider',
10
+ 'textDocument/signatureHelp': 'signatureHelpProvider',
11
+ 'textDocument/definition': 'definitionProvider',
12
+ 'textDocument/declaration': 'declarationProvider',
13
+ 'textDocument/typeDefinition': 'typeDefinitionProvider',
14
+ 'textDocument/implementation': 'implementationProvider',
15
+ 'textDocument/references': 'referencesProvider',
16
+ 'textDocument/prepareRename': 'renameProvider',
17
+ 'textDocument/documentHighlight': 'documentHighlightProvider',
18
+ 'textDocument/documentSymbol': 'documentSymbolProvider',
19
+ 'workspace/symbol': 'workspaceSymbolProvider',
20
+ 'workspaceSymbol/resolve': 'workspaceSymbolProvider',
21
+ 'textDocument/rename': 'renameProvider',
22
+ 'textDocument/codeAction': 'codeActionProvider',
23
+ 'codeAction/resolve': 'codeActionProvider',
24
+ 'textDocument/codeLens': 'codeLensProvider',
25
+ 'codeLens/resolve': 'codeLensProvider',
26
+ 'workspace/executeCommand': 'executeCommandProvider',
27
+ 'textDocument/formatting': 'documentFormattingProvider',
28
+ 'textDocument/rangeFormatting': 'documentRangeFormattingProvider',
29
+ 'textDocument/foldingRange': 'foldingRangeProvider',
30
+ 'textDocument/selectionRange': 'selectionRangeProvider',
31
+ 'textDocument/semanticTokens/full': 'semanticTokensProvider',
32
+ 'textDocument/semanticTokens/full/delta': 'semanticTokensProvider',
33
+ 'textDocument/semanticTokens/range': 'semanticTokensProvider',
34
+ 'textDocument/inlayHint': 'inlayHintProvider',
35
+ 'inlayHint/resolve': 'inlayHintProvider',
36
+ 'textDocument/diagnostic': 'diagnosticProvider'
37
+ };
38
+ /* A method whose dynamic registration is made under the name of another. */
39
+ const REGISTRATION_METHODS = {
40
+ 'completionItem/resolve': 'textDocument/completion',
41
+ 'codeAction/resolve': 'textDocument/codeAction',
42
+ 'codeLens/resolve': 'textDocument/codeLens',
43
+ 'inlayHint/resolve': 'textDocument/inlayHint',
44
+ 'textDocument/prepareRename': 'textDocument/rename',
45
+ 'workspaceSymbol/resolve': 'workspace/symbol',
46
+ 'textDocument/semanticTokens/full': 'textDocument/semanticTokens',
47
+ 'textDocument/semanticTokens/full/delta': 'textDocument/semanticTokens',
48
+ 'textDocument/semanticTokens/range': 'textDocument/semanticTokens'
49
+ };
50
+ const REFRESH_REQUESTS = ['workspace/semanticTokens/refresh', 'workspace/inlayHint/refresh', 'workspace/diagnostic/refresh', 'workspace/codeLens/refresh'];
51
+ function matches(options, document) {
52
+ if (options.documentSelector == null) {
53
+ return true;
54
+ }
55
+ if (!document) {
56
+ return options.documentSelector.length > 0;
57
+ }
58
+ const url = new URL(document.uri);
59
+ return options.documentSelector.some((filter) => (!filter.language || filter.language === document.languageId) &&
60
+ (!filter.scheme || filter.scheme === url.protocol.slice(0, -1)) &&
61
+ (!filter.pattern || globMatch(decodeURIComponent(url.pathname), filter.pattern)));
62
+ }
63
+ /* One conversation with one language server: the handshake, what it can do, and the documents open in it. */
64
+ export class LspSession {
65
+ connection;
66
+ options;
67
+ serverInfo;
68
+ phase = 'new';
69
+ initialized;
70
+ shutdownPromise;
71
+ serverCapabilities = {};
72
+ registrations = new Map();
73
+ documents = new Map();
74
+ diagnosticListeners = new Set();
75
+ capabilityListeners = new Set();
76
+ configuration;
77
+ constructor(transport, options = {}) {
78
+ this.options = options;
79
+ this.connection = new JsonRpcConnection(transport, options.timeoutMs);
80
+ this.configuration = options.configuration ?? {};
81
+ this.connection.onClose(() => {
82
+ this.phase = 'closed';
83
+ for (const document of [...this.documents.values()]) {
84
+ document.disconnect();
85
+ }
86
+ });
87
+ this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
88
+ const diagnostics = params;
89
+ const document = this.documents.get(diagnostics.uri);
90
+ if (!document || document.isClosed || (diagnostics.version !== undefined && diagnostics.version !== document.version)) {
91
+ return;
92
+ }
93
+ this.emitDiagnostics(diagnostics);
94
+ });
95
+ this.connection.onRequest('workspace/configuration', async (params) => {
96
+ const { items } = params;
97
+ return Promise.all(items.map((item) => (options.onConfiguration ? options.onConfiguration(item) : this.readConfiguration(item.section))));
98
+ });
99
+ this.connection.onRequest('workspace/workspaceFolders', () => this.workspaceFolders);
100
+ this.connection.onRequest('workspace/applyEdit', (params) => options.onApplyEdit?.(params) ?? { applied: false, failureReason: 'No workspace edit handler is attached' });
101
+ this.connection.onRequest('window/showMessageRequest', (params) => options.onShowMessage?.(params) ?? null);
102
+ this.connection.onRequest('window/workDoneProgress/create', () => null);
103
+ this.connection.onRequest('client/registerCapability', (params) => {
104
+ const { registrations } = params;
105
+ for (const registration of registrations) {
106
+ this.registrations.set(registration.id, registration);
107
+ }
108
+ this.emitCapabilities();
109
+ return null;
110
+ });
111
+ this.connection.onRequest('client/unregisterCapability', (params) => {
112
+ // The misspelled field is part of the LSP 3.17 wire protocol.
113
+ const { unregisterations } = params;
114
+ for (const registration of unregisterations) {
115
+ this.registrations.delete(registration.id);
116
+ }
117
+ this.emitCapabilities();
118
+ return null;
119
+ });
120
+ for (const method of REFRESH_REQUESTS) {
121
+ this.connection.onRequest(method, () => {
122
+ this.emitCapabilities();
123
+ return null;
124
+ });
125
+ }
126
+ }
127
+ get state() {
128
+ return this.phase;
129
+ }
130
+ get capabilities() {
131
+ return this.serverCapabilities;
132
+ }
133
+ get workspaceFolders() {
134
+ return this.options.workspaceFolders ?? (this.options.rootUri ? [{ uri: this.options.rootUri, name: 'Workspace' }] : null);
135
+ }
136
+ get syncOptions() {
137
+ const sync = this.serverCapabilities.textDocumentSync;
138
+ return typeof sync === 'number' ? { openClose: sync !== 0, change: sync } : (sync ?? {});
139
+ }
140
+ initialize() {
141
+ if (this.initialized) {
142
+ return this.initialized;
143
+ }
144
+ if (this.phase !== 'new') {
145
+ return Promise.reject(new LspError(`Cannot initialize a ${this.phase} session`));
146
+ }
147
+ this.phase = 'initializing';
148
+ this.initialized = (async () => {
149
+ try {
150
+ const result = await this.connection.request('initialize', {
151
+ processId: null,
152
+ rootUri: this.options.rootUri ?? null,
153
+ workspaceFolders: this.workspaceFolders,
154
+ clientInfo: this.options.clientInfo ?? { name: '@adecore/lsp' },
155
+ capabilities: clientCapabilities(this.options),
156
+ initializationOptions: this.options.initializationOptions
157
+ });
158
+ if (result.capabilities.positionEncoding && result.capabilities.positionEncoding !== 'utf-16') {
159
+ throw new LspError(`Unsupported server position encoding: ${result.capabilities.positionEncoding}`);
160
+ }
161
+ this.serverCapabilities = result.capabilities;
162
+ this.serverInfo = result.serverInfo;
163
+ await this.connection.notify('initialized', {});
164
+ this.phase = 'ready';
165
+ if (this.options.configuration !== undefined) {
166
+ await this.connection.notify('workspace/didChangeConfiguration', { settings: this.configuration });
167
+ }
168
+ this.emitCapabilities();
169
+ return result;
170
+ }
171
+ catch (error) {
172
+ await this.connection.close();
173
+ throw error;
174
+ }
175
+ })();
176
+ return this.initialized;
177
+ }
178
+ openDocument(item) {
179
+ this.assertReady();
180
+ if (this.documents.has(item.uri)) {
181
+ throw new LspError(`Document already open: ${item.uri}`);
182
+ }
183
+ const document = new LspDocument(this, item);
184
+ this.documents.set(item.uri, document);
185
+ return document;
186
+ }
187
+ getDocument(uri) {
188
+ return this.documents.get(uri);
189
+ }
190
+ /* The options the server gave a method, from its capabilities or a registration whose selector takes `document`. */
191
+ providerOptions(method, document) {
192
+ const name = REGISTRATION_METHODS[method] ?? method;
193
+ const candidates = [];
194
+ for (const registration of this.registrations.values()) {
195
+ if (registration.method === name && matches(registration.registerOptions ?? {}, document)) {
196
+ candidates.push(registration.registerOptions ?? {});
197
+ }
198
+ }
199
+ const provider = this.serverCapabilities[PROVIDERS[method]];
200
+ if (provider === true) {
201
+ candidates.push({});
202
+ }
203
+ else if (provider && typeof provider === 'object' && matches(provider, document)) {
204
+ candidates.push(provider);
205
+ }
206
+ return candidates.find((options) => {
207
+ if (method.endsWith('/resolve')) {
208
+ return options.resolveProvider === true;
209
+ }
210
+ if (method === 'textDocument/prepareRename') {
211
+ return options.prepareProvider === true;
212
+ }
213
+ if (method === 'textDocument/semanticTokens/full/delta') {
214
+ return typeof options.full === 'object' && options.full.delta === true;
215
+ }
216
+ if (method === 'textDocument/semanticTokens/full') {
217
+ return !!options.full;
218
+ }
219
+ if (method === 'textDocument/semanticTokens/range') {
220
+ return !!options.range;
221
+ }
222
+ return true;
223
+ });
224
+ }
225
+ supports(method, document) {
226
+ return this.phase === 'ready' && this.providerOptions(method, document) !== undefined;
227
+ }
228
+ request(method, params, options) {
229
+ this.assertReady();
230
+ return this.connection.request(method, params, options);
231
+ }
232
+ notify(method, params) {
233
+ this.assertReady();
234
+ return this.connection.notify(method, params);
235
+ }
236
+ executeCommand(command, args, options) {
237
+ if (!this.supports('workspace/executeCommand')) {
238
+ return Promise.reject(new LspError('Server does not support commands', ErrorCodes.MethodNotFound));
239
+ }
240
+ return this.request('workspace/executeCommand', typeof command === 'string' ? { command, arguments: args } : command, options);
241
+ }
242
+ workspaceSymbols(query, options) {
243
+ if (!this.supports('workspace/symbol')) {
244
+ return Promise.reject(new LspError('Server does not support workspace symbols', ErrorCodes.MethodNotFound));
245
+ }
246
+ return this.request('workspace/symbol', { query }, options);
247
+ }
248
+ resolveWorkspaceSymbol(symbol, options) {
249
+ if (!this.supports('workspaceSymbol/resolve')) {
250
+ return Promise.reject(new LspError('Server does not support workspace symbol resolution', ErrorCodes.MethodNotFound));
251
+ }
252
+ return this.request('workspaceSymbol/resolve', symbol, options);
253
+ }
254
+ async setConfiguration(settings) {
255
+ this.configuration = settings;
256
+ await this.notify('workspace/didChangeConfiguration', { settings });
257
+ }
258
+ onRequest(method, handler) {
259
+ return this.connection.onRequest(method, handler);
260
+ }
261
+ onNotification(method, listener) {
262
+ return this.connection.onNotification(method, listener);
263
+ }
264
+ onError(listener) {
265
+ return this.connection.onError(listener);
266
+ }
267
+ onProgress(listener) {
268
+ return this.onNotification('$/progress', (params) => listener(params));
269
+ }
270
+ onDiagnostics(listener) {
271
+ this.diagnosticListeners.add(listener);
272
+ return {
273
+ dispose: () => {
274
+ this.diagnosticListeners.delete(listener);
275
+ }
276
+ };
277
+ }
278
+ /* What the server registered to hear about, which is nothing until it registers for `workspace/didChangeWatchedFiles`. */
279
+ watchedFiles() {
280
+ return [...this.registrations.values()]
281
+ .filter((registration) => registration.method === 'workspace/didChangeWatchedFiles')
282
+ .flatMap((registration) => registration.registerOptions?.watchers ?? []);
283
+ }
284
+ /* Tells the server which files changed on disk. The caller holds back what its patterns do not name. */
285
+ didChangeWatchedFiles(changes) {
286
+ return this.notify('workspace/didChangeWatchedFiles', { changes });
287
+ }
288
+ /* The files a server asked to hear about when they are renamed, from its capabilities and its registrations. */
289
+ fileOperationFilters(operation) {
290
+ const fileOperations = this.serverCapabilities.workspace?.fileOperations;
291
+ const method = `workspace/${operation}Files`;
292
+ return [
293
+ ...(fileOperations?.[operation]?.filters ?? []),
294
+ ...[...this.registrations.values()]
295
+ .filter((registration) => registration.method === method)
296
+ .flatMap((registration) => registration.registerOptions?.filters ?? [])
297
+ ];
298
+ }
299
+ /* What the server would change before these files move, or null when it takes none of them or has nothing to say. */
300
+ async willRenameFiles(files, options) {
301
+ const taken = renamesTaken(this.fileOperationFilters('willRename'), files);
302
+ return taken.length === 0 || this.phase !== 'ready' ? null : this.request('workspace/willRenameFiles', { files: taken }, options);
303
+ }
304
+ /* Tells the server which of these files moved, those its filters take. */
305
+ async didRenameFiles(files) {
306
+ const taken = renamesTaken(this.fileOperationFilters('didRename'), files);
307
+ if (taken.length > 0 && this.phase === 'ready') {
308
+ await this.notify('workspace/didRenameFiles', { files: taken });
309
+ }
310
+ }
311
+ /* Registrations and the refresh requests of the server both land here, since each can change what a document may ask. */
312
+ onCapabilitiesChanged(listener) {
313
+ this.capabilityListeners.add(listener);
314
+ return {
315
+ dispose: () => {
316
+ this.capabilityListeners.delete(listener);
317
+ }
318
+ };
319
+ }
320
+ shutdown() {
321
+ this.shutdownPromise ??= this.stop();
322
+ return this.shutdownPromise;
323
+ }
324
+ /* Called by a document that closed, so its diagnostics go with it. */
325
+ forgetDocument(document) {
326
+ if (this.documents.get(document.uri) !== document) {
327
+ return;
328
+ }
329
+ this.documents.delete(document.uri);
330
+ this.emitDiagnostics({ uri: document.uri, diagnostics: [] });
331
+ }
332
+ assertReady() {
333
+ if (this.phase !== 'ready') {
334
+ throw new LspError(`LSP session is ${this.phase}; await initialize() first`, ErrorCodes.ServerNotInitialized);
335
+ }
336
+ }
337
+ async stop() {
338
+ try {
339
+ if (this.phase === 'initializing') {
340
+ await this.initialized;
341
+ }
342
+ if (this.phase === 'ready') {
343
+ this.phase = 'closing';
344
+ await Promise.all([...this.documents.values()].map((document) => document.close()));
345
+ await this.connection.request('shutdown', undefined, { timeoutMs: 5_000 });
346
+ await this.connection.notify('exit');
347
+ }
348
+ }
349
+ finally {
350
+ await this.connection.close();
351
+ this.phase = 'closed';
352
+ }
353
+ }
354
+ readConfiguration(section) {
355
+ if (!section) {
356
+ return this.configuration;
357
+ }
358
+ if (Object.hasOwn(this.configuration, section)) {
359
+ return this.configuration[section];
360
+ }
361
+ let result = this.configuration;
362
+ for (const key of section.split('.')) {
363
+ if (!result || typeof result !== 'object' || !Object.hasOwn(result, key)) {
364
+ return null;
365
+ }
366
+ result = result[key];
367
+ }
368
+ return result ?? null;
369
+ }
370
+ emitDiagnostics(params) {
371
+ for (const listener of this.diagnosticListeners) {
372
+ try {
373
+ listener(params);
374
+ }
375
+ catch (error) {
376
+ this.connection.reportError(error);
377
+ }
378
+ }
379
+ }
380
+ emitCapabilities() {
381
+ for (const listener of this.capabilityListeners) {
382
+ try {
383
+ listener();
384
+ }
385
+ catch (error) {
386
+ this.connection.reportError(error);
387
+ }
388
+ }
389
+ }
390
+ }
391
+ function clientCapabilities(options) {
392
+ const dynamic = { dynamicRegistration: true };
393
+ const symbolKind = { valueSet: Array.from({ length: 26 }, (_, index) => index + 1) };
394
+ return {
395
+ general: { positionEncodings: ['utf-16'] },
396
+ workspace: {
397
+ applyEdit: !!options.onApplyEdit,
398
+ workspaceEdit: { documentChanges: true, resourceOperations: ['rename'], failureHandling: 'abort' },
399
+ fileOperations: { dynamicRegistration: true, willRename: true, didRename: true },
400
+ configuration: true,
401
+ workspaceFolders: true,
402
+ executeCommand: dynamic,
403
+ didChangeWatchedFiles: { ...dynamic, relativePatternSupport: true },
404
+ symbol: { ...dynamic, symbolKind, tagSupport: { valueSet: [1] }, resolveSupport: { properties: ['location.range'] } },
405
+ semanticTokens: { refreshSupport: true },
406
+ inlayHint: { refreshSupport: true },
407
+ codeLens: { refreshSupport: true },
408
+ diagnostics: { refreshSupport: true }
409
+ },
410
+ window: { workDoneProgress: true, showMessage: { messageActionItem: { additionalPropertiesSupport: false } } },
411
+ textDocument: {
412
+ synchronization: { dynamicRegistration: false, didSave: true },
413
+ publishDiagnostics: {
414
+ versionSupport: true,
415
+ relatedInformation: true,
416
+ tagSupport: { valueSet: [1, 2] },
417
+ codeDescriptionSupport: true,
418
+ dataSupport: true
419
+ },
420
+ completion: {
421
+ ...dynamic,
422
+ contextSupport: true,
423
+ completionItem: {
424
+ snippetSupport: options.snippetSupport ?? false,
425
+ commitCharactersSupport: true,
426
+ documentationFormat: ['markdown', 'plaintext'],
427
+ deprecatedSupport: true,
428
+ preselectSupport: true,
429
+ insertReplaceSupport: true,
430
+ labelDetailsSupport: true,
431
+ resolveSupport: { properties: ['documentation', 'detail', 'additionalTextEdits', 'command'] }
432
+ }
433
+ },
434
+ hover: { ...dynamic, contentFormat: ['markdown', 'plaintext'] },
435
+ signatureHelp: {
436
+ ...dynamic,
437
+ contextSupport: true,
438
+ signatureInformation: {
439
+ documentationFormat: ['markdown', 'plaintext'],
440
+ parameterInformation: { labelOffsetSupport: true },
441
+ activeParameterSupport: true
442
+ }
443
+ },
444
+ definition: { ...dynamic, linkSupport: true },
445
+ declaration: { ...dynamic, linkSupport: true },
446
+ typeDefinition: { ...dynamic, linkSupport: true },
447
+ implementation: { ...dynamic, linkSupport: true },
448
+ references: dynamic,
449
+ documentHighlight: dynamic,
450
+ documentSymbol: { ...dynamic, symbolKind, hierarchicalDocumentSymbolSupport: true, tagSupport: { valueSet: [1] }, labelSupport: true },
451
+ rename: { ...dynamic, prepareSupport: true, prepareSupportDefaultBehavior: 1 },
452
+ codeAction: {
453
+ ...dynamic,
454
+ dataSupport: true,
455
+ isPreferredSupport: true,
456
+ disabledSupport: true,
457
+ codeActionLiteralSupport: {
458
+ codeActionKind: {
459
+ valueSet: [
460
+ '',
461
+ 'quickfix',
462
+ 'refactor',
463
+ 'refactor.extract',
464
+ 'refactor.inline',
465
+ 'refactor.rewrite',
466
+ 'refactor.move',
467
+ 'source',
468
+ 'source.organizeImports',
469
+ 'source.fixAll'
470
+ ]
471
+ }
472
+ },
473
+ resolveSupport: { properties: ['edit', 'command'] }
474
+ },
475
+ codeLens: dynamic,
476
+ formatting: dynamic,
477
+ rangeFormatting: dynamic,
478
+ foldingRange: { ...dynamic, lineFoldingOnly: false, foldingRange: { collapsedText: false } },
479
+ selectionRange: dynamic,
480
+ semanticTokens: {
481
+ ...dynamic,
482
+ requests: { range: true, full: { delta: true } },
483
+ tokenTypes: [
484
+ 'namespace',
485
+ 'type',
486
+ 'class',
487
+ 'enum',
488
+ 'interface',
489
+ 'struct',
490
+ 'typeParameter',
491
+ 'parameter',
492
+ 'variable',
493
+ 'property',
494
+ 'enumMember',
495
+ 'event',
496
+ 'function',
497
+ 'method',
498
+ 'macro',
499
+ 'keyword',
500
+ 'modifier',
501
+ 'comment',
502
+ 'string',
503
+ 'number',
504
+ 'regexp',
505
+ 'operator',
506
+ 'decorator'
507
+ ],
508
+ tokenModifiers: [
509
+ 'declaration',
510
+ 'definition',
511
+ 'readonly',
512
+ 'static',
513
+ 'deprecated',
514
+ 'abstract',
515
+ 'async',
516
+ 'modification',
517
+ 'documentation',
518
+ 'defaultLibrary'
519
+ ],
520
+ formats: ['relative'],
521
+ overlappingTokenSupport: false,
522
+ multilineTokenSupport: false
523
+ },
524
+ inlayHint: { ...dynamic, resolveSupport: { properties: ['tooltip', 'textEdits', 'label.tooltip', 'label.location', 'label.command'] } },
525
+ diagnostic: { ...dynamic, relatedDocumentSupport: true }
526
+ }
527
+ };
528
+ }
@@ -0,0 +1,33 @@
1
+ import { LspSession } from './session.ts';
2
+ import type { LspTransport, RpcMessage, ServerCapabilities } from './protocol.ts';
3
+ export declare class ScriptedTransport implements LspTransport {
4
+ sent: RpcMessage[];
5
+ messages: Set<(message: unknown) => void>;
6
+ closes: Set<(error?: Error) => void>;
7
+ autoInitialize: ServerCapabilities | undefined;
8
+ send(message: RpcMessage): void;
9
+ onMessage(listener: (message: unknown) => void): {
10
+ dispose: () => void;
11
+ };
12
+ onClose(listener: (error?: Error) => void): {
13
+ dispose: () => void;
14
+ };
15
+ close(): void;
16
+ emit(message: RpcMessage): void;
17
+ request(method: string): {
18
+ id: number;
19
+ params: unknown;
20
+ };
21
+ respond(method: string, result: unknown): void;
22
+ methods(): string[];
23
+ }
24
+ export declare function flush(): Promise<void>;
25
+ export declare const origin: {
26
+ line: number;
27
+ character: number;
28
+ };
29
+ export declare function sessionWith(capabilities?: ServerCapabilities): Promise<{
30
+ transport: ScriptedTransport;
31
+ session: LspSession;
32
+ }>;
33
+ export declare function rejecting(promise: Promise<unknown>, check: (error: unknown) => void): Promise<void>;
@@ -0,0 +1,72 @@
1
+ import { LspSession } from './session.js';
2
+ /* A transport that records what is sent and lets a test say what comes back, for tests that assert on the exact wire. */
3
+ export class ScriptedTransport {
4
+ sent = [];
5
+ messages = new Set();
6
+ closes = new Set();
7
+ autoInitialize;
8
+ send(message) {
9
+ this.sent.push(message);
10
+ if ('method' in message && message.method === 'initialize' && 'id' in message && this.autoInitialize) {
11
+ queueMicrotask(() => this.emit({ jsonrpc: '2.0', id: message.id, result: { capabilities: this.autoInitialize } }));
12
+ }
13
+ if ('method' in message && message.method === 'shutdown' && 'id' in message) {
14
+ queueMicrotask(() => this.emit({ jsonrpc: '2.0', id: message.id, result: null }));
15
+ }
16
+ }
17
+ onMessage(listener) {
18
+ this.messages.add(listener);
19
+ return {
20
+ dispose: () => {
21
+ this.messages.delete(listener);
22
+ }
23
+ };
24
+ }
25
+ onClose(listener) {
26
+ this.closes.add(listener);
27
+ return {
28
+ dispose: () => {
29
+ this.closes.delete(listener);
30
+ }
31
+ };
32
+ }
33
+ close() {
34
+ for (const listener of this.closes) {
35
+ listener();
36
+ }
37
+ }
38
+ emit(message) {
39
+ for (const listener of this.messages) {
40
+ listener(message);
41
+ }
42
+ }
43
+ /* The last request of `method` the client sent. */
44
+ request(method) {
45
+ return [...this.sent].reverse().find((message) => 'method' in message && message.method === method && 'id' in message);
46
+ }
47
+ respond(method, result) {
48
+ this.emit({ jsonrpc: '2.0', id: this.request(method).id, result });
49
+ }
50
+ methods() {
51
+ return this.sent.flatMap((message) => ('method' in message ? [message.method] : []));
52
+ }
53
+ }
54
+ export async function flush() {
55
+ for (let i = 0; i < 15; i++) {
56
+ await Promise.resolve();
57
+ }
58
+ }
59
+ export const origin = { line: 0, character: 0 };
60
+ export async function sessionWith(capabilities = {}) {
61
+ const transport = new ScriptedTransport();
62
+ transport.autoInitialize = { textDocumentSync: 2, ...capabilities };
63
+ const session = new LspSession(transport);
64
+ await session.initialize();
65
+ return { transport, session };
66
+ }
67
+ /* Bun runs the event loop inside `expect(promise).rejects` of a promise that has not settled, so a rejection that comes later is checked here. */
68
+ export function rejecting(promise, check) {
69
+ return promise.then(() => {
70
+ throw new Error('Expected the promise to reject');
71
+ }, (error) => check(error));
72
+ }
@@ -0,0 +1,33 @@
1
+ import { JsonRpcConnection, type RequestHandler } from './connection.ts';
2
+ import type { Diagnostic, InitializeResult, LspTransport, ServerCapabilities } from './protocol.ts';
3
+ export { createMemoryTransportPair } from './transport.ts';
4
+ export interface FakeLanguageServerOptions {
5
+ capabilities?: ServerCapabilities;
6
+ serverInfo?: InitializeResult['serverInfo'];
7
+ handlers?: Record<string, RequestHandler>;
8
+ silent?: boolean;
9
+ }
10
+ export interface FakeDocument {
11
+ languageId: string;
12
+ text: string;
13
+ version: number;
14
+ }
15
+ export interface ReceivedMessage {
16
+ method: string;
17
+ params: unknown;
18
+ id?: number | string;
19
+ }
20
+ export declare class FakeLanguageServer {
21
+ readonly connection: JsonRpcConnection;
22
+ readonly received: ReceivedMessage[];
23
+ readonly documents: Map<string, FakeDocument>;
24
+ shutdownRequested: boolean;
25
+ exited: boolean;
26
+ constructor(transport: LspTransport, options?: FakeLanguageServerOptions);
27
+ handle(method: string, handler: RequestHandler): void;
28
+ paramsOf(method: string): unknown[];
29
+ publishDiagnostics(uri: string, diagnostics: Diagnostic[], version?: number): Promise<void>;
30
+ notify(method: string, params?: unknown): Promise<void>;
31
+ request<T = unknown>(method: string, params?: unknown): Promise<T>;
32
+ crash(): Promise<void>;
33
+ }