@adecore/lsp 0.39.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/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
- const taken = renamesTaken(this.fileOperationFilters('willRename'), files);
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
- const taken = renamesTaken(this.fileOperationFilters('didRename'), files);
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
- const taken = createsTaken(this.fileOperationFilters('willCreate'), files);
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
- const taken = createsTaken(this.fileOperationFilters('didCreate'), files);
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
- const taken = deletesTaken(this.fileOperationFilters('willDelete'), files);
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
- const taken = deletesTaken(this.fileOperationFilters('didDelete'), files);
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: {
@@ -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 [...this.sent].reverse().find((message) => 'method' in message && message.method === method && 'id' in message) as {
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
  }
@@ -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
- if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
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
  }