@adecore/lsp 0.21.1 → 0.21.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.
@@ -0,0 +1 @@
1
+ export declare function asError(error: unknown): Error;
@@ -0,0 +1,3 @@
1
+ export function asError(error) {
2
+ return error instanceof Error ? error : new Error(String(error));
3
+ }
@@ -1,3 +1,4 @@
1
+ import { asError } from './as-error.js';
1
2
  /* The JSON-RPC and LSP codes this package answers with. */
2
3
  export const ErrorCodes = {
3
4
  InvalidRequest: -32600,
@@ -26,8 +27,8 @@ export class StaleResultError extends LspError {
26
27
  this.uri = uri;
27
28
  }
28
29
  }
29
- function asError(error) {
30
- return error instanceof Error ? error : new Error(String(error));
30
+ function cancelledError() {
31
+ return new LspError('Request cancelled', ErrorCodes.RequestCancelled);
31
32
  }
32
33
  /* JSON-RPC 2.0 over an `LspTransport`: correlation, cancellation, and the requests a server sends back. */
33
34
  export class JsonRpcConnection {
@@ -62,7 +63,7 @@ export class JsonRpcConnection {
62
63
  return Promise.reject(new LspError('LSP connection is closed'));
63
64
  }
64
65
  if (options.signal?.aborted) {
65
- return Promise.reject(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
66
+ return Promise.reject(cancelledError());
66
67
  }
67
68
  const id = ++this.nextId;
68
69
  return new Promise((resolve, reject) => {
@@ -74,7 +75,7 @@ export class JsonRpcConnection {
74
75
  this.settle(id, undefined, error);
75
76
  void this.notify('$/cancelRequest', { id }).catch((failure) => this.reportError(failure));
76
77
  };
77
- const abort = () => cancel(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
78
+ const abort = () => cancel(cancelledError());
78
79
  this.pending.set(id, {
79
80
  resolve: (value) => resolve(value),
80
81
  reject,
@@ -108,17 +109,13 @@ export class JsonRpcConnection {
108
109
  };
109
110
  }
110
111
  onNotification(method, listener) {
111
- let listeners = this.notifications.get(method);
112
- if (!listeners) {
113
- listeners = new Set();
114
- this.notifications.set(method, listeners);
115
- }
116
- const registered = listeners;
117
- registered.add(listener);
112
+ const listeners = this.notifications.get(method) ?? new Set();
113
+ this.notifications.set(method, listeners);
114
+ listeners.add(listener);
118
115
  return {
119
116
  dispose: () => {
120
- registered.delete(listener);
121
- if (registered.size === 0) {
117
+ listeners.delete(listener);
118
+ if (listeners.size === 0) {
122
119
  this.notifications.delete(method);
123
120
  }
124
121
  }
@@ -242,7 +239,7 @@ export class JsonRpcConnection {
242
239
  }
243
240
  const result = await handler(message.params, controller.signal);
244
241
  if (controller.signal.aborted) {
245
- throw new LspError('Request cancelled', ErrorCodes.RequestCancelled);
242
+ throw cancelledError();
246
243
  }
247
244
  await this.send({ jsonrpc: '2.0', id: message.id, result: result ?? null });
248
245
  }
package/dist/edits.js CHANGED
@@ -1,4 +1,6 @@
1
1
  import { ErrorCodes, LspError } from './connection.js';
2
+ const CR = 13;
3
+ const LF = 10;
2
4
  /* A line ends at `\n`, `\r\n` or a lone `\r`, which is how the protocol counts lines. */
3
5
  export function offsetAt(text, position) {
4
6
  if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
@@ -8,13 +10,13 @@ export function offsetAt(text, position) {
8
10
  let offset = 0;
9
11
  while (line < position.line && offset < text.length) {
10
12
  const char = text.charCodeAt(offset++);
11
- if (char === 13) {
12
- if (text.charCodeAt(offset) === 10) {
13
+ if (char === CR) {
14
+ if (text.charCodeAt(offset) === LF) {
13
15
  offset++;
14
16
  }
15
17
  line++;
16
18
  }
17
- else if (char === 10) {
19
+ else if (char === LF) {
18
20
  line++;
19
21
  }
20
22
  }
@@ -39,7 +41,7 @@ export function positionAt(text, offset) {
39
41
  let i = 0;
40
42
  while (i < offset) {
41
43
  const code = text.charCodeAt(i);
42
- if (code === 13 && text.charCodeAt(i + 1) === 10) {
44
+ if (code === CR && text.charCodeAt(i + 1) === LF) {
43
45
  if (i + 2 > offset) {
44
46
  break;
45
47
  }
@@ -47,7 +49,7 @@ export function positionAt(text, offset) {
47
49
  line++;
48
50
  lineStart = i;
49
51
  }
50
- else if (code === 13 || code === 10) {
52
+ else if (code === CR || code === LF) {
51
53
  i++;
52
54
  line++;
53
55
  lineStart = i;
package/dist/session.d.ts CHANGED
@@ -75,4 +75,5 @@ export declare class LspSession {
75
75
  private readConfiguration;
76
76
  private emitDiagnostics;
77
77
  private emitCapabilities;
78
+ private emit;
78
79
  }
package/dist/session.js CHANGED
@@ -60,6 +60,25 @@ function matches(options, document) {
60
60
  (!filter.scheme || filter.scheme === url.protocol.slice(0, -1)) &&
61
61
  (!filter.pattern || globMatch(decodeURIComponent(url.pathname), filter.pattern)));
62
62
  }
63
+ /* Whether provider options cover a method that a provider only offers on top of its feature. */
64
+ function offersMethod(method, options) {
65
+ if (method.endsWith('/resolve')) {
66
+ return options.resolveProvider === true;
67
+ }
68
+ if (method === 'textDocument/prepareRename') {
69
+ return options.prepareProvider === true;
70
+ }
71
+ if (method === 'textDocument/semanticTokens/full/delta') {
72
+ return typeof options.full === 'object' && options.full.delta === true;
73
+ }
74
+ if (method === 'textDocument/semanticTokens/full') {
75
+ return !!options.full;
76
+ }
77
+ if (method === 'textDocument/semanticTokens/range') {
78
+ return !!options.range;
79
+ }
80
+ return true;
81
+ }
63
82
  /* One conversation with one language server: the handshake, what it can do, and the documents open in it. */
64
83
  export class LspSession {
65
84
  connection;
@@ -203,24 +222,7 @@ export class LspSession {
203
222
  else if (provider && typeof provider === 'object' && matches(provider, document)) {
204
223
  candidates.push(provider);
205
224
  }
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
- });
225
+ return candidates.find((options) => offersMethod(method, options));
224
226
  }
225
227
  supports(method, document) {
226
228
  return this.phase === 'ready' && this.providerOptions(method, document) !== undefined;
@@ -368,19 +370,16 @@ export class LspSession {
368
370
  return result ?? null;
369
371
  }
370
372
  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
- }
373
+ this.emit(this.diagnosticListeners, params);
379
374
  }
380
375
  emitCapabilities() {
381
- for (const listener of this.capabilityListeners) {
376
+ this.emit(this.capabilityListeners, undefined);
377
+ }
378
+ /* A listener that throws is reported and does not keep the others from hearing it. */
379
+ emit(listeners, value) {
380
+ for (const listener of listeners) {
382
381
  try {
383
- listener();
382
+ listener(value);
384
383
  }
385
384
  catch (error) {
386
385
  this.connection.reportError(error);
package/dist/transport.js CHANGED
@@ -1,8 +1,6 @@
1
+ import { asError } from './as-error.js';
1
2
  const DEFAULT_MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
2
3
  const MAX_HEADER_BYTES = 8192;
3
- function asError(error) {
4
- return error instanceof Error ? error : new Error(String(error));
5
- }
6
4
  /*
7
5
  * The listeners of a transport. A message that arrives before anyone listens is held and handed to
8
6
  * the first listener, since a server may speak the moment its pipe opens.
@@ -13,7 +11,7 @@ class TransportEvents {
13
11
  closes = new Set();
14
12
  backlog = [];
15
13
  error;
16
- onMessage = (listener) => {
14
+ onMessage(listener) {
17
15
  this.messages.add(listener);
18
16
  for (const message of this.backlog.splice(0)) {
19
17
  listener(message);
@@ -23,8 +21,8 @@ class TransportEvents {
23
21
  this.messages.delete(listener);
24
22
  }
25
23
  };
26
- };
27
- onClose = (listener) => {
24
+ }
25
+ onClose(listener) {
28
26
  this.closes.add(listener);
29
27
  if (this.closed) {
30
28
  listener(this.error);
@@ -34,7 +32,7 @@ class TransportEvents {
34
32
  this.closes.delete(listener);
35
33
  }
36
34
  };
37
- };
35
+ }
38
36
  receive(message) {
39
37
  if (this.closed) {
40
38
  return;
@@ -73,9 +71,11 @@ function concat(chunks, size) {
73
71
  }
74
72
  return joined;
75
73
  }
74
+ const CR = 13;
75
+ const LF = 10;
76
76
  function indexOfHeaderEnd(bytes) {
77
77
  for (let i = 0; i + 3 < bytes.length; i++) {
78
- if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {
78
+ if (bytes[i] === CR && bytes[i + 1] === LF && bytes[i + 2] === CR && bytes[i + 3] === LF) {
79
79
  return i;
80
80
  }
81
81
  }
@@ -155,8 +155,8 @@ export function createStreamTransport(stream, options = {}) {
155
155
  });
156
156
  stream.onClose((error) => events.finish(error));
157
157
  return {
158
- onMessage: events.onMessage,
159
- onClose: events.onClose,
158
+ onMessage: (listener) => events.onMessage(listener),
159
+ onClose: (listener) => events.onClose(listener),
160
160
  async send(message) {
161
161
  if (events.closed) {
162
162
  throw new Error('LSP stream is closed');
@@ -174,8 +174,8 @@ export function createMemoryTransportPair() {
174
174
  const left = new TransportEvents();
175
175
  const right = new TransportEvents();
176
176
  const end = (inbox, outbox) => ({
177
- onMessage: inbox.onMessage,
178
- onClose: inbox.onClose,
177
+ onMessage: (listener) => inbox.onMessage(listener),
178
+ onClose: (listener) => inbox.onClose(listener),
179
179
  send(message) {
180
180
  if (inbox.closed || outbox.closed) {
181
181
  throw new Error('LSP transport is closed');
@@ -239,8 +239,8 @@ export async function connectWebSocket(url, options = {}) {
239
239
  options.signal?.addEventListener('abort', abort, { once: true });
240
240
  });
241
241
  return {
242
- onMessage: events.onMessage,
243
- onClose: events.onClose,
242
+ onMessage: (listener) => events.onMessage(listener),
243
+ onClose: (listener) => events.onClose(listener),
244
244
  send(message) {
245
245
  if (events.closed || socket.readyState !== 1) {
246
246
  throw new Error('LSP WebSocket is closed');
@@ -1,12 +1,13 @@
1
1
  import { globMatch } from './glob.js';
2
2
  import { fileUriToPath } from './uris.js';
3
+ const EVERY_CHANGE = 7;
3
4
  /* Folders whose files a server hears about only when a pattern of its own names the folder. */
4
5
  const SPARING_FOLDERS = ['node_modules', 'vendor', '.git'];
5
6
  function relativeTo(root, path) {
6
7
  const prefix = root.endsWith('/') ? root : `${root}/`;
7
8
  return path.startsWith(prefix) ? path.slice(prefix.length) : null;
8
9
  }
9
- /* A pattern with the folder it is relative to, or null for a relative pattern whose base is no folder of this machine. `base` is undefined for a string pattern. */
10
+ /* A pattern with the folder it is relative to, or null for a relative pattern whose base is no folder of this machine. */
10
11
  function patternOf(watcher) {
11
12
  const { globPattern } = watcher;
12
13
  if (typeof globPattern === 'string') {
@@ -24,7 +25,7 @@ export function watchesFile(watchers, root, path, type) {
24
25
  const inProject = relativeTo(root, path);
25
26
  const segments = (inProject ?? path).split('/');
26
27
  return watchers.some((watcher) => {
27
- if (((watcher.kind ?? 7) & (1 << (type - 1))) === 0) {
28
+ if (((watcher.kind ?? EVERY_CHANGE) & (1 << (type - 1))) === 0) {
28
29
  return false;
29
30
  }
30
31
  const parts = patternOf(watcher);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/lsp",
3
- "version": "0.21.1",
3
+ "version": "0.21.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -0,0 +1,3 @@
1
+ export function asError(error: unknown): Error {
2
+ return error instanceof Error ? error : new Error(String(error));
3
+ }
package/src/connection.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { asError } from './as-error.ts';
1
2
  import type { Disposable, LspTransport, RequestId, RequestOptions, RpcMessage, RpcRequest } from './protocol.ts';
2
3
 
3
4
  /* The JSON-RPC and LSP codes this package answers with. */
@@ -42,8 +43,8 @@ interface PendingRequest {
42
43
  cleanup(): void;
43
44
  }
44
45
 
45
- function asError(error: unknown): Error {
46
- return error instanceof Error ? error : new Error(String(error));
46
+ function cancelledError(): LspError {
47
+ return new LspError('Request cancelled', ErrorCodes.RequestCancelled);
47
48
  }
48
49
 
49
50
  /* JSON-RPC 2.0 over an `LspTransport`: correlation, cancellation, and the requests a server sends back. */
@@ -82,7 +83,7 @@ export class JsonRpcConnection {
82
83
  return Promise.reject(new LspError('LSP connection is closed'));
83
84
  }
84
85
  if (options.signal?.aborted) {
85
- return Promise.reject(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
86
+ return Promise.reject(cancelledError());
86
87
  }
87
88
  const id = ++this.nextId;
88
89
  return new Promise<T>((resolve, reject) => {
@@ -94,7 +95,7 @@ export class JsonRpcConnection {
94
95
  this.settle(id, undefined, error);
95
96
  void this.notify('$/cancelRequest', { id }).catch((failure) => this.reportError(failure));
96
97
  };
97
- const abort = (): void => cancel(new LspError('Request cancelled', ErrorCodes.RequestCancelled));
98
+ const abort = (): void => cancel(cancelledError());
98
99
  this.pending.set(id, {
99
100
  resolve: (value) => resolve(value as T),
100
101
  reject,
@@ -131,17 +132,13 @@ export class JsonRpcConnection {
131
132
  }
132
133
 
133
134
  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);
135
+ const listeners = this.notifications.get(method) ?? new Set<NotificationHandler>();
136
+ this.notifications.set(method, listeners);
137
+ listeners.add(listener);
141
138
  return {
142
139
  dispose: () => {
143
- registered.delete(listener);
144
- if (registered.size === 0) {
140
+ listeners.delete(listener);
141
+ if (listeners.size === 0) {
145
142
  this.notifications.delete(method);
146
143
  }
147
144
  }
@@ -264,7 +261,7 @@ export class JsonRpcConnection {
264
261
  }
265
262
  const result = await handler(message.params, controller.signal);
266
263
  if (controller.signal.aborted) {
267
- throw new LspError('Request cancelled', ErrorCodes.RequestCancelled);
264
+ throw cancelledError();
268
265
  }
269
266
  await this.send({ jsonrpc: '2.0', id: message.id, result: result ?? null });
270
267
  } catch (error) {
package/src/edits.ts CHANGED
@@ -15,6 +15,9 @@ export interface PlannedDocumentEdit {
15
15
  created?: true;
16
16
  }
17
17
 
18
+ const CR = 13;
19
+ const LF = 10;
20
+
18
21
  /* A line ends at `\n`, `\r\n` or a lone `\r`, which is how the protocol counts lines. */
19
22
  export function offsetAt(text: string, position: Position): number {
20
23
  if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
@@ -24,12 +27,12 @@ export function offsetAt(text: string, position: Position): number {
24
27
  let offset = 0;
25
28
  while (line < position.line && offset < text.length) {
26
29
  const char = text.charCodeAt(offset++);
27
- if (char === 13) {
28
- if (text.charCodeAt(offset) === 10) {
30
+ if (char === CR) {
31
+ if (text.charCodeAt(offset) === LF) {
29
32
  offset++;
30
33
  }
31
34
  line++;
32
- } else if (char === 10) {
35
+ } else if (char === LF) {
33
36
  line++;
34
37
  }
35
38
  }
@@ -55,14 +58,14 @@ export function positionAt(text: string, offset: number): Position {
55
58
  let i = 0;
56
59
  while (i < offset) {
57
60
  const code = text.charCodeAt(i);
58
- if (code === 13 && text.charCodeAt(i + 1) === 10) {
61
+ if (code === CR && text.charCodeAt(i + 1) === LF) {
59
62
  if (i + 2 > offset) {
60
63
  break;
61
64
  }
62
65
  i += 2;
63
66
  line++;
64
67
  lineStart = i;
65
- } else if (code === 13 || code === 10) {
68
+ } else if (code === CR || code === LF) {
66
69
  i++;
67
70
  line++;
68
71
  lineStart = i;
package/src/session.ts CHANGED
@@ -11,7 +11,6 @@ import type {
11
11
  DocumentItem,
12
12
  FileEvent,
13
13
  FileOperationFilter,
14
- FileRename,
15
14
  FileSystemWatcher,
16
15
  InitializeResult,
17
16
  LspTransport,
@@ -109,6 +108,26 @@ function matches(options: ProviderOptions, document?: Pick<DocumentItem, 'uri' |
109
108
  );
110
109
  }
111
110
 
111
+ /* Whether provider options cover a method that a provider only offers on top of its feature. */
112
+ function offersMethod(method: string, options: ProviderOptions): boolean {
113
+ if (method.endsWith('/resolve')) {
114
+ return options.resolveProvider === true;
115
+ }
116
+ if (method === 'textDocument/prepareRename') {
117
+ return options.prepareProvider === true;
118
+ }
119
+ if (method === 'textDocument/semanticTokens/full/delta') {
120
+ return typeof options.full === 'object' && options.full.delta === true;
121
+ }
122
+ if (method === 'textDocument/semanticTokens/full') {
123
+ return !!options.full;
124
+ }
125
+ if (method === 'textDocument/semanticTokens/range') {
126
+ return !!options.range;
127
+ }
128
+ return true;
129
+ }
130
+
112
131
  /* One conversation with one language server: the handshake, what it can do, and the documents open in it. */
113
132
  export class LspSession {
114
133
  readonly connection: JsonRpcConnection;
@@ -265,24 +284,7 @@ export class LspSession {
265
284
  } else if (provider && typeof provider === 'object' && matches(provider as ProviderOptions, document)) {
266
285
  candidates.push(provider as ProviderOptions);
267
286
  }
268
- return candidates.find((options) => {
269
- if (method.endsWith('/resolve')) {
270
- return options.resolveProvider === true;
271
- }
272
- if (method === 'textDocument/prepareRename') {
273
- return options.prepareProvider === true;
274
- }
275
- if (method === 'textDocument/semanticTokens/full/delta') {
276
- return typeof options.full === 'object' && options.full.delta === true;
277
- }
278
- if (method === 'textDocument/semanticTokens/full') {
279
- return !!options.full;
280
- }
281
- if (method === 'textDocument/semanticTokens/range') {
282
- return !!options.range;
283
- }
284
- return true;
285
- });
287
+ return candidates.find((options) => offersMethod(method, options));
286
288
  }
287
289
 
288
290
  supports(method: string, document?: Pick<DocumentItem, 'uri' | 'languageId'>): boolean {
@@ -382,7 +384,7 @@ export class LspSession {
382
384
 
383
385
  /* Tells the server which of these files moved, those its filters take. */
384
386
  async didRenameFiles(files: readonly RenamedFile[]): Promise<void> {
385
- const taken: FileRename[] = renamesTaken(this.fileOperationFilters('didRename'), files);
387
+ const taken = renamesTaken(this.fileOperationFilters('didRename'), files);
386
388
  if (taken.length > 0 && this.phase === 'ready') {
387
389
  await this.notify('workspace/didRenameFiles', { files: taken });
388
390
  }
@@ -453,19 +455,18 @@ export class LspSession {
453
455
  }
454
456
 
455
457
  private emitDiagnostics(params: PublishDiagnosticsParams): void {
456
- for (const listener of this.diagnosticListeners) {
457
- try {
458
- listener(params);
459
- } catch (error) {
460
- this.connection.reportError(error);
461
- }
462
- }
458
+ this.emit(this.diagnosticListeners, params);
463
459
  }
464
460
 
465
461
  private emitCapabilities(): void {
466
- for (const listener of this.capabilityListeners) {
462
+ this.emit(this.capabilityListeners, undefined);
463
+ }
464
+
465
+ /* A listener that throws is reported and does not keep the others from hearing it. */
466
+ private emit<T>(listeners: ReadonlySet<(value: T) => void>, value: T): void {
467
+ for (const listener of listeners) {
467
468
  try {
468
- listener();
469
+ listener(value);
469
470
  } catch (error) {
470
471
  this.connection.reportError(error);
471
472
  }
package/src/transport.ts CHANGED
@@ -1,12 +1,9 @@
1
+ import { asError } from './as-error.ts';
1
2
  import type { Disposable, LspTransport, RpcMessage } from './protocol.ts';
2
3
 
3
4
  const DEFAULT_MAX_MESSAGE_BYTES = 32 * 1024 * 1024;
4
5
  const MAX_HEADER_BYTES = 8192;
5
6
 
6
- function asError(error: unknown): Error {
7
- return error instanceof Error ? error : new Error(String(error));
8
- }
9
-
10
7
  /*
11
8
  * The listeners of a transport. A message that arrives before anyone listens is held and handed to
12
9
  * the first listener, since a server may speak the moment its pipe opens.
@@ -18,7 +15,7 @@ class TransportEvents {
18
15
  private backlog: unknown[] = [];
19
16
  private error?: Error;
20
17
 
21
- onMessage = (listener: (message: unknown) => void): Disposable => {
18
+ onMessage(listener: (message: unknown) => void): Disposable {
22
19
  this.messages.add(listener);
23
20
  for (const message of this.backlog.splice(0)) {
24
21
  listener(message);
@@ -28,9 +25,9 @@ class TransportEvents {
28
25
  this.messages.delete(listener);
29
26
  }
30
27
  };
31
- };
28
+ }
32
29
 
33
- onClose = (listener: (error?: Error) => void): Disposable => {
30
+ onClose(listener: (error?: Error) => void): Disposable {
34
31
  this.closes.add(listener);
35
32
  if (this.closed) {
36
33
  listener(this.error);
@@ -40,7 +37,7 @@ class TransportEvents {
40
37
  this.closes.delete(listener);
41
38
  }
42
39
  };
43
- };
40
+ }
44
41
 
45
42
  receive(message: unknown): void {
46
43
  if (this.closed) {
@@ -83,9 +80,12 @@ function concat(chunks: readonly Uint8Array[], size: number): Uint8Array {
83
80
  return joined;
84
81
  }
85
82
 
83
+ const CR = 13;
84
+ const LF = 10;
85
+
86
86
  function indexOfHeaderEnd(bytes: Uint8Array): number {
87
87
  for (let i = 0; i + 3 < bytes.length; i++) {
88
- if (bytes[i] === 13 && bytes[i + 1] === 10 && bytes[i + 2] === 13 && bytes[i + 3] === 10) {
88
+ if (bytes[i] === CR && bytes[i + 1] === LF && bytes[i + 2] === CR && bytes[i + 3] === LF) {
89
89
  return i;
90
90
  }
91
91
  }
@@ -178,8 +178,8 @@ export function createStreamTransport(stream: ByteStream, options: { maxMessageB
178
178
  });
179
179
  stream.onClose((error) => events.finish(error));
180
180
  return {
181
- onMessage: events.onMessage,
182
- onClose: events.onClose,
181
+ onMessage: (listener) => events.onMessage(listener),
182
+ onClose: (listener) => events.onClose(listener),
183
183
  async send(message) {
184
184
  if (events.closed) {
185
185
  throw new Error('LSP stream is closed');
@@ -198,8 +198,8 @@ export function createMemoryTransportPair(): [LspTransport, LspTransport] {
198
198
  const left = new TransportEvents();
199
199
  const right = new TransportEvents();
200
200
  const end = (inbox: TransportEvents, outbox: TransportEvents): LspTransport => ({
201
- onMessage: inbox.onMessage,
202
- onClose: inbox.onClose,
201
+ onMessage: (listener) => inbox.onMessage(listener),
202
+ onClose: (listener) => inbox.onClose(listener),
203
203
  send(message) {
204
204
  if (inbox.closed || outbox.closed) {
205
205
  throw new Error('LSP transport is closed');
@@ -271,8 +271,8 @@ export async function connectWebSocket(url: string, options: WebSocketTransportO
271
271
  options.signal?.addEventListener('abort', abort, { once: true });
272
272
  });
273
273
  return {
274
- onMessage: events.onMessage,
275
- onClose: events.onClose,
274
+ onMessage: (listener) => events.onMessage(listener),
275
+ onClose: (listener) => events.onClose(listener),
276
276
  send(message) {
277
277
  if (events.closed || socket.readyState !== 1) {
278
278
  throw new Error('LSP WebSocket is closed');
@@ -5,6 +5,8 @@ import { fileUriToPath } from './uris.ts';
5
5
  /* A change of a file as the protocol numbers it: created, changed, deleted. */
6
6
  export type FileChangeType = 1 | 2 | 3;
7
7
 
8
+ const EVERY_CHANGE = 7;
9
+
8
10
  /* Folders whose files a server hears about only when a pattern of its own names the folder. */
9
11
  const SPARING_FOLDERS = ['node_modules', 'vendor', '.git'];
10
12
 
@@ -13,7 +15,7 @@ function relativeTo(root: string, path: string): string | null {
13
15
  return path.startsWith(prefix) ? path.slice(prefix.length) : null;
14
16
  }
15
17
 
16
- /* A pattern with the folder it is relative to, or null for a relative pattern whose base is no folder of this machine. `base` is undefined for a string pattern. */
18
+ /* A pattern with the folder it is relative to, or null for a relative pattern whose base is no folder of this machine. */
17
19
  function patternOf(watcher: FileSystemWatcher): { base: string | undefined; pattern: string } | null {
18
20
  const { globPattern } = watcher;
19
21
  if (typeof globPattern === 'string') {
@@ -32,7 +34,7 @@ export function watchesFile(watchers: readonly FileSystemWatcher[], root: string
32
34
  const inProject = relativeTo(root, path);
33
35
  const segments = (inProject ?? path).split('/');
34
36
  return watchers.some((watcher) => {
35
- if (((watcher.kind ?? 7) & (1 << (type - 1))) === 0) {
37
+ if (((watcher.kind ?? EVERY_CHANGE) & (1 << (type - 1))) === 0) {
36
38
  return false;
37
39
  }
38
40
  const parts = patternOf(watcher);