@askdkc/dsh-lsp-server 0.1.12 → 0.1.14
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/README.md +8 -0
- package/lib/code-intelligence-DvhxPnpu.js +890 -0
- package/lib/code-intelligence-DvhxPnpu.js.map +1 -0
- package/lib/code-intelligence-contracts-CWXTofdI.js +8 -0
- package/lib/code-intelligence-contracts-CWXTofdI.js.map +1 -0
- package/lib/code-intelligence-contracts.js +3 -0
- package/lib/code-intelligence.js +5 -0
- package/lib/errors-DtyWAUi0.js.map +1 -1
- package/lib/extra-tool.js +135 -7
- package/lib/extra-tool.js.map +1 -1
- package/lib/grammars/manifest.json +40 -0
- package/lib/grammars/tree-sitter-javascript.wasm +0 -0
- package/lib/grammars/tree-sitter-tsx.wasm +0 -0
- package/lib/grammars/tree-sitter-typescript.wasm +0 -0
- package/lib/launch-ByUvgkVv.js.map +1 -1
- package/lib/parser-worker.mjs +200 -0
- package/lib/provider.js +639 -41
- package/lib/provider.js.map +1 -1
- package/lib/types/code-intelligence-contracts.d.ts +113 -0
- package/lib/types/code-intelligence.d.ts +5 -0
- package/lib/types/errors.d.ts +1 -1
- package/lib/types/extra-tool.d.ts +2 -1
- package/lib/types/provider.d.ts +4 -0
- package/lib/types/runtime/webstack-runtime.d.ts +13 -1
- package/lib/types/service.d.ts +16 -0
- package/lib/types/structure/code-intelligence.d.ts +23 -0
- package/lib/types/structure/coordinates.d.ts +14 -0
- package/lib/types/structure/engine.d.ts +49 -0
- package/lib/types/structure/protocol.d.ts +19 -0
- package/lib/types/structure/queries.d.ts +4 -0
- package/lib/types/structure/snapshots.d.ts +37 -0
- package/lib/types/structure/types.d.ts +147 -0
- package/lib/types/structure/worker-pool.d.ts +66 -0
- package/package.json +17 -4
|
@@ -0,0 +1,890 @@
|
|
|
1
|
+
import { WebstackLspError } from "./errors-DtyWAUi0.js";
|
|
2
|
+
import { isAbsolute } from "node:path";
|
|
3
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
+
import { Worker } from "node:worker_threads";
|
|
5
|
+
|
|
6
|
+
//#region src/structure/snapshots.ts
|
|
7
|
+
const SNAPSHOT_TTL_MS = 5 * 6e4;
|
|
8
|
+
const SNAPSHOT_MAX_DOCUMENTS = 32;
|
|
9
|
+
const SNAPSHOT_MAX_BYTES = 32 * 1024 * 1024;
|
|
10
|
+
const STRUCTURE_MAX_DOCUMENT_BYTES = 2 * 1024 * 1024;
|
|
11
|
+
const EXTENSION_LANGUAGE = {
|
|
12
|
+
".js": "javascript",
|
|
13
|
+
".jsx": "javascript",
|
|
14
|
+
".mjs": "javascript",
|
|
15
|
+
".cjs": "javascript",
|
|
16
|
+
".ts": "typescript",
|
|
17
|
+
".mts": "typescript",
|
|
18
|
+
".cts": "typescript",
|
|
19
|
+
".tsx": "tsx"
|
|
20
|
+
};
|
|
21
|
+
var DocumentSnapshotStore = class {
|
|
22
|
+
records = /* @__PURE__ */ new Map();
|
|
23
|
+
versions = /* @__PURE__ */ new Map();
|
|
24
|
+
constructor(readSource, maxDocumentBytes) {
|
|
25
|
+
this.readSource = readSource;
|
|
26
|
+
this.maxDocumentBytes = maxDocumentBytes;
|
|
27
|
+
}
|
|
28
|
+
async open(scope, filePath, signal) {
|
|
29
|
+
validateScope(scope);
|
|
30
|
+
const normalizedPath = normalizeRelativePath(filePath);
|
|
31
|
+
signal?.throwIfAborted();
|
|
32
|
+
const first = await this.readSource(scope.workspaceRoot, normalizedPath, Math.min(this.maxDocumentBytes, STRUCTURE_MAX_DOCUMENT_BYTES), signal);
|
|
33
|
+
signal?.throwIfAborted();
|
|
34
|
+
const second = await this.readSource(scope.workspaceRoot, normalizedPath, Math.min(this.maxDocumentBytes, STRUCTURE_MAX_DOCUMENT_BYTES), signal);
|
|
35
|
+
if (hashText(first.text) !== hashText(second.text) || first.uri !== second.uri || first.key !== second.key) throw new WebstackLspError("LSP_STALE_SNAPSHOT", "document changed while opening the snapshot");
|
|
36
|
+
const ownerKey = scopeKey(scope);
|
|
37
|
+
const hash = hashText(second.text);
|
|
38
|
+
const fileKey = JSON.stringify([
|
|
39
|
+
ownerKey,
|
|
40
|
+
second.key,
|
|
41
|
+
second.uri
|
|
42
|
+
]);
|
|
43
|
+
const version = this.rememberVersion(fileKey, scope, hash);
|
|
44
|
+
const sourceBytes = Buffer.byteLength(second.text, "utf8");
|
|
45
|
+
this.evictExpired();
|
|
46
|
+
if (this.records.size >= SNAPSHOT_MAX_DOCUMENTS || this.totalBytes() + sourceBytes > SNAPSHOT_MAX_BYTES) throw new WebstackLspError("LSP_SNAPSHOT_LIMIT", "snapshot capacity is full; release an existing snapshot and retry");
|
|
47
|
+
const descriptor = {
|
|
48
|
+
handle: randomUUID(),
|
|
49
|
+
version,
|
|
50
|
+
filePath: normalizedPath,
|
|
51
|
+
language: languageForPath(normalizedPath),
|
|
52
|
+
sourceKind: "disk",
|
|
53
|
+
contentHash: hash,
|
|
54
|
+
sourceBytes
|
|
55
|
+
};
|
|
56
|
+
const previous = [...this.records.values()].filter((item) => item.scopeKey === ownerKey && item.source.key === second.key && item.source.uri === second.uri).sort((a, b) => b.openedAt - a.openedAt)[0];
|
|
57
|
+
const record = {
|
|
58
|
+
descriptor,
|
|
59
|
+
scopeKey: ownerKey,
|
|
60
|
+
source: second,
|
|
61
|
+
openedAt: Date.now(),
|
|
62
|
+
lastUsedAt: Date.now(),
|
|
63
|
+
...previous ? { previousSnapshotHandle: previous.descriptor.handle } : {}
|
|
64
|
+
};
|
|
65
|
+
this.records.set(descriptor.handle, record);
|
|
66
|
+
return record;
|
|
67
|
+
}
|
|
68
|
+
async current(scope, handle, signal) {
|
|
69
|
+
validateScope(scope);
|
|
70
|
+
signal?.throwIfAborted();
|
|
71
|
+
const record = this.records.get(handle);
|
|
72
|
+
if (!record || record.scopeKey !== scopeKey(scope)) throw staleSnapshot();
|
|
73
|
+
if (Date.now() - record.openedAt > SNAPSHOT_TTL_MS) {
|
|
74
|
+
this.records.delete(handle);
|
|
75
|
+
throw staleSnapshot();
|
|
76
|
+
}
|
|
77
|
+
const current = await this.readSource(scope.workspaceRoot, record.descriptor.filePath, Math.min(this.maxDocumentBytes, STRUCTURE_MAX_DOCUMENT_BYTES), signal);
|
|
78
|
+
signal?.throwIfAborted();
|
|
79
|
+
if (current.uri !== record.source.uri || current.key !== record.source.key || hashText(current.text) !== record.descriptor.contentHash) {
|
|
80
|
+
this.records.delete(handle);
|
|
81
|
+
throw staleSnapshot();
|
|
82
|
+
}
|
|
83
|
+
record.lastUsedAt = Date.now();
|
|
84
|
+
return record;
|
|
85
|
+
}
|
|
86
|
+
async readFresh(scope, filePath, signal) {
|
|
87
|
+
validateScope(scope);
|
|
88
|
+
signal?.throwIfAborted();
|
|
89
|
+
const source = await this.readSource(scope.workspaceRoot, filePath, this.maxDocumentBytes, signal);
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
const fileKey = JSON.stringify([
|
|
92
|
+
scopeKey(scope),
|
|
93
|
+
source.key,
|
|
94
|
+
source.uri
|
|
95
|
+
]);
|
|
96
|
+
this.rememberVersion(fileKey, scope, hashText(source.text));
|
|
97
|
+
return source;
|
|
98
|
+
}
|
|
99
|
+
release(scope, handle) {
|
|
100
|
+
validateScope(scope);
|
|
101
|
+
const record = this.records.get(handle);
|
|
102
|
+
if (!record || record.scopeKey !== scopeKey(scope)) return false;
|
|
103
|
+
return this.records.delete(handle);
|
|
104
|
+
}
|
|
105
|
+
releaseOwner(scope) {
|
|
106
|
+
validateScope(scope);
|
|
107
|
+
const owner = scopeKey(scope);
|
|
108
|
+
let released = 0;
|
|
109
|
+
for (const [handle, record] of this.records) if (record.scopeKey === owner) {
|
|
110
|
+
this.records.delete(handle);
|
|
111
|
+
released++;
|
|
112
|
+
}
|
|
113
|
+
for (const [key, version] of this.versions) if (version.scopeKey === owner) this.versions.delete(key);
|
|
114
|
+
return released;
|
|
115
|
+
}
|
|
116
|
+
clear() {
|
|
117
|
+
this.records.clear();
|
|
118
|
+
this.versions.clear();
|
|
119
|
+
}
|
|
120
|
+
get size() {
|
|
121
|
+
return this.records.size;
|
|
122
|
+
}
|
|
123
|
+
get documentByteLimit() {
|
|
124
|
+
return this.maxDocumentBytes;
|
|
125
|
+
}
|
|
126
|
+
totalBytes() {
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
for (const record of this.records.values()) bytes += record.descriptor.sourceBytes;
|
|
129
|
+
return bytes;
|
|
130
|
+
}
|
|
131
|
+
rememberVersion(fileKey, scope, hash) {
|
|
132
|
+
const owner = scopeKey(scope), previous = this.versions.get(fileKey);
|
|
133
|
+
const version = previous ? previous.hash === hash ? previous.version : previous.version + 1 : 1;
|
|
134
|
+
this.versions.delete(fileKey);
|
|
135
|
+
this.versions.set(fileKey, {
|
|
136
|
+
hash,
|
|
137
|
+
version,
|
|
138
|
+
scopeKey: owner
|
|
139
|
+
});
|
|
140
|
+
while (this.versions.size > 4096) this.versions.delete(this.versions.keys().next().value);
|
|
141
|
+
return version;
|
|
142
|
+
}
|
|
143
|
+
evictExpired() {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
for (const [handle, record] of this.records) if (now - record.openedAt > SNAPSHOT_TTL_MS) this.records.delete(handle);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
function languageForPath(filePath) {
|
|
149
|
+
const suffix = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
|
|
150
|
+
return EXTENSION_LANGUAGE[suffix] ?? null;
|
|
151
|
+
}
|
|
152
|
+
function validateScope(scope) {
|
|
153
|
+
if (!scope || typeof scope !== "object" || !scope.ownerId?.trim() || !scope.sessionId?.trim() || !scope.workspaceRoot?.trim() || !isAbsolute(scope.workspaceRoot) || /[\u0000-\u001f\u007f]/u.test(`${scope.ownerId}${scope.sessionId}${scope.workspaceRoot}`)) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "trusted owner, session and absolute workspace root are required");
|
|
154
|
+
}
|
|
155
|
+
function normalizeRelativePath(filePath) {
|
|
156
|
+
if (typeof filePath !== "string" || !filePath.trim() || filePath.length > 4096 || /[\u0000-\u001f\u007f]/u.test(filePath) || isAbsolute(filePath)) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "file path must be a relative workspace path");
|
|
157
|
+
const segments = filePath.replaceAll("\\", "/").split("/");
|
|
158
|
+
if (segments.some((segment) => segment === ".." || segment === "")) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "file path contains an invalid segment");
|
|
159
|
+
return segments.filter((segment) => segment !== ".").join("/");
|
|
160
|
+
}
|
|
161
|
+
function scopeIdentity(scope) {
|
|
162
|
+
return JSON.stringify([
|
|
163
|
+
scope.ownerId,
|
|
164
|
+
scope.sessionId,
|
|
165
|
+
scope.workspaceRoot
|
|
166
|
+
]);
|
|
167
|
+
}
|
|
168
|
+
function scopeKey(scope) {
|
|
169
|
+
return scopeIdentity(scope);
|
|
170
|
+
}
|
|
171
|
+
function hashText(text) {
|
|
172
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
173
|
+
}
|
|
174
|
+
function staleSnapshot() {
|
|
175
|
+
return new WebstackLspError("LSP_STALE_SNAPSHOT", "snapshot is stale, expired or belongs to another owner");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
//#endregion
|
|
179
|
+
//#region src/structure/coordinates.ts
|
|
180
|
+
/** web-tree-sitter 0.27.1 exposes JS-string indices as UTF-16 code-unit offsets. */
|
|
181
|
+
var Utf16Coordinates = class {
|
|
182
|
+
lines;
|
|
183
|
+
constructor(text) {
|
|
184
|
+
this.text = text;
|
|
185
|
+
this.lines = [];
|
|
186
|
+
let start = 0;
|
|
187
|
+
for (let index = 0; index < text.length;) {
|
|
188
|
+
const char = text[index];
|
|
189
|
+
if (char !== "\r" && char !== "\n") {
|
|
190
|
+
index++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
this.lines.push({
|
|
194
|
+
start,
|
|
195
|
+
end: index
|
|
196
|
+
});
|
|
197
|
+
if (char === "\r" && text[index + 1] === "\n") index++;
|
|
198
|
+
index++;
|
|
199
|
+
start = index;
|
|
200
|
+
}
|
|
201
|
+
this.lines.push({
|
|
202
|
+
start,
|
|
203
|
+
end: text.length
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
positionToIndex(position) {
|
|
207
|
+
if (!isPosition(position)) throw new RangeError("position must use non-negative zero-based UTF-16 coordinates");
|
|
208
|
+
const line = this.lines[position.line];
|
|
209
|
+
if (!line || position.character > line.end - line.start) throw new RangeError("position is outside document");
|
|
210
|
+
const index = line.start + position.character;
|
|
211
|
+
if (isBetweenSurrogatePair(this.text, index)) throw new RangeError("position splits a UTF-16 surrogate pair");
|
|
212
|
+
return index;
|
|
213
|
+
}
|
|
214
|
+
indexToPosition(index) {
|
|
215
|
+
if (!Number.isSafeInteger(index) || index < 0 || index > this.text.length) throw new RangeError("Tree-sitter index is outside document");
|
|
216
|
+
let low = 0, high = this.lines.length;
|
|
217
|
+
while (low < high) {
|
|
218
|
+
const middle = low + high >>> 1;
|
|
219
|
+
if (this.lines[middle].start <= index) low = middle + 1;
|
|
220
|
+
else high = middle;
|
|
221
|
+
}
|
|
222
|
+
const lineNumber = Math.max(0, low - 1);
|
|
223
|
+
const line = this.lines[lineNumber];
|
|
224
|
+
return {
|
|
225
|
+
line: lineNumber,
|
|
226
|
+
character: Math.min(index, line.end) - line.start
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
rangeToIndices(range) {
|
|
230
|
+
const startIndex = this.positionToIndex(range.start);
|
|
231
|
+
const endIndex = this.positionToIndex(range.end);
|
|
232
|
+
if (endIndex < startIndex) throw new RangeError("range end precedes range start");
|
|
233
|
+
return {
|
|
234
|
+
startIndex,
|
|
235
|
+
endIndex
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
indicesToRange(startIndex, endIndex) {
|
|
239
|
+
if (endIndex < startIndex) throw new RangeError("range end precedes range start");
|
|
240
|
+
return {
|
|
241
|
+
start: this.indexToPosition(startIndex),
|
|
242
|
+
end: this.indexToPosition(endIndex)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
function isPosition(value) {
|
|
247
|
+
if (typeof value !== "object" || value === null) return false;
|
|
248
|
+
const position = value;
|
|
249
|
+
return Number.isSafeInteger(position.line) && Number.isSafeInteger(position.character) && position.line >= 0 && position.character >= 0;
|
|
250
|
+
}
|
|
251
|
+
function isBetweenSurrogatePair(text, index) {
|
|
252
|
+
if (index <= 0 || index >= text.length) return false;
|
|
253
|
+
const previous = text.charCodeAt(index - 1), next = text.charCodeAt(index);
|
|
254
|
+
return previous >= 55296 && previous <= 56319 && next >= 56320 && next <= 57343;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
//#endregion
|
|
258
|
+
//#region src/structure/queries.ts
|
|
259
|
+
const declarations = {
|
|
260
|
+
javascript: [
|
|
261
|
+
"(function_declaration name: (_) @name) @declaration",
|
|
262
|
+
"(class_declaration name: (_) @name) @declaration",
|
|
263
|
+
"(method_definition name: (_) @name) @declaration",
|
|
264
|
+
"(variable_declarator name: (_) @name) @declaration"
|
|
265
|
+
],
|
|
266
|
+
typescript: [
|
|
267
|
+
"(function_declaration name: (_) @name) @declaration",
|
|
268
|
+
"(class_declaration name: (_) @name) @declaration",
|
|
269
|
+
"(interface_declaration name: (_) @name) @declaration",
|
|
270
|
+
"(type_alias_declaration name: (_) @name) @declaration",
|
|
271
|
+
"(enum_declaration name: (_) @name) @declaration",
|
|
272
|
+
"(method_definition name: (_) @name) @declaration",
|
|
273
|
+
"(variable_declarator name: (_) @name) @declaration"
|
|
274
|
+
],
|
|
275
|
+
tsx: [
|
|
276
|
+
"(function_declaration name: (_) @name) @declaration",
|
|
277
|
+
"(class_declaration name: (_) @name) @declaration",
|
|
278
|
+
"(interface_declaration name: (_) @name) @declaration",
|
|
279
|
+
"(type_alias_declaration name: (_) @name) @declaration",
|
|
280
|
+
"(enum_declaration name: (_) @name) @declaration",
|
|
281
|
+
"(method_definition name: (_) @name) @declaration",
|
|
282
|
+
"(variable_declarator name: (_) @name) @declaration"
|
|
283
|
+
]
|
|
284
|
+
};
|
|
285
|
+
const imports = {
|
|
286
|
+
javascript: ["(import_statement) @import", "(export_statement) @export"],
|
|
287
|
+
typescript: ["(import_statement) @import", "(export_statement) @export"],
|
|
288
|
+
tsx: ["(import_statement) @import", "(export_statement) @export"]
|
|
289
|
+
};
|
|
290
|
+
const calls = {
|
|
291
|
+
javascript: ["(call_expression function: (_) @call) @callsite"],
|
|
292
|
+
typescript: ["(call_expression function: (_) @call) @callsite"],
|
|
293
|
+
tsx: ["(call_expression function: (_) @call) @callsite"]
|
|
294
|
+
};
|
|
295
|
+
function getRegisteredQuery(language, queryId) {
|
|
296
|
+
return (queryId === "declarations" ? declarations[language] : queryId === "calls" ? calls[language] : queryId === "imports" ? imports[language] : void 0)?.join("\n");
|
|
297
|
+
}
|
|
298
|
+
function isRegisteredQueryId(value) {
|
|
299
|
+
return value === "declarations" || value === "calls" || value === "imports";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
//#endregion
|
|
303
|
+
//#region src/structure/worker-pool.ts
|
|
304
|
+
const MAX_WORKERS = 2;
|
|
305
|
+
const MAX_QUEUE = 16;
|
|
306
|
+
const WORKER_TASK_LIMIT = 100;
|
|
307
|
+
const PARSE_TIMEOUT_MS = 2e3;
|
|
308
|
+
/** CPU-bound parsing runs off the event loop; cancellation terminates the active worker. */
|
|
309
|
+
var StructureWorkerPool = class {
|
|
310
|
+
slots = /* @__PURE__ */ new Set();
|
|
311
|
+
queue = [];
|
|
312
|
+
disposed = false;
|
|
313
|
+
execute(request, signal) {
|
|
314
|
+
if (this.disposed) return Promise.reject(new WebstackLspError("LSP_DISPOSED", "structure worker pool is disposed"));
|
|
315
|
+
if (signal?.aborted) return Promise.reject(abortError());
|
|
316
|
+
if (this.queue.length >= MAX_QUEUE) return Promise.reject(new WebstackLspError("LSP_STRUCTURE_BUSY", "structure worker queue is full"));
|
|
317
|
+
return new Promise((resolve$1, reject) => {
|
|
318
|
+
const task = {
|
|
319
|
+
id: randomUUID(),
|
|
320
|
+
request,
|
|
321
|
+
...signal ? { signal } : {},
|
|
322
|
+
resolve: resolve$1,
|
|
323
|
+
reject
|
|
324
|
+
};
|
|
325
|
+
if (signal) {
|
|
326
|
+
const abort = () => {
|
|
327
|
+
const index = this.queue.indexOf(task);
|
|
328
|
+
if (index >= 0) {
|
|
329
|
+
this.queue.splice(index, 1);
|
|
330
|
+
this.finishTask(task, abortError());
|
|
331
|
+
} else {
|
|
332
|
+
const slot = [...this.slots].find((candidate) => candidate.current === task);
|
|
333
|
+
if (slot) this.retire(slot, abortError());
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
337
|
+
task.removeAbort = () => signal.removeEventListener("abort", abort);
|
|
338
|
+
}
|
|
339
|
+
this.queue.push(task);
|
|
340
|
+
this.pump();
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
async dispose() {
|
|
344
|
+
this.disposed = true;
|
|
345
|
+
for (const task of this.queue.splice(0)) this.finishTask(task, new WebstackLspError("LSP_DISPOSED", "structure worker pool is disposed"));
|
|
346
|
+
await Promise.all([...this.slots].map((slot) => this.retire(slot, new WebstackLspError("LSP_DISPOSED", "structure worker pool is disposed"))));
|
|
347
|
+
}
|
|
348
|
+
releaseSnapshot(snapshotHandle) {
|
|
349
|
+
for (const slot of this.slots) if (!slot.failed) try {
|
|
350
|
+
slot.worker.postMessage({ releaseSnapshotHandle: snapshotHandle });
|
|
351
|
+
} catch {
|
|
352
|
+
this.retire(slot, new WebstackLspError("LSP_STRUCTURE_UNAVAILABLE", "structure worker could not release a cached snapshot"));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
get queued() {
|
|
356
|
+
return this.queue.length;
|
|
357
|
+
}
|
|
358
|
+
get active() {
|
|
359
|
+
return [...this.slots].filter((slot) => slot.current !== void 0).length;
|
|
360
|
+
}
|
|
361
|
+
pump() {
|
|
362
|
+
if (this.disposed) return;
|
|
363
|
+
while (this.queue.length) {
|
|
364
|
+
let slot = [...this.slots].find((candidate) => !candidate.current && !candidate.failed);
|
|
365
|
+
if (!slot && this.slots.size < MAX_WORKERS) slot = this.createWorker();
|
|
366
|
+
if (!slot) return;
|
|
367
|
+
const task = this.queue.shift();
|
|
368
|
+
if (task.signal?.aborted) {
|
|
369
|
+
this.finishTask(task, abortError());
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
slot.current = task;
|
|
373
|
+
task.timer = setTimeout(() => {
|
|
374
|
+
this.retire(slot, new WebstackLspError("LSP_STRUCTURE_TIMEOUT", "structure parsing exceeded its time limit"));
|
|
375
|
+
}, PARSE_TIMEOUT_MS);
|
|
376
|
+
try {
|
|
377
|
+
slot.worker.postMessage({
|
|
378
|
+
id: task.id,
|
|
379
|
+
...task.request
|
|
380
|
+
});
|
|
381
|
+
} catch (error) {
|
|
382
|
+
this.retire(slot, error);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
createWorker() {
|
|
387
|
+
const worker = new Worker(new URL("./parser-worker.mjs", import.meta.url));
|
|
388
|
+
const slot = {
|
|
389
|
+
worker,
|
|
390
|
+
current: void 0,
|
|
391
|
+
completed: 0,
|
|
392
|
+
failed: false
|
|
393
|
+
};
|
|
394
|
+
this.slots.add(slot);
|
|
395
|
+
worker.on("message", (reply) => {
|
|
396
|
+
if (!slot.current || reply.id !== slot.current.id) {
|
|
397
|
+
this.retire(slot, new WebstackLspError("LSP_MALFORMED_RESPONSE", "structure worker returned an unexpected task identity"));
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const task = slot.current;
|
|
401
|
+
slot.current = void 0;
|
|
402
|
+
slot.completed++;
|
|
403
|
+
if (!reply.ok || !reply.value) this.finishTask(task, new WebstackLspError("LSP_STRUCTURE_UNAVAILABLE", reply.error ?? "structure parsing failed"));
|
|
404
|
+
else this.finishTask(task, void 0, reply.value);
|
|
405
|
+
if (slot.completed >= WORKER_TASK_LIMIT) this.retire(slot);
|
|
406
|
+
this.pump();
|
|
407
|
+
});
|
|
408
|
+
worker.on("error", (error) => {
|
|
409
|
+
this.retire(slot, error);
|
|
410
|
+
});
|
|
411
|
+
worker.on("exit", (code) => {
|
|
412
|
+
if (this.slots.has(slot) && !slot.failed) this.retire(slot, code === 0 ? void 0 : new WebstackLspError("LSP_STRUCTURE_UNAVAILABLE", `structure worker exited (${code})`));
|
|
413
|
+
});
|
|
414
|
+
return slot;
|
|
415
|
+
}
|
|
416
|
+
async retire(slot, error) {
|
|
417
|
+
if (slot.failed) return;
|
|
418
|
+
slot.failed = true;
|
|
419
|
+
this.slots.delete(slot);
|
|
420
|
+
const task = slot.current;
|
|
421
|
+
slot.current = void 0;
|
|
422
|
+
if (task) this.finishTask(task, error ?? new WebstackLspError("LSP_STRUCTURE_UNAVAILABLE", "structure worker was restarted"));
|
|
423
|
+
await slot.worker.terminate().catch(() => 0);
|
|
424
|
+
this.pump();
|
|
425
|
+
}
|
|
426
|
+
finishTask(task, error, value) {
|
|
427
|
+
if (task.timer) clearTimeout(task.timer);
|
|
428
|
+
task.removeAbort?.();
|
|
429
|
+
if (error !== void 0) task.reject(error);
|
|
430
|
+
else task.resolve(value);
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
function abortError() {
|
|
434
|
+
return new DOMException("The operation was aborted", "AbortError");
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
//#endregion
|
|
438
|
+
//#region src/structure/engine.ts
|
|
439
|
+
const MAX_CAPTURES = 200;
|
|
440
|
+
const MAX_OUTLINE = 100;
|
|
441
|
+
const MAX_SPAN_CHARS = 8e3;
|
|
442
|
+
const MAX_STRUCTURE_RESPONSE_BYTES = 32 * 1024;
|
|
443
|
+
const MAX_NODE_HANDLES = 1e4;
|
|
444
|
+
var StructureEngine = class {
|
|
445
|
+
nodeHandles = /* @__PURE__ */ new Map();
|
|
446
|
+
snapshotOwners = /* @__PURE__ */ new Map();
|
|
447
|
+
parserFailures = /* @__PURE__ */ new Map();
|
|
448
|
+
constructor(workers = new StructureWorkerPool()) {
|
|
449
|
+
this.workers = workers;
|
|
450
|
+
}
|
|
451
|
+
async inspect(record, signal) {
|
|
452
|
+
this.snapshotOwners.set(record.descriptor.handle, record.scopeKey);
|
|
453
|
+
const language = record.descriptor.language;
|
|
454
|
+
if (!language) return {
|
|
455
|
+
available: false,
|
|
456
|
+
parseErrors: false,
|
|
457
|
+
reason: "grammar_missing"
|
|
458
|
+
};
|
|
459
|
+
try {
|
|
460
|
+
const result = await this.runWorker(language, {
|
|
461
|
+
operation: "inspect",
|
|
462
|
+
language,
|
|
463
|
+
text: record.source.text,
|
|
464
|
+
querySource: "",
|
|
465
|
+
limit: 1,
|
|
466
|
+
snapshotHandle: record.descriptor.handle,
|
|
467
|
+
...record.previousSnapshotHandle ? { previousSnapshotHandle: record.previousSnapshotHandle } : {}
|
|
468
|
+
}, signal);
|
|
469
|
+
this.parserFailures.delete(language);
|
|
470
|
+
return {
|
|
471
|
+
available: true,
|
|
472
|
+
parseErrors: result.errors.hasError
|
|
473
|
+
};
|
|
474
|
+
} catch (error) {
|
|
475
|
+
return {
|
|
476
|
+
available: false,
|
|
477
|
+
parseErrors: false,
|
|
478
|
+
reason: error instanceof Error ? error.message.slice(0, 128) : "parser_unavailable"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async outline(record, scope, options = {}, signal) {
|
|
483
|
+
const language = record.descriptor.language;
|
|
484
|
+
if (!language) return {
|
|
485
|
+
status: "unsupported",
|
|
486
|
+
handle: record.descriptor.handle,
|
|
487
|
+
version: record.descriptor.version,
|
|
488
|
+
declarations: [],
|
|
489
|
+
truncated: false,
|
|
490
|
+
processed: 0,
|
|
491
|
+
errors: {
|
|
492
|
+
hasError: false,
|
|
493
|
+
errorNodes: 0,
|
|
494
|
+
missingNodes: 0,
|
|
495
|
+
truncated: false
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
const limit = positiveLimit(options.limit, MAX_OUTLINE, "outline limit");
|
|
499
|
+
const coordinates = new Utf16Coordinates(record.source.text);
|
|
500
|
+
const range = options.range ? coordinates.rangeToIndices(options.range) : void 0;
|
|
501
|
+
const result = await this.runMatches(language, record, "declarations", limit, range, signal);
|
|
502
|
+
const declarations$1 = [];
|
|
503
|
+
for (const match of result.matches ?? []) {
|
|
504
|
+
const declaration = match.captures.find((capture) => capture.name === "declaration");
|
|
505
|
+
const nameCapture = match.captures.find((capture) => capture.name === "name");
|
|
506
|
+
if (!declaration) continue;
|
|
507
|
+
const node = this.node(record, scope, declaration.kind, declaration.startIndex, declaration.endIndex, nameCapture ? record.source.text.slice(nameCapture.startIndex, nameCapture.endIndex).slice(0, 256) : void 0, declaration.childCount, coordinates);
|
|
508
|
+
declarations$1.push(node);
|
|
509
|
+
}
|
|
510
|
+
return this.outlineResult(record, declarations$1, result);
|
|
511
|
+
}
|
|
512
|
+
async enclosing(record, scope, position, kinds, signal) {
|
|
513
|
+
const language = record.descriptor.language;
|
|
514
|
+
if (!language) return {
|
|
515
|
+
status: "unsupported",
|
|
516
|
+
handle: record.descriptor.handle,
|
|
517
|
+
version: record.descriptor.version,
|
|
518
|
+
ancestors: []
|
|
519
|
+
};
|
|
520
|
+
const coordinates = new Utf16Coordinates(record.source.text);
|
|
521
|
+
const index = coordinates.positionToIndex(position);
|
|
522
|
+
if (kinds && (!Array.isArray(kinds) || kinds.length > 32 || kinds.some((kind) => typeof kind !== "string" || kind.length > 128 || !/^[a-z][a-z0-9_]*$/u.test(kind)))) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "kinds must be a list of at most 32 node type names");
|
|
523
|
+
const result = await this.runWorker(language, {
|
|
524
|
+
operation: "enclosing",
|
|
525
|
+
language,
|
|
526
|
+
text: record.source.text,
|
|
527
|
+
querySource: "",
|
|
528
|
+
limit: 16,
|
|
529
|
+
snapshotHandle: record.descriptor.handle,
|
|
530
|
+
index,
|
|
531
|
+
...kinds ? { kinds: [...kinds] } : {}
|
|
532
|
+
}, signal);
|
|
533
|
+
const [node, ...ancestors] = (result.nodes ?? []).map((item) => this.node(record, scope, item.kind, item.startIndex, item.endIndex, void 0, item.childCount, coordinates));
|
|
534
|
+
return {
|
|
535
|
+
status: result.status,
|
|
536
|
+
handle: record.descriptor.handle,
|
|
537
|
+
version: record.descriptor.version,
|
|
538
|
+
...node ? { node } : {},
|
|
539
|
+
ancestors,
|
|
540
|
+
errors: publicErrors(result.errors)
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
async query(record, scope, queryId, options = {}, signal) {
|
|
544
|
+
const language = record.descriptor.language;
|
|
545
|
+
if (!language) throw new WebstackLspError("LSP_STRUCTURE_UNSUPPORTED", "no bundled Tree-sitter grammar is available for this document");
|
|
546
|
+
if (!isRegisteredQueryId(queryId)) throw new WebstackLspError("LSP_STRUCTURE_UNSUPPORTED", "queryId is not in the registered query allowlist");
|
|
547
|
+
const limit = positiveLimit(options.limit, MAX_CAPTURES, "capture limit");
|
|
548
|
+
const coordinates = new Utf16Coordinates(record.source.text);
|
|
549
|
+
const range = options.range ? coordinates.rangeToIndices(options.range) : void 0;
|
|
550
|
+
const result = await this.runMatches(language, record, queryId, limit, range, signal);
|
|
551
|
+
const captures = [];
|
|
552
|
+
captureLoop: for (const match of result.matches ?? []) for (const capture of match.captures) {
|
|
553
|
+
const node = this.node(record, scope, capture.kind, capture.startIndex, capture.endIndex, void 0, capture.childCount, coordinates);
|
|
554
|
+
captures.push({
|
|
555
|
+
capture: capture.name,
|
|
556
|
+
kind: capture.kind,
|
|
557
|
+
range: node.range,
|
|
558
|
+
nodeHandle: node.handle,
|
|
559
|
+
...capture.name === "name" ? { name: record.source.text.slice(capture.startIndex, capture.endIndex).slice(0, 256) } : {}
|
|
560
|
+
});
|
|
561
|
+
if (captures.length >= limit) break captureLoop;
|
|
562
|
+
}
|
|
563
|
+
if (Buffer.byteLength(JSON.stringify(captures), "utf8") > MAX_STRUCTURE_RESPONSE_BYTES) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "query result exceeds the response byte limit");
|
|
564
|
+
return {
|
|
565
|
+
status: result.status,
|
|
566
|
+
handle: record.descriptor.handle,
|
|
567
|
+
version: record.descriptor.version,
|
|
568
|
+
queryId,
|
|
569
|
+
captures,
|
|
570
|
+
truncated: Boolean(result.truncated || (result.matches?.length ?? 0) > limit),
|
|
571
|
+
processed: captures.length,
|
|
572
|
+
errors: publicErrors(result.errors)
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
async span(record, scope, nodeHandle, maxChars = MAX_SPAN_CHARS) {
|
|
576
|
+
const handle = this.nodeHandles.get(nodeHandle);
|
|
577
|
+
if (!handle || handle.snapshotHandle !== record.descriptor.handle || handle.scopeKey !== scopeIdentity(scope)) throw new WebstackLspError("LSP_STALE_SNAPSHOT", "node handle is stale or belongs to another owner");
|
|
578
|
+
if (!Number.isSafeInteger(maxChars) || maxChars < 1 || maxChars > MAX_SPAN_CHARS) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", `maxChars must be between 1 and ${MAX_SPAN_CHARS}`);
|
|
579
|
+
const full = record.source.text.slice(handle.startIndex, handle.endIndex);
|
|
580
|
+
const codePoints = Array.from(full);
|
|
581
|
+
const truncated = codePoints.length > maxChars;
|
|
582
|
+
const text = codePoints.slice(0, maxChars).join("");
|
|
583
|
+
if (Buffer.byteLength(text, "utf8") > MAX_STRUCTURE_RESPONSE_BYTES) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "span result exceeds the response byte limit");
|
|
584
|
+
return {
|
|
585
|
+
status: truncated ? "partial" : "ok",
|
|
586
|
+
text,
|
|
587
|
+
truncated,
|
|
588
|
+
range: handle.range
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
releaseSnapshot(handle) {
|
|
592
|
+
for (const [nodeHandle, node] of this.nodeHandles) if (node.snapshotHandle === handle) this.nodeHandles.delete(nodeHandle);
|
|
593
|
+
this.snapshotOwners.delete(handle);
|
|
594
|
+
this.workers.releaseSnapshot(handle);
|
|
595
|
+
}
|
|
596
|
+
releaseSnapshotForOwner(scope, handle) {
|
|
597
|
+
if (this.snapshotOwners.get(handle) === scopeIdentity(scope)) this.releaseSnapshot(handle);
|
|
598
|
+
}
|
|
599
|
+
snapshotForNode(scope, nodeHandle) {
|
|
600
|
+
const record = this.nodeHandles.get(nodeHandle);
|
|
601
|
+
if (!record || record.scopeKey !== scopeIdentity(scope)) throw new WebstackLspError("LSP_STALE_SNAPSHOT", "node handle is stale or belongs to another owner");
|
|
602
|
+
return record.snapshotHandle;
|
|
603
|
+
}
|
|
604
|
+
releaseOwner(scope) {
|
|
605
|
+
const owner = scopeIdentity(scope);
|
|
606
|
+
for (const [nodeHandle, node] of this.nodeHandles) if (node.scopeKey === owner) this.nodeHandles.delete(nodeHandle);
|
|
607
|
+
for (const [snapshotHandle, snapshotOwner] of this.snapshotOwners) if (snapshotOwner === owner) this.releaseSnapshot(snapshotHandle);
|
|
608
|
+
}
|
|
609
|
+
capabilities() {
|
|
610
|
+
return [
|
|
611
|
+
this.capability("javascript", [
|
|
612
|
+
".js",
|
|
613
|
+
".jsx",
|
|
614
|
+
".mjs",
|
|
615
|
+
".cjs"
|
|
616
|
+
], 15),
|
|
617
|
+
this.capability("typescript", [
|
|
618
|
+
".ts",
|
|
619
|
+
".mts",
|
|
620
|
+
".cts"
|
|
621
|
+
], 14),
|
|
622
|
+
this.capability("tsx", [".tsx"], 14)
|
|
623
|
+
];
|
|
624
|
+
}
|
|
625
|
+
async dispose() {
|
|
626
|
+
this.nodeHandles.clear();
|
|
627
|
+
this.snapshotOwners.clear();
|
|
628
|
+
await this.workers.dispose();
|
|
629
|
+
}
|
|
630
|
+
async runMatches(language, record, queryId, limit, range, signal) {
|
|
631
|
+
const querySource = getRegisteredQuery(language, queryId);
|
|
632
|
+
if (!querySource) throw new WebstackLspError("LSP_STRUCTURE_UNSUPPORTED", `query ${queryId} is not registered for ${language}`);
|
|
633
|
+
return this.runWorker(language, {
|
|
634
|
+
operation: "matches",
|
|
635
|
+
language,
|
|
636
|
+
text: record.source.text,
|
|
637
|
+
querySource,
|
|
638
|
+
limit,
|
|
639
|
+
snapshotHandle: record.descriptor.handle,
|
|
640
|
+
...range ?? {}
|
|
641
|
+
}, signal);
|
|
642
|
+
}
|
|
643
|
+
capability(language, extensions, abiVersion) {
|
|
644
|
+
const lastError = this.parserFailures.get(language);
|
|
645
|
+
return {
|
|
646
|
+
language,
|
|
647
|
+
extensions,
|
|
648
|
+
available: !lastError,
|
|
649
|
+
abiVersion,
|
|
650
|
+
...lastError ? { lastError } : {}
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
async runWorker(language, request, signal) {
|
|
654
|
+
try {
|
|
655
|
+
const result = await this.workers.execute(request, signal);
|
|
656
|
+
this.parserFailures.delete(language);
|
|
657
|
+
return result;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
if (error instanceof WebstackLspError && ["LSP_STRUCTURE_TIMEOUT", "LSP_STRUCTURE_UNAVAILABLE"].includes(error.code)) this.parserFailures.set(language, error.message.slice(0, 128));
|
|
660
|
+
throw error;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
outlineResult(record, declarations$1, result) {
|
|
664
|
+
const truncated = Boolean(result.truncated || declarations$1.length > MAX_OUTLINE);
|
|
665
|
+
const selected = declarations$1.slice(0, MAX_OUTLINE);
|
|
666
|
+
if (Buffer.byteLength(JSON.stringify(selected), "utf8") > MAX_STRUCTURE_RESPONSE_BYTES) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "outline result exceeds the response byte limit");
|
|
667
|
+
return {
|
|
668
|
+
status: result.status,
|
|
669
|
+
handle: record.descriptor.handle,
|
|
670
|
+
version: record.descriptor.version,
|
|
671
|
+
declarations: selected,
|
|
672
|
+
truncated,
|
|
673
|
+
processed: selected.length,
|
|
674
|
+
errors: publicErrors(result.errors)
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
node(record, scope, kind, startIndex, endIndex, name, childCount, coordinates = new Utf16Coordinates(record.source.text)) {
|
|
678
|
+
if (this.nodeHandles.size >= MAX_NODE_HANDLES) throw new WebstackLspError("LSP_SNAPSHOT_LIMIT", "node handle capacity is full; release a snapshot and retry");
|
|
679
|
+
const handle = randomUUID();
|
|
680
|
+
const range = coordinates.indicesToRange(startIndex, endIndex);
|
|
681
|
+
this.nodeHandles.set(handle, {
|
|
682
|
+
scopeKey: scopeIdentity(scope),
|
|
683
|
+
snapshotHandle: record.descriptor.handle,
|
|
684
|
+
startIndex,
|
|
685
|
+
endIndex,
|
|
686
|
+
kind,
|
|
687
|
+
range
|
|
688
|
+
});
|
|
689
|
+
return {
|
|
690
|
+
handle,
|
|
691
|
+
kind: publicNodeKind(kind),
|
|
692
|
+
range,
|
|
693
|
+
...name ? { name } : {},
|
|
694
|
+
...childCount === void 0 ? {} : { childCount }
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
function positiveLimit(value, maximum, label) {
|
|
699
|
+
const limit = value ?? maximum;
|
|
700
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > maximum) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", `${label} must be between 1 and ${maximum}`);
|
|
701
|
+
return limit;
|
|
702
|
+
}
|
|
703
|
+
function publicErrors(errors) {
|
|
704
|
+
return { ...errors };
|
|
705
|
+
}
|
|
706
|
+
function publicNodeKind(kind) {
|
|
707
|
+
return {
|
|
708
|
+
function_declaration: "function",
|
|
709
|
+
class_declaration: "class",
|
|
710
|
+
interface_declaration: "interface",
|
|
711
|
+
type_alias_declaration: "type",
|
|
712
|
+
enum_declaration: "enum",
|
|
713
|
+
method_definition: "method",
|
|
714
|
+
variable_declarator: "variable"
|
|
715
|
+
}[kind] ?? kind;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
//#endregion
|
|
719
|
+
//#region src/structure/code-intelligence.ts
|
|
720
|
+
var CodeIntelligenceRuntime = class {
|
|
721
|
+
version = 1;
|
|
722
|
+
snapshot;
|
|
723
|
+
structure;
|
|
724
|
+
semantic;
|
|
725
|
+
store;
|
|
726
|
+
engine;
|
|
727
|
+
constructor(options) {
|
|
728
|
+
this.store = options.snapshots;
|
|
729
|
+
this.engine = new StructureEngine();
|
|
730
|
+
this.snapshot = {
|
|
731
|
+
open: async (scope, filePath, signal) => {
|
|
732
|
+
const record = await this.store.open(scope, filePath, signal);
|
|
733
|
+
try {
|
|
734
|
+
const parser = await this.engine.inspect(record, signal);
|
|
735
|
+
await this.store.current(scope, record.descriptor.handle, signal);
|
|
736
|
+
return {
|
|
737
|
+
status: "ok",
|
|
738
|
+
...record.descriptor,
|
|
739
|
+
parserAvailable: parser.available,
|
|
740
|
+
parseErrors: parser.parseErrors
|
|
741
|
+
};
|
|
742
|
+
} catch (error) {
|
|
743
|
+
this.store.release(scope, record.descriptor.handle);
|
|
744
|
+
this.engine.releaseSnapshot(record.descriptor.handle);
|
|
745
|
+
throw error;
|
|
746
|
+
}
|
|
747
|
+
},
|
|
748
|
+
release: async (scope, handle) => {
|
|
749
|
+
const released = this.store.release(scope, handle);
|
|
750
|
+
if (released) this.engine.releaseSnapshot(handle);
|
|
751
|
+
return { released };
|
|
752
|
+
}
|
|
753
|
+
};
|
|
754
|
+
this.structure = {
|
|
755
|
+
outline: (scope, handle, request, signal) => this.withSnapshot(scope, handle, signal, (record) => this.engine.outline(record, scope, request, signal)),
|
|
756
|
+
enclosing: (scope, handle, position, request, signal) => this.withSnapshot(scope, handle, signal, (record) => this.engine.enclosing(record, scope, position, request?.kinds, signal)),
|
|
757
|
+
query: (scope, handle, queryId, request, signal) => this.withSnapshot(scope, handle, signal, (record) => this.engine.query(record, scope, queryId, request, signal)),
|
|
758
|
+
span: (scope, nodeHandle, maxChars, signal) => this.span(scope, nodeHandle, maxChars, signal)
|
|
759
|
+
};
|
|
760
|
+
this.semantic = { query: async (scope, handle, request, signal) => {
|
|
761
|
+
validateSemanticQuery(request);
|
|
762
|
+
return this.withSnapshot(scope, handle, signal, async (record) => {
|
|
763
|
+
const raw = await options.semanticQuery(scope, record, request, signal ?? new AbortController().signal);
|
|
764
|
+
await this.store.current(scope, handle, signal);
|
|
765
|
+
const bounded = limitSemanticResult(raw, request.maxResults ?? 200);
|
|
766
|
+
const truncated = "truncated" in bounded && Boolean(bounded.truncated);
|
|
767
|
+
const value = {
|
|
768
|
+
status: semanticIsPartial(bounded) || truncated ? "partial" : "ok",
|
|
769
|
+
operation: request.operation,
|
|
770
|
+
result: bounded,
|
|
771
|
+
...truncated ? {
|
|
772
|
+
truncated: true,
|
|
773
|
+
omitted: "omitted" in bounded && typeof bounded.omitted === "number" ? bounded.omitted : 0
|
|
774
|
+
} : {}
|
|
775
|
+
};
|
|
776
|
+
if (Buffer.byteLength(JSON.stringify(value), "utf8") > MAX_STRUCTURE_RESPONSE_BYTES) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "semantic result exceeds the response byte limit");
|
|
777
|
+
return value;
|
|
778
|
+
});
|
|
779
|
+
} };
|
|
780
|
+
}
|
|
781
|
+
capabilities() {
|
|
782
|
+
return {
|
|
783
|
+
version: 1,
|
|
784
|
+
parsers: this.engine.capabilities().map((parser) => ({
|
|
785
|
+
...parser,
|
|
786
|
+
runtimeAbi: {
|
|
787
|
+
minimum: 13,
|
|
788
|
+
current: 15
|
|
789
|
+
}
|
|
790
|
+
})),
|
|
791
|
+
sourceKind: "disk",
|
|
792
|
+
overlay: "unavailable",
|
|
793
|
+
limits: {
|
|
794
|
+
maxDocumentBytes: Math.min(this.store.documentByteLimit, 2 * 1024 * 1024),
|
|
795
|
+
maxCaptures: MAX_CAPTURES,
|
|
796
|
+
maxOutline: MAX_OUTLINE,
|
|
797
|
+
maxSpanChars: MAX_SPAN_CHARS,
|
|
798
|
+
maxResponseBytes: MAX_STRUCTURE_RESPONSE_BYTES
|
|
799
|
+
},
|
|
800
|
+
incrementalParsing: true
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
async releaseOwner(scope) {
|
|
804
|
+
this.engine.releaseOwner(scope);
|
|
805
|
+
return this.store.releaseOwner(scope);
|
|
806
|
+
}
|
|
807
|
+
async dispose() {
|
|
808
|
+
this.store.clear();
|
|
809
|
+
await this.engine.dispose();
|
|
810
|
+
}
|
|
811
|
+
async span(scope, nodeHandle, maxChars = MAX_SPAN_CHARS, signal) {
|
|
812
|
+
const snapshotHandle = this.engine.snapshotForNode(scope, nodeHandle);
|
|
813
|
+
return this.withSnapshot(scope, snapshotHandle, signal, (record) => this.engine.span(record, scope, nodeHandle, maxChars));
|
|
814
|
+
}
|
|
815
|
+
async withSnapshot(scope, handle, signal, execute) {
|
|
816
|
+
let record;
|
|
817
|
+
try {
|
|
818
|
+
record = await this.store.current(scope, handle, signal);
|
|
819
|
+
} catch (error) {
|
|
820
|
+
if (error instanceof WebstackLspError && error.code === "LSP_STALE_SNAPSHOT") this.engine.releaseSnapshotForOwner(scope, handle);
|
|
821
|
+
throw error;
|
|
822
|
+
}
|
|
823
|
+
try {
|
|
824
|
+
const result = await execute(record);
|
|
825
|
+
await this.store.current(scope, handle, signal);
|
|
826
|
+
return result;
|
|
827
|
+
} catch (error) {
|
|
828
|
+
if (error instanceof WebstackLspError && error.code === "LSP_STALE_SNAPSHOT") this.engine.releaseSnapshotForOwner(scope, handle);
|
|
829
|
+
throw error;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
function createCodeIntelligenceToolServiceV1(options) {
|
|
834
|
+
return new CodeIntelligenceRuntime(options);
|
|
835
|
+
}
|
|
836
|
+
function validateSemanticQuery(request) {
|
|
837
|
+
if (!request || ![
|
|
838
|
+
"definition",
|
|
839
|
+
"references",
|
|
840
|
+
"implementation",
|
|
841
|
+
"hover",
|
|
842
|
+
"diagnostics",
|
|
843
|
+
"completion"
|
|
844
|
+
].includes(request.operation)) throw new WebstackLspError("LSP_UNSUPPORTED_OPERATION", "semantic operation is not supported");
|
|
845
|
+
if (request.operation !== "diagnostics" && !request.position) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "semantic position is required for this operation");
|
|
846
|
+
if (request.position && (!Number.isSafeInteger(request.position.line) || !Number.isSafeInteger(request.position.character) || request.position.line < 0 || request.position.character < 0)) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "semantic position must be a zero-based UTF-16 position");
|
|
847
|
+
if (request.servers && (!Array.isArray(request.servers) || request.servers.length > 6 || request.servers.some((server) => typeof server !== "string" || server.length > 64))) throw new WebstackLspError("LSP_WORKSPACE_REQUIRED", "semantic server filter is invalid");
|
|
848
|
+
if (request.maxResults !== void 0 && (!Number.isSafeInteger(request.maxResults) || request.maxResults < 1 || request.maxResults > 200)) throw new WebstackLspError("LSP_STRUCTURE_LIMIT", "maxResults must be between 1 and 200");
|
|
849
|
+
}
|
|
850
|
+
function semanticIsPartial(result) {
|
|
851
|
+
if ("diagnostics" in result) return Boolean(result.omittedServers?.length || result.truncated);
|
|
852
|
+
if ("items" in result) return Boolean(result.truncated);
|
|
853
|
+
if (result.kind === "locations") return Boolean(result.truncated);
|
|
854
|
+
return false;
|
|
855
|
+
}
|
|
856
|
+
function limitSemanticResult(result, limit) {
|
|
857
|
+
if ("items" in result) {
|
|
858
|
+
if (result.items.length <= limit) return result;
|
|
859
|
+
return {
|
|
860
|
+
...result,
|
|
861
|
+
items: result.items.slice(0, limit),
|
|
862
|
+
truncated: {
|
|
863
|
+
items: result.items.length - limit,
|
|
864
|
+
characters: 0
|
|
865
|
+
}
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
if ("diagnostics" in result) {
|
|
869
|
+
if (result.diagnostics.length <= limit) return result;
|
|
870
|
+
return {
|
|
871
|
+
...result,
|
|
872
|
+
diagnostics: result.diagnostics.slice(0, limit),
|
|
873
|
+
truncated: {
|
|
874
|
+
diagnostics: result.diagnostics.length - limit,
|
|
875
|
+
characters: 0
|
|
876
|
+
}
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
if (result.kind === "locations" && result.locations.length > limit) return {
|
|
880
|
+
...result,
|
|
881
|
+
locations: result.locations.slice(0, limit),
|
|
882
|
+
truncated: true,
|
|
883
|
+
omitted: result.locations.length - limit
|
|
884
|
+
};
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
//#endregion
|
|
889
|
+
export { DocumentSnapshotStore, createCodeIntelligenceToolServiceV1 };
|
|
890
|
+
//# sourceMappingURL=code-intelligence-DvhxPnpu.js.map
|