@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/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
|
@@ -3,12 +3,10 @@ function validRange(range, singleLine = true) {
|
|
|
3
3
|
return false;
|
|
4
4
|
}
|
|
5
5
|
const { start, end } = range;
|
|
6
|
-
return (start
|
|
6
|
+
return (typeof start === 'object' &&
|
|
7
7
|
start !== null &&
|
|
8
|
-
typeof start === 'object' &&
|
|
9
|
-
end !== undefined &&
|
|
10
|
-
end !== null &&
|
|
11
8
|
typeof end === 'object' &&
|
|
9
|
+
end !== null &&
|
|
12
10
|
[start.line, start.character, end.line, end.character].every((value) => Number.isSafeInteger(value) && value >= 0) &&
|
|
13
11
|
(!singleLine || start.line === end.line) &&
|
|
14
12
|
(start.line < end.line || (start.line === end.line && start.character <= end.character)));
|
package/dist/edits.d.ts
CHANGED
|
@@ -10,10 +10,14 @@ export interface PlannedDocumentEdit {
|
|
|
10
10
|
text: string;
|
|
11
11
|
created?: true;
|
|
12
12
|
}
|
|
13
|
+
export declare const CR = 13;
|
|
14
|
+
export declare const LF = 10;
|
|
15
|
+
export declare function checkPosition(position: Position): void;
|
|
13
16
|
export declare function offsetAt(text: string, position: Position): number;
|
|
14
17
|
export declare function positionAt(text: string, offset: number): Position;
|
|
15
18
|
export declare function endPosition(text: string): Position;
|
|
16
19
|
export declare function applyTextEdits(text: string, edits: readonly TextEdit[]): string;
|
|
17
20
|
export declare function applyContentChanges(text: string, changes: readonly ContentChange[]): string;
|
|
18
21
|
export declare function minimalChange(before: string, after: string): ContentChange;
|
|
22
|
+
export declare function documentChangesOf(edit: WorkspaceEdit): NonNullable<WorkspaceEdit['documentChanges']>;
|
|
19
23
|
export declare function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit[];
|
package/dist/edits.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ErrorCodes, LspError } from './connection.js';
|
|
2
2
|
import { fileUriKey } from './uris.js';
|
|
3
|
-
const CR = 13;
|
|
4
|
-
const LF = 10;
|
|
5
|
-
function checkPosition(position) {
|
|
3
|
+
export const CR = 13;
|
|
4
|
+
export const LF = 10;
|
|
5
|
+
export function checkPosition(position) {
|
|
6
6
|
if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
|
|
7
7
|
throw new LspError('Invalid edit position');
|
|
8
8
|
}
|
|
@@ -192,6 +192,10 @@ function planCreate(change, snapshots, planned) {
|
|
|
192
192
|
throw new LspError(`${change.uri} already exists`);
|
|
193
193
|
}
|
|
194
194
|
}
|
|
195
|
+
/* The changes of an edit in order, with the older `changes` map read as unversioned text edits. */
|
|
196
|
+
export function documentChangesOf(edit) {
|
|
197
|
+
return edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
|
|
198
|
+
}
|
|
195
199
|
/*
|
|
196
200
|
* Plans text edits and created files in memory, in the order the edit gives them. `snapshots` holds the
|
|
197
201
|
* files that are there; a file the edit creates is absent from it. A rename or delete needs the host's own handling and is refused.
|
|
@@ -199,8 +203,7 @@ function planCreate(change, snapshots, planned) {
|
|
|
199
203
|
export function planWorkspaceEdit(edit, snapshots) {
|
|
200
204
|
const keyed = new Map([...snapshots].map(([uri, snapshot]) => [fileUriKey(uri), { uri, snapshot }]));
|
|
201
205
|
const planned = new Map();
|
|
202
|
-
const
|
|
203
|
-
for (const change of changes) {
|
|
206
|
+
for (const change of documentChangesOf(edit)) {
|
|
204
207
|
if ('kind' in change) {
|
|
205
208
|
if (change.kind !== 'create') {
|
|
206
209
|
throw new LspError(`A host file-operation handler is required for ${change.kind}`);
|
package/dist/file-operations.js
CHANGED
|
@@ -17,15 +17,18 @@ function filterTakes(filter, uri, directory) {
|
|
|
17
17
|
const path = decodeURIComponent(url.pathname);
|
|
18
18
|
return options?.ignoreCase === true ? globMatch(path.toLowerCase(), glob.toLowerCase()) : globMatch(path, glob);
|
|
19
19
|
}
|
|
20
|
+
function anyTakes(filters, uri, directory) {
|
|
21
|
+
return filters.some((filter) => filterTakes(filter, uri, directory));
|
|
22
|
+
}
|
|
20
23
|
/* The renames a server asked to hear about, by the filters it registered. A rename is matched on the place it leaves. */
|
|
21
24
|
export function renamesTaken(filters, files) {
|
|
22
|
-
return files.filter((file) => filters
|
|
25
|
+
return files.filter((file) => anyTakes(filters, file.oldUri, file.directory)).map(({ oldUri, newUri }) => ({ oldUri, newUri }));
|
|
23
26
|
}
|
|
24
27
|
/* The files made that a server asked to hear about, by the filters it registered. */
|
|
25
28
|
export function createsTaken(filters, files) {
|
|
26
|
-
return files.filter((file) => filters
|
|
29
|
+
return files.filter((file) => anyTakes(filters, file.uri, file.directory)).map(({ uri }) => ({ uri }));
|
|
27
30
|
}
|
|
28
31
|
/* The files deleted that a server asked to hear about, by the filters it registered. A folder is matched as one, not by what was in it. */
|
|
29
32
|
export function deletesTaken(filters, files) {
|
|
30
|
-
return files.filter((file) => filters
|
|
33
|
+
return files.filter((file) => anyTakes(filters, file.uri, file.directory)).map(({ uri }) => ({ uri }));
|
|
31
34
|
}
|
package/dist/session.d.ts
CHANGED
package/dist/session.js
CHANGED
|
@@ -307,39 +307,27 @@ export class LspSession {
|
|
|
307
307
|
}
|
|
308
308
|
/* What the server would change before these files move, or null when it takes none of them or has nothing to say. */
|
|
309
309
|
async willRenameFiles(files, options) {
|
|
310
|
-
|
|
311
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request('workspace/willRenameFiles', { files: taken }, options);
|
|
310
|
+
return this.askBefore('willRename', renamesTaken(this.fileOperationFilters('willRename'), files), options);
|
|
312
311
|
}
|
|
313
312
|
/* Tells the server which of these files moved, those its filters take. */
|
|
314
313
|
async didRenameFiles(files) {
|
|
315
|
-
|
|
316
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
317
|
-
await this.notify('workspace/didRenameFiles', { files: taken });
|
|
318
|
-
}
|
|
314
|
+
await this.tellAfter('didRename', renamesTaken(this.fileOperationFilters('didRename'), files));
|
|
319
315
|
}
|
|
320
316
|
/* What the server would change before these files are made, or null when it takes none of them or has nothing to say. */
|
|
321
317
|
async willCreateFiles(files, options) {
|
|
322
|
-
|
|
323
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request('workspace/willCreateFiles', { files: taken }, options);
|
|
318
|
+
return this.askBefore('willCreate', createsTaken(this.fileOperationFilters('willCreate'), files), options);
|
|
324
319
|
}
|
|
325
320
|
/* Tells the server which of these files were made, those its filters take. */
|
|
326
321
|
async didCreateFiles(files) {
|
|
327
|
-
|
|
328
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
329
|
-
await this.notify('workspace/didCreateFiles', { files: taken });
|
|
330
|
-
}
|
|
322
|
+
await this.tellAfter('didCreate', createsTaken(this.fileOperationFilters('didCreate'), files));
|
|
331
323
|
}
|
|
332
324
|
/* What the server would change before these files are deleted, or null when it takes none of them or has nothing to say. */
|
|
333
325
|
async willDeleteFiles(files, options) {
|
|
334
|
-
|
|
335
|
-
return taken.length === 0 || this.phase !== 'ready' ? null : this.request('workspace/willDeleteFiles', { files: taken }, options);
|
|
326
|
+
return this.askBefore('willDelete', deletesTaken(this.fileOperationFilters('willDelete'), files), options);
|
|
336
327
|
}
|
|
337
328
|
/* Tells the server which of these files were deleted, those its filters take. */
|
|
338
329
|
async didDeleteFiles(files) {
|
|
339
|
-
|
|
340
|
-
if (taken.length > 0 && this.phase === 'ready') {
|
|
341
|
-
await this.notify('workspace/didDeleteFiles', { files: taken });
|
|
342
|
-
}
|
|
330
|
+
await this.tellAfter('didDelete', deletesTaken(this.fileOperationFilters('didDelete'), files));
|
|
343
331
|
}
|
|
344
332
|
/* Registrations and the refresh requests of the server both land here, since each can change what a document may ask. */
|
|
345
333
|
onCapabilitiesChanged(listener) {
|
|
@@ -367,6 +355,14 @@ export class LspSession {
|
|
|
367
355
|
throw new LspError(`LSP session is ${this.phase}; await initialize() first`, ErrorCodes.ServerNotInitialized);
|
|
368
356
|
}
|
|
369
357
|
}
|
|
358
|
+
async askBefore(operation, files, options) {
|
|
359
|
+
return files.length === 0 || this.phase !== 'ready' ? null : this.request(`workspace/${operation}Files`, { files }, options);
|
|
360
|
+
}
|
|
361
|
+
async tellAfter(operation, files) {
|
|
362
|
+
if (files.length > 0 && this.phase === 'ready') {
|
|
363
|
+
await this.notify(`workspace/${operation}Files`, { files });
|
|
364
|
+
}
|
|
365
|
+
}
|
|
370
366
|
async stop() {
|
|
371
367
|
try {
|
|
372
368
|
if (this.phase === 'initializing') {
|
package/dist/test-transport.js
CHANGED
|
@@ -42,7 +42,7 @@ export class ScriptedTransport {
|
|
|
42
42
|
}
|
|
43
43
|
/* The last request of `method` the client sent. */
|
|
44
44
|
request(method) {
|
|
45
|
-
return
|
|
45
|
+
return this.sent.findLast((message) => 'method' in message && message.method === method && 'id' in message);
|
|
46
46
|
}
|
|
47
47
|
respond(method, result) {
|
|
48
48
|
this.emit({ jsonrpc: '2.0', id: this.request(method).id, result });
|
package/dist/text-mirror.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { LspError } from './connection.js';
|
|
2
|
-
|
|
3
|
-
const LF = 10;
|
|
2
|
+
import { CR, LF, checkPosition } from './edits.js';
|
|
4
3
|
/* Past this many lines a spread into `splice` could exceed the engine's argument limit. */
|
|
5
4
|
const SPREAD_LIMIT = 10_000;
|
|
6
5
|
/* Each line keeps its own break, so the last one is the only line without. */
|
|
@@ -92,9 +91,7 @@ export class TextMirror {
|
|
|
92
91
|
return this.replace(first, end.line - first + 1, inserted);
|
|
93
92
|
}
|
|
94
93
|
lineAt(position) {
|
|
95
|
-
|
|
96
|
-
throw new LspError('Invalid edit position');
|
|
97
|
-
}
|
|
94
|
+
checkPosition(position);
|
|
98
95
|
const line = this.lines[position.line];
|
|
99
96
|
if (line === undefined) {
|
|
100
97
|
throw new LspError('Edit line is outside the document');
|
package/dist/transport.d.ts
CHANGED
package/dist/transport.js
CHANGED
|
@@ -84,6 +84,7 @@ function indexOfHeaderEnd(bytes) {
|
|
|
84
84
|
/* Reads `Content-Length` framed messages, the way a language server speaks over stdio. */
|
|
85
85
|
export class ContentLengthDecoder {
|
|
86
86
|
maxMessageBytes;
|
|
87
|
+
utf8 = new TextDecoder();
|
|
87
88
|
chunks = [];
|
|
88
89
|
size = 0;
|
|
89
90
|
length = null;
|
|
@@ -106,7 +107,7 @@ export class ContentLengthDecoder {
|
|
|
106
107
|
}
|
|
107
108
|
break;
|
|
108
109
|
}
|
|
109
|
-
const header =
|
|
110
|
+
const header = this.utf8.decode(buffer.subarray(0, boundary));
|
|
110
111
|
const match = /^Content-Length:\s*(\d+)\s*$/im.exec(header);
|
|
111
112
|
if (!match || boundary > MAX_HEADER_BYTES) {
|
|
112
113
|
throw new Error('Invalid LSP Content-Length header');
|
|
@@ -123,7 +124,7 @@ export class ContentLengthDecoder {
|
|
|
123
124
|
break;
|
|
124
125
|
}
|
|
125
126
|
const buffer = concat(this.chunks, this.size);
|
|
126
|
-
messages.push(JSON.parse(
|
|
127
|
+
messages.push(JSON.parse(this.utf8.decode(buffer.subarray(0, this.length))));
|
|
127
128
|
this.chunks = [buffer.subarray(this.length)];
|
|
128
129
|
this.size = buffer.length - this.length;
|
|
129
130
|
this.length = null;
|
|
@@ -131,9 +132,11 @@ export class ContentLengthDecoder {
|
|
|
131
132
|
return messages;
|
|
132
133
|
}
|
|
133
134
|
}
|
|
135
|
+
let utf8;
|
|
134
136
|
export function encodeMessage(message) {
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
+
utf8 ??= new TextEncoder();
|
|
138
|
+
const body = utf8.encode(JSON.stringify(message));
|
|
139
|
+
const header = utf8.encode(`Content-Length: ${body.length}\r\n\r\n`);
|
|
137
140
|
const framed = new Uint8Array(header.length + body.length);
|
|
138
141
|
framed.set(header, 0);
|
|
139
142
|
framed.set(body, header.length);
|
package/dist/uris.d.ts
CHANGED
package/dist/uris.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
function isWindowsDrive(segment) {
|
|
2
2
|
return /^[A-Za-z]:$/.test(segment);
|
|
3
3
|
}
|
|
4
|
+
/* A path with a drive or a UNC share. A backslash separates only in such a path; in a POSIX one it is part of a name. */
|
|
5
|
+
export function isWindowsPath(path) {
|
|
6
|
+
return /^[A-Za-z]:[\\/]/.test(path) || path.startsWith('\\\\');
|
|
7
|
+
}
|
|
4
8
|
/* An absolute path, POSIX, with a Windows drive or UNC, as the `file:` URI a language server names it by. */
|
|
5
9
|
export function pathToFileUri(path) {
|
|
6
|
-
|
|
7
|
-
const windows = /^[A-Za-z]:[\\/]/.test(path) || path.startsWith('\\\\');
|
|
10
|
+
const windows = isWindowsPath(path);
|
|
8
11
|
const normalized = windows ? path.replaceAll('\\', '/') : path;
|
|
9
12
|
if (windows && normalized.startsWith('//')) {
|
|
10
13
|
const [host = '', ...segments] = normalized.slice(2).split('/');
|
package/dist/watched-files.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { globMatch } from './glob.js';
|
|
2
|
-
import { fileUriToPath } from './uris.js';
|
|
2
|
+
import { fileUriToPath, isWindowsPath } from './uris.js';
|
|
3
3
|
const EVERY_CHANGE = 7;
|
|
4
4
|
/* Folders whose files a server hears about only when a pattern of its own names the folder. */
|
|
5
5
|
const SPARING_FOLDERS = ['node_modules', 'vendor', '.git'];
|
|
6
6
|
/* A Windows path with forward slashes and its drive in capitals, so it compares with a pattern and a root however it was written. A POSIX path is left alone, a backslash being part of a name there. */
|
|
7
7
|
function slashed(path) {
|
|
8
|
-
if (
|
|
8
|
+
if (!isWindowsPath(path)) {
|
|
9
9
|
return path;
|
|
10
10
|
}
|
|
11
11
|
const forward = path.replaceAll('\\', '/');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ErrorCodes, LspError } from './connection.js';
|
|
2
|
-
import { applyTextEdits } from './edits.js';
|
|
2
|
+
import { applyTextEdits, documentChangesOf } from './edits.js';
|
|
3
3
|
/* The key a URI is known by: a folder spelled with a trailing slash is the same folder, except the root. */
|
|
4
4
|
function keyOf(uri) {
|
|
5
5
|
const root = /^[^/:]+:\/\/[^/]*\/+$/.test(uri);
|
|
@@ -122,8 +122,7 @@ export async function planWorkspaceOperations(edit, source) {
|
|
|
122
122
|
}
|
|
123
123
|
return { ...change, uri: keyOf(change.uri) };
|
|
124
124
|
};
|
|
125
|
-
const
|
|
126
|
-
for (const original of changes) {
|
|
125
|
+
for (const original of documentChangesOf(edit)) {
|
|
127
126
|
const change = keyed(original);
|
|
128
127
|
if ('textDocument' in change) {
|
|
129
128
|
const { uri, version } = change.textDocument;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/lsp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"test": "bun test --conditions=source"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
|
-
"@types/bun": "^1.4.
|
|
23
|
+
"@types/bun": "^1.4.4",
|
|
24
24
|
"typescript": "^7.0.2"
|
|
25
25
|
},
|
|
26
26
|
"license": "FSL-1.1-MIT",
|
|
@@ -4,14 +4,14 @@ function validRange(range: Range, singleLine = true): boolean {
|
|
|
4
4
|
if (range === null || typeof range !== 'object') {
|
|
5
5
|
return false;
|
|
6
6
|
}
|
|
7
|
+
|
|
7
8
|
const { start, end } = range;
|
|
9
|
+
|
|
8
10
|
return (
|
|
9
|
-
start !== undefined &&
|
|
10
|
-
start !== null &&
|
|
11
11
|
typeof start === 'object' &&
|
|
12
|
-
|
|
13
|
-
end !== null &&
|
|
12
|
+
start !== null &&
|
|
14
13
|
typeof end === 'object' &&
|
|
14
|
+
end !== null &&
|
|
15
15
|
[start.line, start.character, end.line, end.character].every((value) => Number.isSafeInteger(value) && value >= 0) &&
|
|
16
16
|
(!singleLine || start.line === end.line) &&
|
|
17
17
|
(start.line < end.line || (start.line === end.line && start.character <= end.character))
|
|
@@ -22,9 +22,11 @@ function validEditRange(range: Range | { insert: Range; replace: Range }, single
|
|
|
22
22
|
if (range === null || typeof range !== 'object' || Array.isArray(range)) {
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
|
+
|
|
25
26
|
if ('start' in range) {
|
|
26
27
|
return validRange(range, singleLine);
|
|
27
28
|
}
|
|
29
|
+
|
|
28
30
|
return (
|
|
29
31
|
validRange(range.insert) &&
|
|
30
32
|
validRange(range.replace) &&
|
|
@@ -42,13 +44,16 @@ export function applyCompletionDefaults(
|
|
|
42
44
|
if (typeof item?.label !== 'string') {
|
|
43
45
|
throw new TypeError('A completion item must have a string label.');
|
|
44
46
|
}
|
|
47
|
+
|
|
45
48
|
for (const value of [item.filterText, item.sortText, item.insertText, item.textEditText]) {
|
|
46
49
|
if (value !== undefined && typeof value !== 'string') {
|
|
47
50
|
throw new TypeError('Completion ranking and insertion text must be strings.');
|
|
48
51
|
}
|
|
49
52
|
}
|
|
53
|
+
|
|
50
54
|
if (item.textEdit !== undefined) {
|
|
51
55
|
const edit = item.textEdit;
|
|
56
|
+
|
|
52
57
|
if (
|
|
53
58
|
edit === null ||
|
|
54
59
|
typeof edit !== 'object' ||
|
|
@@ -58,40 +63,55 @@ export function applyCompletionDefaults(
|
|
|
58
63
|
throw new RangeError('Invalid completion item edit.');
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
|
|
61
67
|
if (defaults === undefined) {
|
|
62
68
|
return item;
|
|
63
69
|
}
|
|
70
|
+
|
|
64
71
|
const next = { ...item };
|
|
72
|
+
|
|
65
73
|
if (next.commitCharacters === undefined && defaults.commitCharacters !== undefined) {
|
|
66
74
|
next.commitCharacters = defaults.commitCharacters;
|
|
67
75
|
}
|
|
76
|
+
|
|
68
77
|
if (next.insertTextFormat === undefined && defaults.insertTextFormat !== undefined) {
|
|
69
78
|
next.insertTextFormat = defaults.insertTextFormat;
|
|
70
79
|
}
|
|
80
|
+
|
|
71
81
|
if (next.insertTextMode === undefined && defaults.insertTextMode !== undefined) {
|
|
72
82
|
next.insertTextMode = defaults.insertTextMode;
|
|
73
83
|
}
|
|
84
|
+
|
|
74
85
|
if (next.data === undefined && defaults.data !== undefined) {
|
|
75
86
|
next.data = defaults.data;
|
|
76
87
|
}
|
|
88
|
+
|
|
77
89
|
if (next.textEdit === undefined && defaults.editRange !== undefined) {
|
|
78
90
|
const range = defaults.editRange;
|
|
79
91
|
const newText = item.textEditText ?? item.label;
|
|
92
|
+
|
|
80
93
|
if (typeof newText !== 'string') {
|
|
81
94
|
throw new TypeError('Completion edit text must be a string.');
|
|
82
95
|
}
|
|
96
|
+
|
|
83
97
|
const invalid = (message: string): CompletionItem => {
|
|
84
98
|
const error = new RangeError(message);
|
|
99
|
+
|
|
85
100
|
if (options?.onInvalidRange === undefined) {
|
|
86
101
|
throw error;
|
|
87
102
|
}
|
|
103
|
+
|
|
88
104
|
options.onInvalidRange(error);
|
|
105
|
+
|
|
89
106
|
return next;
|
|
90
107
|
};
|
|
108
|
+
|
|
91
109
|
if (!validEditRange(range)) {
|
|
92
110
|
return invalid('Invalid completion default edit range.');
|
|
93
111
|
}
|
|
112
|
+
|
|
94
113
|
next.textEdit = 'start' in range ? { range, newText } : { insert: range.insert, replace: range.replace, newText };
|
|
95
114
|
}
|
|
115
|
+
|
|
96
116
|
return next;
|
|
97
117
|
}
|
package/src/connection.ts
CHANGED
|
@@ -66,6 +66,7 @@ export class JsonRpcConnection {
|
|
|
66
66
|
constructor(transport: LspTransport, timeoutMs = 30_000) {
|
|
67
67
|
this.transport = transport;
|
|
68
68
|
this.timeoutMs = timeoutMs;
|
|
69
|
+
|
|
69
70
|
this.subscriptions = [
|
|
70
71
|
transport.onMessage((message) => {
|
|
71
72
|
void this.receive(message).catch((error) => this.reportError(error));
|
|
@@ -82,20 +83,27 @@ export class JsonRpcConnection {
|
|
|
82
83
|
if (this.closed) {
|
|
83
84
|
return Promise.reject(new LspError('LSP connection is closed'));
|
|
84
85
|
}
|
|
86
|
+
|
|
85
87
|
if (options.signal?.aborted) {
|
|
86
88
|
return Promise.reject(cancelledError());
|
|
87
89
|
}
|
|
90
|
+
|
|
88
91
|
const id = ++this.nextId;
|
|
92
|
+
|
|
89
93
|
return new Promise<T>((resolve, reject) => {
|
|
90
94
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
95
|
+
|
|
91
96
|
const cancel = (error: LspError): void => {
|
|
92
97
|
if (!this.pending.has(id)) {
|
|
93
98
|
return;
|
|
94
99
|
}
|
|
100
|
+
|
|
95
101
|
this.settle(id, undefined, error);
|
|
96
102
|
void this.notify('$/cancelRequest', { id }).catch((failure) => this.reportError(failure));
|
|
97
103
|
};
|
|
104
|
+
|
|
98
105
|
const abort = (): void => cancel(cancelledError());
|
|
106
|
+
|
|
99
107
|
this.pending.set(id, {
|
|
100
108
|
resolve: (value) => resolve(value as T),
|
|
101
109
|
reject,
|
|
@@ -104,11 +112,14 @@ export class JsonRpcConnection {
|
|
|
104
112
|
options.signal?.removeEventListener('abort', abort);
|
|
105
113
|
}
|
|
106
114
|
});
|
|
115
|
+
|
|
107
116
|
options.signal?.addEventListener('abort', abort, { once: true });
|
|
108
117
|
const timeout = options.timeoutMs ?? this.timeoutMs;
|
|
118
|
+
|
|
109
119
|
if (timeout > 0) {
|
|
110
120
|
timer = setTimeout(() => cancel(new LspError(`LSP request timed out: ${method}`, ErrorCodes.RequestCancelled)), timeout);
|
|
111
121
|
}
|
|
122
|
+
|
|
112
123
|
void this.send({ jsonrpc: '2.0', id, method, params }).catch((error) => this.settle(id, undefined, error));
|
|
113
124
|
});
|
|
114
125
|
}
|
|
@@ -121,7 +132,9 @@ export class JsonRpcConnection {
|
|
|
121
132
|
if (this.handlers.has(method)) {
|
|
122
133
|
throw new Error(`LSP request handler already registered: ${method}`);
|
|
123
134
|
}
|
|
135
|
+
|
|
124
136
|
this.handlers.set(method, handler);
|
|
137
|
+
|
|
125
138
|
return {
|
|
126
139
|
dispose: () => {
|
|
127
140
|
if (this.handlers.get(method) === handler) {
|
|
@@ -135,9 +148,11 @@ export class JsonRpcConnection {
|
|
|
135
148
|
const listeners = this.notifications.get(method) ?? new Set<NotificationHandler>();
|
|
136
149
|
this.notifications.set(method, listeners);
|
|
137
150
|
listeners.add(listener);
|
|
151
|
+
|
|
138
152
|
return {
|
|
139
153
|
dispose: () => {
|
|
140
154
|
listeners.delete(listener);
|
|
155
|
+
|
|
141
156
|
if (listeners.size === 0) {
|
|
142
157
|
this.notifications.delete(method);
|
|
143
158
|
}
|
|
@@ -147,6 +162,7 @@ export class JsonRpcConnection {
|
|
|
147
162
|
|
|
148
163
|
onError(listener: (error: Error) => void): Disposable {
|
|
149
164
|
this.errors.add(listener);
|
|
165
|
+
|
|
150
166
|
return {
|
|
151
167
|
dispose: () => {
|
|
152
168
|
this.errors.delete(listener);
|
|
@@ -156,9 +172,11 @@ export class JsonRpcConnection {
|
|
|
156
172
|
|
|
157
173
|
onClose(listener: (error?: Error) => void): Disposable {
|
|
158
174
|
this.closes.add(listener);
|
|
175
|
+
|
|
159
176
|
if (this.closed) {
|
|
160
177
|
listener();
|
|
161
178
|
}
|
|
179
|
+
|
|
162
180
|
return {
|
|
163
181
|
dispose: () => {
|
|
164
182
|
this.closes.delete(listener);
|
|
@@ -168,6 +186,7 @@ export class JsonRpcConnection {
|
|
|
168
186
|
|
|
169
187
|
reportError(error: unknown): void {
|
|
170
188
|
const normalized = asError(error);
|
|
189
|
+
|
|
171
190
|
for (const listener of this.errors) {
|
|
172
191
|
try {
|
|
173
192
|
listener(normalized);
|
|
@@ -187,19 +206,25 @@ export class JsonRpcConnection {
|
|
|
187
206
|
if (this.closed) {
|
|
188
207
|
throw new LspError('LSP connection is closed');
|
|
189
208
|
}
|
|
209
|
+
|
|
190
210
|
await this.transport.send(message);
|
|
191
211
|
});
|
|
212
|
+
|
|
192
213
|
this.sendQueue = sent.catch((error) => this.finish(asError(error)));
|
|
214
|
+
|
|
193
215
|
return sent;
|
|
194
216
|
}
|
|
195
217
|
|
|
196
218
|
private settle(id: RequestId, result?: unknown, error?: unknown): void {
|
|
197
219
|
const request = this.pending.get(id);
|
|
220
|
+
|
|
198
221
|
if (!request) {
|
|
199
222
|
return;
|
|
200
223
|
}
|
|
224
|
+
|
|
201
225
|
this.pending.delete(id);
|
|
202
226
|
request.cleanup();
|
|
227
|
+
|
|
203
228
|
if (error !== undefined) {
|
|
204
229
|
request.reject(error);
|
|
205
230
|
} else {
|
|
@@ -211,21 +236,27 @@ export class JsonRpcConnection {
|
|
|
211
236
|
if (this.closed) {
|
|
212
237
|
return;
|
|
213
238
|
}
|
|
239
|
+
|
|
214
240
|
if (!value || typeof value !== 'object' || !('jsonrpc' in value) || value.jsonrpc !== '2.0') {
|
|
215
241
|
throw new LspError('Invalid JSON-RPC message', ErrorCodes.InvalidRequest);
|
|
216
242
|
}
|
|
243
|
+
|
|
217
244
|
const message = value as RpcMessage;
|
|
245
|
+
|
|
218
246
|
if ('method' in message) {
|
|
219
247
|
if (typeof message.method !== 'string') {
|
|
220
248
|
throw new LspError('Invalid JSON-RPC method', ErrorCodes.InvalidRequest);
|
|
221
249
|
}
|
|
250
|
+
|
|
222
251
|
if ('id' in message) {
|
|
223
252
|
if (typeof message.id !== 'number' && typeof message.id !== 'string') {
|
|
224
253
|
throw new LspError('Invalid request id', ErrorCodes.InvalidRequest);
|
|
225
254
|
}
|
|
255
|
+
|
|
226
256
|
await this.handleRequest(message);
|
|
227
257
|
} else if (message.method === '$/cancelRequest') {
|
|
228
258
|
const id = (message.params as { id?: RequestId } | undefined)?.id;
|
|
259
|
+
|
|
229
260
|
if (id !== undefined) {
|
|
230
261
|
this.incoming.get(id)?.abort();
|
|
231
262
|
}
|
|
@@ -254,18 +285,24 @@ export class JsonRpcConnection {
|
|
|
254
285
|
private async handleRequest(message: RpcRequest): Promise<void> {
|
|
255
286
|
const controller = new AbortController();
|
|
256
287
|
this.incoming.set(message.id, controller);
|
|
288
|
+
|
|
257
289
|
try {
|
|
258
290
|
const handler = this.handlers.get(message.method);
|
|
291
|
+
|
|
259
292
|
if (!handler) {
|
|
260
293
|
throw new LspError(`Unsupported server request: ${message.method}`, ErrorCodes.MethodNotFound);
|
|
261
294
|
}
|
|
295
|
+
|
|
262
296
|
const result = await handler(message.params, controller.signal);
|
|
297
|
+
|
|
263
298
|
if (controller.signal.aborted) {
|
|
264
299
|
throw cancelledError();
|
|
265
300
|
}
|
|
301
|
+
|
|
266
302
|
await this.send({ jsonrpc: '2.0', id: message.id, result: result ?? null });
|
|
267
303
|
} catch (error) {
|
|
268
304
|
const failure = error instanceof LspError ? error : new LspError(asError(error).message);
|
|
305
|
+
|
|
269
306
|
if (!this.closed) {
|
|
270
307
|
await this.send({ jsonrpc: '2.0', id: message.id, error: { code: failure.code, message: failure.message, data: failure.data } });
|
|
271
308
|
}
|
|
@@ -278,18 +315,24 @@ export class JsonRpcConnection {
|
|
|
278
315
|
if (this.closed) {
|
|
279
316
|
return;
|
|
280
317
|
}
|
|
318
|
+
|
|
281
319
|
this.closed = true;
|
|
320
|
+
|
|
282
321
|
for (const id of [...this.pending.keys()]) {
|
|
283
322
|
this.settle(id, undefined, error ?? new LspError('LSP connection closed'));
|
|
284
323
|
}
|
|
324
|
+
|
|
285
325
|
for (const controller of this.incoming.values()) {
|
|
286
326
|
controller.abort();
|
|
287
327
|
}
|
|
328
|
+
|
|
288
329
|
this.incoming.clear();
|
|
330
|
+
|
|
289
331
|
// A transport that is closed already calls back from inside the constructor, before the subscriptions exist.
|
|
290
332
|
for (const subscription of this.subscriptions ?? []) {
|
|
291
333
|
subscription.dispose();
|
|
292
334
|
}
|
|
335
|
+
|
|
293
336
|
for (const listener of this.closes) {
|
|
294
337
|
try {
|
|
295
338
|
listener(error);
|
|
@@ -297,9 +340,11 @@ export class JsonRpcConnection {
|
|
|
297
340
|
this.reportError(failure);
|
|
298
341
|
}
|
|
299
342
|
}
|
|
343
|
+
|
|
300
344
|
this.handlers.clear();
|
|
301
345
|
this.notifications.clear();
|
|
302
346
|
this.closes.clear();
|
|
347
|
+
|
|
303
348
|
if (error) {
|
|
304
349
|
this.reportError(error);
|
|
305
350
|
}
|