@adecore/lsp 0.0.1 → 0.17.0-beta.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.
- package/LICENSE +110 -0
- package/README.md +47 -0
- package/dist/connection.d.ts +50 -0
- package/dist/connection.js +290 -0
- package/dist/document.d.ts +60 -0
- package/dist/document.js +282 -0
- package/dist/edits.d.ts +18 -0
- package/dist/edits.js +161 -0
- package/dist/file-operations.d.ts +5 -0
- package/dist/file-operations.js +23 -0
- package/dist/glob.d.ts +1 -0
- package/dist/glob.js +67 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +10 -0
- package/dist/protocol.d.ts +470 -0
- package/dist/protocol.js +1 -0
- package/dist/service.d.ts +53 -0
- package/dist/service.js +1 -0
- package/dist/session.d.ts +78 -0
- package/dist/session.js +528 -0
- package/dist/test-transport.d.ts +33 -0
- package/dist/test-transport.js +72 -0
- package/dist/testing.d.ts +33 -0
- package/dist/testing.js +72 -0
- package/dist/transport.d.ts +27 -0
- package/dist/transport.js +258 -0
- package/dist/uris.d.ts +2 -0
- package/dist/uris.js +28 -0
- package/dist/vue.d.ts +6 -0
- package/dist/vue.js +86 -0
- package/dist/watched-files.d.ts +3 -0
- package/dist/watched-files.js +41 -0
- package/package.json +36 -3
- package/src/connection.ts +310 -0
- package/src/document.ts +357 -0
- package/src/edits.ts +182 -0
- package/src/file-operations.ts +30 -0
- package/src/glob.ts +60 -0
- package/src/index.ts +30 -0
- package/src/protocol.ts +479 -0
- package/src/service.ts +109 -0
- package/src/session.ts +613 -0
- package/src/test-transport.ts +92 -0
- package/src/testing.ts +105 -0
- package/src/transport.ts +290 -0
- package/src/uris.ts +29 -0
- package/src/vue.ts +93 -0
- package/src/watched-files.ts +49 -0
package/src/edits.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { ErrorCodes, LspError } from './connection.ts';
|
|
2
|
+
import type { ContentChange, Position, TextEdit, WorkspaceEdit } from './protocol.ts';
|
|
3
|
+
|
|
4
|
+
export interface DocumentSnapshot {
|
|
5
|
+
text: string;
|
|
6
|
+
version: number | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PlannedDocumentEdit {
|
|
10
|
+
uri: string;
|
|
11
|
+
version: number | null;
|
|
12
|
+
before: string;
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* A line ends at `\n`, `\r\n` or a lone `\r`, which is how the protocol counts lines. */
|
|
17
|
+
export function offsetAt(text: string, position: Position): number {
|
|
18
|
+
if (!Number.isInteger(position.line) || !Number.isInteger(position.character) || position.line < 0 || position.character < 0) {
|
|
19
|
+
throw new LspError('Invalid edit position');
|
|
20
|
+
}
|
|
21
|
+
let line = 0;
|
|
22
|
+
let offset = 0;
|
|
23
|
+
while (line < position.line && offset < text.length) {
|
|
24
|
+
const char = text.charCodeAt(offset++);
|
|
25
|
+
if (char === 13) {
|
|
26
|
+
if (text.charCodeAt(offset) === 10) {
|
|
27
|
+
offset++;
|
|
28
|
+
}
|
|
29
|
+
line++;
|
|
30
|
+
} else if (char === 10) {
|
|
31
|
+
line++;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (line !== position.line) {
|
|
35
|
+
throw new LspError('Edit line is outside the document');
|
|
36
|
+
}
|
|
37
|
+
let end = offset;
|
|
38
|
+
while (end < text.length && text[end] !== '\r' && text[end] !== '\n') {
|
|
39
|
+
end++;
|
|
40
|
+
}
|
|
41
|
+
if (offset + position.character > end) {
|
|
42
|
+
throw new LspError('Edit character is outside the line');
|
|
43
|
+
}
|
|
44
|
+
return offset + position.character;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function positionAt(text: string, offset: number): Position {
|
|
48
|
+
if (!Number.isInteger(offset) || offset < 0 || offset > text.length) {
|
|
49
|
+
throw new LspError('Offset is outside the document');
|
|
50
|
+
}
|
|
51
|
+
let line = 0;
|
|
52
|
+
let lineStart = 0;
|
|
53
|
+
let i = 0;
|
|
54
|
+
while (i < offset) {
|
|
55
|
+
const code = text.charCodeAt(i);
|
|
56
|
+
if (code === 13 && text.charCodeAt(i + 1) === 10) {
|
|
57
|
+
if (i + 2 > offset) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
i += 2;
|
|
61
|
+
line++;
|
|
62
|
+
lineStart = i;
|
|
63
|
+
} else if (code === 13 || code === 10) {
|
|
64
|
+
i++;
|
|
65
|
+
line++;
|
|
66
|
+
lineStart = i;
|
|
67
|
+
} else {
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { line, character: offset - lineStart };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* The position after the last character. */
|
|
75
|
+
export function endPosition(text: string): Position {
|
|
76
|
+
return positionAt(text, text.length);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* Edits are simultaneous and in the coordinates of `text`; two inserts at one place keep their order. */
|
|
80
|
+
export function applyTextEdits(text: string, edits: readonly TextEdit[]): string {
|
|
81
|
+
const spans = edits
|
|
82
|
+
.map((edit, index) => ({
|
|
83
|
+
start: offsetAt(text, edit.range.start),
|
|
84
|
+
end: offsetAt(text, edit.range.end),
|
|
85
|
+
text: edit.newText,
|
|
86
|
+
index
|
|
87
|
+
}))
|
|
88
|
+
.sort((a, b) => a.start - b.start || a.index - b.index);
|
|
89
|
+
let cursor = 0;
|
|
90
|
+
const parts: string[] = [];
|
|
91
|
+
for (const span of spans) {
|
|
92
|
+
if (span.end < span.start) {
|
|
93
|
+
throw new LspError('Edit range is reversed');
|
|
94
|
+
}
|
|
95
|
+
if (span.start < cursor) {
|
|
96
|
+
throw new LspError('Workspace edits overlap');
|
|
97
|
+
}
|
|
98
|
+
parts.push(text.slice(cursor, span.start), span.text);
|
|
99
|
+
cursor = span.end;
|
|
100
|
+
}
|
|
101
|
+
parts.push(text.slice(cursor));
|
|
102
|
+
return parts.join('');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/* `didChange` entries apply one after the other, each in the coordinates the one before left. */
|
|
106
|
+
export function applyContentChanges(text: string, changes: readonly ContentChange[]): string {
|
|
107
|
+
let result = text;
|
|
108
|
+
for (const change of changes) {
|
|
109
|
+
if (!change.range) {
|
|
110
|
+
result = change.text;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const start = offsetAt(result, change.range.start);
|
|
114
|
+
const end = offsetAt(result, change.range.end);
|
|
115
|
+
if (end < start) {
|
|
116
|
+
throw new LspError('Change range is reversed');
|
|
117
|
+
}
|
|
118
|
+
result = result.slice(0, start) + change.text + result.slice(end);
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function isHighSurrogate(code: number): boolean {
|
|
124
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isLowSurrogate(code: number): boolean {
|
|
128
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/*
|
|
132
|
+
* The one change that turns `before` into `after`: what is left once the shared start and end are
|
|
133
|
+
* taken off. A range never starts or ends inside a surrogate pair or a CRLF, which no position can
|
|
134
|
+
* name.
|
|
135
|
+
*/
|
|
136
|
+
export function minimalChange(before: string, after: string): ContentChange {
|
|
137
|
+
const shortest = Math.min(before.length, after.length);
|
|
138
|
+
let start = 0;
|
|
139
|
+
while (start < shortest && before.charCodeAt(start) === after.charCodeAt(start)) {
|
|
140
|
+
start++;
|
|
141
|
+
}
|
|
142
|
+
if (start > 0 && (isHighSurrogate(before.charCodeAt(start - 1)) || (before[start - 1] === '\r' && before[start] === '\n'))) {
|
|
143
|
+
start--;
|
|
144
|
+
}
|
|
145
|
+
let end = 0;
|
|
146
|
+
while (end < shortest - start && before.charCodeAt(before.length - 1 - end) === after.charCodeAt(after.length - 1 - end)) {
|
|
147
|
+
end++;
|
|
148
|
+
}
|
|
149
|
+
if (
|
|
150
|
+
end > 0 &&
|
|
151
|
+
(isLowSurrogate(before.charCodeAt(before.length - end)) || (before[before.length - end] === '\n' && before[before.length - end - 1] === '\r'))
|
|
152
|
+
) {
|
|
153
|
+
end--;
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
range: { start: positionAt(before, start), end: positionAt(before, before.length - end) },
|
|
157
|
+
text: after.slice(start, after.length - end)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/* Plans text edits in memory. A create, rename or delete needs the host's own handling and is refused. */
|
|
162
|
+
export function planWorkspaceEdit(edit: WorkspaceEdit, snapshots: ReadonlyMap<string, DocumentSnapshot>): PlannedDocumentEdit[] {
|
|
163
|
+
const planned = new Map<string, PlannedDocumentEdit>();
|
|
164
|
+
const changes = edit.documentChanges ?? Object.entries(edit.changes ?? {}).map(([uri, edits]) => ({ textDocument: { uri, version: null }, edits }));
|
|
165
|
+
for (const change of changes) {
|
|
166
|
+
if ('kind' in change) {
|
|
167
|
+
throw new LspError(`A host file-operation handler is required for ${change.kind}`);
|
|
168
|
+
}
|
|
169
|
+
const { uri, version } = change.textDocument;
|
|
170
|
+
const snapshot = snapshots.get(uri);
|
|
171
|
+
if (!snapshot) {
|
|
172
|
+
throw new LspError(`Missing workspace snapshot: ${uri}`);
|
|
173
|
+
}
|
|
174
|
+
if (version !== null && snapshot.version !== version) {
|
|
175
|
+
throw new LspError(`Workspace edit version mismatch: ${uri}`, ErrorCodes.ContentModified);
|
|
176
|
+
}
|
|
177
|
+
const before = planned.get(uri)?.text ?? snapshot.text;
|
|
178
|
+
const text = applyTextEdits(before, change.edits);
|
|
179
|
+
planned.set(uri, { uri, version: snapshot.version, before: snapshot.text, text });
|
|
180
|
+
}
|
|
181
|
+
return [...planned.values()];
|
|
182
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { globMatch } from './glob.ts';
|
|
2
|
+
import type { FileOperationFilter, FileRename } from './protocol.ts';
|
|
3
|
+
|
|
4
|
+
/* A file or folder that moves, which says what it is since a filter may take only one of the two. */
|
|
5
|
+
export interface RenamedFile extends FileRename {
|
|
6
|
+
directory: boolean;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function filterTakes(filter: FileOperationFilter, file: RenamedFile): boolean {
|
|
10
|
+
const { matches, glob, options } = filter.pattern;
|
|
11
|
+
if (matches !== undefined && (matches === 'folder') !== file.directory) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
let url: URL;
|
|
15
|
+
try {
|
|
16
|
+
url = new URL(file.oldUri);
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (filter.scheme !== undefined && filter.scheme !== url.protocol.slice(0, -1)) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const path = decodeURIComponent(url.pathname);
|
|
24
|
+
return options?.ignoreCase === true ? globMatch(path.toLowerCase(), glob.toLowerCase()) : globMatch(path, glob);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* The renames a server asked to hear about, by the filters it registered. A rename is matched on the place it leaves. */
|
|
28
|
+
export function renamesTaken(filters: readonly FileOperationFilter[], files: readonly RenamedFile[]): FileRename[] {
|
|
29
|
+
return files.filter((file) => filters.some((filter) => filterTakes(filter, file))).map(({ oldUri, newUri }) => ({ oldUri, newUri }));
|
|
30
|
+
}
|
package/src/glob.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const compiled = new Map<string, RegExp | null>();
|
|
2
|
+
|
|
3
|
+
function escapeRegex(char: string): string {
|
|
4
|
+
return char.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function compileGlob(pattern: string): RegExp | null {
|
|
8
|
+
let expression = '';
|
|
9
|
+
let depth = 0;
|
|
10
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
11
|
+
const char = pattern[i];
|
|
12
|
+
if (char === '*') {
|
|
13
|
+
if (pattern[i + 1] === '*') {
|
|
14
|
+
i++;
|
|
15
|
+
if (pattern[i + 1] === '/') {
|
|
16
|
+
expression += '(?:.*/)?';
|
|
17
|
+
i++;
|
|
18
|
+
} else {
|
|
19
|
+
expression += '.*';
|
|
20
|
+
}
|
|
21
|
+
} else {
|
|
22
|
+
expression += '[^/]*';
|
|
23
|
+
}
|
|
24
|
+
} else if (char === '?') {
|
|
25
|
+
expression += '[^/]';
|
|
26
|
+
} else if (char === '[') {
|
|
27
|
+
const close = pattern.indexOf(']', i + 2);
|
|
28
|
+
if (close === -1) {
|
|
29
|
+
expression += '\\[';
|
|
30
|
+
} else {
|
|
31
|
+
const body = pattern.slice(i + 1, close);
|
|
32
|
+
expression += `[${body.startsWith('!') ? `^${body.slice(1)}` : body}]`;
|
|
33
|
+
i = close;
|
|
34
|
+
}
|
|
35
|
+
} else if (char === '{') {
|
|
36
|
+
depth++;
|
|
37
|
+
expression += '(?:';
|
|
38
|
+
} else if (char === '}' && depth > 0) {
|
|
39
|
+
depth--;
|
|
40
|
+
expression += ')';
|
|
41
|
+
} else if (char === ',' && depth > 0) {
|
|
42
|
+
expression += '|';
|
|
43
|
+
} else {
|
|
44
|
+
expression += escapeRegex(char);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
return new RegExp(`^${expression}$`);
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* The glob of the protocol: `*`, `**`, `?`, `[a-z]`, `[!a-z]` and `{a,b}`. */
|
|
55
|
+
export function globMatch(path: string, pattern: string): boolean {
|
|
56
|
+
if (!compiled.has(pattern)) {
|
|
57
|
+
compiled.set(pattern, compileGlob(pattern));
|
|
58
|
+
}
|
|
59
|
+
return compiled.get(pattern)?.test(path) ?? false;
|
|
60
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export { ErrorCodes, JsonRpcConnection, LspError, StaleResultError, type RequestHandler } from './connection.ts';
|
|
2
|
+
export { LspDocument } from './document.ts';
|
|
3
|
+
export {
|
|
4
|
+
applyContentChanges,
|
|
5
|
+
applyTextEdits,
|
|
6
|
+
endPosition,
|
|
7
|
+
minimalChange,
|
|
8
|
+
offsetAt,
|
|
9
|
+
planWorkspaceEdit,
|
|
10
|
+
positionAt,
|
|
11
|
+
type DocumentSnapshot,
|
|
12
|
+
type PlannedDocumentEdit
|
|
13
|
+
} from './edits.ts';
|
|
14
|
+
export { renamesTaken, type RenamedFile } from './file-operations.ts';
|
|
15
|
+
export { watchesFile, type FileChangeType } from './watched-files.ts';
|
|
16
|
+
export { LspSession, type LspSessionOptions, type SessionState } from './session.ts';
|
|
17
|
+
export type { DiagnosticsReport, LanguageDocument, LanguageRequestOptions, LanguageService } from './service.ts';
|
|
18
|
+
export {
|
|
19
|
+
ContentLengthDecoder,
|
|
20
|
+
connectWebSocket,
|
|
21
|
+
createMemoryTransportPair,
|
|
22
|
+
createStreamTransport,
|
|
23
|
+
encodeMessage,
|
|
24
|
+
type ByteStream,
|
|
25
|
+
type WebSocketTransportOptions
|
|
26
|
+
} from './transport.ts';
|
|
27
|
+
export { globMatch } from './glob.ts';
|
|
28
|
+
export { fileUriToPath, pathToFileUri } from './uris.ts';
|
|
29
|
+
export { bridgeVueTypeScript, isVueExpression, vueServerOrder, type VueServer } from './vue.ts';
|
|
30
|
+
export type * from './protocol.ts';
|