@adecore/lsp 0.38.0 → 0.40.0
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.
- package/dist/completion-defaults.js +2 -4
- package/dist/edits.d.ts +4 -0
- package/dist/edits.js +8 -5
- package/dist/file-operations.js +6 -3
- package/dist/session.d.ts +2 -0
- package/dist/session.js +14 -18
- package/dist/test-transport.js +1 -1
- package/dist/text-mirror.js +2 -5
- package/dist/transport.d.ts +1 -0
- package/dist/transport.js +7 -4
- package/dist/uris.d.ts +1 -0
- package/dist/uris.js +5 -2
- package/dist/watched-files.js +2 -2
- package/dist/workspace-operations.js +2 -3
- package/package.json +2 -2
- package/src/completion-defaults.ts +24 -4
- package/src/connection.ts +45 -0
- package/src/document.ts +39 -0
- package/src/edits.ts +58 -5
- package/src/file-operations.ts +13 -3
- package/src/glob.ts +6 -0
- package/src/session.ts +84 -18
- package/src/test-transport.ts +6 -1
- package/src/testing.ts +11 -0
- package/src/text-mirror.ts +27 -6
- package/src/transport.ts +58 -4
- package/src/uris.ts +17 -2
- package/src/vue.ts +15 -0
- package/src/watched-files.ts +14 -2
- package/src/workspace-operations.ts +51 -3
package/src/session.ts
CHANGED
|
@@ -115,10 +115,13 @@ function matches(options: ProviderOptions, document?: Pick<DocumentItem, 'uri' |
|
|
|
115
115
|
if (options.documentSelector == null) {
|
|
116
116
|
return true;
|
|
117
117
|
}
|
|
118
|
+
|
|
118
119
|
if (!document) {
|
|
119
120
|
return options.documentSelector.length > 0;
|
|
120
121
|
}
|
|
122
|
+
|
|
121
123
|
const url = new URL(document.uri);
|
|
124
|
+
|
|
122
125
|
return options.documentSelector.some(
|
|
123
126
|
(filter) =>
|
|
124
127
|
(!filter.language || filter.language === document.languageId) &&
|
|
@@ -132,18 +135,23 @@ function offersMethod(method: string, options: ProviderOptions): boolean {
|
|
|
132
135
|
if (method.endsWith('/resolve')) {
|
|
133
136
|
return options.resolveProvider === true;
|
|
134
137
|
}
|
|
138
|
+
|
|
135
139
|
if (method === 'textDocument/prepareRename') {
|
|
136
140
|
return options.prepareProvider === true;
|
|
137
141
|
}
|
|
142
|
+
|
|
138
143
|
if (method === 'textDocument/semanticTokens/full/delta') {
|
|
139
144
|
return typeof options.full === 'object' && options.full.delta === true;
|
|
140
145
|
}
|
|
146
|
+
|
|
141
147
|
if (method === 'textDocument/semanticTokens/full') {
|
|
142
148
|
return !!options.full;
|
|
143
149
|
}
|
|
150
|
+
|
|
144
151
|
if (method === 'textDocument/semanticTokens/range') {
|
|
145
152
|
return !!options.range;
|
|
146
153
|
}
|
|
154
|
+
|
|
147
155
|
return true;
|
|
148
156
|
}
|
|
149
157
|
|
|
@@ -166,54 +174,75 @@ export class LspSession {
|
|
|
166
174
|
this.options = options;
|
|
167
175
|
this.connection = new JsonRpcConnection(transport, options.timeoutMs);
|
|
168
176
|
this.configuration = options.configuration ?? {};
|
|
177
|
+
|
|
169
178
|
this.connection.onClose(() => {
|
|
170
179
|
this.phase = 'closed';
|
|
180
|
+
|
|
171
181
|
for (const document of [...this.documents.values()]) {
|
|
172
182
|
document.disconnect();
|
|
173
183
|
}
|
|
174
184
|
});
|
|
185
|
+
|
|
175
186
|
this.connection.onNotification('textDocument/publishDiagnostics', (params) => {
|
|
176
187
|
const diagnostics = params as PublishDiagnosticsParams;
|
|
177
188
|
const document = this.documents.get(fileUriKey(diagnostics.uri));
|
|
189
|
+
|
|
178
190
|
if (!document || document.isClosed || (diagnostics.version !== undefined && diagnostics.version !== document.version)) {
|
|
179
191
|
return;
|
|
180
192
|
}
|
|
193
|
+
|
|
181
194
|
this.emitDiagnostics({ ...diagnostics, uri: document.uri });
|
|
182
195
|
});
|
|
196
|
+
|
|
183
197
|
this.connection.onRequest('workspace/configuration', async (params) => {
|
|
184
198
|
const { items } = params as { items: ConfigurationItem[] };
|
|
199
|
+
|
|
185
200
|
return Promise.all(items.map((item) => (options.onConfiguration ? options.onConfiguration(item) : this.readConfiguration(item.section))));
|
|
186
201
|
});
|
|
202
|
+
|
|
187
203
|
this.connection.onRequest('workspace/workspaceFolders', () => this.workspaceFolders);
|
|
204
|
+
|
|
188
205
|
this.connection.onRequest(
|
|
189
206
|
'workspace/applyEdit',
|
|
190
207
|
(params) => options.onApplyEdit?.(params as ApplyWorkspaceEditParams) ?? { applied: false, failureReason: 'No workspace edit handler is attached' }
|
|
191
208
|
);
|
|
209
|
+
|
|
192
210
|
this.connection.onRequest(
|
|
193
211
|
'window/showMessageRequest',
|
|
194
212
|
(params) => options.onShowMessage?.(params as Parameters<NonNullable<LspSessionOptions['onShowMessage']>>[0]) ?? null
|
|
195
213
|
);
|
|
214
|
+
|
|
196
215
|
this.connection.onRequest('window/workDoneProgress/create', () => null);
|
|
216
|
+
|
|
197
217
|
this.connection.onRequest('client/registerCapability', (params) => {
|
|
198
218
|
const { registrations } = params as { registrations: Registration[] };
|
|
219
|
+
|
|
199
220
|
for (const registration of registrations) {
|
|
200
221
|
this.registrations.set(registration.id, registration);
|
|
201
222
|
}
|
|
223
|
+
|
|
202
224
|
this.emitCapabilities();
|
|
225
|
+
|
|
203
226
|
return null;
|
|
204
227
|
});
|
|
228
|
+
|
|
205
229
|
this.connection.onRequest('client/unregisterCapability', (params) => {
|
|
206
230
|
// The misspelled field is part of the LSP 3.17 wire protocol.
|
|
207
231
|
const { unregisterations } = params as { unregisterations: { id: string; method: string }[] };
|
|
232
|
+
|
|
208
233
|
for (const registration of unregisterations) {
|
|
209
234
|
this.registrations.delete(registration.id);
|
|
210
235
|
}
|
|
236
|
+
|
|
211
237
|
this.emitCapabilities();
|
|
238
|
+
|
|
212
239
|
return null;
|
|
213
240
|
});
|
|
241
|
+
|
|
214
242
|
for (const method of REFRESH_REQUESTS) {
|
|
215
243
|
this.connection.onRequest(method, () => {
|
|
216
244
|
this.emitCapabilities();
|
|
245
|
+
|
|
217
246
|
return null;
|
|
218
247
|
});
|
|
219
248
|
}
|
|
@@ -233,6 +262,7 @@ export class LspSession {
|
|
|
233
262
|
|
|
234
263
|
get syncOptions(): TextDocumentSyncOptions {
|
|
235
264
|
const sync = this.serverCapabilities.textDocumentSync;
|
|
265
|
+
|
|
236
266
|
return typeof sync === 'number' ? { openClose: sync !== 0, change: sync } : (sync ?? {});
|
|
237
267
|
}
|
|
238
268
|
|
|
@@ -240,10 +270,13 @@ export class LspSession {
|
|
|
240
270
|
if (this.initialized) {
|
|
241
271
|
return this.initialized;
|
|
242
272
|
}
|
|
273
|
+
|
|
243
274
|
if (this.phase !== 'new') {
|
|
244
275
|
return Promise.reject(new LspError(`Cannot initialize a ${this.phase} session`));
|
|
245
276
|
}
|
|
277
|
+
|
|
246
278
|
this.phase = 'initializing';
|
|
279
|
+
|
|
247
280
|
this.initialized = (async () => {
|
|
248
281
|
try {
|
|
249
282
|
const result = await this.connection.request<InitializeResult>('initialize', {
|
|
@@ -254,33 +287,43 @@ export class LspSession {
|
|
|
254
287
|
capabilities: clientCapabilities(this.options),
|
|
255
288
|
initializationOptions: this.options.initializationOptions
|
|
256
289
|
});
|
|
290
|
+
|
|
257
291
|
if (result.capabilities.positionEncoding && result.capabilities.positionEncoding !== 'utf-16') {
|
|
258
292
|
throw new LspError(`Unsupported server position encoding: ${result.capabilities.positionEncoding}`);
|
|
259
293
|
}
|
|
294
|
+
|
|
260
295
|
this.serverCapabilities = result.capabilities;
|
|
261
296
|
this.serverInfo = result.serverInfo;
|
|
262
297
|
await this.connection.notify('initialized', {});
|
|
263
298
|
this.phase = 'ready';
|
|
299
|
+
|
|
264
300
|
if (this.options.configuration !== undefined) {
|
|
265
301
|
await this.connection.notify('workspace/didChangeConfiguration', { settings: this.configuration });
|
|
266
302
|
}
|
|
303
|
+
|
|
267
304
|
this.emitCapabilities();
|
|
305
|
+
|
|
268
306
|
return result;
|
|
269
307
|
} catch (error) {
|
|
270
308
|
await this.connection.close();
|
|
309
|
+
|
|
271
310
|
throw error;
|
|
272
311
|
}
|
|
273
312
|
})();
|
|
313
|
+
|
|
274
314
|
return this.initialized;
|
|
275
315
|
}
|
|
276
316
|
|
|
277
317
|
openDocument(item: DocumentItem): LspDocument {
|
|
278
318
|
this.assertReady();
|
|
319
|
+
|
|
279
320
|
if (this.documents.has(fileUriKey(item.uri))) {
|
|
280
321
|
throw new LspError(`Document already open: ${item.uri}`);
|
|
281
322
|
}
|
|
323
|
+
|
|
282
324
|
const document = new LspDocument(this, item);
|
|
283
325
|
this.documents.set(fileUriKey(item.uri), document);
|
|
326
|
+
|
|
284
327
|
return document;
|
|
285
328
|
}
|
|
286
329
|
|
|
@@ -292,17 +335,21 @@ export class LspSession {
|
|
|
292
335
|
providerOptions(method: string, document?: Pick<DocumentItem, 'uri' | 'languageId'>): ProviderOptions | undefined {
|
|
293
336
|
const name = REGISTRATION_METHODS[method] ?? method;
|
|
294
337
|
const candidates: ProviderOptions[] = [];
|
|
338
|
+
|
|
295
339
|
for (const registration of this.registrations.values()) {
|
|
296
340
|
if (registration.method === name && matches(registration.registerOptions ?? {}, document)) {
|
|
297
341
|
candidates.push(registration.registerOptions ?? {});
|
|
298
342
|
}
|
|
299
343
|
}
|
|
344
|
+
|
|
300
345
|
const provider = this.serverCapabilities[PROVIDERS[method]];
|
|
346
|
+
|
|
301
347
|
if (provider === true) {
|
|
302
348
|
candidates.push({});
|
|
303
349
|
} else if (provider && typeof provider === 'object' && matches(provider as ProviderOptions, document)) {
|
|
304
350
|
candidates.push(provider as ProviderOptions);
|
|
305
351
|
}
|
|
352
|
+
|
|
306
353
|
return candidates.find((options) => offersMethod(method, options));
|
|
307
354
|
}
|
|
308
355
|
|
|
@@ -312,11 +359,13 @@ export class LspSession {
|
|
|
312
359
|
|
|
313
360
|
request<T = unknown>(method: string, params?: unknown, options?: RequestOptions): Promise<T> {
|
|
314
361
|
this.assertReady();
|
|
362
|
+
|
|
315
363
|
return this.connection.request<T>(method, params, options);
|
|
316
364
|
}
|
|
317
365
|
|
|
318
366
|
notify(method: string, params?: unknown): Promise<void> {
|
|
319
367
|
this.assertReady();
|
|
368
|
+
|
|
320
369
|
return this.connection.notify(method, params);
|
|
321
370
|
}
|
|
322
371
|
|
|
@@ -324,6 +373,7 @@ export class LspSession {
|
|
|
324
373
|
if (!this.supports('workspace/executeCommand')) {
|
|
325
374
|
return Promise.reject(new LspError('Server does not support commands', ErrorCodes.MethodNotFound));
|
|
326
375
|
}
|
|
376
|
+
|
|
327
377
|
return this.request('workspace/executeCommand', typeof command === 'string' ? { command, arguments: args } : command, options);
|
|
328
378
|
}
|
|
329
379
|
|
|
@@ -331,6 +381,7 @@ export class LspSession {
|
|
|
331
381
|
if (!this.supports('workspace/symbol')) {
|
|
332
382
|
return Promise.reject(new LspError('Server does not support workspace symbols', ErrorCodes.MethodNotFound));
|
|
333
383
|
}
|
|
384
|
+
|
|
334
385
|
return this.request('workspace/symbol', { query }, options);
|
|
335
386
|
}
|
|
336
387
|
|
|
@@ -338,6 +389,7 @@ export class LspSession {
|
|
|
338
389
|
if (!this.supports('workspaceSymbol/resolve')) {
|
|
339
390
|
return Promise.reject(new LspError('Server does not support workspace symbol resolution', ErrorCodes.MethodNotFound));
|
|
340
391
|
}
|
|
392
|
+
|
|
341
393
|
return this.request('workspaceSymbol/resolve', symbol, options);
|
|
342
394
|
}
|
|
343
395
|
|
|
@@ -364,6 +416,7 @@ export class LspSession {
|
|
|
364
416
|
|
|
365
417
|
onDiagnostics(listener: (params: PublishDiagnosticsParams) => void): Disposable {
|
|
366
418
|
this.diagnosticListeners.add(listener);
|
|
419
|
+
|
|
367
420
|
return {
|
|
368
421
|
dispose: () => {
|
|
369
422
|
this.diagnosticListeners.delete(listener);
|
|
@@ -387,6 +440,7 @@ export class LspSession {
|
|
|
387
440
|
fileOperationFilters(operation: FileOperation): FileOperationFilter[] {
|
|
388
441
|
const fileOperations = (this.serverCapabilities.workspace as { fileOperations?: Record<string, ProviderOptions> } | undefined)?.fileOperations;
|
|
389
442
|
const method = `workspace/${operation}Files`;
|
|
443
|
+
|
|
390
444
|
return [
|
|
391
445
|
...(fileOperations?.[operation]?.filters ?? []),
|
|
392
446
|
...[...this.registrations.values()]
|
|
@@ -397,49 +451,38 @@ export class LspSession {
|
|
|
397
451
|
|
|
398
452
|
/* What the server would change before these files move, or null when it takes none of them or has nothing to say. */
|
|
399
453
|
async willRenameFiles(files: readonly RenamedFile[], options?: RequestOptions): Promise<WorkspaceEdit | null> {
|
|
400
|
-
|
|
401
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request<WorkspaceEdit | null>('workspace/willRenameFiles', { files: taken }, options);
|
|
454
|
+
return this.askBefore('willRename', renamesTaken(this.fileOperationFilters('willRename'), files), options);
|
|
402
455
|
}
|
|
403
456
|
|
|
404
457
|
/* Tells the server which of these files moved, those its filters take. */
|
|
405
458
|
async didRenameFiles(files: readonly RenamedFile[]): Promise<void> {
|
|
406
|
-
|
|
407
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
408
|
-
await this.notify('workspace/didRenameFiles', { files: taken });
|
|
409
|
-
}
|
|
459
|
+
await this.tellAfter('didRename', renamesTaken(this.fileOperationFilters('didRename'), files));
|
|
410
460
|
}
|
|
411
461
|
|
|
412
462
|
/* What the server would change before these files are made, or null when it takes none of them or has nothing to say. */
|
|
413
463
|
async willCreateFiles(files: readonly CreatedFile[], options?: RequestOptions): Promise<WorkspaceEdit | null> {
|
|
414
|
-
|
|
415
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request<WorkspaceEdit | null>('workspace/willCreateFiles', { files: taken }, options);
|
|
464
|
+
return this.askBefore('willCreate', createsTaken(this.fileOperationFilters('willCreate'), files), options);
|
|
416
465
|
}
|
|
417
466
|
|
|
418
467
|
/* Tells the server which of these files were made, those its filters take. */
|
|
419
468
|
async didCreateFiles(files: readonly CreatedFile[]): Promise<void> {
|
|
420
|
-
|
|
421
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
422
|
-
await this.notify('workspace/didCreateFiles', { files: taken });
|
|
423
|
-
}
|
|
469
|
+
await this.tellAfter('didCreate', createsTaken(this.fileOperationFilters('didCreate'), files));
|
|
424
470
|
}
|
|
425
471
|
|
|
426
472
|
/* What the server would change before these files are deleted, or null when it takes none of them or has nothing to say. */
|
|
427
473
|
async willDeleteFiles(files: readonly DeletedFile[], options?: RequestOptions): Promise<WorkspaceEdit | null> {
|
|
428
|
-
|
|
429
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request<WorkspaceEdit | null>('workspace/willDeleteFiles', { files: taken }, options);
|
|
474
|
+
return this.askBefore('willDelete', deletesTaken(this.fileOperationFilters('willDelete'), files), options);
|
|
430
475
|
}
|
|
431
476
|
|
|
432
477
|
/* Tells the server which of these files were deleted, those its filters take. */
|
|
433
478
|
async didDeleteFiles(files: readonly DeletedFile[]): Promise<void> {
|
|
434
|
-
|
|
435
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
436
|
-
await this.notify('workspace/didDeleteFiles', { files: taken });
|
|
437
|
-
}
|
|
479
|
+
await this.tellAfter('didDelete', deletesTaken(this.fileOperationFilters('didDelete'), files));
|
|
438
480
|
}
|
|
439
481
|
|
|
440
482
|
/* Registrations and the refresh requests of the server both land here, since each can change what a document may ask. */
|
|
441
483
|
onCapabilitiesChanged(listener: () => void): Disposable {
|
|
442
484
|
this.capabilityListeners.add(listener);
|
|
485
|
+
|
|
443
486
|
return {
|
|
444
487
|
dispose: () => {
|
|
445
488
|
this.capabilityListeners.delete(listener);
|
|
@@ -449,6 +492,7 @@ export class LspSession {
|
|
|
449
492
|
|
|
450
493
|
shutdown(): Promise<void> {
|
|
451
494
|
this.shutdownPromise ??= this.stop();
|
|
495
|
+
|
|
452
496
|
return this.shutdownPromise;
|
|
453
497
|
}
|
|
454
498
|
|
|
@@ -457,6 +501,7 @@ export class LspSession {
|
|
|
457
501
|
if (this.documents.get(fileUriKey(document.uri)) !== document) {
|
|
458
502
|
return;
|
|
459
503
|
}
|
|
504
|
+
|
|
460
505
|
this.documents.delete(fileUriKey(document.uri));
|
|
461
506
|
this.emitDiagnostics({ uri: document.uri, diagnostics: [] });
|
|
462
507
|
}
|
|
@@ -467,11 +512,26 @@ export class LspSession {
|
|
|
467
512
|
}
|
|
468
513
|
}
|
|
469
514
|
|
|
515
|
+
private async askBefore(
|
|
516
|
+
operation: 'willRename' | 'willCreate' | 'willDelete',
|
|
517
|
+
files: readonly object[],
|
|
518
|
+
options?: RequestOptions
|
|
519
|
+
): Promise<WorkspaceEdit | null> {
|
|
520
|
+
return files.length === 0 || this.phase !== 'ready' ? null : this.request<WorkspaceEdit | null>(`workspace/${operation}Files`, { files }, options);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private async tellAfter(operation: 'didRename' | 'didCreate' | 'didDelete', files: readonly object[]): Promise<void> {
|
|
524
|
+
if (files.length > 0 && this.phase === 'ready') {
|
|
525
|
+
await this.notify(`workspace/${operation}Files`, { files });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
470
529
|
private async stop(): Promise<void> {
|
|
471
530
|
try {
|
|
472
531
|
if (this.phase === 'initializing') {
|
|
473
532
|
await this.initialized;
|
|
474
533
|
}
|
|
534
|
+
|
|
475
535
|
if (this.phase === 'ready') {
|
|
476
536
|
this.phase = 'closing';
|
|
477
537
|
await Promise.all([...this.documents.values()].map((document) => document.close()));
|
|
@@ -488,16 +548,21 @@ export class LspSession {
|
|
|
488
548
|
if (!section) {
|
|
489
549
|
return this.configuration;
|
|
490
550
|
}
|
|
551
|
+
|
|
491
552
|
if (Object.hasOwn(this.configuration, section)) {
|
|
492
553
|
return this.configuration[section];
|
|
493
554
|
}
|
|
555
|
+
|
|
494
556
|
let result: unknown = this.configuration;
|
|
557
|
+
|
|
495
558
|
for (const key of section.split('.')) {
|
|
496
559
|
if (!result || typeof result !== 'object' || !Object.hasOwn(result, key)) {
|
|
497
560
|
return null;
|
|
498
561
|
}
|
|
562
|
+
|
|
499
563
|
result = (result as Record<string, unknown>)[key];
|
|
500
564
|
}
|
|
565
|
+
|
|
501
566
|
return result ?? null;
|
|
502
567
|
}
|
|
503
568
|
|
|
@@ -524,6 +589,7 @@ export class LspSession {
|
|
|
524
589
|
function clientCapabilities(options: LspSessionOptions): object {
|
|
525
590
|
const dynamic = { dynamicRegistration: true };
|
|
526
591
|
const symbolKind = { valueSet: Array.from({ length: 26 }, (_, index) => index + 1) };
|
|
592
|
+
|
|
527
593
|
return {
|
|
528
594
|
general: { positionEncodings: ['utf-16'] },
|
|
529
595
|
workspace: {
|
package/src/test-transport.ts
CHANGED
|
@@ -10,9 +10,11 @@ export class ScriptedTransport implements LspTransport {
|
|
|
10
10
|
|
|
11
11
|
send(message: RpcMessage): void {
|
|
12
12
|
this.sent.push(message);
|
|
13
|
+
|
|
13
14
|
if ('method' in message && message.method === 'initialize' && 'id' in message && this.autoInitialize) {
|
|
14
15
|
queueMicrotask(() => this.emit({ jsonrpc: '2.0', id: message.id, result: { capabilities: this.autoInitialize } }));
|
|
15
16
|
}
|
|
17
|
+
|
|
16
18
|
if ('method' in message && message.method === 'shutdown' && 'id' in message) {
|
|
17
19
|
queueMicrotask(() => this.emit({ jsonrpc: '2.0', id: message.id, result: null }));
|
|
18
20
|
}
|
|
@@ -20,6 +22,7 @@ export class ScriptedTransport implements LspTransport {
|
|
|
20
22
|
|
|
21
23
|
onMessage(listener: (message: unknown) => void) {
|
|
22
24
|
this.messages.add(listener);
|
|
25
|
+
|
|
23
26
|
return {
|
|
24
27
|
dispose: () => {
|
|
25
28
|
this.messages.delete(listener);
|
|
@@ -29,6 +32,7 @@ export class ScriptedTransport implements LspTransport {
|
|
|
29
32
|
|
|
30
33
|
onClose(listener: (error?: Error) => void) {
|
|
31
34
|
this.closes.add(listener);
|
|
35
|
+
|
|
32
36
|
return {
|
|
33
37
|
dispose: () => {
|
|
34
38
|
this.closes.delete(listener);
|
|
@@ -50,7 +54,7 @@ export class ScriptedTransport implements LspTransport {
|
|
|
50
54
|
|
|
51
55
|
/* The last request of `method` the client sent. */
|
|
52
56
|
request(method: string) {
|
|
53
|
-
return
|
|
57
|
+
return this.sent.findLast((message) => 'method' in message && message.method === method && 'id' in message) as {
|
|
54
58
|
id: number;
|
|
55
59
|
params: unknown;
|
|
56
60
|
};
|
|
@@ -78,6 +82,7 @@ export async function sessionWith(capabilities: ServerCapabilities = {}, options
|
|
|
78
82
|
transport.autoInitialize = { textDocumentSync: 2, ...capabilities };
|
|
79
83
|
const session = new LspSession(transport, options);
|
|
80
84
|
await session.initialize();
|
|
85
|
+
|
|
81
86
|
return { transport, session };
|
|
82
87
|
}
|
|
83
88
|
|
package/src/testing.ts
CHANGED
|
@@ -39,39 +39,50 @@ export class FakeLanguageServer {
|
|
|
39
39
|
|
|
40
40
|
constructor(transport: LspTransport, options: FakeLanguageServerOptions = {}) {
|
|
41
41
|
this.connection = new JsonRpcConnection(transport, 0);
|
|
42
|
+
|
|
42
43
|
transport.onMessage((message) => {
|
|
43
44
|
const { method, params, id } = message as ReceivedMessage;
|
|
45
|
+
|
|
44
46
|
if (typeof method === 'string') {
|
|
45
47
|
this.received.push({ method, params, id });
|
|
46
48
|
}
|
|
47
49
|
});
|
|
50
|
+
|
|
48
51
|
this.connection.onRequest('initialize', () =>
|
|
49
52
|
options.silent
|
|
50
53
|
? new Promise<never>(() => undefined)
|
|
51
54
|
: { capabilities: options.capabilities ?? { textDocumentSync: 2 }, serverInfo: options.serverInfo }
|
|
52
55
|
);
|
|
56
|
+
|
|
53
57
|
this.connection.onRequest('shutdown', () => {
|
|
54
58
|
this.shutdownRequested = true;
|
|
59
|
+
|
|
55
60
|
return null;
|
|
56
61
|
});
|
|
62
|
+
|
|
57
63
|
this.connection.onNotification('exit', () => {
|
|
58
64
|
this.exited = true;
|
|
59
65
|
});
|
|
66
|
+
|
|
60
67
|
this.connection.onNotification('textDocument/didOpen', (params) => {
|
|
61
68
|
const { textDocument } = params as { textDocument: { uri: string; languageId: string; text: string; version: number } };
|
|
62
69
|
this.documents.set(textDocument.uri, { languageId: textDocument.languageId, text: textDocument.text, version: textDocument.version });
|
|
63
70
|
});
|
|
71
|
+
|
|
64
72
|
this.connection.onNotification('textDocument/didChange', (params) => {
|
|
65
73
|
const { textDocument, contentChanges } = params as { textDocument: { uri: string; version: number }; contentChanges: ContentChange[] };
|
|
66
74
|
const document = this.documents.get(textDocument.uri);
|
|
75
|
+
|
|
67
76
|
if (document) {
|
|
68
77
|
document.text = applyContentChanges(document.text, contentChanges);
|
|
69
78
|
document.version = textDocument.version;
|
|
70
79
|
}
|
|
71
80
|
});
|
|
81
|
+
|
|
72
82
|
this.connection.onNotification('textDocument/didClose', (params) => {
|
|
73
83
|
this.documents.delete((params as { textDocument: { uri: string } }).textDocument.uri);
|
|
74
84
|
});
|
|
85
|
+
|
|
75
86
|
for (const [method, handler] of Object.entries(options.handlers ?? {})) {
|
|
76
87
|
this.connection.onRequest(method, handler);
|
|
77
88
|
}
|
package/src/text-mirror.ts
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { LspError } from './connection.ts';
|
|
2
|
+
import { CR, LF, checkPosition } from './edits.ts';
|
|
2
3
|
import type { ContentChange, Position } from './protocol.ts';
|
|
3
4
|
|
|
4
|
-
const CR = 13;
|
|
5
|
-
const LF = 10;
|
|
6
|
-
|
|
7
5
|
/* Past this many lines a spread into `splice` could exceed the engine's argument limit. */
|
|
8
6
|
const SPREAD_LIMIT = 10_000;
|
|
9
7
|
|
|
@@ -11,25 +9,32 @@ const SPREAD_LIMIT = 10_000;
|
|
|
11
9
|
function splitLines(text: string): string[] {
|
|
12
10
|
const lines: string[] = [];
|
|
13
11
|
let start = 0;
|
|
12
|
+
|
|
14
13
|
for (let i = 0; i < text.length; i++) {
|
|
15
14
|
const code = text.charCodeAt(i);
|
|
15
|
+
|
|
16
16
|
if (code === CR || code === LF) {
|
|
17
17
|
if (code === CR && text.charCodeAt(i + 1) === LF) {
|
|
18
18
|
i++;
|
|
19
19
|
}
|
|
20
|
+
|
|
20
21
|
lines.push(text.slice(start, i + 1));
|
|
21
22
|
start = i + 1;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
25
|
+
|
|
24
26
|
lines.push(text.slice(start));
|
|
27
|
+
|
|
25
28
|
return lines;
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
function breakLength(line: string): number {
|
|
29
32
|
const last = line.charCodeAt(line.length - 1);
|
|
33
|
+
|
|
30
34
|
if (last === LF) {
|
|
31
35
|
return line.charCodeAt(line.length - 2) === CR ? 2 : 1;
|
|
32
36
|
}
|
|
37
|
+
|
|
33
38
|
return last === CR ? 1 : 0;
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -54,6 +59,7 @@ export class TextMirror {
|
|
|
54
59
|
|
|
55
60
|
get text(): string {
|
|
56
61
|
this.joined ??= this.lines.join('');
|
|
62
|
+
|
|
57
63
|
return this.joined;
|
|
58
64
|
}
|
|
59
65
|
|
|
@@ -64,6 +70,7 @@ export class TextMirror {
|
|
|
64
70
|
/* Applies the entries in order, each in the text the one before it left. When one does not fit, it throws and the text stays as it was. */
|
|
65
71
|
applyChanges(changes: readonly ContentChange[]): void {
|
|
66
72
|
const applied: Splice[] = [];
|
|
73
|
+
|
|
67
74
|
try {
|
|
68
75
|
for (const change of changes) {
|
|
69
76
|
applied.push(
|
|
@@ -71,13 +78,16 @@ export class TextMirror {
|
|
|
71
78
|
? this.applyRange(change.range.start, change.range.end, change.text)
|
|
72
79
|
: this.replace(0, this.lines.length, splitLines(change.text))
|
|
73
80
|
);
|
|
81
|
+
|
|
74
82
|
this.joined = change.range ? null : change.text;
|
|
75
83
|
}
|
|
76
84
|
} catch (error) {
|
|
77
85
|
for (const splice of applied.reverse()) {
|
|
78
86
|
this.replace(splice.start, splice.inserted, splice.removed);
|
|
79
87
|
}
|
|
88
|
+
|
|
80
89
|
this.joined = null;
|
|
90
|
+
|
|
81
91
|
throw error;
|
|
82
92
|
}
|
|
83
93
|
}
|
|
@@ -85,53 +95,64 @@ export class TextMirror {
|
|
|
85
95
|
private applyRange(start: Position, end: Position, text: string): Splice {
|
|
86
96
|
const startLine = this.lineAt(start);
|
|
87
97
|
const endLine = this.lineAt(end);
|
|
98
|
+
|
|
88
99
|
if (end.line < start.line || (end.line === start.line && end.character < start.character)) {
|
|
89
100
|
throw new LspError('Change range is reversed');
|
|
90
101
|
}
|
|
102
|
+
|
|
91
103
|
const inserted = splitLines(text);
|
|
92
104
|
const last = inserted.length - 1;
|
|
93
105
|
let suffix = endLine.slice(end.character);
|
|
106
|
+
|
|
94
107
|
// An inserted `\r` and the `\n` after the range become one break.
|
|
95
108
|
if (text.charCodeAt(text.length - 1) === CR && suffix.charCodeAt(0) === LF) {
|
|
96
109
|
inserted[last - 1] += '\n';
|
|
97
110
|
suffix = suffix.slice(1);
|
|
98
111
|
}
|
|
112
|
+
|
|
99
113
|
inserted[last] += suffix;
|
|
100
114
|
inserted[0] = startLine.slice(0, start.character) + inserted[0];
|
|
115
|
+
|
|
101
116
|
if (inserted[last] === '' && end.line < this.lines.length - 1) {
|
|
102
117
|
inserted.pop();
|
|
103
118
|
}
|
|
119
|
+
|
|
104
120
|
let first = start.line;
|
|
121
|
+
|
|
105
122
|
// A line that ends in a lone `\r` takes a `\n` that now follows it as the rest of its break.
|
|
106
123
|
if (inserted[0] === '\n' && first > 0 && this.lines[first - 1].charCodeAt(this.lines[first - 1].length - 1) === CR) {
|
|
107
124
|
first--;
|
|
108
125
|
inserted[0] = this.lines[first] + '\n';
|
|
109
126
|
}
|
|
127
|
+
|
|
110
128
|
return this.replace(first, end.line - first + 1, inserted);
|
|
111
129
|
}
|
|
112
130
|
|
|
113
131
|
private lineAt(position: Position): string {
|
|
114
|
-
|
|
115
|
-
throw new LspError('Invalid edit position');
|
|
116
|
-
}
|
|
132
|
+
checkPosition(position);
|
|
117
133
|
const line = this.lines[position.line];
|
|
134
|
+
|
|
118
135
|
if (line === undefined) {
|
|
119
136
|
throw new LspError('Edit line is outside the document');
|
|
120
137
|
}
|
|
138
|
+
|
|
121
139
|
if (position.character > line.length - breakLength(line)) {
|
|
122
140
|
throw new LspError('Edit character is outside the line');
|
|
123
141
|
}
|
|
142
|
+
|
|
124
143
|
return line;
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
private replace(start: number, count: number, lines: string[]): Splice {
|
|
128
147
|
let removed: string[];
|
|
148
|
+
|
|
129
149
|
if (lines.length <= SPREAD_LIMIT) {
|
|
130
150
|
removed = this.lines.splice(start, count, ...lines);
|
|
131
151
|
} else {
|
|
132
152
|
removed = this.lines.slice(start, start + count);
|
|
133
153
|
this.lines = [...this.lines.slice(0, start), ...lines, ...this.lines.slice(start + count)];
|
|
134
154
|
}
|
|
155
|
+
|
|
135
156
|
return { start, removed, inserted: lines.length };
|
|
136
157
|
}
|
|
137
158
|
}
|