@adecore/lsp 0.21.1 → 0.22.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/as-error.d.ts +1 -0
- package/dist/as-error.js +3 -0
- package/dist/connection.js +11 -14
- package/dist/edits.js +7 -5
- package/dist/session.d.ts +1 -0
- package/dist/session.js +27 -28
- package/dist/transport.js +14 -14
- package/dist/watched-files.js +3 -2
- package/package.json +1 -1
- package/src/as-error.ts +3 -0
- package/src/connection.ts +11 -14
- package/src/edits.ts +8 -5
- package/src/session.ts +30 -29
- package/src/transport.ts +15 -15
- package/src/watched-files.ts +4 -2
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function asError(error: unknown): Error;
|
package/dist/as-error.js
ADDED
package/dist/connection.js
CHANGED
|
@@ -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
|
|
30
|
-
return
|
|
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(
|
|
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(
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
121
|
-
if (
|
|
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
|
|
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 ===
|
|
12
|
-
if (text.charCodeAt(offset) ===
|
|
13
|
+
if (char === CR) {
|
|
14
|
+
if (text.charCodeAt(offset) === LF) {
|
|
13
15
|
offset++;
|
|
14
16
|
}
|
|
15
17
|
line++;
|
|
16
18
|
}
|
|
17
|
-
else if (char ===
|
|
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 ===
|
|
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 ===
|
|
52
|
+
else if (code === CR || code === LF) {
|
|
51
53
|
i++;
|
|
52
54
|
line++;
|
|
53
55
|
lineStart = i;
|
package/dist/session.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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] ===
|
|
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');
|
package/dist/watched-files.js
CHANGED
|
@@ -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.
|
|
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 ??
|
|
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
package/src/as-error.ts
ADDED
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
|
|
46
|
-
return
|
|
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(
|
|
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(
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
if (
|
|
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
|
|
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 ===
|
|
28
|
-
if (text.charCodeAt(offset) ===
|
|
30
|
+
if (char === CR) {
|
|
31
|
+
if (text.charCodeAt(offset) === LF) {
|
|
29
32
|
offset++;
|
|
30
33
|
}
|
|
31
34
|
line++;
|
|
32
|
-
} else if (char ===
|
|
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 ===
|
|
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 ===
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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] ===
|
|
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');
|
package/src/watched-files.ts
CHANGED
|
@@ -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.
|
|
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 ??
|
|
37
|
+
if (((watcher.kind ?? EVERY_CHANGE) & (1 << (type - 1))) === 0) {
|
|
36
38
|
return false;
|
|
37
39
|
}
|
|
38
40
|
const parts = patternOf(watcher);
|