semantic-js-mcp 0.8.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/.codex-plugin/plugin.json +25 -0
- package/.mcp.json +12 -0
- package/.prettierrc.json +9 -0
- package/CHANGELOG.md +29 -0
- package/CONTRIBUTING.md +64 -0
- package/LICENSE +21 -0
- package/README.md +278 -0
- package/ROADMAP.md +37 -0
- package/SECURITY.md +22 -0
- package/cli.mjs +45 -0
- package/docs/architecture-decisions.md +72 -0
- package/docs/distribution.md +54 -0
- package/lib/doctor.mjs +319 -0
- package/lib/runtime.mjs +69 -0
- package/package.json +70 -0
- package/protocol.mjs +356 -0
- package/scripts/benchmark.mjs +88 -0
- package/scripts/check-protocol-literals.mjs +126 -0
- package/scripts/check-runtime.mjs +32 -0
- package/scripts/ci-smoke.mjs +147 -0
- package/scripts/distribution-policy.mjs +33 -0
- package/scripts/distribution-smoke.mjs +128 -0
- package/scripts/generate-protocol-reference.mjs +154 -0
- package/scripts/semantic-js-mcp-ci.mjs +122 -0
- package/scripts/smoke.mjs +686 -0
- package/scripts/vue-smoke.mjs +82 -0
- package/server.mjs +2702 -0
- package/skills/semantic-navigation/SKILL.md +125 -0
- package/skills/semantic-navigation/agents/openai.yaml +4 -0
- package/skills/semantic-navigation/references/protocol-literals.md +381 -0
package/server.mjs
ADDED
|
@@ -0,0 +1,2702 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {spawn} from "node:child_process";
|
|
4
|
+
import {createHash, randomUUID} from "node:crypto";
|
|
5
|
+
import {createReadStream, existsSync} from "node:fs";
|
|
6
|
+
import {readFile, realpath, stat} from "node:fs/promises";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import {fileURLToPath, pathToFileURL} from "node:url";
|
|
9
|
+
import {McpServer} from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
10
|
+
import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
+
import * as z from "zod/v4";
|
|
12
|
+
import {stringify as stringifyYaml} from "yaml";
|
|
13
|
+
import {PACKAGE_ROOT, inspectRuntimeComponents, resolveRuntimeComponent, runtimeDependencyRoot} from "./lib/runtime.mjs";
|
|
14
|
+
import {
|
|
15
|
+
ACCOUNTING_STATUS,
|
|
16
|
+
COLLECTION_STATUS,
|
|
17
|
+
CONTENT_FRESHNESS,
|
|
18
|
+
DEFAULT,
|
|
19
|
+
DEFINITION_MATCH,
|
|
20
|
+
DEFINITION_RESOLUTION_METHOD,
|
|
21
|
+
DIAGNOSTIC_EVIDENCE_REASON,
|
|
22
|
+
DIAGNOSTIC_FRESHNESS,
|
|
23
|
+
DIAGNOSTIC_SEVERITY,
|
|
24
|
+
EVIDENCE_STATUS,
|
|
25
|
+
ENVIRONMENT_VARIABLE,
|
|
26
|
+
ERROR_CODE,
|
|
27
|
+
EVIDENCE_TYPE,
|
|
28
|
+
FINGERPRINT_ALGORITHM,
|
|
29
|
+
FORBIDDEN_PUBLIC_FIELD,
|
|
30
|
+
LANGUAGE_ID,
|
|
31
|
+
LIMIT_MODE,
|
|
32
|
+
NODE_EVENT,
|
|
33
|
+
INTERNAL_RESOLUTION_SOURCE,
|
|
34
|
+
PRESENTATION_MODE,
|
|
35
|
+
PROCESS_EXIT_CODE,
|
|
36
|
+
PRODUCT,
|
|
37
|
+
REFERENCE_DISCOVERY_METHOD,
|
|
38
|
+
REFERENCE_SET_CHANGE_TYPE,
|
|
39
|
+
REQUIRED_RUNTIME_COMPONENT,
|
|
40
|
+
RUNTIME_COMMAND,
|
|
41
|
+
RESULT_SCHEMA,
|
|
42
|
+
SERVER_VERSION,
|
|
43
|
+
SIGNATURE_SOURCE,
|
|
44
|
+
TOOL,
|
|
45
|
+
TOOL_ORDER,
|
|
46
|
+
TYPESCRIPT_PROJECT_KIND,
|
|
47
|
+
WORKSPACE_CONFIGURATION_FILE_NAMES,
|
|
48
|
+
WORKSPACE_ROOT_MARKER_FILE_NAMES,
|
|
49
|
+
SOURCE_EXCLUDED_GLOBS,
|
|
50
|
+
SOURCE_EXTENSION,
|
|
51
|
+
SOURCE_FILE_GLOBS,
|
|
52
|
+
UNRESOLVED_REFERENCE_REASON,
|
|
53
|
+
VUE_SCRIPT_LANGUAGE,
|
|
54
|
+
} from "./protocol.mjs";
|
|
55
|
+
|
|
56
|
+
const PLUGIN_ROOT = PACKAGE_ROOT;
|
|
57
|
+
const CONFIGURED_PROCESS_CWD = process.env[ENVIRONMENT_VARIABLE.PROCESS_CWD]
|
|
58
|
+
? path.resolve(process.env[ENVIRONMENT_VARIABLE.PROCESS_CWD])
|
|
59
|
+
: undefined;
|
|
60
|
+
const REQUEST_TIMEOUT_MS = DEFAULT.REQUEST_TIMEOUT_MS;
|
|
61
|
+
const DIAGNOSTIC_WAIT_MS = DEFAULT.DIAGNOSTIC_WAIT_MS;
|
|
62
|
+
let vueParsingDependenciesPromise;
|
|
63
|
+
|
|
64
|
+
function vueParsingDependencies() {
|
|
65
|
+
if (!vueParsingDependenciesPromise) {
|
|
66
|
+
vueParsingDependenciesPromise = Promise.all([import("@vue/compiler-sfc"), import("typescript")]).then(([compiler, typescript]) => ({
|
|
67
|
+
parseVueSfc: compiler.parse,
|
|
68
|
+
ts: typescript.default,
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
return vueParsingDependenciesPromise;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function positiveEnvironmentInteger(name, fallback) {
|
|
75
|
+
const parsed = Number(process.env[name]);
|
|
76
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const CLIENT_IDLE_TIMEOUT_MS = positiveEnvironmentInteger(ENVIRONMENT_VARIABLE.CLIENT_IDLE_TIMEOUT_MS, DEFAULT.CLIENT_IDLE_TIMEOUT_MS);
|
|
80
|
+
const CLIENT_MINIMUM_EVICTION_AGE_MS = positiveEnvironmentInteger(
|
|
81
|
+
ENVIRONMENT_VARIABLE.CLIENT_MINIMUM_EVICTION_AGE_MS,
|
|
82
|
+
DEFAULT.CLIENT_MINIMUM_EVICTION_AGE_MS,
|
|
83
|
+
);
|
|
84
|
+
const MAXIMUM_ACTIVE_CLIENTS = positiveEnvironmentInteger(ENVIRONMENT_VARIABLE.MAXIMUM_ACTIVE_CLIENTS, DEFAULT.MAXIMUM_ACTIVE_CLIENTS);
|
|
85
|
+
const REFERENCE_SET_TTL_MS = positiveEnvironmentInteger(ENVIRONMENT_VARIABLE.REFERENCE_SET_TTL_MS, DEFAULT.REFERENCE_SET_TTL_MS);
|
|
86
|
+
const MAXIMUM_REFERENCE_SETS = positiveEnvironmentInteger(ENVIRONMENT_VARIABLE.MAXIMUM_REFERENCE_SETS, DEFAULT.MAXIMUM_REFERENCE_SETS);
|
|
87
|
+
const MAXIMUM_CHANGED_REFERENCE_SET_MARKERS = positiveEnvironmentInteger(
|
|
88
|
+
ENVIRONMENT_VARIABLE.MAXIMUM_CHANGED_REFERENCE_SET_MARKERS,
|
|
89
|
+
DEFAULT.MAXIMUM_CHANGED_REFERENCE_SET_MARKERS,
|
|
90
|
+
);
|
|
91
|
+
const MAXIMUM_CACHED_REFERENCE_LOCATIONS = positiveEnvironmentInteger(
|
|
92
|
+
ENVIRONMENT_VARIABLE.MAXIMUM_CACHED_REFERENCE_LOCATIONS,
|
|
93
|
+
DEFAULT.MAXIMUM_CACHED_REFERENCE_LOCATIONS,
|
|
94
|
+
);
|
|
95
|
+
const DEFAULT_REFERENCE_PAGE_SIZE = positiveEnvironmentInteger(
|
|
96
|
+
ENVIRONMENT_VARIABLE.DEFAULT_REFERENCE_PAGE_SIZE,
|
|
97
|
+
DEFAULT.REFERENCE_PAGE_SIZE,
|
|
98
|
+
);
|
|
99
|
+
const CROSS_WORKSPACE_CONCURRENCY = positiveEnvironmentInteger(
|
|
100
|
+
ENVIRONMENT_VARIABLE.CROSS_WORKSPACE_CONCURRENCY,
|
|
101
|
+
DEFAULT.CROSS_WORKSPACE_CONCURRENCY,
|
|
102
|
+
);
|
|
103
|
+
const PUBLIC_TOOL_NAMES = new Set(TOOL_ORDER);
|
|
104
|
+
const COLLECTION_STATUSES = new Set(Object.values(COLLECTION_STATUS));
|
|
105
|
+
const PRESENTATION_MODES = new Set(Object.values(PRESENTATION_MODE));
|
|
106
|
+
const AMBIGUOUS_PUBLIC_KEYS = new Set(FORBIDDEN_PUBLIC_FIELD);
|
|
107
|
+
|
|
108
|
+
const symbolKinds = [
|
|
109
|
+
"File",
|
|
110
|
+
"Module",
|
|
111
|
+
"Namespace",
|
|
112
|
+
"Package",
|
|
113
|
+
"Class",
|
|
114
|
+
"Method",
|
|
115
|
+
"Property",
|
|
116
|
+
"Field",
|
|
117
|
+
"Constructor",
|
|
118
|
+
"Enum",
|
|
119
|
+
"Interface",
|
|
120
|
+
"Function",
|
|
121
|
+
"Variable",
|
|
122
|
+
"Constant",
|
|
123
|
+
"String",
|
|
124
|
+
"Number",
|
|
125
|
+
"Boolean",
|
|
126
|
+
"Array",
|
|
127
|
+
"Object",
|
|
128
|
+
"Key",
|
|
129
|
+
"Null",
|
|
130
|
+
"EnumMember",
|
|
131
|
+
"Struct",
|
|
132
|
+
"Event",
|
|
133
|
+
"Operator",
|
|
134
|
+
"TypeParameter",
|
|
135
|
+
];
|
|
136
|
+
const diagnosticSeverities = Object.values(DIAGNOSTIC_SEVERITY);
|
|
137
|
+
|
|
138
|
+
function verifyBundledRuntime() {
|
|
139
|
+
const missingComponents = inspectRuntimeComponents(PLUGIN_ROOT).filter(({available}) => !available);
|
|
140
|
+
if (missingComponents.length > 0) {
|
|
141
|
+
const error = new Error(
|
|
142
|
+
`Required language-server components are missing. Reinstall ${PRODUCT.NAME} dependencies before starting the MCP server.`,
|
|
143
|
+
);
|
|
144
|
+
error.code = ERROR_CODE.RUNTIME_DEPENDENCY_MISSING;
|
|
145
|
+
error.details = {missingComponents};
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function runtimeNodeModules() {
|
|
151
|
+
return runtimeDependencyRoot(REQUIRED_RUNTIME_COMPONENT.VUE_TYPESCRIPT_PLUGIN, PLUGIN_ROOT);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isObject(value) {
|
|
155
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function processCwd(workspaceRoot) {
|
|
159
|
+
if (CONFIGURED_PROCESS_CWD && existsSync(CONFIGURED_PROCESS_CWD)) return CONFIGURED_PROCESS_CWD;
|
|
160
|
+
return workspaceRoot;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toUri(file) {
|
|
164
|
+
return pathToFileURL(file).href;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function fromUri(uri) {
|
|
168
|
+
return fileURLToPath(uri);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function lspPosition(line, column) {
|
|
172
|
+
return {line: line - 1, character: column - 1};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function displayPosition(position) {
|
|
176
|
+
return {line: position.line + 1, column: position.character + 1};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function displayRange(range) {
|
|
180
|
+
return {start: displayPosition(range.start), end: displayPosition(range.end)};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function limit(items, maxResults) {
|
|
184
|
+
if (maxResults === undefined) return items.slice();
|
|
185
|
+
return items.slice(0, maxResults);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function normalizedLimit(value) {
|
|
189
|
+
return value === undefined ? {mode: LIMIT_MODE.UNLIMITED} : {mode: LIMIT_MODE.MAXIMUM, maximum: value};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function diagnosticEvidenceReason(freshness) {
|
|
193
|
+
if (freshness === DIAGNOSTIC_FRESHNESS.CURRENT) {
|
|
194
|
+
return DIAGNOSTIC_EVIDENCE_REASON.CURRENT_DOCUMENT_VERSION_CONFIRMED;
|
|
195
|
+
}
|
|
196
|
+
if (freshness === DIAGNOSTIC_FRESHNESS.VERSION_NOT_REPORTED) {
|
|
197
|
+
return DIAGNOSTIC_EVIDENCE_REASON.LANGUAGE_SERVER_VERSION_NOT_REPORTED;
|
|
198
|
+
}
|
|
199
|
+
if (freshness === DIAGNOSTIC_FRESHNESS.DIFFERENT_VERSION) {
|
|
200
|
+
return DIAGNOSTIC_EVIDENCE_REASON.LANGUAGE_SERVER_REPORTED_DIFFERENT_VERSION;
|
|
201
|
+
}
|
|
202
|
+
return DIAGNOSTIC_EVIDENCE_REASON.LANGUAGE_SERVER_DID_NOT_REPORT_CURRENT_DOCUMENT;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function textFingerprint(text) {
|
|
206
|
+
return createHash(FINGERPRINT_ALGORITHM.SHA_256).update(text).digest("hex");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collectionStatus({stoppedByLimit, unresolvedCount = 0, failed = false}) {
|
|
210
|
+
if (failed) return COLLECTION_STATUS.FAILED;
|
|
211
|
+
if (stoppedByLimit) return COLLECTION_STATUS.LIMITED;
|
|
212
|
+
if (unresolvedCount > 0) return COLLECTION_STATUS.PARTIAL;
|
|
213
|
+
return COLLECTION_STATUS.COMPLETE;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function existingDirectory(candidate) {
|
|
217
|
+
const resolved = await realpath(path.resolve(candidate));
|
|
218
|
+
if (!(await stat(resolved)).isDirectory()) {
|
|
219
|
+
throw new Error(`Not a directory: ${candidate}`);
|
|
220
|
+
}
|
|
221
|
+
return resolved;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function existingFile(candidate) {
|
|
225
|
+
const resolved = await realpath(path.resolve(candidate));
|
|
226
|
+
if (!(await stat(resolved)).isFile()) {
|
|
227
|
+
throw new Error(`Not a file: ${candidate}`);
|
|
228
|
+
}
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function discoverRoots(file, requestedRoot) {
|
|
233
|
+
let boundaryRoot;
|
|
234
|
+
if (requestedRoot) {
|
|
235
|
+
boundaryRoot = await existingDirectory(requestedRoot);
|
|
236
|
+
if (file !== boundaryRoot && !file.startsWith(`${boundaryRoot}${path.sep}`)) {
|
|
237
|
+
throw new Error(`File is outside requested workspace root: ${boundaryRoot}`);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let current = path.dirname(file);
|
|
242
|
+
let nearestProject;
|
|
243
|
+
let repositoryRoot;
|
|
244
|
+
while (true) {
|
|
245
|
+
if (!nearestProject && WORKSPACE_ROOT_MARKER_FILE_NAMES.some((name) => existsSync(path.join(current, name)))) {
|
|
246
|
+
nearestProject = current;
|
|
247
|
+
}
|
|
248
|
+
if (existsSync(path.join(current, ".git"))) {
|
|
249
|
+
repositoryRoot = current;
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
const parent = path.dirname(current);
|
|
253
|
+
if (parent === current) {
|
|
254
|
+
repositoryRoot = nearestProject || path.dirname(file);
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
current = parent;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
boundaryRoot: boundaryRoot || repositoryRoot,
|
|
262
|
+
repositoryRoot,
|
|
263
|
+
workspaceRoot: nearestProject || repositoryRoot,
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function locationKey(location) {
|
|
268
|
+
return `${location.file}:${location.range.start.line}:${location.range.start.column}`;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function workspaceConfigurationFiles(workspaceRoot, repositoryRoot) {
|
|
272
|
+
const files = [];
|
|
273
|
+
let current = workspaceRoot;
|
|
274
|
+
while (true) {
|
|
275
|
+
for (const name of WORKSPACE_CONFIGURATION_FILE_NAMES) {
|
|
276
|
+
files.push(path.join(current, name));
|
|
277
|
+
}
|
|
278
|
+
if (current === repositoryRoot) break;
|
|
279
|
+
const parent = path.dirname(current);
|
|
280
|
+
if (parent === current || !current.startsWith(`${repositoryRoot}${path.sep}`)) break;
|
|
281
|
+
current = parent;
|
|
282
|
+
}
|
|
283
|
+
return files;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function dedupeLocations(locations) {
|
|
287
|
+
const seen = new Set();
|
|
288
|
+
return locations.filter((location) => {
|
|
289
|
+
const key = locationKey(location);
|
|
290
|
+
if (seen.has(key)) return false;
|
|
291
|
+
seen.add(key);
|
|
292
|
+
return true;
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function mapLimit(items, concurrency, mapper) {
|
|
297
|
+
const results = new Array(items.length);
|
|
298
|
+
let nextIndex = 0;
|
|
299
|
+
async function worker() {
|
|
300
|
+
while (nextIndex < items.length) {
|
|
301
|
+
const index = nextIndex++;
|
|
302
|
+
results[index] = await mapper(items[index], index);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return Promise.all(Array.from({length: Math.min(concurrency, items.length)}, worker)).then(() => results);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function runProcess(command, args, cwd) {
|
|
309
|
+
return new Promise((resolve, reject) => {
|
|
310
|
+
const child = spawn(command, args, {
|
|
311
|
+
cwd: processCwd(cwd),
|
|
312
|
+
env: process.env,
|
|
313
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
314
|
+
});
|
|
315
|
+
const stdout = [];
|
|
316
|
+
const stderr = [];
|
|
317
|
+
child.stdout.on("data", (chunk) => stdout.push(chunk));
|
|
318
|
+
child.stderr.on("data", (chunk) => stderr.push(chunk));
|
|
319
|
+
child.on(NODE_EVENT.ERROR, reject);
|
|
320
|
+
child.on("exit", (code) => {
|
|
321
|
+
if (code === 0 || code === 1) resolve(Buffer.concat(stdout).toString("utf8"));
|
|
322
|
+
else reject(new Error(`${command} exited ${code}: ${Buffer.concat(stderr).toString("utf8").trim()}`));
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function identifierAt(file, line, column) {
|
|
328
|
+
const lines = (await readFile(file, "utf8")).split(/\r?\n/);
|
|
329
|
+
const text = lines[line - 1];
|
|
330
|
+
if (text === undefined) throw new Error(`Line ${line} is outside ${file}`);
|
|
331
|
+
let index = Math.min(column - 1, text.length);
|
|
332
|
+
if (!/[A-Za-z0-9_$]/.test(text[index] || "")) {
|
|
333
|
+
if (/[A-Za-z_$]/.test(text[index + 1] || "")) index++;
|
|
334
|
+
else if (index > 0) index--;
|
|
335
|
+
}
|
|
336
|
+
let start = index;
|
|
337
|
+
let end = index;
|
|
338
|
+
while (start > 0 && /[A-Za-z0-9_$]/.test(text[start - 1])) start--;
|
|
339
|
+
while (end < text.length && /[A-Za-z0-9_$]/.test(text[end])) end++;
|
|
340
|
+
const identifier = text.slice(start, end);
|
|
341
|
+
if (!/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) {
|
|
342
|
+
throw new Error(`No JavaScript identifier at ${file}:${line}:${column}`);
|
|
343
|
+
}
|
|
344
|
+
return {identifier, line, column: start + 1};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function rgIdentifierCandidates(root, identifier, maxCandidates, wholeIdentifier = true) {
|
|
348
|
+
const startedAt = performance.now();
|
|
349
|
+
const output = await runProcess(
|
|
350
|
+
RUNTIME_COMMAND.RIPGREP,
|
|
351
|
+
[
|
|
352
|
+
"--json",
|
|
353
|
+
"--fixed-strings",
|
|
354
|
+
...SOURCE_FILE_GLOBS.flatMap((glob) => ["--glob", glob]),
|
|
355
|
+
...SOURCE_EXCLUDED_GLOBS.flatMap((glob) => ["--glob", glob]),
|
|
356
|
+
identifier,
|
|
357
|
+
root,
|
|
358
|
+
],
|
|
359
|
+
root,
|
|
360
|
+
);
|
|
361
|
+
const candidates = [];
|
|
362
|
+
const candidateFiles = new Set();
|
|
363
|
+
let totalCandidateCount = 0;
|
|
364
|
+
for (const recordLine of output.split("\n")) {
|
|
365
|
+
if (!recordLine) continue;
|
|
366
|
+
let record;
|
|
367
|
+
try {
|
|
368
|
+
record = JSON.parse(recordLine);
|
|
369
|
+
} catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (record.type !== "match") continue;
|
|
373
|
+
const file = path.isAbsolute(record.data.path.text) ? record.data.path.text : path.resolve(root, record.data.path.text);
|
|
374
|
+
const lineText = record.data.lines.text;
|
|
375
|
+
for (const match of record.data.submatches || []) {
|
|
376
|
+
const prefix = Buffer.from(lineText, "utf8").subarray(0, match.start).toString("utf8");
|
|
377
|
+
const start = prefix.length;
|
|
378
|
+
const before = lineText[start - 1] || "";
|
|
379
|
+
const after = lineText[start + identifier.length] || "";
|
|
380
|
+
if (wholeIdentifier && (/[A-Za-z0-9_$]/.test(before) || /[A-Za-z0-9_$]/.test(after))) continue;
|
|
381
|
+
totalCandidateCount++;
|
|
382
|
+
candidateFiles.add(file);
|
|
383
|
+
if (maxCandidates === undefined || candidates.length < maxCandidates) {
|
|
384
|
+
candidates.push({file, line: record.data.line_number, column: start + 1});
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return {
|
|
389
|
+
candidates,
|
|
390
|
+
totalCandidateCount,
|
|
391
|
+
totalCandidateFileCount: candidateFiles.size,
|
|
392
|
+
truncated: totalCandidateCount > candidates.length,
|
|
393
|
+
elapsedMilliseconds: Math.round(performance.now() - startedAt),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function languageId(file) {
|
|
398
|
+
switch (path.extname(file).toLowerCase()) {
|
|
399
|
+
case SOURCE_EXTENSION.TYPESCRIPT:
|
|
400
|
+
case SOURCE_EXTENSION.TYPESCRIPT_MODULE:
|
|
401
|
+
case SOURCE_EXTENSION.TYPESCRIPT_COMMONJS:
|
|
402
|
+
return LANGUAGE_ID.TYPESCRIPT;
|
|
403
|
+
case SOURCE_EXTENSION.TYPESCRIPT_REACT:
|
|
404
|
+
return LANGUAGE_ID.TYPESCRIPT_REACT;
|
|
405
|
+
case SOURCE_EXTENSION.JAVASCRIPT:
|
|
406
|
+
case SOURCE_EXTENSION.JAVASCRIPT_MODULE:
|
|
407
|
+
case SOURCE_EXTENSION.JAVASCRIPT_COMMONJS:
|
|
408
|
+
return LANGUAGE_ID.JAVASCRIPT;
|
|
409
|
+
case SOURCE_EXTENSION.JAVASCRIPT_REACT:
|
|
410
|
+
return LANGUAGE_ID.JAVASCRIPT_REACT;
|
|
411
|
+
case SOURCE_EXTENSION.VUE:
|
|
412
|
+
return LANGUAGE_ID.VUE;
|
|
413
|
+
default:
|
|
414
|
+
throw new Error(`Unsupported file type: ${path.extname(file) || "none"}`);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function serverKind(file) {
|
|
419
|
+
return path.extname(file).toLowerCase() === SOURCE_EXTENSION.VUE ? LANGUAGE_ID.VUE : LANGUAGE_ID.TYPESCRIPT;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function findTsdk(root) {
|
|
423
|
+
let current = root;
|
|
424
|
+
while (true) {
|
|
425
|
+
const workspaceTsdk = path.join(current, "node_modules", "typescript", "lib");
|
|
426
|
+
if (existsSync(path.join(workspaceTsdk, "tsserver.js"))) return workspaceTsdk;
|
|
427
|
+
const parent = path.dirname(current);
|
|
428
|
+
if (parent === current) break;
|
|
429
|
+
current = parent;
|
|
430
|
+
}
|
|
431
|
+
return path.dirname(resolveRuntimeComponent(REQUIRED_RUNTIME_COMPONENT.TYPESCRIPT_SERVER, PLUGIN_ROOT));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function languageServerEntry(kind) {
|
|
435
|
+
return kind === LANGUAGE_ID.VUE
|
|
436
|
+
? resolveRuntimeComponent(REQUIRED_RUNTIME_COMPONENT.VUE_LANGUAGE_SERVER, PLUGIN_ROOT)
|
|
437
|
+
: resolveRuntimeComponent(REQUIRED_RUNTIME_COMPONENT.TYPESCRIPT_LANGUAGE_SERVER, PLUGIN_ROOT);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
class TsserverBridge {
|
|
441
|
+
constructor(root, tsdk, enableVuePlugin = false) {
|
|
442
|
+
this.root = root;
|
|
443
|
+
this.nextId = 1;
|
|
444
|
+
this.pending = new Map();
|
|
445
|
+
this.openFiles = new Set();
|
|
446
|
+
this.buffer = Buffer.alloc(0);
|
|
447
|
+
const args = [path.join(tsdk, "tsserver.js"), "--useInferredProjectPerProjectRoot", "--disableAutomaticTypingAcquisition"];
|
|
448
|
+
if (enableVuePlugin) {
|
|
449
|
+
args.push("--globalPlugins", "@vue/typescript-plugin", "--pluginProbeLocations", runtimeNodeModules(), "--allowLocalPluginLoads");
|
|
450
|
+
}
|
|
451
|
+
this.process = spawn(process.execPath, args, {
|
|
452
|
+
cwd: processCwd(root),
|
|
453
|
+
env: process.env,
|
|
454
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
455
|
+
});
|
|
456
|
+
this.process.stdout.on("data", (chunk) => this.onData(chunk));
|
|
457
|
+
this.process.stderr.on("data", (chunk) => {
|
|
458
|
+
const message = chunk.toString().trim();
|
|
459
|
+
if (message) process.stderr.write(`[${PRODUCT.NAME}:vue-tsserver] ${message}\n`);
|
|
460
|
+
});
|
|
461
|
+
this.process.on("exit", (code, signal) => {
|
|
462
|
+
const error = new Error(`Vue tsserver bridge exited (${code ?? signal ?? "unknown"})`);
|
|
463
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
464
|
+
this.pending.clear();
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
onData(chunk) {
|
|
469
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
470
|
+
while (true) {
|
|
471
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
472
|
+
if (headerEnd < 0) return;
|
|
473
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
474
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
475
|
+
if (!match) {
|
|
476
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const length = Number(match[1]);
|
|
480
|
+
const bodyStart = headerEnd + 4;
|
|
481
|
+
if (this.buffer.length < bodyStart + length) return;
|
|
482
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length).toString("utf8");
|
|
483
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
484
|
+
try {
|
|
485
|
+
const message = JSON.parse(body);
|
|
486
|
+
if (message.type === "response") {
|
|
487
|
+
const pending = this.pending.get(message.request_seq);
|
|
488
|
+
if (!pending) continue;
|
|
489
|
+
clearTimeout(pending.timer);
|
|
490
|
+
this.pending.delete(message.request_seq);
|
|
491
|
+
if (message.success === false) pending.reject(new Error(message.message || "tsserver request failed"));
|
|
492
|
+
else pending.resolve(message.body);
|
|
493
|
+
}
|
|
494
|
+
} catch (error) {
|
|
495
|
+
process.stderr.write(`[${PRODUCT.NAME}:vue-tsserver] Invalid message: ${error.message}\n`);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
send(command, args, expectResponse = true) {
|
|
501
|
+
const seq = this.nextId++;
|
|
502
|
+
const message = JSON.stringify({seq, type: "request", command, arguments: args});
|
|
503
|
+
this.process.stdin.write(`${message}\n`);
|
|
504
|
+
if (!expectResponse) return Promise.resolve(undefined);
|
|
505
|
+
return new Promise((resolve, reject) => {
|
|
506
|
+
const timer = setTimeout(() => {
|
|
507
|
+
this.pending.delete(seq);
|
|
508
|
+
reject(new Error(`tsserver request timed out: ${command}`));
|
|
509
|
+
}, REQUEST_TIMEOUT_MS);
|
|
510
|
+
this.pending.set(seq, {resolve, reject, timer});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
async request(command, args) {
|
|
515
|
+
const file = args?.file;
|
|
516
|
+
if (file && !this.openFiles.has(file)) {
|
|
517
|
+
this.openFiles.add(file);
|
|
518
|
+
await this.send("open", {file, projectRootPath: this.root}, false);
|
|
519
|
+
}
|
|
520
|
+
return this.send(command, args);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
close() {
|
|
524
|
+
if (!this.process.killed) this.process.kill("SIGTERM");
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
class LspClient {
|
|
529
|
+
constructor(root, kind) {
|
|
530
|
+
this.root = root;
|
|
531
|
+
this.kind = kind;
|
|
532
|
+
this.process = undefined;
|
|
533
|
+
this.buffer = Buffer.alloc(0);
|
|
534
|
+
this.nextId = 1;
|
|
535
|
+
this.pending = new Map();
|
|
536
|
+
this.documents = new Map();
|
|
537
|
+
this.diagnosticsCache = new Map();
|
|
538
|
+
this.diagnosticWaiters = new Map();
|
|
539
|
+
this.tsserverBridge = undefined;
|
|
540
|
+
this.ready = this.start();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async start() {
|
|
544
|
+
const entry = languageServerEntry(this.kind);
|
|
545
|
+
if (!existsSync(entry)) {
|
|
546
|
+
throw new Error(`Language server is not installed: ${entry}`);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
this.process = spawn(process.execPath, [entry, "--stdio"], {
|
|
550
|
+
cwd: processCwd(this.root),
|
|
551
|
+
env: {...process.env, PATH: `${path.join(runtimeNodeModules(), ".bin")}${path.delimiter}${process.env.PATH || ""}`},
|
|
552
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
553
|
+
});
|
|
554
|
+
process.stderr.write(`[${PRODUCT.NAME}:${this.kind}] starting in ${this.root}\n`);
|
|
555
|
+
this.process.stdout.on("data", (chunk) => this.onData(chunk));
|
|
556
|
+
this.process.stderr.on("data", (chunk) => {
|
|
557
|
+
const message = chunk.toString().trim();
|
|
558
|
+
if (message) process.stderr.write(`[${PRODUCT.NAME}:${this.kind}] ${message}\n`);
|
|
559
|
+
});
|
|
560
|
+
this.process.on("exit", (code, signal) => {
|
|
561
|
+
process.stderr.write(`[${PRODUCT.NAME}:${this.kind}] exited (${code ?? signal ?? "unknown"})\n`);
|
|
562
|
+
const error = new Error(`${this.kind} language server exited (${code ?? signal ?? "unknown"})`);
|
|
563
|
+
for (const pending of this.pending.values()) pending.reject(error);
|
|
564
|
+
this.pending.clear();
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
const tsdk = findTsdk(this.root);
|
|
568
|
+
if (this.kind === LANGUAGE_ID.VUE) this.tsserverBridge = new TsserverBridge(this.root, tsdk, true);
|
|
569
|
+
await this.request(
|
|
570
|
+
"initialize",
|
|
571
|
+
{
|
|
572
|
+
processId: process.pid,
|
|
573
|
+
rootUri: toUri(this.root),
|
|
574
|
+
workspaceFolders: [{uri: toUri(this.root), name: path.basename(this.root)}],
|
|
575
|
+
capabilities: {
|
|
576
|
+
textDocument: {
|
|
577
|
+
synchronization: {didSave: false, dynamicRegistration: false},
|
|
578
|
+
hover: {contentFormat: ["markdown", "plaintext"]},
|
|
579
|
+
definition: {linkSupport: true},
|
|
580
|
+
references: {},
|
|
581
|
+
documentSymbol: {hierarchicalDocumentSymbolSupport: true},
|
|
582
|
+
publishDiagnostics: {relatedInformation: true},
|
|
583
|
+
},
|
|
584
|
+
workspace: {symbol: {}},
|
|
585
|
+
},
|
|
586
|
+
initializationOptions:
|
|
587
|
+
this.kind === LANGUAGE_ID.VUE
|
|
588
|
+
? {typescript: {tsdk}, vue: {hybridMode: false}}
|
|
589
|
+
: {tsserver: {path: path.join(tsdk, "tsserver.js")}},
|
|
590
|
+
},
|
|
591
|
+
true,
|
|
592
|
+
);
|
|
593
|
+
process.stderr.write(`[${PRODUCT.NAME}:${this.kind}] initialized\n`);
|
|
594
|
+
this.notify("initialized", {});
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
onData(chunk) {
|
|
598
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
599
|
+
while (true) {
|
|
600
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
601
|
+
if (headerEnd < 0) return;
|
|
602
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
603
|
+
const match = /content-length:\s*(\d+)/i.exec(header);
|
|
604
|
+
if (!match) {
|
|
605
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const length = Number(match[1]);
|
|
609
|
+
const bodyStart = headerEnd + 4;
|
|
610
|
+
if (this.buffer.length < bodyStart + length) return;
|
|
611
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + length).toString("utf8");
|
|
612
|
+
this.buffer = this.buffer.subarray(bodyStart + length);
|
|
613
|
+
try {
|
|
614
|
+
this.onMessage(JSON.parse(body));
|
|
615
|
+
} catch (error) {
|
|
616
|
+
process.stderr.write(`[${PRODUCT.NAME}] Invalid LSP message: ${error.message}\n`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
onMessage(message) {
|
|
622
|
+
if (message.id !== undefined && (message.result !== undefined || message.error !== undefined) && !message.method) {
|
|
623
|
+
const pending = this.pending.get(message.id);
|
|
624
|
+
if (!pending) return;
|
|
625
|
+
clearTimeout(pending.timer);
|
|
626
|
+
this.pending.delete(message.id);
|
|
627
|
+
if (message.error) pending.reject(new Error(message.error.message || JSON.stringify(message.error)));
|
|
628
|
+
else pending.resolve(message.result);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (message.method === "textDocument/publishDiagnostics") {
|
|
633
|
+
const openDocument = this.documents.get(message.params.uri);
|
|
634
|
+
const entry = {
|
|
635
|
+
items: message.params.diagnostics || [],
|
|
636
|
+
reportedDocumentVersion: message.params.version,
|
|
637
|
+
openDocumentVersionAtReceipt: openDocument?.version,
|
|
638
|
+
receivedAt: Date.now(),
|
|
639
|
+
};
|
|
640
|
+
this.diagnosticsCache.set(message.params.uri, entry);
|
|
641
|
+
const waiters = this.diagnosticWaiters.get(message.params.uri) || [];
|
|
642
|
+
this.diagnosticWaiters.delete(message.params.uri);
|
|
643
|
+
for (const resolve of waiters) resolve(entry);
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
if (message.method === "tsserver/request") {
|
|
648
|
+
const params = Array.isArray(message.params?.[0]) ? message.params[0] : message.params;
|
|
649
|
+
const [requestId, command, args] = params || [];
|
|
650
|
+
void this.tsserverBridge
|
|
651
|
+
?.request(command, args)
|
|
652
|
+
.then((result) => this.notify("tsserver/response", [[requestId, result]]))
|
|
653
|
+
.catch((error) => {
|
|
654
|
+
process.stderr.write(`[${PRODUCT.NAME}:vue-tsserver] ${error.message}\n`);
|
|
655
|
+
this.notify("tsserver/response", [[requestId, undefined]]);
|
|
656
|
+
});
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (message.id !== undefined && message.method) {
|
|
661
|
+
let result = null;
|
|
662
|
+
if (message.method === "workspace/configuration") {
|
|
663
|
+
result = (message.params?.items || []).map(() => ({}));
|
|
664
|
+
} else if (message.method === "workspace/applyEdit") {
|
|
665
|
+
result = {applied: false, failureReason: `${PRODUCT.DISPLAY_NAME} is read-only`};
|
|
666
|
+
}
|
|
667
|
+
this.respond(message.id, result);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
send(message) {
|
|
672
|
+
if (!this.process?.stdin.writable) throw new Error(`${this.kind} language server is not writable`);
|
|
673
|
+
const body = JSON.stringify({...message, jsonrpc: "2.0"});
|
|
674
|
+
this.process.stdin.write(`Content-Length: ${Buffer.byteLength(body)}\r\n\r\n${body}`);
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
request(method, params, duringInitialization = false) {
|
|
678
|
+
if (!duringInitialization && !this.process) throw new Error("Language server has not started");
|
|
679
|
+
const id = this.nextId++;
|
|
680
|
+
return new Promise((resolve, reject) => {
|
|
681
|
+
const timer = setTimeout(() => {
|
|
682
|
+
this.pending.delete(id);
|
|
683
|
+
reject(new Error(`LSP request timed out: ${method}`));
|
|
684
|
+
}, REQUEST_TIMEOUT_MS);
|
|
685
|
+
this.pending.set(id, {resolve, reject, timer});
|
|
686
|
+
this.send({id, method, params});
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
notify(method, params) {
|
|
691
|
+
this.send({method, params});
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
respond(id, result) {
|
|
695
|
+
this.send({id, result});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async syncDocument(file) {
|
|
699
|
+
await this.ready;
|
|
700
|
+
const uri = toUri(file);
|
|
701
|
+
const text = await readFile(file, "utf8");
|
|
702
|
+
const current = this.documents.get(uri);
|
|
703
|
+
if (!current) {
|
|
704
|
+
this.documents.set(uri, {text, version: 1});
|
|
705
|
+
this.notify("textDocument/didOpen", {textDocument: {uri, languageId: languageId(file), version: 1, text}});
|
|
706
|
+
} else if (current.text !== text) {
|
|
707
|
+
invalidateReferenceSetsForFile(file);
|
|
708
|
+
this.diagnosticsCache.delete(uri);
|
|
709
|
+
const version = current.version + 1;
|
|
710
|
+
this.documents.set(uri, {text, version});
|
|
711
|
+
this.notify("textDocument/didChange", {textDocument: {uri, version}, contentChanges: [{text}]});
|
|
712
|
+
}
|
|
713
|
+
return uri;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async textRequest(method, file, extra = {}) {
|
|
717
|
+
const uri = await this.syncDocument(file);
|
|
718
|
+
return this.request(method, {textDocument: {uri}, ...extra});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
rawTsserver() {
|
|
722
|
+
if (!this.tsserverBridge) {
|
|
723
|
+
this.tsserverBridge = new TsserverBridge(this.root, findTsdk(this.root), this.kind === LANGUAGE_ID.VUE);
|
|
724
|
+
}
|
|
725
|
+
return this.tsserverBridge;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
async diagnostics(file) {
|
|
729
|
+
const uri = await this.syncDocument(file);
|
|
730
|
+
const document = this.documents.get(uri);
|
|
731
|
+
const documentVersion = document?.version;
|
|
732
|
+
const startedAt = Date.now();
|
|
733
|
+
const published = await new Promise((resolve) => {
|
|
734
|
+
const cached = this.diagnosticsCache.get(uri);
|
|
735
|
+
if (cached?.reportedDocumentVersion === documentVersion) {
|
|
736
|
+
resolve(cached);
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const wrapped = (entry) => {
|
|
740
|
+
clearTimeout(timer);
|
|
741
|
+
resolve(entry);
|
|
742
|
+
};
|
|
743
|
+
const timer = setTimeout(() => {
|
|
744
|
+
const waiters = this.diagnosticWaiters.get(uri) || [];
|
|
745
|
+
this.diagnosticWaiters.set(
|
|
746
|
+
uri,
|
|
747
|
+
waiters.filter((waiter) => waiter !== wrapped),
|
|
748
|
+
);
|
|
749
|
+
resolve(this.diagnosticsCache.get(uri));
|
|
750
|
+
}, DIAGNOSTIC_WAIT_MS);
|
|
751
|
+
this.diagnosticWaiters.set(uri, [...(this.diagnosticWaiters.get(uri) || []), wrapped]);
|
|
752
|
+
});
|
|
753
|
+
const reportedDocumentVersion = published?.reportedDocumentVersion;
|
|
754
|
+
const freshness = !published
|
|
755
|
+
? DIAGNOSTIC_FRESHNESS.NOT_REPORTED_FOR_CURRENT_DOCUMENT
|
|
756
|
+
: reportedDocumentVersion === undefined
|
|
757
|
+
? DIAGNOSTIC_FRESHNESS.VERSION_NOT_REPORTED
|
|
758
|
+
: reportedDocumentVersion === documentVersion
|
|
759
|
+
? DIAGNOSTIC_FRESHNESS.CURRENT
|
|
760
|
+
: DIAGNOSTIC_FRESHNESS.DIFFERENT_VERSION;
|
|
761
|
+
return {
|
|
762
|
+
items: published?.items || [],
|
|
763
|
+
documentVersion,
|
|
764
|
+
reportedDocumentVersion,
|
|
765
|
+
freshness,
|
|
766
|
+
documentContentFingerprint: textFingerprint(document?.text || ""),
|
|
767
|
+
waitedMilliseconds: Date.now() - startedAt,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
close() {
|
|
772
|
+
this.documents.clear();
|
|
773
|
+
this.diagnosticsCache.clear();
|
|
774
|
+
this.diagnosticWaiters.clear();
|
|
775
|
+
this.tsserverBridge?.close();
|
|
776
|
+
if (this.process && !this.process.killed) this.process.kill("SIGTERM");
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
const clients = new Map();
|
|
781
|
+
|
|
782
|
+
function clientIsBusy(client) {
|
|
783
|
+
return client.pending.size > 0 || client.diagnosticWaiters.size > 0 || (client.tsserverBridge?.pending.size || 0) > 0;
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function closeClientEntry(key, entry) {
|
|
787
|
+
entry.client.close();
|
|
788
|
+
clients.delete(key);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function pruneClients(now = Date.now()) {
|
|
792
|
+
for (const [key, entry] of clients) {
|
|
793
|
+
if (!clientIsBusy(entry.client) && now - entry.lastUsedAt >= CLIENT_IDLE_TIMEOUT_MS) {
|
|
794
|
+
closeClientEntry(key, entry);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
if (clients.size <= MAXIMUM_ACTIVE_CLIENTS) return;
|
|
798
|
+
const removable = [...clients.entries()]
|
|
799
|
+
.filter(([, entry]) => !clientIsBusy(entry.client) && now - entry.lastUsedAt >= CLIENT_MINIMUM_EVICTION_AGE_MS)
|
|
800
|
+
.sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
|
|
801
|
+
while (clients.size > MAXIMUM_ACTIVE_CLIENTS && removable.length > 0) {
|
|
802
|
+
const [key, entry] = removable.shift();
|
|
803
|
+
if (clients.get(key) === entry) closeClientEntry(key, entry);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
function getOrCreateClient(key, root, kind) {
|
|
808
|
+
const now = Date.now();
|
|
809
|
+
let entry = clients.get(key);
|
|
810
|
+
if (!entry) {
|
|
811
|
+
entry = {client: new LspClient(root, kind), lastUsedAt: now};
|
|
812
|
+
clients.set(key, entry);
|
|
813
|
+
} else {
|
|
814
|
+
entry.lastUsedAt = now;
|
|
815
|
+
}
|
|
816
|
+
pruneClients(now);
|
|
817
|
+
return entry.client;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
async function waitForReadyClient(key, client) {
|
|
821
|
+
try {
|
|
822
|
+
await client.ready;
|
|
823
|
+
return client;
|
|
824
|
+
} catch (error) {
|
|
825
|
+
const entry = clients.get(key);
|
|
826
|
+
if (entry?.client === client) closeClientEntry(key, entry);
|
|
827
|
+
throw error;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const clientCleanupTimer = setInterval(() => pruneClients(), Math.min(CLIENT_IDLE_TIMEOUT_MS, 15_000));
|
|
832
|
+
clientCleanupTimer.unref();
|
|
833
|
+
|
|
834
|
+
async function clientForFile(fileInput, rootInput) {
|
|
835
|
+
let file;
|
|
836
|
+
try {
|
|
837
|
+
file = await existingFile(fileInput);
|
|
838
|
+
} catch (error) {
|
|
839
|
+
if (!rootInput) throw error;
|
|
840
|
+
const root = await existingDirectory(rootInput);
|
|
841
|
+
const basename = path.basename(fileInput);
|
|
842
|
+
const matches = (await runProcess(RUNTIME_COMMAND.RIPGREP, ["--files", "--glob", `**/${basename}`, root], root))
|
|
843
|
+
.split("\n")
|
|
844
|
+
.filter(Boolean)
|
|
845
|
+
.slice(0, DEFAULT.FILE_SUGGESTION_COUNT)
|
|
846
|
+
.map((candidate) => path.resolve(root, candidate));
|
|
847
|
+
const suggestion = matches.length > 0 ? ` Possible matches: ${matches.join(", ")}` : "";
|
|
848
|
+
throw new Error(`Source file not found: ${fileInput}.${suggestion}`);
|
|
849
|
+
}
|
|
850
|
+
languageId(file);
|
|
851
|
+
const kind = serverKind(file);
|
|
852
|
+
const roots = await discoverRoots(file, rootInput);
|
|
853
|
+
const key = `${kind}:${roots.workspaceRoot}`;
|
|
854
|
+
const client = getOrCreateClient(key, roots.workspaceRoot, kind);
|
|
855
|
+
await waitForReadyClient(key, client);
|
|
856
|
+
return {client, file, root: roots.workspaceRoot, ...roots};
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function clientForRoot(rootInput) {
|
|
860
|
+
const root = await existingDirectory(rootInput || process.cwd());
|
|
861
|
+
const key = `typescript:${root}`;
|
|
862
|
+
const client = getOrCreateClient(key, root, LANGUAGE_ID.TYPESCRIPT);
|
|
863
|
+
await waitForReadyClient(key, client);
|
|
864
|
+
return {client, root};
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
function normalizeLocation(location) {
|
|
868
|
+
const uri = location.uri || location.targetUri;
|
|
869
|
+
const range = location.range || location.targetSelectionRange || location.targetRange;
|
|
870
|
+
if (!uri || !range) return undefined;
|
|
871
|
+
return {file: fromUri(uri), range: displayRange(range)};
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
function normalizeLocations(value) {
|
|
875
|
+
const items = Array.isArray(value) ? value : value ? [value] : [];
|
|
876
|
+
return items.map(normalizeLocation).filter(Boolean);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function normalizeTsserverDefinitions(value) {
|
|
880
|
+
return (value?.definitions || []).map((definition) => ({
|
|
881
|
+
file: path.resolve(definition.file),
|
|
882
|
+
range: {
|
|
883
|
+
start: {line: definition.start.line, column: definition.start.offset},
|
|
884
|
+
end: {line: definition.end.line, column: definition.end.offset},
|
|
885
|
+
},
|
|
886
|
+
}));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
async function definitionsAtWithoutVueTemplateFallback(file, root, line, column) {
|
|
890
|
+
const context = await clientForFile(file, root);
|
|
891
|
+
const raw = await context.client.textRequest("textDocument/definition", context.file, {position: lspPosition(line, column)});
|
|
892
|
+
const lspDefinitions = normalizeLocations(raw);
|
|
893
|
+
if (lspDefinitions.length > 0) {
|
|
894
|
+
return {context, definitions: lspDefinitions, via: INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP};
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
const tsserverResult = await context.client.rawTsserver().request("definitionAndBoundSpan", {
|
|
898
|
+
file: context.file,
|
|
899
|
+
line,
|
|
900
|
+
offset: column,
|
|
901
|
+
});
|
|
902
|
+
const definitions = normalizeTsserverDefinitions(tsserverResult);
|
|
903
|
+
const project = await typescriptProjectEvidence(context);
|
|
904
|
+
return {
|
|
905
|
+
context,
|
|
906
|
+
definitions,
|
|
907
|
+
via: definitions.length > 0 ? INTERNAL_RESOLUTION_SOURCE.TYPESCRIPT_SERVER_FALLBACK : INTERNAL_RESOLUTION_SOURCE.UNRESOLVED,
|
|
908
|
+
attempts: [INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP, INTERNAL_RESOLUTION_SOURCE.TYPESCRIPT_SERVER_FALLBACK],
|
|
909
|
+
project,
|
|
910
|
+
};
|
|
911
|
+
} catch (error) {
|
|
912
|
+
const project = await typescriptProjectEvidence(context);
|
|
913
|
+
return {
|
|
914
|
+
context,
|
|
915
|
+
definitions: [],
|
|
916
|
+
via: INTERNAL_RESOLUTION_SOURCE.UNRESOLVED,
|
|
917
|
+
attempts: [INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP, INTERNAL_RESOLUTION_SOURCE.TYPESCRIPT_SERVER_FALLBACK],
|
|
918
|
+
failure: error instanceof Error ? error.message : String(error),
|
|
919
|
+
project,
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function textOffsetAtPosition(text, line, column) {
|
|
925
|
+
let offset = 0;
|
|
926
|
+
for (let currentLine = 1; currentLine < line; currentLine++) {
|
|
927
|
+
const newline = text.indexOf("\n", offset);
|
|
928
|
+
if (newline < 0) return text.length;
|
|
929
|
+
offset = newline + 1;
|
|
930
|
+
}
|
|
931
|
+
return Math.min(text.length, offset + column - 1);
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
function displayPositionAtTextOffset(text, offset) {
|
|
935
|
+
const prefix = text.slice(0, offset);
|
|
936
|
+
const lines = prefix.split("\n");
|
|
937
|
+
return {line: lines.length, column: lines.at(-1).length + 1};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
function importedBindingNodes(sourceFile, identifier, ts) {
|
|
941
|
+
const nodes = [];
|
|
942
|
+
for (const statement of sourceFile.statements) {
|
|
943
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
944
|
+
const clause = statement.importClause;
|
|
945
|
+
if (clause?.name?.text === identifier) nodes.push(clause.name);
|
|
946
|
+
const bindings = clause?.namedBindings;
|
|
947
|
+
if (bindings && ts.isNamespaceImport(bindings) && bindings.name.text === identifier) nodes.push(bindings.name);
|
|
948
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
949
|
+
for (const element of bindings.elements) {
|
|
950
|
+
if (element.name.text === identifier) nodes.push(element.name);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
return nodes;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
async function vueTemplateImportPositions(file, line, column, identifier) {
|
|
958
|
+
const {parseVueSfc, ts} = await vueParsingDependencies();
|
|
959
|
+
const text = await readFile(file, "utf8");
|
|
960
|
+
const {descriptor, errors} = parseVueSfc(text, {filename: file});
|
|
961
|
+
if (errors.length > 0 || !descriptor.template) return [];
|
|
962
|
+
const queryOffset = textOffsetAtPosition(text, line, column);
|
|
963
|
+
if (queryOffset < descriptor.template.loc.start.offset || queryOffset > descriptor.template.loc.end.offset) return [];
|
|
964
|
+
const positions = [];
|
|
965
|
+
for (const block of [descriptor.script, descriptor.scriptSetup].filter(Boolean)) {
|
|
966
|
+
const scriptKind =
|
|
967
|
+
block.lang === VUE_SCRIPT_LANGUAGE.JAVASCRIPT || block.lang === VUE_SCRIPT_LANGUAGE.JAVASCRIPT_REACT
|
|
968
|
+
? ts.ScriptKind.JS
|
|
969
|
+
: ts.ScriptKind.TS;
|
|
970
|
+
const sourceFile = ts.createSourceFile(file, block.content, ts.ScriptTarget.Latest, true, scriptKind);
|
|
971
|
+
for (const node of importedBindingNodes(sourceFile, identifier, ts)) {
|
|
972
|
+
positions.push(displayPositionAtTextOffset(text, block.loc.start.offset + node.getStart(sourceFile)));
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return positions;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async function definitionsAt(file, root, line, column) {
|
|
979
|
+
const primary = await definitionsAtWithoutVueTemplateFallback(file, root, line, column);
|
|
980
|
+
if (primary.definitions.length > 0 || path.extname(file).toLowerCase() !== SOURCE_EXTENSION.VUE) return primary;
|
|
981
|
+
const token = await identifierAt(file, line, column);
|
|
982
|
+
for (const position of await vueTemplateImportPositions(file, line, column, token.identifier)) {
|
|
983
|
+
const imported = await definitionsAtWithoutVueTemplateFallback(file, root, position.line, position.column);
|
|
984
|
+
if (imported.definitions.length > 0) {
|
|
985
|
+
return {
|
|
986
|
+
...imported,
|
|
987
|
+
via: INTERNAL_RESOLUTION_SOURCE.VUE_TEMPLATE_IMPORT_BINDING,
|
|
988
|
+
attempts: [
|
|
989
|
+
...(primary.attempts || [INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP]),
|
|
990
|
+
INTERNAL_RESOLUTION_SOURCE.VUE_TEMPLATE_IMPORT_BINDING,
|
|
991
|
+
],
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return primary;
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async function typescriptProjectEvidence(context) {
|
|
999
|
+
try {
|
|
1000
|
+
const info = await context.client.rawTsserver().request("projectInfo", {
|
|
1001
|
+
file: context.file,
|
|
1002
|
+
needFileNameList: false,
|
|
1003
|
+
});
|
|
1004
|
+
const configurationFile = info?.configFileName;
|
|
1005
|
+
const normalized = configurationFile?.replaceAll("\\", "/").toLowerCase();
|
|
1006
|
+
const kind = !configurationFile
|
|
1007
|
+
? TYPESCRIPT_PROJECT_KIND.UNKNOWN
|
|
1008
|
+
: normalized.includes("inferredproject")
|
|
1009
|
+
? TYPESCRIPT_PROJECT_KIND.INFERRED
|
|
1010
|
+
: TYPESCRIPT_PROJECT_KIND.CONFIGURED;
|
|
1011
|
+
return {kind, configurationFile};
|
|
1012
|
+
} catch {
|
|
1013
|
+
return {kind: TYPESCRIPT_PROJECT_KIND.UNKNOWN};
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
function unresolvedReference(candidateLocation, identifier, result, reason, failure) {
|
|
1018
|
+
return {
|
|
1019
|
+
file: candidateLocation.file,
|
|
1020
|
+
range: candidateLocation.range,
|
|
1021
|
+
identifier,
|
|
1022
|
+
owningWorkspace: result?.context?.workspaceRoot,
|
|
1023
|
+
typescriptProject: result?.project,
|
|
1024
|
+
reason,
|
|
1025
|
+
attemptedMethods: result?.attempts?.map(publicDefinitionMethod) || [
|
|
1026
|
+
DEFINITION_RESOLUTION_METHOD.LANGUAGE_SERVER,
|
|
1027
|
+
DEFINITION_RESOLUTION_METHOD.TYPESCRIPT_SERVER,
|
|
1028
|
+
],
|
|
1029
|
+
failure,
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
async function crossWorkspaceReferences(context, line, column, maxCandidates, knownReferenceKeys = new Set()) {
|
|
1034
|
+
const startedAt = performance.now();
|
|
1035
|
+
const token = await identifierAt(context.file, line, column);
|
|
1036
|
+
const target = await definitionsAt(context.file, context.boundaryRoot, token.line, token.column);
|
|
1037
|
+
const targetKeys = new Set(target.definitions.map(locationKey));
|
|
1038
|
+
if (targetKeys.size === 0) {
|
|
1039
|
+
targetKeys.add(`${context.file}:${token.line}:${token.column}`);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
const search = await rgIdentifierCandidates(context.repositoryRoot, token.identifier, maxCandidates);
|
|
1043
|
+
let unresolvedCandidateCount = 0;
|
|
1044
|
+
const unresolvedCandidates = [];
|
|
1045
|
+
let definitionMismatchCount = 0;
|
|
1046
|
+
let semanticRequests = 0;
|
|
1047
|
+
let semanticRequestsAvoidedByOwningWorkspace = 0;
|
|
1048
|
+
const configurationFiles = new Set(workspaceConfigurationFiles(context.workspaceRoot, context.repositoryRoot));
|
|
1049
|
+
const semanticVerificationStartedAt = performance.now();
|
|
1050
|
+
const verified = await mapLimit(search.candidates, CROSS_WORKSPACE_CONCURRENCY, async (candidate) => {
|
|
1051
|
+
const candidateLocation = {
|
|
1052
|
+
file: candidate.file,
|
|
1053
|
+
range: {
|
|
1054
|
+
start: {line: candidate.line, column: candidate.column},
|
|
1055
|
+
end: {line: candidate.line, column: candidate.column + token.identifier.length},
|
|
1056
|
+
},
|
|
1057
|
+
};
|
|
1058
|
+
if (knownReferenceKeys.has(locationKey(candidateLocation))) {
|
|
1059
|
+
const roots = await discoverRoots(candidate.file, context.repositoryRoot);
|
|
1060
|
+
for (const file of workspaceConfigurationFiles(roots.workspaceRoot, context.repositoryRoot)) configurationFiles.add(file);
|
|
1061
|
+
semanticRequestsAvoidedByOwningWorkspace++;
|
|
1062
|
+
return {...candidateLocation, via: INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP};
|
|
1063
|
+
}
|
|
1064
|
+
try {
|
|
1065
|
+
semanticRequests++;
|
|
1066
|
+
const result = await definitionsAt(candidate.file, context.repositoryRoot, candidate.line, candidate.column);
|
|
1067
|
+
for (const file of workspaceConfigurationFiles(result.context.workspaceRoot, context.repositoryRoot)) configurationFiles.add(file);
|
|
1068
|
+
if (result.via === INTERNAL_RESOLUTION_SOURCE.UNRESOLVED) {
|
|
1069
|
+
unresolvedCandidateCount++;
|
|
1070
|
+
unresolvedCandidates.push(
|
|
1071
|
+
unresolvedReference(
|
|
1072
|
+
candidateLocation,
|
|
1073
|
+
token.identifier,
|
|
1074
|
+
result,
|
|
1075
|
+
result.project?.kind === TYPESCRIPT_PROJECT_KIND.INFERRED
|
|
1076
|
+
? UNRESOLVED_REFERENCE_REASON.CANDIDATE_OPENED_IN_INFERRED_TYPESCRIPT_PROJECT
|
|
1077
|
+
: result.failure
|
|
1078
|
+
? UNRESOLVED_REFERENCE_REASON.TYPESCRIPT_SERVER_REQUEST_FAILED
|
|
1079
|
+
: UNRESOLVED_REFERENCE_REASON.DEFINITION_TOOLS_RETURNED_NO_LOCATION,
|
|
1080
|
+
result.failure,
|
|
1081
|
+
),
|
|
1082
|
+
);
|
|
1083
|
+
return undefined;
|
|
1084
|
+
}
|
|
1085
|
+
if (!result.definitions.some((definition) => targetKeys.has(locationKey(definition)))) {
|
|
1086
|
+
definitionMismatchCount++;
|
|
1087
|
+
return undefined;
|
|
1088
|
+
}
|
|
1089
|
+
return {...candidateLocation, via: INTERNAL_RESOLUTION_SOURCE.CROSS_WORKSPACE_DEFINITION};
|
|
1090
|
+
} catch (error) {
|
|
1091
|
+
unresolvedCandidateCount++;
|
|
1092
|
+
unresolvedCandidates.push(
|
|
1093
|
+
unresolvedReference(
|
|
1094
|
+
candidateLocation,
|
|
1095
|
+
token.identifier,
|
|
1096
|
+
undefined,
|
|
1097
|
+
UNRESOLVED_REFERENCE_REASON.CANDIDATE_ANALYSIS_FAILED,
|
|
1098
|
+
error instanceof Error ? error.message : String(error),
|
|
1099
|
+
),
|
|
1100
|
+
);
|
|
1101
|
+
return undefined;
|
|
1102
|
+
}
|
|
1103
|
+
});
|
|
1104
|
+
return {
|
|
1105
|
+
identifier: token.identifier,
|
|
1106
|
+
references: verified.filter(Boolean),
|
|
1107
|
+
scannedCandidateCount: search.candidates.length,
|
|
1108
|
+
totalTextualCandidateCount: search.totalCandidateCount,
|
|
1109
|
+
semanticallyMatchedCandidateCount: verified.filter(Boolean).length,
|
|
1110
|
+
rejectedCandidateCount: definitionMismatchCount,
|
|
1111
|
+
rejectedCandidatesByReason: {definitionMismatch: definitionMismatchCount},
|
|
1112
|
+
candidateScanTruncated: search.truncated,
|
|
1113
|
+
unresolvedCandidateCount,
|
|
1114
|
+
unresolvedCandidates: unresolvedCandidates.sort(
|
|
1115
|
+
(left, right) =>
|
|
1116
|
+
left.file.localeCompare(right.file) ||
|
|
1117
|
+
left.range.start.line - right.range.start.line ||
|
|
1118
|
+
left.range.start.column - right.range.start.column,
|
|
1119
|
+
),
|
|
1120
|
+
candidateFiles: [...new Set(search.candidates.map((candidate) => candidate.file))],
|
|
1121
|
+
configurationFiles: [...configurationFiles],
|
|
1122
|
+
targetDefinitions: target.definitions,
|
|
1123
|
+
performance: {
|
|
1124
|
+
textSearchMilliseconds: search.elapsedMilliseconds,
|
|
1125
|
+
semanticVerificationMilliseconds: Math.round(performance.now() - semanticVerificationStartedAt),
|
|
1126
|
+
totalMilliseconds: Math.round(performance.now() - startedAt),
|
|
1127
|
+
semanticRequests,
|
|
1128
|
+
semanticRequestsAvoidedByOwningWorkspace,
|
|
1129
|
+
maximumConcurrentSemanticRequests: CROSS_WORKSPACE_CONCURRENCY,
|
|
1130
|
+
},
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
async function collectReferences(context, line, column, includeDeclaration, crossWorkspace, maxCandidates) {
|
|
1135
|
+
const token = await identifierAt(context.file, line, column);
|
|
1136
|
+
const nativeResult = await context.client.textRequest("textDocument/references", context.file, {
|
|
1137
|
+
position: lspPosition(line, column),
|
|
1138
|
+
context: {includeDeclaration},
|
|
1139
|
+
});
|
|
1140
|
+
const nativeReferences = normalizeLocations(nativeResult).map((location) => ({...location, via: INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP}));
|
|
1141
|
+
const cross = crossWorkspace
|
|
1142
|
+
? await crossWorkspaceReferences(context, line, column, maxCandidates, new Set(nativeReferences.map(locationKey)))
|
|
1143
|
+
: {
|
|
1144
|
+
identifier: token.identifier,
|
|
1145
|
+
references: [],
|
|
1146
|
+
scannedCandidateCount: 0,
|
|
1147
|
+
totalTextualCandidateCount: 0,
|
|
1148
|
+
semanticallyMatchedCandidateCount: 0,
|
|
1149
|
+
rejectedCandidateCount: 0,
|
|
1150
|
+
rejectedCandidatesByReason: {definitionMismatch: 0},
|
|
1151
|
+
candidateScanTruncated: false,
|
|
1152
|
+
unresolvedCandidateCount: 0,
|
|
1153
|
+
unresolvedCandidates: [],
|
|
1154
|
+
candidateFiles: [],
|
|
1155
|
+
configurationFiles: workspaceConfigurationFiles(context.workspaceRoot, context.repositoryRoot),
|
|
1156
|
+
targetDefinitions: [],
|
|
1157
|
+
performance: {
|
|
1158
|
+
textSearchMilliseconds: 0,
|
|
1159
|
+
semanticVerificationMilliseconds: 0,
|
|
1160
|
+
totalMilliseconds: 0,
|
|
1161
|
+
semanticRequests: 0,
|
|
1162
|
+
semanticRequestsAvoidedByOwningWorkspace: 0,
|
|
1163
|
+
maximumConcurrentSemanticRequests: 0,
|
|
1164
|
+
},
|
|
1165
|
+
};
|
|
1166
|
+
|
|
1167
|
+
const nativeKeys = new Set(nativeReferences.map(locationKey));
|
|
1168
|
+
const declarationKeys = new Set(cross.targetDefinitions.map(locationKey));
|
|
1169
|
+
const nativeReferencesForResult = includeDeclaration
|
|
1170
|
+
? nativeReferences
|
|
1171
|
+
: nativeReferences.filter((location) => !declarationKeys.has(locationKey(location)));
|
|
1172
|
+
const verifiedCrossWorkspace = cross.references.filter(
|
|
1173
|
+
(location) => !nativeKeys.has(locationKey(location)) && (includeDeclaration || !declarationKeys.has(locationKey(location))),
|
|
1174
|
+
);
|
|
1175
|
+
const all = dedupeLocations([...nativeReferencesForResult, ...verifiedCrossWorkspace]);
|
|
1176
|
+
const collectionTruncated = cross.candidateScanTruncated;
|
|
1177
|
+
return {
|
|
1178
|
+
identifier: cross.identifier,
|
|
1179
|
+
references: all,
|
|
1180
|
+
nativeReferenceCount: nativeReferencesForResult.length,
|
|
1181
|
+
verifiedCrossWorkspaceCount: verifiedCrossWorkspace.length,
|
|
1182
|
+
verifiedReferenceCount: all.length,
|
|
1183
|
+
scannedCandidateCount: cross.scannedCandidateCount,
|
|
1184
|
+
totalTextualCandidateCount: cross.totalTextualCandidateCount,
|
|
1185
|
+
semanticallyMatchedCandidateCount: cross.semanticallyMatchedCandidateCount,
|
|
1186
|
+
rejectedCandidateCount: cross.rejectedCandidateCount,
|
|
1187
|
+
rejectedCandidatesByReason: cross.rejectedCandidatesByReason,
|
|
1188
|
+
unresolvedCandidateCount: cross.unresolvedCandidateCount,
|
|
1189
|
+
unresolvedCandidates: cross.unresolvedCandidates,
|
|
1190
|
+
collectionTruncated,
|
|
1191
|
+
referenceFiles: groupedReferenceFiles(all),
|
|
1192
|
+
evidenceFiles: [
|
|
1193
|
+
...new Set([
|
|
1194
|
+
context.file,
|
|
1195
|
+
...nativeReferences.map((reference) => reference.file),
|
|
1196
|
+
...cross.candidateFiles,
|
|
1197
|
+
...cross.configurationFiles,
|
|
1198
|
+
]),
|
|
1199
|
+
],
|
|
1200
|
+
performance: {
|
|
1201
|
+
...cross.performance,
|
|
1202
|
+
residentSetBytesAfterCollection: process.memoryUsage().rss,
|
|
1203
|
+
heapUsedBytesAfterCollection: process.memoryUsage().heapUsed,
|
|
1204
|
+
},
|
|
1205
|
+
};
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
const referenceSetsById = new Map();
|
|
1209
|
+
const referenceSetIdByKey = new Map();
|
|
1210
|
+
const changedReferenceSetsById = new Map();
|
|
1211
|
+
|
|
1212
|
+
class ReferenceSetStaleError extends Error {
|
|
1213
|
+
constructor(referenceSetId, details) {
|
|
1214
|
+
super("Reference set no longer matches current repository state. Call lsp_references again to collect current locations.");
|
|
1215
|
+
this.code = ERROR_CODE.REFERENCE_SET_CONTENT_CHANGED;
|
|
1216
|
+
this.details = {referenceSetId, ...details};
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
class ReferenceSetUnavailableError extends Error {
|
|
1221
|
+
constructor(referenceSetId) {
|
|
1222
|
+
super("Reference set expired or was not found. Call lsp_references again to create a current reference set.");
|
|
1223
|
+
this.code = ERROR_CODE.REFERENCE_SET_NOT_FOUND_OR_EXPIRED;
|
|
1224
|
+
this.details = {referenceSetId};
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
class RepositoryChangedDuringCollectionError extends Error {
|
|
1229
|
+
constructor(repositoryRoot, attempts) {
|
|
1230
|
+
super("Repository source inventory changed while references were being collected. Retry after repository edits settle.");
|
|
1231
|
+
this.code = ERROR_CODE.REPOSITORY_CHANGED_DURING_COLLECTION;
|
|
1232
|
+
this.details = {repositoryRoot, attempts};
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function contentFingerprint(file) {
|
|
1237
|
+
return new Promise((resolve, reject) => {
|
|
1238
|
+
const hash = createHash(FINGERPRINT_ALGORITHM.SHA_256);
|
|
1239
|
+
const stream = createReadStream(file);
|
|
1240
|
+
stream.on("data", (chunk) => hash.update(chunk));
|
|
1241
|
+
stream.on("end", () => resolve(hash.digest("hex")));
|
|
1242
|
+
stream.on(NODE_EVENT.ERROR, (error) => {
|
|
1243
|
+
if (error?.code === "ENOENT") resolve(null);
|
|
1244
|
+
else reject(error);
|
|
1245
|
+
});
|
|
1246
|
+
});
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
async function fingerprintFiles(files) {
|
|
1250
|
+
const uniqueFiles = [...new Set(files)].sort();
|
|
1251
|
+
const fingerprints = await mapLimit(uniqueFiles, DEFAULT.FILE_FINGERPRINT_CONCURRENCY, async (file) => ({
|
|
1252
|
+
file,
|
|
1253
|
+
sha256: await contentFingerprint(file),
|
|
1254
|
+
}));
|
|
1255
|
+
return fingerprints;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
async function changedFingerprintFiles(fingerprints) {
|
|
1259
|
+
const checked = await mapLimit(fingerprints, DEFAULT.FILE_FINGERPRINT_CONCURRENCY, async (fingerprint) => ({
|
|
1260
|
+
file: fingerprint.file,
|
|
1261
|
+
changed: (await contentFingerprint(fingerprint.file)) !== fingerprint.sha256,
|
|
1262
|
+
}));
|
|
1263
|
+
return checked.filter((item) => item.changed).map((item) => item.file);
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
async function repositorySourceInventory(repositoryRoot) {
|
|
1267
|
+
const startedAt = performance.now();
|
|
1268
|
+
const output = await runProcess(
|
|
1269
|
+
RUNTIME_COMMAND.RIPGREP,
|
|
1270
|
+
[
|
|
1271
|
+
"--files",
|
|
1272
|
+
...SOURCE_FILE_GLOBS.flatMap((glob) => ["--glob", glob]),
|
|
1273
|
+
...SOURCE_EXCLUDED_GLOBS.flatMap((glob) => ["--glob", glob]),
|
|
1274
|
+
repositoryRoot,
|
|
1275
|
+
],
|
|
1276
|
+
repositoryRoot,
|
|
1277
|
+
);
|
|
1278
|
+
const files = [
|
|
1279
|
+
...new Set(
|
|
1280
|
+
output
|
|
1281
|
+
.split("\n")
|
|
1282
|
+
.filter(Boolean)
|
|
1283
|
+
.map((file) => (path.isAbsolute(file) ? path.resolve(file) : path.resolve(repositoryRoot, file))),
|
|
1284
|
+
),
|
|
1285
|
+
].sort();
|
|
1286
|
+
const entries = await mapLimit(files, DEFAULT.INVENTORY_STAT_CONCURRENCY, async (file) => {
|
|
1287
|
+
try {
|
|
1288
|
+
const metadata = await stat(file, {bigint: true});
|
|
1289
|
+
return `${file}\0${metadata.size}\0${metadata.mtimeNs}`;
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
if (error?.code === "ENOENT") return `${file}\0missing`;
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1294
|
+
});
|
|
1295
|
+
const hash = createHash(FINGERPRINT_ALGORITHM.SHA_256);
|
|
1296
|
+
for (const entry of entries) hash.update(entry).update("\n");
|
|
1297
|
+
return {
|
|
1298
|
+
sha256: hash.digest("hex"),
|
|
1299
|
+
sourceFileCount: files.length,
|
|
1300
|
+
elapsedMilliseconds: Math.round(performance.now() - startedAt),
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function sameRepositoryInventory(left, right) {
|
|
1305
|
+
return left.sha256 === right.sha256 && left.sourceFileCount === right.sourceFileCount;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
async function verifyReferenceSetFreshness(entry) {
|
|
1309
|
+
const startedAt = performance.now();
|
|
1310
|
+
const [changedFiles, currentInventory] = await Promise.all([
|
|
1311
|
+
changedFingerprintFiles(entry.fileFingerprints),
|
|
1312
|
+
repositorySourceInventory(entry.repositoryRoot),
|
|
1313
|
+
]);
|
|
1314
|
+
if (changedFiles.length > 0) {
|
|
1315
|
+
return {
|
|
1316
|
+
current: false,
|
|
1317
|
+
details: {
|
|
1318
|
+
changeType: REFERENCE_SET_CHANGE_TYPE.EVIDENCE_FILE_CONTENT_CHANGED,
|
|
1319
|
+
changedFiles,
|
|
1320
|
+
},
|
|
1321
|
+
};
|
|
1322
|
+
}
|
|
1323
|
+
if (!sameRepositoryInventory(entry.repositorySourceInventory, currentInventory)) {
|
|
1324
|
+
return {
|
|
1325
|
+
current: false,
|
|
1326
|
+
details: {
|
|
1327
|
+
changeType: REFERENCE_SET_CHANGE_TYPE.REPOSITORY_SOURCE_INVENTORY_CHANGED,
|
|
1328
|
+
previousSourceFileCount: entry.repositorySourceInventory.sourceFileCount,
|
|
1329
|
+
currentSourceFileCount: currentInventory.sourceFileCount,
|
|
1330
|
+
},
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
return {
|
|
1334
|
+
current: true,
|
|
1335
|
+
repositorySourceInventory: currentInventory,
|
|
1336
|
+
elapsedMilliseconds: Math.round(performance.now() - startedAt),
|
|
1337
|
+
};
|
|
1338
|
+
}
|
|
1339
|
+
|
|
1340
|
+
function deleteReferenceSet(id, entry) {
|
|
1341
|
+
referenceSetsById.delete(id);
|
|
1342
|
+
if (entry && referenceSetIdByKey.get(entry.key) === id) referenceSetIdByKey.delete(entry.key);
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
function rememberChangedReferenceSet(id, details) {
|
|
1346
|
+
const now = Date.now();
|
|
1347
|
+
changedReferenceSetsById.set(id, {details, createdAt: now, expiresAt: now + REFERENCE_SET_TTL_MS});
|
|
1348
|
+
pruneChangedReferenceSets(now);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function pruneChangedReferenceSets(now = Date.now()) {
|
|
1352
|
+
for (const [id, entry] of changedReferenceSetsById) {
|
|
1353
|
+
if (entry.expiresAt <= now) changedReferenceSetsById.delete(id);
|
|
1354
|
+
}
|
|
1355
|
+
const oldest = [...changedReferenceSetsById.entries()].sort((left, right) => left[1].createdAt - right[1].createdAt);
|
|
1356
|
+
while (changedReferenceSetsById.size > MAXIMUM_CHANGED_REFERENCE_SET_MARKERS && oldest.length > 0) {
|
|
1357
|
+
changedReferenceSetsById.delete(oldest.shift()[0]);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
function invalidateReferenceSetsForFile(file) {
|
|
1362
|
+
for (const [id, entry] of referenceSetsById) {
|
|
1363
|
+
const belongsToRepository = file === entry.repositoryRoot || file.startsWith(`${entry.repositoryRoot}${path.sep}`);
|
|
1364
|
+
if (!belongsToRepository) continue;
|
|
1365
|
+
const isDirectEvidenceFile = entry.fileFingerprints.some((fingerprint) => fingerprint.file === file);
|
|
1366
|
+
rememberChangedReferenceSet(id, {
|
|
1367
|
+
changeType: isDirectEvidenceFile
|
|
1368
|
+
? REFERENCE_SET_CHANGE_TYPE.EVIDENCE_FILE_CONTENT_CHANGED
|
|
1369
|
+
: REFERENCE_SET_CHANGE_TYPE.REPOSITORY_SOURCE_INVENTORY_CHANGED,
|
|
1370
|
+
changedFiles: [file],
|
|
1371
|
+
});
|
|
1372
|
+
deleteReferenceSet(id, entry);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function pruneReferenceSets(now = Date.now()) {
|
|
1377
|
+
pruneChangedReferenceSets(now);
|
|
1378
|
+
for (const [id, entry] of referenceSetsById) {
|
|
1379
|
+
if (entry.expiresAt <= now) {
|
|
1380
|
+
deleteReferenceSet(id, entry);
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
const oldest = [...referenceSetsById.entries()].sort((left, right) => left[1].lastUsedAt - right[1].lastUsedAt);
|
|
1384
|
+
let cachedLocations = [...referenceSetsById.values()].reduce((total, entry) => total + entry.analysis.references.length, 0);
|
|
1385
|
+
while (
|
|
1386
|
+
(referenceSetsById.size > MAXIMUM_REFERENCE_SETS || cachedLocations > MAXIMUM_CACHED_REFERENCE_LOCATIONS) &&
|
|
1387
|
+
referenceSetsById.size > 1 &&
|
|
1388
|
+
oldest.length > 0
|
|
1389
|
+
) {
|
|
1390
|
+
const [id, entry] = oldest.shift();
|
|
1391
|
+
deleteReferenceSet(id, entry);
|
|
1392
|
+
cachedLocations -= entry.analysis.references.length;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function referenceSetKey(context, line, column, includeDeclaration, crossWorkspace, maxCandidates) {
|
|
1397
|
+
return JSON.stringify({
|
|
1398
|
+
repositoryRoot: context.repositoryRoot,
|
|
1399
|
+
file: context.file,
|
|
1400
|
+
line,
|
|
1401
|
+
column,
|
|
1402
|
+
includeDeclaration,
|
|
1403
|
+
crossWorkspace,
|
|
1404
|
+
maxCandidates: maxCandidates ?? null,
|
|
1405
|
+
});
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
async function getReferenceSet(context, line, column, includeDeclaration, crossWorkspace, maxCandidates) {
|
|
1409
|
+
const now = Date.now();
|
|
1410
|
+
pruneReferenceSets(now);
|
|
1411
|
+
const key = referenceSetKey(context, line, column, includeDeclaration, crossWorkspace, maxCandidates);
|
|
1412
|
+
const existingId = referenceSetIdByKey.get(key);
|
|
1413
|
+
const existing = existingId ? referenceSetsById.get(existingId) : undefined;
|
|
1414
|
+
if (existing && existing.expiresAt > now) {
|
|
1415
|
+
const freshness = await verifyReferenceSetFreshness(existing);
|
|
1416
|
+
if (freshness.current) {
|
|
1417
|
+
const checkedAt = Date.now();
|
|
1418
|
+
existing.lastUsedAt = checkedAt;
|
|
1419
|
+
existing.expiresAt = checkedAt + REFERENCE_SET_TTL_MS;
|
|
1420
|
+
existing.repositorySourceInventory = freshness.repositorySourceInventory;
|
|
1421
|
+
return {...existing, reused: true, freshnessCheckedAt: checkedAt, freshnessCheckMilliseconds: freshness.elapsedMilliseconds};
|
|
1422
|
+
}
|
|
1423
|
+
rememberChangedReferenceSet(existing.id, freshness.details);
|
|
1424
|
+
deleteReferenceSet(existing.id, existing);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
let stableCollection;
|
|
1428
|
+
for (let attempt = 1; attempt <= DEFAULT.COLLECTION_STABILITY_ATTEMPTS; attempt++) {
|
|
1429
|
+
const inventoryBefore = await repositorySourceInventory(context.repositoryRoot);
|
|
1430
|
+
const analysis = await collectReferences(context, line, column, includeDeclaration, crossWorkspace, maxCandidates);
|
|
1431
|
+
const [fileFingerprints, inventoryAfter] = await Promise.all([
|
|
1432
|
+
fingerprintFiles(analysis.evidenceFiles),
|
|
1433
|
+
repositorySourceInventory(context.repositoryRoot),
|
|
1434
|
+
]);
|
|
1435
|
+
if (sameRepositoryInventory(inventoryBefore, inventoryAfter)) {
|
|
1436
|
+
stableCollection = {analysis, fileFingerprints, repositorySourceInventory: inventoryAfter, attempt};
|
|
1437
|
+
break;
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
if (!stableCollection) {
|
|
1441
|
+
throw new RepositoryChangedDuringCollectionError(context.repositoryRoot, DEFAULT.COLLECTION_STABILITY_ATTEMPTS);
|
|
1442
|
+
}
|
|
1443
|
+
const completedAt = Date.now();
|
|
1444
|
+
const id = `references-${randomUUID()}`;
|
|
1445
|
+
const entry = {
|
|
1446
|
+
id,
|
|
1447
|
+
key,
|
|
1448
|
+
analysis: stableCollection.analysis,
|
|
1449
|
+
workspaceRoot: context.workspaceRoot,
|
|
1450
|
+
repositoryRoot: context.repositoryRoot,
|
|
1451
|
+
source: {file: context.file, line, column},
|
|
1452
|
+
createdAt: completedAt,
|
|
1453
|
+
lastUsedAt: completedAt,
|
|
1454
|
+
expiresAt: completedAt + REFERENCE_SET_TTL_MS,
|
|
1455
|
+
fileFingerprints: stableCollection.fileFingerprints,
|
|
1456
|
+
repositorySourceInventory: stableCollection.repositorySourceInventory,
|
|
1457
|
+
collectionStabilityAttempts: stableCollection.attempt,
|
|
1458
|
+
};
|
|
1459
|
+
referenceSetsById.set(id, entry);
|
|
1460
|
+
referenceSetIdByKey.set(key, id);
|
|
1461
|
+
pruneReferenceSets(now);
|
|
1462
|
+
return {
|
|
1463
|
+
...entry,
|
|
1464
|
+
reused: false,
|
|
1465
|
+
freshnessCheckedAt: completedAt,
|
|
1466
|
+
freshnessCheckMilliseconds: stableCollection.repositorySourceInventory.elapsedMilliseconds,
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
async function getReferenceSetById(id) {
|
|
1471
|
+
const now = Date.now();
|
|
1472
|
+
pruneReferenceSets(now);
|
|
1473
|
+
const entry = referenceSetsById.get(id);
|
|
1474
|
+
if (!entry) {
|
|
1475
|
+
const changed = changedReferenceSetsById.get(id);
|
|
1476
|
+
if (changed) throw new ReferenceSetStaleError(id, changed.details);
|
|
1477
|
+
return undefined;
|
|
1478
|
+
}
|
|
1479
|
+
const freshness = await verifyReferenceSetFreshness(entry);
|
|
1480
|
+
if (!freshness.current) {
|
|
1481
|
+
rememberChangedReferenceSet(id, freshness.details);
|
|
1482
|
+
deleteReferenceSet(id, entry);
|
|
1483
|
+
throw new ReferenceSetStaleError(id, freshness.details);
|
|
1484
|
+
}
|
|
1485
|
+
const checkedAt = Date.now();
|
|
1486
|
+
entry.lastUsedAt = checkedAt;
|
|
1487
|
+
entry.expiresAt = checkedAt + REFERENCE_SET_TTL_MS;
|
|
1488
|
+
entry.freshnessCheckedAt = checkedAt;
|
|
1489
|
+
entry.freshnessCheckMilliseconds = freshness.elapsedMilliseconds;
|
|
1490
|
+
entry.repositorySourceInventory = freshness.repositorySourceInventory;
|
|
1491
|
+
return entry;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function presentReferenceSet(entry, offset, pageSize) {
|
|
1495
|
+
const start = Math.max(0, offset);
|
|
1496
|
+
const references = entry.analysis.references.slice(start, start + pageSize).map((reference) => ({
|
|
1497
|
+
file: reference.file,
|
|
1498
|
+
range: reference.range,
|
|
1499
|
+
discoveryMethod: publicReferenceMethod(reference.via),
|
|
1500
|
+
}));
|
|
1501
|
+
const nextOffset = start + references.length;
|
|
1502
|
+
return {
|
|
1503
|
+
references,
|
|
1504
|
+
presentation: {
|
|
1505
|
+
mode: PRESENTATION_MODE.PAGE,
|
|
1506
|
+
locationsAvailable: entry.analysis.references.length,
|
|
1507
|
+
locationsReturned: references.length,
|
|
1508
|
+
offset: start,
|
|
1509
|
+
pageSize,
|
|
1510
|
+
nextCursor: nextOffset < entry.analysis.references.length ? String(nextOffset) : undefined,
|
|
1511
|
+
locationsReturnedAreSubset: nextOffset < entry.analysis.references.length || start > 0,
|
|
1512
|
+
},
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function presentUnresolvedReferenceSet(entry, offset, pageSize) {
|
|
1517
|
+
const start = Math.max(0, offset);
|
|
1518
|
+
const candidates = entry.analysis.unresolvedCandidates.slice(start, start + pageSize);
|
|
1519
|
+
const nextOffset = start + candidates.length;
|
|
1520
|
+
return {
|
|
1521
|
+
candidates,
|
|
1522
|
+
presentation: {
|
|
1523
|
+
mode: PRESENTATION_MODE.PAGE,
|
|
1524
|
+
candidatesAvailable: entry.analysis.unresolvedCandidates.length,
|
|
1525
|
+
candidatesReturned: candidates.length,
|
|
1526
|
+
offset: start,
|
|
1527
|
+
pageSize,
|
|
1528
|
+
nextCursor: nextOffset < entry.analysis.unresolvedCandidates.length ? String(nextOffset) : undefined,
|
|
1529
|
+
candidatesReturnedAreSubset: nextOffset < entry.analysis.unresolvedCandidates.length || start > 0,
|
|
1530
|
+
},
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
async function semanticWorkspaceSymbols(root, query, maxCandidates) {
|
|
1535
|
+
const search = await rgIdentifierCandidates(root, query, maxCandidates, false);
|
|
1536
|
+
const candidateFiles = [...new Set(search.candidates.map((candidate) => candidate.file))];
|
|
1537
|
+
const files = candidateFiles;
|
|
1538
|
+
let unresolvedFileCount = 0;
|
|
1539
|
+
const groups = await mapLimit(files, DEFAULT.WORKSPACE_FILE_CONCURRENCY, async (file) => {
|
|
1540
|
+
try {
|
|
1541
|
+
const context = await clientForFile(file, root);
|
|
1542
|
+
const raw = await context.client.textRequest("textDocument/documentSymbol", context.file);
|
|
1543
|
+
return flattenDocumentSymbols(raw || [], context.file).filter((symbol) => symbol.name.toLowerCase().includes(query.toLowerCase()));
|
|
1544
|
+
} catch {
|
|
1545
|
+
unresolvedFileCount++;
|
|
1546
|
+
return [];
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
return {
|
|
1550
|
+
symbols: groups.flat(),
|
|
1551
|
+
candidateCount: search.candidates.length,
|
|
1552
|
+
totalTextualCandidateCount: search.totalCandidateCount,
|
|
1553
|
+
candidateScanTruncated: search.truncated,
|
|
1554
|
+
unresolvedFileCount,
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function hoverText(contents) {
|
|
1559
|
+
if (typeof contents === "string") return contents;
|
|
1560
|
+
if (Array.isArray(contents)) return contents.map(hoverText).filter(Boolean).join("\n\n");
|
|
1561
|
+
if (isObject(contents) && typeof contents.value === "string") return contents.value;
|
|
1562
|
+
return contents ? JSON.stringify(contents) : "";
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function flattenDocumentSymbols(symbols, file, parent = [], output = []) {
|
|
1566
|
+
for (const symbol of symbols || []) {
|
|
1567
|
+
if (symbol.location) {
|
|
1568
|
+
const normalized = normalizeLocation(symbol.location);
|
|
1569
|
+
output.push({name: symbol.name, kind: symbolKinds[symbol.kind - 1] || symbol.kind, container: symbol.containerName, ...normalized});
|
|
1570
|
+
} else {
|
|
1571
|
+
output.push({
|
|
1572
|
+
name: symbol.name,
|
|
1573
|
+
kind: symbolKinds[symbol.kind - 1] || symbol.kind,
|
|
1574
|
+
container: parent.join("."),
|
|
1575
|
+
file,
|
|
1576
|
+
range: displayRange(symbol.selectionRange || symbol.range),
|
|
1577
|
+
});
|
|
1578
|
+
flattenDocumentSymbols(symbol.children, file, [...parent, symbol.name], output);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
return output;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
function normalizeDiagnostics(raw, maxResults) {
|
|
1585
|
+
return limit(raw, maxResults).map((item) => ({
|
|
1586
|
+
severity: diagnosticSeverities[item.severity - 1] || DIAGNOSTIC_SEVERITY.NOT_REPORTED,
|
|
1587
|
+
code: item.code,
|
|
1588
|
+
source: item.source,
|
|
1589
|
+
message: item.message,
|
|
1590
|
+
range: displayRange(item.range),
|
|
1591
|
+
relatedInformation: item.relatedInformation?.map((related) => ({
|
|
1592
|
+
message: related.message,
|
|
1593
|
+
...normalizeLocation(related.location),
|
|
1594
|
+
})),
|
|
1595
|
+
}));
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async function auditSymbolAtPosition(file, root, line, column, options) {
|
|
1599
|
+
const context = await clientForFile(file, root);
|
|
1600
|
+
await context.client.syncDocument(context.file);
|
|
1601
|
+
const [definitionResult, hover, referenceSet, rawDiagnostics] = await Promise.all([
|
|
1602
|
+
definitionsAt(context.file, context.boundaryRoot, line, column),
|
|
1603
|
+
context.client.textRequest("textDocument/hover", context.file, {position: lspPosition(line, column)}),
|
|
1604
|
+
getReferenceSet(context, line, column, options.includeDeclaration, options.crossWorkspace ?? true, options.maxCandidates),
|
|
1605
|
+
options.includeDiagnostics ? context.client.diagnostics(context.file) : Promise.resolve([]),
|
|
1606
|
+
]);
|
|
1607
|
+
const diagnostics = options.includeDiagnostics ? normalizeDiagnostics(rawDiagnostics.items, options.maxDiagnostics) : [];
|
|
1608
|
+
let effectiveHover = hover;
|
|
1609
|
+
let signatureSource = hoverText(hover?.contents) ? SIGNATURE_SOURCE.QUERY_POSITION_HOVER : SIGNATURE_SOURCE.NOT_REPORTED;
|
|
1610
|
+
let signatureDefinition;
|
|
1611
|
+
if (signatureSource === SIGNATURE_SOURCE.NOT_REPORTED) {
|
|
1612
|
+
for (const definition of definitionResult.definitions) {
|
|
1613
|
+
try {
|
|
1614
|
+
const definitionContext = await clientForFile(definition.file, context.repositoryRoot);
|
|
1615
|
+
const definitionHover = await definitionContext.client.textRequest("textDocument/hover", definitionContext.file, {
|
|
1616
|
+
position: lspPosition(definition.range.start.line, definition.range.start.column),
|
|
1617
|
+
});
|
|
1618
|
+
if (!hoverText(definitionHover?.contents)) continue;
|
|
1619
|
+
effectiveHover = definitionHover;
|
|
1620
|
+
signatureSource = SIGNATURE_SOURCE.RESOLVED_DEFINITION_HOVER;
|
|
1621
|
+
signatureDefinition = definition;
|
|
1622
|
+
break;
|
|
1623
|
+
} catch {
|
|
1624
|
+
// Another resolved definition may still provide the signature.
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
const referenceAnalysis = referenceSet.analysis;
|
|
1629
|
+
return {
|
|
1630
|
+
workspaceRoot: context.workspaceRoot,
|
|
1631
|
+
repositoryRoot: context.repositoryRoot,
|
|
1632
|
+
identifier: referenceAnalysis.identifier,
|
|
1633
|
+
definition: {
|
|
1634
|
+
locations: definitionResult.definitions,
|
|
1635
|
+
via: definitionResult.via,
|
|
1636
|
+
attempts: definitionResult.attempts,
|
|
1637
|
+
failure: definitionResult.failure,
|
|
1638
|
+
},
|
|
1639
|
+
hover: {
|
|
1640
|
+
contents: hoverText(effectiveHover?.contents),
|
|
1641
|
+
range: effectiveHover?.range ? displayRange(effectiveHover.range) : undefined,
|
|
1642
|
+
source: signatureSource,
|
|
1643
|
+
definition: signatureDefinition,
|
|
1644
|
+
},
|
|
1645
|
+
references: referenceAnalysis.references,
|
|
1646
|
+
referenceSetId: referenceSet.id,
|
|
1647
|
+
referenceSetReused: referenceSet.reused,
|
|
1648
|
+
referenceSetContentFilesChecked: referenceSet.fileFingerprints.length,
|
|
1649
|
+
referenceSetFreshnessCheckedAt: referenceSet.freshnessCheckedAt,
|
|
1650
|
+
referenceSetFreshnessCheckMilliseconds: referenceSet.freshnessCheckMilliseconds,
|
|
1651
|
+
repositorySourceFilesChecked: referenceSet.repositorySourceInventory.sourceFileCount,
|
|
1652
|
+
collectionStabilityAttempts: referenceSet.collectionStabilityAttempts,
|
|
1653
|
+
referenceFiles: referenceAnalysis.referenceFiles,
|
|
1654
|
+
referenceSummary: {
|
|
1655
|
+
nativeReferenceCount: referenceAnalysis.nativeReferenceCount,
|
|
1656
|
+
verifiedCrossWorkspaceCount: referenceAnalysis.verifiedCrossWorkspaceCount,
|
|
1657
|
+
verifiedReferenceCount: referenceAnalysis.verifiedReferenceCount,
|
|
1658
|
+
scannedCandidateCount: referenceAnalysis.scannedCandidateCount,
|
|
1659
|
+
totalTextualCandidateCount: referenceAnalysis.totalTextualCandidateCount,
|
|
1660
|
+
semanticallyMatchedCandidateCount: referenceAnalysis.semanticallyMatchedCandidateCount,
|
|
1661
|
+
rejectedCandidateCount: referenceAnalysis.rejectedCandidateCount,
|
|
1662
|
+
rejectedCandidatesByReason: referenceAnalysis.rejectedCandidatesByReason,
|
|
1663
|
+
unresolvedCandidateCount: referenceAnalysis.unresolvedCandidateCount,
|
|
1664
|
+
unresolvedCandidates: referenceAnalysis.unresolvedCandidates,
|
|
1665
|
+
collectionTruncated: referenceAnalysis.collectionTruncated,
|
|
1666
|
+
performance: referenceAnalysis.performance,
|
|
1667
|
+
},
|
|
1668
|
+
diagnostics: options.includeDiagnostics
|
|
1669
|
+
? {
|
|
1670
|
+
items: diagnostics,
|
|
1671
|
+
totalCount: rawDiagnostics.items.length,
|
|
1672
|
+
itemsReturnedAreSubset: rawDiagnostics.items.length > diagnostics.length,
|
|
1673
|
+
documentVersion: rawDiagnostics.documentVersion,
|
|
1674
|
+
reportedDocumentVersion: rawDiagnostics.reportedDocumentVersion,
|
|
1675
|
+
freshness: rawDiagnostics.freshness,
|
|
1676
|
+
}
|
|
1677
|
+
: {included: false},
|
|
1678
|
+
};
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
function groupedReferenceFiles(references) {
|
|
1682
|
+
const groups = new Map();
|
|
1683
|
+
for (const reference of references) {
|
|
1684
|
+
const current = groups.get(reference.file) || {file: reference.file, count: 0, via: new Set()};
|
|
1685
|
+
current.count++;
|
|
1686
|
+
if (reference.via) current.via.add(reference.via);
|
|
1687
|
+
groups.set(reference.file, current);
|
|
1688
|
+
}
|
|
1689
|
+
return [...groups.values()]
|
|
1690
|
+
.map((group) => ({file: group.file, count: group.count, via: [...group.via].sort()}))
|
|
1691
|
+
.sort((left, right) => left.file.localeCompare(right.file));
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function publicReferenceMethod(method) {
|
|
1695
|
+
if (method === INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP) return REFERENCE_DISCOVERY_METHOD.OWNING_WORKSPACE_LANGUAGE_SERVER;
|
|
1696
|
+
if (method === INTERNAL_RESOLUTION_SOURCE.CROSS_WORKSPACE_DEFINITION)
|
|
1697
|
+
return REFERENCE_DISCOVERY_METHOD.DEFINITION_MATCH_FROM_ANOTHER_WORKSPACE;
|
|
1698
|
+
return method;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
function publicDefinitionMethod(method) {
|
|
1702
|
+
if (method === INTERNAL_RESOLUTION_SOURCE.NATIVE_LSP) return DEFINITION_RESOLUTION_METHOD.LANGUAGE_SERVER;
|
|
1703
|
+
if (method === INTERNAL_RESOLUTION_SOURCE.TYPESCRIPT_SERVER_FALLBACK) return DEFINITION_RESOLUTION_METHOD.TYPESCRIPT_SERVER;
|
|
1704
|
+
if (method === INTERNAL_RESOLUTION_SOURCE.VUE_TEMPLATE_IMPORT_BINDING) return DEFINITION_RESOLUTION_METHOD.VUE_TEMPLATE_IMPORT_BINDING;
|
|
1705
|
+
return DEFINITION_RESOLUTION_METHOD.UNRESOLVED;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function referenceFacts(audit) {
|
|
1709
|
+
const summary = audit.referenceSummary;
|
|
1710
|
+
const classifiedTextMatches =
|
|
1711
|
+
summary.semanticallyMatchedCandidateCount + summary.rejectedCandidateCount + summary.unresolvedCandidateCount;
|
|
1712
|
+
const allTextMatchesAccountedFor =
|
|
1713
|
+
!summary.collectionTruncated &&
|
|
1714
|
+
summary.scannedCandidateCount === summary.totalTextualCandidateCount &&
|
|
1715
|
+
classifiedTextMatches === summary.scannedCandidateCount;
|
|
1716
|
+
return {
|
|
1717
|
+
references: {
|
|
1718
|
+
verifiedTotal: summary.verifiedReferenceCount,
|
|
1719
|
+
foundByOwningWorkspaceLanguageServer: summary.nativeReferenceCount,
|
|
1720
|
+
verifiedFromOtherWorkspaces: summary.verifiedCrossWorkspaceCount,
|
|
1721
|
+
},
|
|
1722
|
+
textSearch: {
|
|
1723
|
+
matchesFound: summary.totalTextualCandidateCount,
|
|
1724
|
+
matchesChecked: summary.scannedCandidateCount,
|
|
1725
|
+
matchesToRequestedSymbol: summary.semanticallyMatchedCandidateCount,
|
|
1726
|
+
matchesToDifferentSymbols: summary.rejectedCandidateCount,
|
|
1727
|
+
matchesWhoseDefinitionCouldNotBeResolved: summary.unresolvedCandidateCount,
|
|
1728
|
+
accountingStatus: allTextMatchesAccountedFor ? ACCOUNTING_STATUS.COMPLETE : ACCOUNTING_STATUS.INCOMPLETE,
|
|
1729
|
+
},
|
|
1730
|
+
unresolvedReferences: {
|
|
1731
|
+
candidatesAvailable: summary.unresolvedCandidates.length,
|
|
1732
|
+
referenceSetId: audit.referenceSetId,
|
|
1733
|
+
pageWithTool: summary.unresolvedCandidates.length > 0 ? TOOL.UNRESOLVED_REFERENCE_PAGE : undefined,
|
|
1734
|
+
},
|
|
1735
|
+
collection: {
|
|
1736
|
+
status: collectionStatus({
|
|
1737
|
+
stoppedByLimit: summary.collectionTruncated,
|
|
1738
|
+
unresolvedCount: summary.unresolvedCandidateCount,
|
|
1739
|
+
}),
|
|
1740
|
+
stoppedByLimit: summary.collectionTruncated,
|
|
1741
|
+
referenceSetId: audit.referenceSetId,
|
|
1742
|
+
reusedPreviousCollection: audit.referenceSetReused,
|
|
1743
|
+
expiresInSeconds: Math.ceil(REFERENCE_SET_TTL_MS / DEFAULT.MILLISECONDS_PER_SECOND),
|
|
1744
|
+
contentFreshness: CONTENT_FRESHNESS.VERIFIED_CURRENT,
|
|
1745
|
+
contentFingerprintAlgorithm: FINGERPRINT_ALGORITHM.SHA_256,
|
|
1746
|
+
contentFilesChecked: audit.referenceSetContentFilesChecked,
|
|
1747
|
+
contentCheckedAt: new Date(audit.referenceSetFreshnessCheckedAt).toISOString(),
|
|
1748
|
+
repositoryInventoryFreshness: CONTENT_FRESHNESS.VERIFIED_REPOSITORY_SOURCE_INVENTORY,
|
|
1749
|
+
repositorySourceFilesChecked: audit.repositorySourceFilesChecked,
|
|
1750
|
+
freshnessCheckMilliseconds: audit.referenceSetFreshnessCheckMilliseconds,
|
|
1751
|
+
collectionStabilityAttempts: audit.collectionStabilityAttempts,
|
|
1752
|
+
performance: summary.performance,
|
|
1753
|
+
},
|
|
1754
|
+
referenceFiles: audit.referenceFiles.map((group) => ({
|
|
1755
|
+
file: group.file,
|
|
1756
|
+
references: group.count,
|
|
1757
|
+
discoveryMethods: group.via.map(publicReferenceMethod),
|
|
1758
|
+
})),
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
function factsForReferenceSet(entry, reused = true) {
|
|
1763
|
+
const analysis = entry.analysis;
|
|
1764
|
+
return referenceFacts({
|
|
1765
|
+
referenceSummary: {
|
|
1766
|
+
nativeReferenceCount: analysis.nativeReferenceCount,
|
|
1767
|
+
verifiedCrossWorkspaceCount: analysis.verifiedCrossWorkspaceCount,
|
|
1768
|
+
verifiedReferenceCount: analysis.verifiedReferenceCount,
|
|
1769
|
+
scannedCandidateCount: analysis.scannedCandidateCount,
|
|
1770
|
+
totalTextualCandidateCount: analysis.totalTextualCandidateCount,
|
|
1771
|
+
semanticallyMatchedCandidateCount: analysis.semanticallyMatchedCandidateCount,
|
|
1772
|
+
rejectedCandidateCount: analysis.rejectedCandidateCount,
|
|
1773
|
+
unresolvedCandidateCount: analysis.unresolvedCandidateCount,
|
|
1774
|
+
unresolvedCandidates: analysis.unresolvedCandidates,
|
|
1775
|
+
collectionTruncated: analysis.collectionTruncated,
|
|
1776
|
+
performance: analysis.performance,
|
|
1777
|
+
},
|
|
1778
|
+
referenceFiles: analysis.referenceFiles,
|
|
1779
|
+
referenceSetId: entry.id,
|
|
1780
|
+
referenceSetReused: reused,
|
|
1781
|
+
referenceSetContentFilesChecked: entry.fileFingerprints.length,
|
|
1782
|
+
referenceSetFreshnessCheckedAt: entry.freshnessCheckedAt,
|
|
1783
|
+
referenceSetFreshnessCheckMilliseconds: entry.freshnessCheckMilliseconds,
|
|
1784
|
+
repositorySourceFilesChecked: entry.repositorySourceInventory.sourceFileCount,
|
|
1785
|
+
collectionStabilityAttempts: entry.collectionStabilityAttempts,
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function publicDefinition(definition) {
|
|
1790
|
+
return {
|
|
1791
|
+
match: definition.locations.length > 0 ? DEFINITION_MATCH.RESOLVED : DEFINITION_MATCH.UNRESOLVED,
|
|
1792
|
+
locations: definition.locations,
|
|
1793
|
+
method: publicDefinitionMethod(definition.via),
|
|
1794
|
+
attemptedMethods: definition.attempts?.map(publicDefinitionMethod),
|
|
1795
|
+
failure: definition.failure,
|
|
1796
|
+
};
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1799
|
+
function publicSymbol(symbol) {
|
|
1800
|
+
if (!symbol) return undefined;
|
|
1801
|
+
return {
|
|
1802
|
+
name: symbol.name,
|
|
1803
|
+
kind: symbol.kind,
|
|
1804
|
+
container: symbol.container,
|
|
1805
|
+
file: symbol.file,
|
|
1806
|
+
range: symbol.range,
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
|
|
1810
|
+
function auditSummary(audit, symbol, includeSignature = true) {
|
|
1811
|
+
const facts = referenceFacts(audit);
|
|
1812
|
+
return {
|
|
1813
|
+
symbol: publicSymbol(symbol),
|
|
1814
|
+
identifier: audit.identifier,
|
|
1815
|
+
definition: publicDefinition(audit.definition),
|
|
1816
|
+
signature: includeSignature ? audit.hover.contents : undefined,
|
|
1817
|
+
signatureSource: includeSignature ? audit.hover.source : undefined,
|
|
1818
|
+
signatureDefinition: includeSignature ? audit.hover.definition : undefined,
|
|
1819
|
+
references: facts.references,
|
|
1820
|
+
textSearch: facts.textSearch,
|
|
1821
|
+
unresolvedReferences: facts.unresolvedReferences,
|
|
1822
|
+
referenceFiles: facts.referenceFiles,
|
|
1823
|
+
referenceSetId: facts.collection.referenceSetId,
|
|
1824
|
+
collection: facts.collection,
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function countSummary(audit, symbol) {
|
|
1829
|
+
const facts = referenceFacts(audit);
|
|
1830
|
+
return {
|
|
1831
|
+
symbol: publicSymbol(symbol),
|
|
1832
|
+
identifier: audit.identifier,
|
|
1833
|
+
definitionMatch: audit.definition.locations.length > 0 ? DEFINITION_MATCH.RESOLVED : DEFINITION_MATCH.UNRESOLVED,
|
|
1834
|
+
references: facts.references,
|
|
1835
|
+
textSearch: facts.textSearch,
|
|
1836
|
+
unresolvedReferences: facts.unresolvedReferences,
|
|
1837
|
+
filesContainingReferences: facts.referenceFiles.length,
|
|
1838
|
+
referenceSetId: facts.collection.referenceSetId,
|
|
1839
|
+
collection: facts.collection,
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
async function collectNamedSymbolAudits({root, symbol, fileHint, maxDefinitions, includeDeclaration, maxCandidates}) {
|
|
1844
|
+
const resolvedRoot = await existingDirectory(root);
|
|
1845
|
+
const discovery = await semanticWorkspaceSymbols(resolvedRoot, symbol, maxCandidates);
|
|
1846
|
+
const exactDefinitions = dedupeLocations(discovery.symbols.filter((candidate) => candidate.name === symbol));
|
|
1847
|
+
const normalizedHint = fileHint?.replaceAll("\\", "/").toLowerCase();
|
|
1848
|
+
const matchingDefinitions = normalizedHint
|
|
1849
|
+
? exactDefinitions.filter((candidate) => candidate.file.replaceAll("\\", "/").toLowerCase().includes(normalizedHint))
|
|
1850
|
+
: exactDefinitions;
|
|
1851
|
+
const selectedDefinitions = maxDefinitions === undefined ? matchingDefinitions : matchingDefinitions.slice(0, maxDefinitions);
|
|
1852
|
+
const audits = await mapLimit(selectedDefinitions, DEFAULT.NAMED_DEFINITION_CONCURRENCY, (definition) =>
|
|
1853
|
+
auditSymbolAtPosition(definition.file, resolvedRoot, definition.range.start.line, definition.range.start.column, {
|
|
1854
|
+
includeDeclaration,
|
|
1855
|
+
maxCandidates,
|
|
1856
|
+
includeDiagnostics: false,
|
|
1857
|
+
maxDiagnostics: undefined,
|
|
1858
|
+
}),
|
|
1859
|
+
);
|
|
1860
|
+
const stoppedByDefinitionLimit = selectedDefinitions.length < matchingDefinitions.length;
|
|
1861
|
+
const stoppedByCandidateLimit = discovery.candidateScanTruncated || audits.some((audit) => audit.referenceSummary.collectionTruncated);
|
|
1862
|
+
const unresolvedCount =
|
|
1863
|
+
discovery.unresolvedFileCount + audits.reduce((total, audit) => total + audit.referenceSummary.unresolvedCandidateCount, 0);
|
|
1864
|
+
return {
|
|
1865
|
+
resolvedRoot,
|
|
1866
|
+
discovery,
|
|
1867
|
+
exactDefinitions,
|
|
1868
|
+
matchingDefinitions,
|
|
1869
|
+
selectedDefinitions,
|
|
1870
|
+
audits,
|
|
1871
|
+
collection: {
|
|
1872
|
+
status: collectionStatus({stoppedByLimit: stoppedByDefinitionLimit || stoppedByCandidateLimit, unresolvedCount}),
|
|
1873
|
+
stoppedByLimit: stoppedByDefinitionLimit || stoppedByCandidateLimit,
|
|
1874
|
+
symbolDefinitionsFound: exactDefinitions.length,
|
|
1875
|
+
definitionsMatchingFileFilter: matchingDefinitions.length,
|
|
1876
|
+
definitionsAnalyzed: audits.length,
|
|
1877
|
+
sourceFilesWhoseSymbolsCouldNotBeRead: discovery.unresolvedFileCount,
|
|
1878
|
+
},
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
function toolResult(tool, body) {
|
|
1883
|
+
const data = JSON.parse(
|
|
1884
|
+
JSON.stringify({
|
|
1885
|
+
server: {name: PRODUCT.NAME, version: SERVER_VERSION},
|
|
1886
|
+
resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION},
|
|
1887
|
+
tool,
|
|
1888
|
+
...body,
|
|
1889
|
+
}),
|
|
1890
|
+
);
|
|
1891
|
+
validatePublicResult(data);
|
|
1892
|
+
return {
|
|
1893
|
+
_meta: {resultSchema: RESULT_SCHEMA.NAME, resultSchemaVersion: RESULT_SCHEMA.VERSION},
|
|
1894
|
+
content: [{type: "text", text: stringifyYaml(data, {lineWidth: 0})}],
|
|
1895
|
+
structuredContent: data,
|
|
1896
|
+
};
|
|
1897
|
+
}
|
|
1898
|
+
|
|
1899
|
+
function validatePublicResult(data) {
|
|
1900
|
+
if (data.server?.name !== PRODUCT.NAME || data.server?.version !== SERVER_VERSION) {
|
|
1901
|
+
throw new Error(`Tool result omitted the ${PRODUCT.NAME} server identity`);
|
|
1902
|
+
}
|
|
1903
|
+
if (data.resultSchema?.name !== RESULT_SCHEMA.NAME || data.resultSchema?.version !== RESULT_SCHEMA.VERSION) {
|
|
1904
|
+
throw new Error(`Tool result omitted the ${RESULT_SCHEMA.NAME} result schema identity`);
|
|
1905
|
+
}
|
|
1906
|
+
if (!PUBLIC_TOOL_NAMES.has(data.tool)) throw new Error(`Unknown public tool name: ${data.tool}`);
|
|
1907
|
+
if (!isObject(data.request) || !isObject(data.result)) {
|
|
1908
|
+
throw new Error(`${data.tool} must return request and result objects`);
|
|
1909
|
+
}
|
|
1910
|
+
if (!isObject(data.collection) || !isObject(data.presentation) || !Array.isArray(data.continueWith)) {
|
|
1911
|
+
throw new Error(`${data.tool} must return collection, presentation, and continueWith fields`);
|
|
1912
|
+
}
|
|
1913
|
+
if (!COLLECTION_STATUSES.has(data.collection.status)) {
|
|
1914
|
+
throw new Error(`${data.tool} returned an invalid collection status`);
|
|
1915
|
+
}
|
|
1916
|
+
if (!PRESENTATION_MODES.has(data.presentation.mode)) {
|
|
1917
|
+
throw new Error(`${data.tool} returned an invalid presentation mode`);
|
|
1918
|
+
}
|
|
1919
|
+
for (const continuation of data.continueWith || []) {
|
|
1920
|
+
if (!PUBLIC_TOOL_NAMES.has(continuation.tool)) {
|
|
1921
|
+
throw new Error(`${data.tool} returned an unknown continuation tool: ${continuation.tool}`);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const visit = (value, pathParts = []) => {
|
|
1926
|
+
if (Array.isArray(value)) {
|
|
1927
|
+
value.forEach((item, index) => visit(item, [...pathParts, String(index)]));
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (!isObject(value)) return;
|
|
1931
|
+
for (const [key, item] of Object.entries(value)) {
|
|
1932
|
+
if (AMBIGUOUS_PUBLIC_KEYS.has(key)) {
|
|
1933
|
+
throw new Error(`${data.tool} returned ambiguous public field ${[...pathParts, key].join(".")}`);
|
|
1934
|
+
}
|
|
1935
|
+
visit(item, [...pathParts, key]);
|
|
1936
|
+
}
|
|
1937
|
+
if (isObject(value.textSearch)) {
|
|
1938
|
+
const search = value.textSearch;
|
|
1939
|
+
const accountedFor =
|
|
1940
|
+
search.matchesToRequestedSymbol + search.matchesToDifferentSymbols + search.matchesWhoseDefinitionCouldNotBeResolved;
|
|
1941
|
+
if (accountedFor !== search.matchesChecked) {
|
|
1942
|
+
throw new Error(`${data.tool} returned inconsistent text-match accounting`);
|
|
1943
|
+
}
|
|
1944
|
+
const expected = search.matchesChecked === search.matchesFound ? ACCOUNTING_STATUS.COMPLETE : ACCOUNTING_STATUS.INCOMPLETE;
|
|
1945
|
+
if (search.accountingStatus !== expected) {
|
|
1946
|
+
throw new Error(`${data.tool} returned an inconsistent text-search accounting status`);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
};
|
|
1950
|
+
visit(data);
|
|
1951
|
+
}
|
|
1952
|
+
|
|
1953
|
+
function toolError(tool, error) {
|
|
1954
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1955
|
+
const data = {
|
|
1956
|
+
server: {name: PRODUCT.NAME, version: SERVER_VERSION},
|
|
1957
|
+
resultSchema: {name: RESULT_SCHEMA.NAME, version: RESULT_SCHEMA.VERSION},
|
|
1958
|
+
tool,
|
|
1959
|
+
request: {},
|
|
1960
|
+
result: {},
|
|
1961
|
+
collection: {status: COLLECTION_STATUS.FAILED, stoppedByLimit: false},
|
|
1962
|
+
presentation: {mode: PRESENTATION_MODE.ALL_ITEMS, itemsAvailable: 0, itemsReturned: 0, itemsReturnedAreSubset: false},
|
|
1963
|
+
continueWith: [],
|
|
1964
|
+
error: {
|
|
1965
|
+
code: typeof error?.code === "string" ? error.code : ERROR_CODE.TOOL_EXECUTION_FAILED,
|
|
1966
|
+
message,
|
|
1967
|
+
details: isObject(error?.details) ? error.details : undefined,
|
|
1968
|
+
},
|
|
1969
|
+
};
|
|
1970
|
+
validatePublicResult(data);
|
|
1971
|
+
return {
|
|
1972
|
+
isError: true,
|
|
1973
|
+
_meta: {resultSchema: RESULT_SCHEMA.NAME, resultSchemaVersion: RESULT_SCHEMA.VERSION},
|
|
1974
|
+
content: [{type: "text", text: stringifyYaml(data, {lineWidth: 0})}],
|
|
1975
|
+
structuredContent: data,
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
|
|
1979
|
+
const fileSchema = {
|
|
1980
|
+
file: z.string().describe("Absolute path to a TypeScript, JavaScript, TSX, JSX, or Vue file"),
|
|
1981
|
+
root: z.string().optional().describe("Optional absolute workspace or repository root"),
|
|
1982
|
+
};
|
|
1983
|
+
const positionSchema = {
|
|
1984
|
+
...fileSchema,
|
|
1985
|
+
line: z.number().int().min(1).describe("1-based line number"),
|
|
1986
|
+
column: z.number().int().min(1).describe("1-based UTF-16 column number"),
|
|
1987
|
+
};
|
|
1988
|
+
const readOnly = {readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false};
|
|
1989
|
+
|
|
1990
|
+
try {
|
|
1991
|
+
verifyBundledRuntime();
|
|
1992
|
+
} catch (error) {
|
|
1993
|
+
process.stderr.write(
|
|
1994
|
+
`${JSON.stringify({
|
|
1995
|
+
error: {
|
|
1996
|
+
code: error?.code || ERROR_CODE.RUNTIME_DEPENDENCY_MISSING,
|
|
1997
|
+
message: error instanceof Error ? error.message : String(error),
|
|
1998
|
+
details: error?.details,
|
|
1999
|
+
},
|
|
2000
|
+
})}\n`,
|
|
2001
|
+
);
|
|
2002
|
+
process.exit(PROCESS_EXIT_CODE.FAILURE);
|
|
2003
|
+
}
|
|
2004
|
+
|
|
2005
|
+
const server = new McpServer(
|
|
2006
|
+
{name: PRODUCT.NAME, version: SERVER_VERSION},
|
|
2007
|
+
{
|
|
2008
|
+
instructions: [
|
|
2009
|
+
`Use ${PRODUCT.NAME} tools as a complementary graph.`,
|
|
2010
|
+
"Use lsp_document_symbols or lsp_workspace_symbols for discovery.",
|
|
2011
|
+
"Use lsp_definition or lsp_hover for one position-based fact.",
|
|
2012
|
+
"Use lsp_count_named_symbol or lsp_count_references for a small response that measures reference scope.",
|
|
2013
|
+
"Use lsp_audit_named_symbol or lsp_audit_symbol for a composed semantic evidence summary.",
|
|
2014
|
+
"Use lsp_references and lsp_reference_page for verified source locations.",
|
|
2015
|
+
"Tool results provide static semantic evidence. Use direct source inspection and focused tests for runtime behavior.",
|
|
2016
|
+
].join(" "),
|
|
2017
|
+
},
|
|
2018
|
+
);
|
|
2019
|
+
|
|
2020
|
+
server.registerTool(
|
|
2021
|
+
TOOL.DOCUMENT_SYMBOLS,
|
|
2022
|
+
{
|
|
2023
|
+
description:
|
|
2024
|
+
"Provides declarations and nested members reported for one file. Evidence scope: document structure. Continue with lsp_definition, lsp_hover, lsp_count_references, or lsp_audit_symbol using an exact position.",
|
|
2025
|
+
inputSchema: {
|
|
2026
|
+
...fileSchema,
|
|
2027
|
+
maxResults: z.number().int().min(1).optional().describe("Optional number of symbols to return; omit to return all collected symbols"),
|
|
2028
|
+
},
|
|
2029
|
+
annotations: readOnly,
|
|
2030
|
+
},
|
|
2031
|
+
async ({file, root, maxResults}) => {
|
|
2032
|
+
const tool = TOOL.DOCUMENT_SYMBOLS;
|
|
2033
|
+
try {
|
|
2034
|
+
const context = await clientForFile(file, root);
|
|
2035
|
+
const raw = await context.client.textRequest("textDocument/documentSymbol", context.file);
|
|
2036
|
+
const all = flattenDocumentSymbols(raw || [], context.file);
|
|
2037
|
+
const symbols = limit(all, maxResults);
|
|
2038
|
+
return toolResult(tool, {
|
|
2039
|
+
request: {file: context.file, searchScope: "document", resultLimit: normalizedLimit(maxResults)},
|
|
2040
|
+
result: {symbols, symbolsFound: all.length},
|
|
2041
|
+
collection: {status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false},
|
|
2042
|
+
presentation: {
|
|
2043
|
+
mode: symbols.length === all.length ? PRESENTATION_MODE.ALL_ITEMS : PRESENTATION_MODE.SUBSET,
|
|
2044
|
+
itemsAvailable: all.length,
|
|
2045
|
+
itemsReturned: symbols.length,
|
|
2046
|
+
itemsReturnedAreSubset: symbols.length < all.length,
|
|
2047
|
+
},
|
|
2048
|
+
continueWith: [
|
|
2049
|
+
{tool: TOOL.DEFINITION, provides: "the exact definition for a source position"},
|
|
2050
|
+
{tool: TOOL.AUDIT_SYMBOL, provides: "a semantic evidence summary for a source position"},
|
|
2051
|
+
],
|
|
2052
|
+
});
|
|
2053
|
+
} catch (error) {
|
|
2054
|
+
return toolError(tool, error);
|
|
2055
|
+
}
|
|
2056
|
+
},
|
|
2057
|
+
);
|
|
2058
|
+
|
|
2059
|
+
server.registerTool(
|
|
2060
|
+
TOOL.WORKSPACE_SYMBOLS,
|
|
2061
|
+
{
|
|
2062
|
+
description:
|
|
2063
|
+
"Provides declaration-shaped symbols whose names contain a query across a workspace or repository. Evidence scope: symbol discovery. Continue with lsp_definition, lsp_count_named_symbol, or lsp_audit_named_symbol using the exact symbol name.",
|
|
2064
|
+
inputSchema: {
|
|
2065
|
+
root: z.string().describe("Absolute workspace or repository root"),
|
|
2066
|
+
query: z.string().min(1).describe("Exact or partial symbol name"),
|
|
2067
|
+
maxResults: z.number().int().min(1).optional().describe("Optional number of symbols to return; omit to return all collected symbols"),
|
|
2068
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to inspect; omit to inspect all matches"),
|
|
2069
|
+
},
|
|
2070
|
+
annotations: readOnly,
|
|
2071
|
+
},
|
|
2072
|
+
async ({root, query, maxResults, maxCandidates}) => {
|
|
2073
|
+
const tool = TOOL.WORKSPACE_SYMBOLS;
|
|
2074
|
+
try {
|
|
2075
|
+
const resolvedRoot = await existingDirectory(root);
|
|
2076
|
+
const semantic = await semanticWorkspaceSymbols(resolvedRoot, query, maxCandidates);
|
|
2077
|
+
const all = dedupeLocations(semantic.symbols);
|
|
2078
|
+
const symbols = limit(all, maxResults);
|
|
2079
|
+
const stoppedByLimit = semantic.candidateScanTruncated;
|
|
2080
|
+
return toolResult(tool, {
|
|
2081
|
+
request: {
|
|
2082
|
+
root: resolvedRoot,
|
|
2083
|
+
query,
|
|
2084
|
+
searchScope: "repository",
|
|
2085
|
+
candidateLimit: normalizedLimit(maxCandidates),
|
|
2086
|
+
resultLimit: normalizedLimit(maxResults),
|
|
2087
|
+
},
|
|
2088
|
+
result: {
|
|
2089
|
+
symbols,
|
|
2090
|
+
symbolsFound: all.length,
|
|
2091
|
+
textMatchesFound: semantic.totalTextualCandidateCount,
|
|
2092
|
+
textMatchesChecked: semantic.candidateCount,
|
|
2093
|
+
sourceFilesWhoseSymbolsCouldNotBeRead: semantic.unresolvedFileCount,
|
|
2094
|
+
},
|
|
2095
|
+
collection: {status: collectionStatus({stoppedByLimit, unresolvedCount: semantic.unresolvedFileCount}), stoppedByLimit},
|
|
2096
|
+
presentation: {
|
|
2097
|
+
mode: symbols.length === all.length ? PRESENTATION_MODE.ALL_ITEMS : PRESENTATION_MODE.SUBSET,
|
|
2098
|
+
itemsAvailable: all.length,
|
|
2099
|
+
itemsReturned: symbols.length,
|
|
2100
|
+
itemsReturnedAreSubset: symbols.length < all.length,
|
|
2101
|
+
},
|
|
2102
|
+
continueWith: [
|
|
2103
|
+
{tool: TOOL.COUNT_NAMED_SYMBOL, provides: "definition and reference counts for an exact symbol name"},
|
|
2104
|
+
{tool: TOOL.AUDIT_NAMED_SYMBOL, provides: "definition identity and a reference summary by file"},
|
|
2105
|
+
],
|
|
2106
|
+
});
|
|
2107
|
+
} catch (error) {
|
|
2108
|
+
return toolError(tool, error);
|
|
2109
|
+
}
|
|
2110
|
+
},
|
|
2111
|
+
);
|
|
2112
|
+
|
|
2113
|
+
server.registerTool(
|
|
2114
|
+
TOOL.DEFINITION,
|
|
2115
|
+
{
|
|
2116
|
+
description:
|
|
2117
|
+
"Provides the exact definitions resolved for one source position. Evidence scope: symbol identity at that position. Continue with lsp_hover, lsp_count_references, or lsp_audit_symbol.",
|
|
2118
|
+
inputSchema: {
|
|
2119
|
+
...positionSchema,
|
|
2120
|
+
maxResults: z
|
|
2121
|
+
.number()
|
|
2122
|
+
.int()
|
|
2123
|
+
.min(1)
|
|
2124
|
+
.optional()
|
|
2125
|
+
.describe("Optional number of definitions to return; omit to return all resolved definitions"),
|
|
2126
|
+
},
|
|
2127
|
+
annotations: readOnly,
|
|
2128
|
+
},
|
|
2129
|
+
async ({file, root, line, column, maxResults}) => {
|
|
2130
|
+
const tool = TOOL.DEFINITION;
|
|
2131
|
+
try {
|
|
2132
|
+
const resolved = await definitionsAt(file, root, line, column);
|
|
2133
|
+
const definitions = limit(resolved.definitions, maxResults);
|
|
2134
|
+
return toolResult(tool, {
|
|
2135
|
+
request: {file: resolved.context.file, line, column, searchScope: "source-position", resultLimit: normalizedLimit(maxResults)},
|
|
2136
|
+
result: {
|
|
2137
|
+
definitionMatch: resolved.definitions.length > 0 ? DEFINITION_MATCH.RESOLVED : DEFINITION_MATCH.UNRESOLVED,
|
|
2138
|
+
definitions,
|
|
2139
|
+
definitionsFound: resolved.definitions.length,
|
|
2140
|
+
resolutionMethod: publicDefinitionMethod(resolved.via),
|
|
2141
|
+
attemptedMethods: resolved.attempts?.map(publicDefinitionMethod),
|
|
2142
|
+
failure: resolved.failure,
|
|
2143
|
+
},
|
|
2144
|
+
collection: {
|
|
2145
|
+
status: resolved.definitions.length > 0 ? COLLECTION_STATUS.COMPLETE : COLLECTION_STATUS.PARTIAL,
|
|
2146
|
+
stoppedByLimit: false,
|
|
2147
|
+
},
|
|
2148
|
+
presentation: {
|
|
2149
|
+
mode: definitions.length === resolved.definitions.length ? PRESENTATION_MODE.ALL_ITEMS : PRESENTATION_MODE.SUBSET,
|
|
2150
|
+
itemsAvailable: resolved.definitions.length,
|
|
2151
|
+
itemsReturned: definitions.length,
|
|
2152
|
+
itemsReturnedAreSubset: definitions.length < resolved.definitions.length,
|
|
2153
|
+
},
|
|
2154
|
+
continueWith: [
|
|
2155
|
+
{tool: TOOL.HOVER, provides: "the inferred type and documentation at the same position"},
|
|
2156
|
+
{tool: TOOL.COUNT_REFERENCES, provides: "verified reference counts for the resolved symbol"},
|
|
2157
|
+
],
|
|
2158
|
+
});
|
|
2159
|
+
} catch (error) {
|
|
2160
|
+
return toolError(tool, error);
|
|
2161
|
+
}
|
|
2162
|
+
},
|
|
2163
|
+
);
|
|
2164
|
+
|
|
2165
|
+
server.registerTool(
|
|
2166
|
+
TOOL.HOVER,
|
|
2167
|
+
{
|
|
2168
|
+
description:
|
|
2169
|
+
"Provides inferred type information and documentation for one source position. Evidence scope: language-server type information. Continue with lsp_definition or lsp_audit_symbol.",
|
|
2170
|
+
inputSchema: positionSchema,
|
|
2171
|
+
annotations: readOnly,
|
|
2172
|
+
},
|
|
2173
|
+
async ({file, root, line, column}) => {
|
|
2174
|
+
const tool = TOOL.HOVER;
|
|
2175
|
+
try {
|
|
2176
|
+
const context = await clientForFile(file, root);
|
|
2177
|
+
const hover = await context.client.textRequest("textDocument/hover", context.file, {position: lspPosition(line, column)});
|
|
2178
|
+
return toolResult(tool, {
|
|
2179
|
+
request: {file: context.file, line, column, searchScope: "source-position"},
|
|
2180
|
+
result: {
|
|
2181
|
+
typeAndDocumentation: hoverText(hover?.contents),
|
|
2182
|
+
range: hover?.range ? displayRange(hover.range) : undefined,
|
|
2183
|
+
informationFound: !!hover?.contents,
|
|
2184
|
+
},
|
|
2185
|
+
collection: {status: hover?.contents ? COLLECTION_STATUS.COMPLETE : COLLECTION_STATUS.PARTIAL, stoppedByLimit: false},
|
|
2186
|
+
presentation: {
|
|
2187
|
+
mode: PRESENTATION_MODE.ALL_ITEMS,
|
|
2188
|
+
itemsAvailable: hover?.contents ? 1 : 0,
|
|
2189
|
+
itemsReturned: hover?.contents ? 1 : 0,
|
|
2190
|
+
itemsReturnedAreSubset: false,
|
|
2191
|
+
},
|
|
2192
|
+
continueWith: [
|
|
2193
|
+
{tool: TOOL.DEFINITION, provides: "the exact definition behind this position"},
|
|
2194
|
+
{tool: TOOL.AUDIT_SYMBOL, provides: "a semantic evidence summary for this position"},
|
|
2195
|
+
],
|
|
2196
|
+
});
|
|
2197
|
+
} catch (error) {
|
|
2198
|
+
return toolError(tool, error);
|
|
2199
|
+
}
|
|
2200
|
+
},
|
|
2201
|
+
);
|
|
2202
|
+
|
|
2203
|
+
server.registerTool(
|
|
2204
|
+
TOOL.DIAGNOSTICS,
|
|
2205
|
+
{
|
|
2206
|
+
description:
|
|
2207
|
+
"Provides versioned diagnostics reported by the owning language server for one file. Only result.diagnosticsForCurrentDocument is verified evidence. When the language server does not confirm the current version, that field is null and result.evidence.status is untrusted.",
|
|
2208
|
+
inputSchema: {
|
|
2209
|
+
...fileSchema,
|
|
2210
|
+
maxResults: z
|
|
2211
|
+
.number()
|
|
2212
|
+
.int()
|
|
2213
|
+
.min(1)
|
|
2214
|
+
.optional()
|
|
2215
|
+
.describe("Optional number of diagnostics to return; omit to return all reported diagnostics"),
|
|
2216
|
+
},
|
|
2217
|
+
annotations: readOnly,
|
|
2218
|
+
},
|
|
2219
|
+
async ({file, root, maxResults}) => {
|
|
2220
|
+
const tool = TOOL.DIAGNOSTICS;
|
|
2221
|
+
try {
|
|
2222
|
+
const context = await clientForFile(file, root);
|
|
2223
|
+
const report = await context.client.diagnostics(context.file);
|
|
2224
|
+
const diagnostics = normalizeDiagnostics(report.items, maxResults);
|
|
2225
|
+
const versionConfirmed = report.freshness === DIAGNOSTIC_FRESHNESS.CURRENT;
|
|
2226
|
+
const diagnosticReport = {
|
|
2227
|
+
items: diagnostics,
|
|
2228
|
+
itemsReported: report.items.length,
|
|
2229
|
+
itemsReturnedAreSubset: diagnostics.length < report.items.length,
|
|
2230
|
+
};
|
|
2231
|
+
return toolResult(tool, {
|
|
2232
|
+
request: {file: context.file, searchScope: "document", resultLimit: normalizedLimit(maxResults)},
|
|
2233
|
+
result: {
|
|
2234
|
+
evidence: {
|
|
2235
|
+
status: versionConfirmed ? EVIDENCE_STATUS.VERIFIED : EVIDENCE_STATUS.UNTRUSTED,
|
|
2236
|
+
reason: diagnosticEvidenceReason(report.freshness),
|
|
2237
|
+
},
|
|
2238
|
+
document: {
|
|
2239
|
+
version: report.documentVersion,
|
|
2240
|
+
contentFingerprintAlgorithm: FINGERPRINT_ALGORITHM.SHA_256,
|
|
2241
|
+
contentFingerprint: report.documentContentFingerprint,
|
|
2242
|
+
},
|
|
2243
|
+
diagnosticsForCurrentDocument: versionConfirmed ? diagnosticReport : null,
|
|
2244
|
+
unconfirmedDiagnosticReport: versionConfirmed
|
|
2245
|
+
? undefined
|
|
2246
|
+
: {
|
|
2247
|
+
...diagnosticReport,
|
|
2248
|
+
languageServerReportedDocumentVersion: report.reportedDocumentVersion,
|
|
2249
|
+
freshness: report.freshness,
|
|
2250
|
+
},
|
|
2251
|
+
waitedMilliseconds: report.waitedMilliseconds,
|
|
2252
|
+
},
|
|
2253
|
+
collection: {
|
|
2254
|
+
status: versionConfirmed ? COLLECTION_STATUS.COMPLETE : COLLECTION_STATUS.PARTIAL,
|
|
2255
|
+
stoppedByLimit: false,
|
|
2256
|
+
currentDocumentVersionConfirmed: versionConfirmed,
|
|
2257
|
+
},
|
|
2258
|
+
presentation: {
|
|
2259
|
+
mode: diagnostics.length === report.items.length ? PRESENTATION_MODE.ALL_ITEMS : PRESENTATION_MODE.SUBSET,
|
|
2260
|
+
itemsAvailable: report.items.length,
|
|
2261
|
+
itemsReturned: diagnostics.length,
|
|
2262
|
+
itemsReturnedAreSubset: diagnostics.length < report.items.length,
|
|
2263
|
+
},
|
|
2264
|
+
continueWith: [
|
|
2265
|
+
{tool: TOOL.DEFINITION, provides: "the definition associated with a diagnostic position"},
|
|
2266
|
+
{tool: TOOL.HOVER, provides: "type information associated with a diagnostic position"},
|
|
2267
|
+
],
|
|
2268
|
+
});
|
|
2269
|
+
} catch (error) {
|
|
2270
|
+
return toolError(tool, error);
|
|
2271
|
+
}
|
|
2272
|
+
},
|
|
2273
|
+
);
|
|
2274
|
+
|
|
2275
|
+
server.registerTool(
|
|
2276
|
+
TOOL.COUNT_TEXT_MATCHES,
|
|
2277
|
+
{
|
|
2278
|
+
description:
|
|
2279
|
+
"Counts exact identifier text matches and containing files without starting semantic definition verification. Evidence scope: repository text only. Use this result to estimate work before lsp_count_named_symbol, lsp_audit_named_symbol, or a position-based reference tool.",
|
|
2280
|
+
inputSchema: {
|
|
2281
|
+
root: z.string().describe("Absolute workspace or repository root"),
|
|
2282
|
+
symbol: z
|
|
2283
|
+
.string()
|
|
2284
|
+
.regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)
|
|
2285
|
+
.describe("Exact JavaScript or TypeScript identifier text to count"),
|
|
2286
|
+
},
|
|
2287
|
+
annotations: readOnly,
|
|
2288
|
+
},
|
|
2289
|
+
async ({root, symbol}) => {
|
|
2290
|
+
const tool = TOOL.COUNT_TEXT_MATCHES;
|
|
2291
|
+
try {
|
|
2292
|
+
const resolvedRoot = await existingDirectory(root);
|
|
2293
|
+
const search = await rgIdentifierCandidates(resolvedRoot, symbol, 1);
|
|
2294
|
+
return toolResult(tool, {
|
|
2295
|
+
request: {root: resolvedRoot, symbol, searchScope: "repository", evidenceType: EVIDENCE_TYPE.EXACT_IDENTIFIER_TEXT_MATCH},
|
|
2296
|
+
result: {
|
|
2297
|
+
matchesFound: search.totalCandidateCount,
|
|
2298
|
+
filesContainingMatches: search.totalCandidateFileCount,
|
|
2299
|
+
semanticVerificationPerformed: false,
|
|
2300
|
+
textSearchMilliseconds: search.elapsedMilliseconds,
|
|
2301
|
+
},
|
|
2302
|
+
collection: {status: COLLECTION_STATUS.COMPLETE, stoppedByLimit: false},
|
|
2303
|
+
presentation: {mode: PRESENTATION_MODE.COUNT_ONLY, referenceLocationsReturned: 0},
|
|
2304
|
+
continueWith: [
|
|
2305
|
+
{tool: TOOL.COUNT_NAMED_SYMBOL, provides: "exact-definition and verified-reference counts for a named symbol"},
|
|
2306
|
+
{tool: TOOL.AUDIT_NAMED_SYMBOL, provides: "definition identity, signatures, and verified-reference summaries"},
|
|
2307
|
+
],
|
|
2308
|
+
});
|
|
2309
|
+
} catch (error) {
|
|
2310
|
+
return toolError(tool, error);
|
|
2311
|
+
}
|
|
2312
|
+
},
|
|
2313
|
+
);
|
|
2314
|
+
|
|
2315
|
+
server.registerTool(
|
|
2316
|
+
TOOL.COUNT_NAMED_SYMBOL,
|
|
2317
|
+
{
|
|
2318
|
+
description:
|
|
2319
|
+
"Provides exact-definition and verified-reference counts for a symbol name with a small response. Evidence scope: repository symbol and text-match accounting. Continue with lsp_audit_named_symbol or lsp_references.",
|
|
2320
|
+
inputSchema: {
|
|
2321
|
+
root: z.string().describe("Absolute workspace or repository root"),
|
|
2322
|
+
symbol: z
|
|
2323
|
+
.string()
|
|
2324
|
+
.regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)
|
|
2325
|
+
.describe("Exact JavaScript or TypeScript identifier"),
|
|
2326
|
+
fileHint: z.string().optional().describe("Optional path fragment used to select exact homonymous definitions"),
|
|
2327
|
+
maxDefinitions: z
|
|
2328
|
+
.number()
|
|
2329
|
+
.int()
|
|
2330
|
+
.min(1)
|
|
2331
|
+
.optional()
|
|
2332
|
+
.describe("Optional number of exact homonymous definitions to analyze; omit to analyze all"),
|
|
2333
|
+
includeDeclaration: z.boolean().default(true),
|
|
2334
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to verify; omit to verify all"),
|
|
2335
|
+
},
|
|
2336
|
+
annotations: readOnly,
|
|
2337
|
+
},
|
|
2338
|
+
async (args) => {
|
|
2339
|
+
const tool = TOOL.COUNT_NAMED_SYMBOL;
|
|
2340
|
+
try {
|
|
2341
|
+
const operation = await collectNamedSymbolAudits(args);
|
|
2342
|
+
return toolResult(tool, {
|
|
2343
|
+
request: {
|
|
2344
|
+
root: operation.resolvedRoot,
|
|
2345
|
+
symbol: args.symbol,
|
|
2346
|
+
fileHint: args.fileHint,
|
|
2347
|
+
searchScope: "repository",
|
|
2348
|
+
definitionLimit: normalizedLimit(args.maxDefinitions),
|
|
2349
|
+
candidateLimit: normalizedLimit(args.maxCandidates),
|
|
2350
|
+
includeDeclaration: args.includeDeclaration,
|
|
2351
|
+
},
|
|
2352
|
+
result: {
|
|
2353
|
+
requestedSymbol: args.symbol,
|
|
2354
|
+
exactDefinitionsFound: operation.exactDefinitions.length,
|
|
2355
|
+
definitionsMatchingFileFilter: operation.matchingDefinitions.length,
|
|
2356
|
+
definitions: operation.audits.map((audit, index) => countSummary(audit, operation.selectedDefinitions[index])),
|
|
2357
|
+
},
|
|
2358
|
+
collection: operation.collection,
|
|
2359
|
+
presentation: {mode: PRESENTATION_MODE.COUNT_ONLY, referenceLocationsReturned: 0},
|
|
2360
|
+
continueWith: [
|
|
2361
|
+
{tool: TOOL.AUDIT_NAMED_SYMBOL, provides: "definition identity, signature, and reference summary by file"},
|
|
2362
|
+
{tool: TOOL.REFERENCES, provides: "a page of verified source locations using a selected definition position"},
|
|
2363
|
+
...(operation.audits.some((audit) => audit.referenceSummary.unresolvedCandidateCount > 0)
|
|
2364
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "unresolved candidates for a selected definition referenceSetId"}]
|
|
2365
|
+
: []),
|
|
2366
|
+
],
|
|
2367
|
+
});
|
|
2368
|
+
} catch (error) {
|
|
2369
|
+
return toolError(tool, error);
|
|
2370
|
+
}
|
|
2371
|
+
},
|
|
2372
|
+
);
|
|
2373
|
+
|
|
2374
|
+
server.registerTool(
|
|
2375
|
+
TOOL.COUNT_REFERENCES,
|
|
2376
|
+
{
|
|
2377
|
+
description:
|
|
2378
|
+
"Provides verified-reference counts for the symbol at one exact position with a small response. Evidence scope: workspace and repository reference accounting. Continue with lsp_audit_symbol or lsp_references.",
|
|
2379
|
+
inputSchema: {
|
|
2380
|
+
...positionSchema,
|
|
2381
|
+
includeDeclaration: z.boolean().default(true),
|
|
2382
|
+
crossWorkspace: z.boolean().default(true).describe("Include text matches from other workspaces after definition verification"),
|
|
2383
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to verify; omit to verify all"),
|
|
2384
|
+
},
|
|
2385
|
+
annotations: readOnly,
|
|
2386
|
+
},
|
|
2387
|
+
async ({file, root, line, column, includeDeclaration, crossWorkspace, maxCandidates}) => {
|
|
2388
|
+
const tool = TOOL.COUNT_REFERENCES;
|
|
2389
|
+
try {
|
|
2390
|
+
const audit = await auditSymbolAtPosition(file, root, line, column, {
|
|
2391
|
+
includeDeclaration,
|
|
2392
|
+
crossWorkspace,
|
|
2393
|
+
maxCandidates,
|
|
2394
|
+
includeDiagnostics: false,
|
|
2395
|
+
maxDiagnostics: undefined,
|
|
2396
|
+
});
|
|
2397
|
+
const summary = countSummary(audit);
|
|
2398
|
+
return toolResult(tool, {
|
|
2399
|
+
request: {
|
|
2400
|
+
file: path.resolve(file),
|
|
2401
|
+
line,
|
|
2402
|
+
column,
|
|
2403
|
+
searchScope: crossWorkspace ? "repository" : "workspace",
|
|
2404
|
+
candidateLimit: normalizedLimit(maxCandidates),
|
|
2405
|
+
includeDeclaration,
|
|
2406
|
+
},
|
|
2407
|
+
result: summary,
|
|
2408
|
+
collection: summary.collection,
|
|
2409
|
+
presentation: {mode: PRESENTATION_MODE.COUNT_ONLY, referenceLocationsReturned: 0},
|
|
2410
|
+
continueWith: [
|
|
2411
|
+
{tool: TOOL.AUDIT_SYMBOL, provides: "definition identity, signature, and reference summary by file"},
|
|
2412
|
+
{tool: TOOL.REFERENCES, provides: "a page of verified source locations"},
|
|
2413
|
+
...(audit.referenceSummary.unresolvedCandidateCount > 0
|
|
2414
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "unresolved candidates from this referenceSetId"}]
|
|
2415
|
+
: []),
|
|
2416
|
+
],
|
|
2417
|
+
});
|
|
2418
|
+
} catch (error) {
|
|
2419
|
+
return toolError(tool, error);
|
|
2420
|
+
}
|
|
2421
|
+
},
|
|
2422
|
+
);
|
|
2423
|
+
|
|
2424
|
+
server.registerTool(
|
|
2425
|
+
TOOL.AUDIT_NAMED_SYMBOL,
|
|
2426
|
+
{
|
|
2427
|
+
description:
|
|
2428
|
+
"Provides a composed semantic evidence summary for an exact symbol name. It reuses compatible count collections and returns definition identity, signatures, reference counts, and files containing references. Continue with lsp_references for source locations.",
|
|
2429
|
+
inputSchema: {
|
|
2430
|
+
root: z.string().describe("Absolute workspace or repository root"),
|
|
2431
|
+
symbol: z
|
|
2432
|
+
.string()
|
|
2433
|
+
.regex(/^[A-Za-z_$][A-Za-z0-9_$]*$/)
|
|
2434
|
+
.describe("Exact JavaScript or TypeScript identifier"),
|
|
2435
|
+
fileHint: z.string().optional().describe("Optional path fragment used to select exact homonymous definitions"),
|
|
2436
|
+
maxDefinitions: z
|
|
2437
|
+
.number()
|
|
2438
|
+
.int()
|
|
2439
|
+
.min(1)
|
|
2440
|
+
.optional()
|
|
2441
|
+
.describe("Optional number of exact homonymous definitions to analyze; omit to analyze all"),
|
|
2442
|
+
includeDeclaration: z.boolean().default(true),
|
|
2443
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to verify; omit to verify all"),
|
|
2444
|
+
},
|
|
2445
|
+
annotations: readOnly,
|
|
2446
|
+
},
|
|
2447
|
+
async (args) => {
|
|
2448
|
+
const tool = TOOL.AUDIT_NAMED_SYMBOL;
|
|
2449
|
+
try {
|
|
2450
|
+
const operation = await collectNamedSymbolAudits(args);
|
|
2451
|
+
return toolResult(tool, {
|
|
2452
|
+
request: {
|
|
2453
|
+
root: operation.resolvedRoot,
|
|
2454
|
+
symbol: args.symbol,
|
|
2455
|
+
fileHint: args.fileHint,
|
|
2456
|
+
searchScope: "repository",
|
|
2457
|
+
definitionLimit: normalizedLimit(args.maxDefinitions),
|
|
2458
|
+
candidateLimit: normalizedLimit(args.maxCandidates),
|
|
2459
|
+
includeDeclaration: args.includeDeclaration,
|
|
2460
|
+
},
|
|
2461
|
+
result: {
|
|
2462
|
+
requestedSymbol: args.symbol,
|
|
2463
|
+
exactDefinitionsFound: operation.exactDefinitions.length,
|
|
2464
|
+
definitionsMatchingFileFilter: operation.matchingDefinitions.length,
|
|
2465
|
+
audits: operation.audits.map((audit, index) => auditSummary(audit, operation.selectedDefinitions[index])),
|
|
2466
|
+
},
|
|
2467
|
+
collection: operation.collection,
|
|
2468
|
+
presentation: {mode: PRESENTATION_MODE.SUMMARY_BY_FILE, referenceLocationsReturned: 0},
|
|
2469
|
+
continueWith: [
|
|
2470
|
+
{tool: TOOL.REFERENCES, provides: "the first page of verified source locations"},
|
|
2471
|
+
{tool: TOOL.DEFINITION, provides: "definition resolution for any individual call or alias position"},
|
|
2472
|
+
...(operation.audits.some((audit) => audit.referenceSummary.unresolvedCandidateCount > 0)
|
|
2473
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "unresolved candidates for a selected audit referenceSetId"}]
|
|
2474
|
+
: []),
|
|
2475
|
+
],
|
|
2476
|
+
});
|
|
2477
|
+
} catch (error) {
|
|
2478
|
+
return toolError(tool, error);
|
|
2479
|
+
}
|
|
2480
|
+
},
|
|
2481
|
+
);
|
|
2482
|
+
|
|
2483
|
+
server.registerTool(
|
|
2484
|
+
TOOL.AUDIT_SYMBOL,
|
|
2485
|
+
{
|
|
2486
|
+
description:
|
|
2487
|
+
"Provides a composed semantic evidence summary for the symbol at one exact position. It reuses compatible count collections and returns definition identity, signature, reference counts, and files containing references. Continue with lsp_references for source locations.",
|
|
2488
|
+
inputSchema: {
|
|
2489
|
+
...positionSchema,
|
|
2490
|
+
includeDeclaration: z.boolean().default(true),
|
|
2491
|
+
crossWorkspace: z.boolean().default(true).describe("Include text matches from other workspaces after definition verification"),
|
|
2492
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to verify; omit to verify all"),
|
|
2493
|
+
},
|
|
2494
|
+
annotations: readOnly,
|
|
2495
|
+
},
|
|
2496
|
+
async ({file, root, line, column, includeDeclaration, crossWorkspace, maxCandidates}) => {
|
|
2497
|
+
const tool = TOOL.AUDIT_SYMBOL;
|
|
2498
|
+
try {
|
|
2499
|
+
const audit = await auditSymbolAtPosition(file, root, line, column, {
|
|
2500
|
+
includeDeclaration,
|
|
2501
|
+
crossWorkspace,
|
|
2502
|
+
maxCandidates,
|
|
2503
|
+
includeDiagnostics: false,
|
|
2504
|
+
maxDiagnostics: undefined,
|
|
2505
|
+
});
|
|
2506
|
+
const summary = auditSummary(audit);
|
|
2507
|
+
return toolResult(tool, {
|
|
2508
|
+
request: {
|
|
2509
|
+
file: path.resolve(file),
|
|
2510
|
+
line,
|
|
2511
|
+
column,
|
|
2512
|
+
searchScope: crossWorkspace ? "repository" : "workspace",
|
|
2513
|
+
candidateLimit: normalizedLimit(maxCandidates),
|
|
2514
|
+
includeDeclaration,
|
|
2515
|
+
},
|
|
2516
|
+
result: summary,
|
|
2517
|
+
collection: summary.collection,
|
|
2518
|
+
presentation: {mode: PRESENTATION_MODE.SUMMARY_BY_FILE, referenceLocationsReturned: 0},
|
|
2519
|
+
continueWith: [
|
|
2520
|
+
{tool: TOOL.REFERENCES, provides: "the first page of verified source locations"},
|
|
2521
|
+
{tool: TOOL.DEFINITION, provides: "definition resolution for any individual call or alias position"},
|
|
2522
|
+
...(audit.referenceSummary.unresolvedCandidateCount > 0
|
|
2523
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "unresolved candidates from this audit referenceSetId"}]
|
|
2524
|
+
: []),
|
|
2525
|
+
],
|
|
2526
|
+
});
|
|
2527
|
+
} catch (error) {
|
|
2528
|
+
return toolError(tool, error);
|
|
2529
|
+
}
|
|
2530
|
+
},
|
|
2531
|
+
);
|
|
2532
|
+
|
|
2533
|
+
server.registerTool(
|
|
2534
|
+
TOOL.REFERENCES,
|
|
2535
|
+
{
|
|
2536
|
+
description:
|
|
2537
|
+
"Provides the first page of verified source locations for the symbol at one exact position. Collection samples the repository source inventory before and after analysis. Read collection.status as complete, limited, partial, or failed. Continue with lsp_reference_page when presentation.nextCursor is present.",
|
|
2538
|
+
inputSchema: {
|
|
2539
|
+
...positionSchema,
|
|
2540
|
+
includeDeclaration: z.boolean().default(true),
|
|
2541
|
+
crossWorkspace: z.boolean().default(true).describe("Include text matches from other workspaces after definition verification"),
|
|
2542
|
+
maxCandidates: z.number().int().min(1).optional().describe("Optional number of text matches to verify; omit to verify all"),
|
|
2543
|
+
pageSize: z
|
|
2544
|
+
.number()
|
|
2545
|
+
.int()
|
|
2546
|
+
.min(1)
|
|
2547
|
+
.optional()
|
|
2548
|
+
.describe(`Number of reference locations to return in this page; default ${DEFAULT.REFERENCE_PAGE_SIZE}`),
|
|
2549
|
+
},
|
|
2550
|
+
annotations: readOnly,
|
|
2551
|
+
},
|
|
2552
|
+
async ({file, root, line, column, includeDeclaration, crossWorkspace, maxCandidates, pageSize}) => {
|
|
2553
|
+
const tool = TOOL.REFERENCES;
|
|
2554
|
+
try {
|
|
2555
|
+
const context = await clientForFile(file, root);
|
|
2556
|
+
const entry = await getReferenceSet(context, line, column, includeDeclaration, crossWorkspace, maxCandidates);
|
|
2557
|
+
const facts = factsForReferenceSet(entry, entry.reused);
|
|
2558
|
+
const page = presentReferenceSet(entry, 0, pageSize || DEFAULT_REFERENCE_PAGE_SIZE);
|
|
2559
|
+
return toolResult(tool, {
|
|
2560
|
+
request: {
|
|
2561
|
+
file: context.file,
|
|
2562
|
+
line,
|
|
2563
|
+
column,
|
|
2564
|
+
searchScope: crossWorkspace ? "repository" : "workspace",
|
|
2565
|
+
candidateLimit: normalizedLimit(maxCandidates),
|
|
2566
|
+
pageSize: pageSize || DEFAULT_REFERENCE_PAGE_SIZE,
|
|
2567
|
+
includeDeclaration,
|
|
2568
|
+
},
|
|
2569
|
+
result: {
|
|
2570
|
+
identifier: entry.analysis.identifier,
|
|
2571
|
+
references: facts.references,
|
|
2572
|
+
textSearch: facts.textSearch,
|
|
2573
|
+
unresolvedReferences: facts.unresolvedReferences,
|
|
2574
|
+
referenceFiles: facts.referenceFiles,
|
|
2575
|
+
referenceSetId: entry.id,
|
|
2576
|
+
locations: page.references,
|
|
2577
|
+
},
|
|
2578
|
+
collection: facts.collection,
|
|
2579
|
+
presentation: page.presentation,
|
|
2580
|
+
continueWith: [
|
|
2581
|
+
...(page.presentation.nextCursor
|
|
2582
|
+
? [{tool: TOOL.REFERENCE_PAGE, provides: "the next page from the same verified reference set"}]
|
|
2583
|
+
: []),
|
|
2584
|
+
...(entry.analysis.unresolvedCandidates.length > 0
|
|
2585
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "the unresolved text-match candidates with literal resolution reasons"}]
|
|
2586
|
+
: []),
|
|
2587
|
+
{tool: TOOL.DEFINITION, provides: "definition resolution for an individual reference position"},
|
|
2588
|
+
],
|
|
2589
|
+
});
|
|
2590
|
+
} catch (error) {
|
|
2591
|
+
return toolError(tool, error);
|
|
2592
|
+
}
|
|
2593
|
+
},
|
|
2594
|
+
);
|
|
2595
|
+
|
|
2596
|
+
server.registerTool(
|
|
2597
|
+
TOOL.REFERENCE_PAGE,
|
|
2598
|
+
{
|
|
2599
|
+
description:
|
|
2600
|
+
"Provides one page from a previously collected verified reference set after checking direct content fingerprints and the repository source inventory. Use the referenceSetId and nextCursor returned by lsp_references or another lsp_reference_page call.",
|
|
2601
|
+
inputSchema: {
|
|
2602
|
+
referenceSetId: z
|
|
2603
|
+
.string()
|
|
2604
|
+
.min(1)
|
|
2605
|
+
.describe("Reference-set identifier returned by lsp_references, lsp_count_references, or an audit tool"),
|
|
2606
|
+
cursor: z.string().regex(/^\d+$/).default("0").describe("Cursor returned by the previous reference page"),
|
|
2607
|
+
pageSize: z
|
|
2608
|
+
.number()
|
|
2609
|
+
.int()
|
|
2610
|
+
.min(1)
|
|
2611
|
+
.optional()
|
|
2612
|
+
.describe(`Number of reference locations to return; default ${DEFAULT.REFERENCE_PAGE_SIZE}`),
|
|
2613
|
+
},
|
|
2614
|
+
annotations: readOnly,
|
|
2615
|
+
},
|
|
2616
|
+
async ({referenceSetId, cursor, pageSize}) => {
|
|
2617
|
+
const tool = TOOL.REFERENCE_PAGE;
|
|
2618
|
+
try {
|
|
2619
|
+
const entry = await getReferenceSetById(referenceSetId);
|
|
2620
|
+
if (!entry) throw new ReferenceSetUnavailableError(referenceSetId);
|
|
2621
|
+
const facts = factsForReferenceSet(entry, true);
|
|
2622
|
+
const page = presentReferenceSet(entry, Number(cursor), pageSize || DEFAULT_REFERENCE_PAGE_SIZE);
|
|
2623
|
+
return toolResult(tool, {
|
|
2624
|
+
request: {referenceSetId, cursor, pageSize: pageSize || DEFAULT_REFERENCE_PAGE_SIZE},
|
|
2625
|
+
result: {identifier: entry.analysis.identifier, references: facts.references, referenceSetId: entry.id, locations: page.references},
|
|
2626
|
+
collection: facts.collection,
|
|
2627
|
+
presentation: page.presentation,
|
|
2628
|
+
continueWith: [
|
|
2629
|
+
...(page.presentation.nextCursor
|
|
2630
|
+
? [{tool: TOOL.REFERENCE_PAGE, provides: "the next page from the same verified reference set"}]
|
|
2631
|
+
: []),
|
|
2632
|
+
...(entry.analysis.unresolvedCandidates.length > 0
|
|
2633
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "the unresolved text-match candidates with literal resolution reasons"}]
|
|
2634
|
+
: []),
|
|
2635
|
+
{tool: TOOL.DEFINITION, provides: "definition resolution for an individual reference position"},
|
|
2636
|
+
],
|
|
2637
|
+
});
|
|
2638
|
+
} catch (error) {
|
|
2639
|
+
return toolError(tool, error);
|
|
2640
|
+
}
|
|
2641
|
+
},
|
|
2642
|
+
);
|
|
2643
|
+
|
|
2644
|
+
server.registerTool(
|
|
2645
|
+
TOOL.UNRESOLVED_REFERENCE_PAGE,
|
|
2646
|
+
{
|
|
2647
|
+
description:
|
|
2648
|
+
"Provides one page of text-match candidates whose definition could not be resolved during a reference collection. Each candidate includes its location, owning workspace when known, attempted methods, and a literal failure reason. The reference set is freshness-checked before the page is returned.",
|
|
2649
|
+
inputSchema: {
|
|
2650
|
+
referenceSetId: z.string().min(1).describe("Reference-set identifier returned by a count, audit, or reference tool"),
|
|
2651
|
+
cursor: z.string().regex(/^\d+$/).default("0").describe("Cursor returned by the previous unresolved-reference page"),
|
|
2652
|
+
pageSize: z
|
|
2653
|
+
.number()
|
|
2654
|
+
.int()
|
|
2655
|
+
.min(1)
|
|
2656
|
+
.optional()
|
|
2657
|
+
.describe(`Number of unresolved candidates to return; default ${DEFAULT.REFERENCE_PAGE_SIZE}`),
|
|
2658
|
+
},
|
|
2659
|
+
annotations: readOnly,
|
|
2660
|
+
},
|
|
2661
|
+
async ({referenceSetId, cursor, pageSize}) => {
|
|
2662
|
+
const tool = TOOL.UNRESOLVED_REFERENCE_PAGE;
|
|
2663
|
+
try {
|
|
2664
|
+
const entry = await getReferenceSetById(referenceSetId);
|
|
2665
|
+
if (!entry) throw new ReferenceSetUnavailableError(referenceSetId);
|
|
2666
|
+
const facts = factsForReferenceSet(entry, true);
|
|
2667
|
+
const page = presentUnresolvedReferenceSet(entry, Number(cursor), pageSize || DEFAULT_REFERENCE_PAGE_SIZE);
|
|
2668
|
+
return toolResult(tool, {
|
|
2669
|
+
request: {referenceSetId, cursor, pageSize: pageSize || DEFAULT_REFERENCE_PAGE_SIZE},
|
|
2670
|
+
result: {
|
|
2671
|
+
identifier: entry.analysis.identifier,
|
|
2672
|
+
unresolvedReferences: facts.unresolvedReferences,
|
|
2673
|
+
referenceSetId: entry.id,
|
|
2674
|
+
candidates: page.candidates,
|
|
2675
|
+
},
|
|
2676
|
+
collection: facts.collection,
|
|
2677
|
+
presentation: page.presentation,
|
|
2678
|
+
continueWith: page.presentation.nextCursor
|
|
2679
|
+
? [{tool: TOOL.UNRESOLVED_REFERENCE_PAGE, provides: "the next page of unresolved candidates from the same reference set"}]
|
|
2680
|
+
: [{tool: TOOL.DEFINITION, provides: "a focused definition retry at an unresolved candidate position"}],
|
|
2681
|
+
});
|
|
2682
|
+
} catch (error) {
|
|
2683
|
+
return toolError(tool, error);
|
|
2684
|
+
}
|
|
2685
|
+
},
|
|
2686
|
+
);
|
|
2687
|
+
async function shutdown() {
|
|
2688
|
+
clearInterval(clientCleanupTimer);
|
|
2689
|
+
for (const entry of clients.values()) entry.client.close();
|
|
2690
|
+
clients.clear();
|
|
2691
|
+
referenceSetsById.clear();
|
|
2692
|
+
referenceSetIdByKey.clear();
|
|
2693
|
+
changedReferenceSetsById.clear();
|
|
2694
|
+
await server.close().catch(() => undefined);
|
|
2695
|
+
}
|
|
2696
|
+
|
|
2697
|
+
process.on("SIGINT", () => void shutdown().finally(() => process.exit(0)));
|
|
2698
|
+
process.on("SIGTERM", () => void shutdown().finally(() => process.exit(0)));
|
|
2699
|
+
|
|
2700
|
+
const transport = new StdioServerTransport();
|
|
2701
|
+
await server.connect(transport);
|
|
2702
|
+
process.stderr.write(`${PRODUCT.DISPLAY_NAME} server ready\n`);
|