@mrclrchtr/supi-lsp 11.2.0 → 11.4.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/README.md +40 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +2 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/declaration-level.ts +34 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +9 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -5
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
- package/node_modules/semver/classes/range.js +2 -2
- package/node_modules/semver/internal/constants.js +30 -0
- package/node_modules/semver/internal/trim-comparator-whitespace.js +438 -0
- package/node_modules/semver/package.json +1 -1
- package/package.json +3 -3
- package/src/client/client-diagnostic-collection.ts +11 -4
- package/src/client/client-diagnostics.ts +77 -6
- package/src/client/client-input-admission.ts +149 -0
- package/src/client/client-typescript-project.ts +40 -0
- package/src/client/client.ts +151 -44
- package/src/config/tsconfig-scope.ts +10 -0
- package/src/diagnostics/evidence.ts +2 -2
- package/src/manager/manager-typescript-coverage.ts +118 -0
- package/src/manager/manager-workspace-symbol.ts +157 -23
- package/src/manager/manager.ts +318 -58
- package/src/manager/typescript-file-local.ts +87 -0
- package/src/provider/semantic-symbol-mapper.ts +22 -17
- package/src/session/runtime-registry.ts +68 -6
- package/src/session/runtime-rename.ts +1 -0
- package/src/session/workspace-lsp-runtime.ts +5 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { statSync } from "node:fs";
|
|
1
|
+
import { type Dirent, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
type CodeQueryResult,
|
|
@@ -25,6 +25,10 @@ type WorkspaceSymbolLike = SymbolInformation | WorkspaceSymbol;
|
|
|
25
25
|
|
|
26
26
|
const DEFAULT_WARM_FILE_DEPTH = 4;
|
|
27
27
|
const DEFAULT_MARKER_SCAN_DEPTH = 6;
|
|
28
|
+
/** Candidates per marker root; later candidates help only when earlier ones land outside the project. */
|
|
29
|
+
const WARM_FILES_PER_ROOT = 3;
|
|
30
|
+
/** Conventional source directories tried before other top-level entries. */
|
|
31
|
+
const PREFERRED_SOURCE_DIRECTORIES = ["src", "lib", "app"];
|
|
28
32
|
|
|
29
33
|
export interface WorkspaceSymbolWarmTarget {
|
|
30
34
|
projectRoot: string;
|
|
@@ -63,6 +67,26 @@ export interface WorkspaceSymbolCollection {
|
|
|
63
67
|
excludedCount?: number;
|
|
64
68
|
/** Bounded paths removed by the automatic path policy. */
|
|
65
69
|
excludedExamples?: string[];
|
|
70
|
+
/** Known coverage limits: the server may not have loaded every project in scope. */
|
|
71
|
+
coverageGap?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Mark a collection whose warm-up never reached a configured project.
|
|
76
|
+
* The server answered only from inferred projects of the opened probe files,
|
|
77
|
+
* so an empty or short result does not prove a symbol is absent.
|
|
78
|
+
*/
|
|
79
|
+
export function workspaceSymbolCoverageGap(
|
|
80
|
+
collection: WorkspaceSymbolCollection,
|
|
81
|
+
inferredProbeCount: number,
|
|
82
|
+
): WorkspaceSymbolCollection {
|
|
83
|
+
const noun = `file${inferredProbeCount === 1 ? "" : "s"}`;
|
|
84
|
+
return {
|
|
85
|
+
...collection,
|
|
86
|
+
coverageGap:
|
|
87
|
+
`Workspace-symbol coverage is limited: ${inferredProbeCount} opened warm-up ${noun} ` +
|
|
88
|
+
"loaded only inferred projects, not a configured project. Open a project source file and retry.",
|
|
89
|
+
};
|
|
66
90
|
}
|
|
67
91
|
|
|
68
92
|
export async function collectWorkspaceSymbols(
|
|
@@ -96,10 +120,8 @@ function workspaceSymbolExclusionReason(collection: WorkspaceSymbolCollection):
|
|
|
96
120
|
const excludedCount = collection.excludedCount ?? 0;
|
|
97
121
|
if (excludedCount === 0) return null;
|
|
98
122
|
const noun = `workspace-symbol result${excludedCount === 1 ? "" : "s"}`;
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
: "";
|
|
102
|
-
return `Automatic path policy excluded ${excludedCount} ${noun}.${examples}`;
|
|
123
|
+
// Paths belong to typed metadata so consumers can distinguish observed aliases from navigation.
|
|
124
|
+
return `Automatic path policy excluded ${excludedCount} ${noun}.`;
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
/** Project one multi-client collection into the shared typed query contract. */
|
|
@@ -115,8 +137,9 @@ export function workspaceSymbolCollectionResult(
|
|
|
115
137
|
);
|
|
116
138
|
}
|
|
117
139
|
const exclusion = workspaceSymbolExclusionReason(collection);
|
|
118
|
-
if (collection.failures.length > 0 || exclusion) {
|
|
140
|
+
if (collection.failures.length > 0 || exclusion || collection.coverageGap) {
|
|
119
141
|
const excludedCount = collection.excludedCount ?? 0;
|
|
142
|
+
const exclusionOnly = collection.failures.length === 0 && !collection.coverageGap;
|
|
120
143
|
const metadata =
|
|
121
144
|
excludedCount > 0
|
|
122
145
|
? {
|
|
@@ -124,17 +147,30 @@ export function workspaceSymbolCollectionResult(
|
|
|
124
147
|
excludedCount,
|
|
125
148
|
examples: [...(collection.excludedExamples ?? [])],
|
|
126
149
|
},
|
|
150
|
+
...(exclusionOnly ? { exclusionOnly: true } : {}),
|
|
127
151
|
}
|
|
128
152
|
: undefined;
|
|
129
153
|
return partialCodeQuery(
|
|
130
154
|
collection.results,
|
|
131
|
-
[collection.failures.join("; "), exclusion]
|
|
155
|
+
[collection.failures.join("; "), collection.coverageGap, exclusion]
|
|
156
|
+
.filter(Boolean)
|
|
157
|
+
.join("; "),
|
|
132
158
|
metadata,
|
|
133
159
|
);
|
|
134
160
|
}
|
|
135
161
|
return completedCodeQuery(collection.results);
|
|
136
162
|
}
|
|
137
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Choose files to open so a workspace-symbol server loads the projects under `root`.
|
|
166
|
+
*
|
|
167
|
+
* Returns up to three candidates per marker root, interleaved so every root's
|
|
168
|
+
* best candidate comes before any root's fallback; nested roots first. Within a
|
|
169
|
+
* root, conventional source directories come first and hidden entries last:
|
|
170
|
+
* root-level dotfiles and tool configs are often outside the configured program
|
|
171
|
+
* (for example `tsconfig.json` `include`), and opening only such a file loads an
|
|
172
|
+
* unrelated project. Callers try candidates in order.
|
|
173
|
+
*/
|
|
138
174
|
export function findWorkspaceSymbolWarmTargets(
|
|
139
175
|
root: string,
|
|
140
176
|
rootMarkers: string[],
|
|
@@ -158,7 +194,7 @@ export function findWorkspaceSymbolWarmTargets(
|
|
|
158
194
|
return searchRoot
|
|
159
195
|
? findWorkspaceSymbolWarmTargets(searchRoot, rootMarkers, fileTypes, {
|
|
160
196
|
...options,
|
|
161
|
-
policy,
|
|
197
|
+
policy: explicitScopePolicy(policy, scope),
|
|
162
198
|
scopes: undefined,
|
|
163
199
|
})
|
|
164
200
|
: [];
|
|
@@ -174,26 +210,76 @@ export function findWorkspaceSymbolWarmTargets(
|
|
|
174
210
|
}
|
|
175
211
|
|
|
176
212
|
const markerRoots = collectMarkerRoots(resolvedRoot, rootMarkers, maxMarkerDepth, policy);
|
|
177
|
-
const
|
|
178
|
-
.map((entry) => {
|
|
179
|
-
const file = findWarmFileRecursive(entry.root, allowed, maxFileDepth, policy);
|
|
180
|
-
return file ? { projectRoot: entry.root, file, priority: entry.priority } : null;
|
|
181
|
-
})
|
|
182
|
-
.filter((entry): entry is { projectRoot: string; file: string; priority: number } =>
|
|
183
|
-
Boolean(entry),
|
|
184
|
-
)
|
|
213
|
+
const perRoot = markerRoots
|
|
185
214
|
.sort(
|
|
186
215
|
(a, b) =>
|
|
187
|
-
a.priority - b.priority ||
|
|
188
|
-
a.projectRoot.length - b.projectRoot.length ||
|
|
189
|
-
a.projectRoot.localeCompare(b.projectRoot),
|
|
216
|
+
a.priority - b.priority || a.root.length - b.root.length || a.root.localeCompare(b.root),
|
|
190
217
|
)
|
|
191
|
-
.map((
|
|
218
|
+
.map((entry) =>
|
|
219
|
+
findWarmFiles(entry.root, allowed, maxFileDepth, policy).map((file) => ({
|
|
220
|
+
projectRoot: entry.root,
|
|
221
|
+
file,
|
|
222
|
+
})),
|
|
223
|
+
);
|
|
224
|
+
// Interleave by rank so each root's best candidate precedes any root's fallback.
|
|
225
|
+
const targets = Array.from({ length: WARM_FILES_PER_ROOT }, (_, rank) =>
|
|
226
|
+
perRoot.flatMap((candidates) => candidates[rank] ?? []),
|
|
227
|
+
).flat();
|
|
228
|
+
|
|
229
|
+
if (targets.length > 0) {
|
|
230
|
+
// Keep the first, most specific root for a file a parent root also reaches.
|
|
231
|
+
const seen = new Set<string>();
|
|
232
|
+
return targets.filter((target) => !seen.has(target.file) && Boolean(seen.add(target.file)));
|
|
233
|
+
}
|
|
192
234
|
|
|
193
|
-
|
|
235
|
+
return findWarmFiles(resolvedRoot, allowed, maxFileDepth, policy).map((file) => ({
|
|
236
|
+
projectRoot: resolvedRoot,
|
|
237
|
+
file,
|
|
238
|
+
}));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* True when a reported file is inside a scope by spelling or physical identity.
|
|
243
|
+
* Servers may report realpaths for files requested through symlinked roots.
|
|
244
|
+
*/
|
|
245
|
+
export function isWithinAnyScope(file: string, scopes: readonly string[]): boolean {
|
|
246
|
+
if (scopes.some((scope) => isWithinOrEqual(scope, file))) return true;
|
|
247
|
+
try {
|
|
248
|
+
const physicalFile = realpathSync(file);
|
|
249
|
+
return scopes.some((scope) => {
|
|
250
|
+
try {
|
|
251
|
+
return isWithinOrEqual(realpathSync(scope), physicalFile);
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
} catch {
|
|
257
|
+
return false;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
194
260
|
|
|
195
|
-
|
|
196
|
-
|
|
261
|
+
/**
|
|
262
|
+
* Keep an explicitly selected excluded file or directory warmable, matching the
|
|
263
|
+
* rows workspace-symbol filtering allows for that scope. Exclusions nested in an
|
|
264
|
+
* eligible directory scope stay excluded.
|
|
265
|
+
*/
|
|
266
|
+
function explicitScopePolicy(
|
|
267
|
+
policy: AutomaticLspPathPolicy,
|
|
268
|
+
scope: string,
|
|
269
|
+
): AutomaticLspPathPolicy {
|
|
270
|
+
const kind = statSync(scope, { throwIfNoEntry: false });
|
|
271
|
+
if (!kind) return policy;
|
|
272
|
+
const selected = kind.isFile()
|
|
273
|
+
? (candidate: string) => path.resolve(candidate) === path.resolve(scope)
|
|
274
|
+
: policy.isEligible(scope, "directory")
|
|
275
|
+
? null
|
|
276
|
+
: (candidate: string) => isWithinOrEqual(scope, candidate);
|
|
277
|
+
if (!selected) return policy;
|
|
278
|
+
return {
|
|
279
|
+
...policy,
|
|
280
|
+
isEligible: (candidate, candidateKind) =>
|
|
281
|
+
selected(candidate) ? true : policy.isEligible(candidate, candidateKind),
|
|
282
|
+
};
|
|
197
283
|
}
|
|
198
284
|
|
|
199
285
|
interface MarkerRootEntry {
|
|
@@ -234,6 +320,54 @@ function collectMarkerRoots(
|
|
|
234
320
|
}));
|
|
235
321
|
}
|
|
236
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Pick at most {@link WARM_FILES_PER_ROOT} files under `root`, one per top-level
|
|
325
|
+
* group: preferred source directories, other directories, root files, then
|
|
326
|
+
* hidden entries.
|
|
327
|
+
*/
|
|
328
|
+
function findWarmFiles(
|
|
329
|
+
root: string,
|
|
330
|
+
allowed: Set<string>,
|
|
331
|
+
depth: number,
|
|
332
|
+
policy: AutomaticLspPathPolicy,
|
|
333
|
+
): string[] {
|
|
334
|
+
let topLevel: readonly Dirent[] = [];
|
|
335
|
+
walkAutomaticLspTree(policy, root, 0, (_directory, entries) => {
|
|
336
|
+
topLevel = entries;
|
|
337
|
+
return true;
|
|
338
|
+
});
|
|
339
|
+
const isAllowedFile = (entry: Dirent) =>
|
|
340
|
+
(entry.isFile() || entry.isSymbolicLink()) &&
|
|
341
|
+
allowed.has(path.extname(entry.name).slice(1).toLowerCase());
|
|
342
|
+
const rank = (entry: Dirent) => {
|
|
343
|
+
const hidden = entry.name.startsWith(".") ? 3 : 0;
|
|
344
|
+
if (!entry.isDirectory()) return hidden + 2;
|
|
345
|
+
return hidden + (PREFERRED_SOURCE_DIRECTORIES.includes(entry.name) ? 0 : 1);
|
|
346
|
+
};
|
|
347
|
+
const ordered = topLevel
|
|
348
|
+
.filter((entry) => entry.isDirectory() || isAllowedFile(entry))
|
|
349
|
+
.map((entry, index) => ({ entry, index, rank: rank(entry) }))
|
|
350
|
+
.sort((a, b) => a.rank - b.rank || a.index - b.index);
|
|
351
|
+
|
|
352
|
+
const files: string[] = [];
|
|
353
|
+
const rootFileRanks = new Set<number>();
|
|
354
|
+
for (const { entry, rank: entryRank } of ordered) {
|
|
355
|
+
if (files.length >= WARM_FILES_PER_ROOT) break;
|
|
356
|
+
const entryPath = path.join(root, entry.name);
|
|
357
|
+
if (!entry.isDirectory()) {
|
|
358
|
+
// One root file per visibility group is enough evidence for that group.
|
|
359
|
+
if (rootFileRanks.has(entryRank)) continue;
|
|
360
|
+
rootFileRanks.add(entryRank);
|
|
361
|
+
files.push(entryPath);
|
|
362
|
+
continue;
|
|
363
|
+
}
|
|
364
|
+
if (depth <= 0) continue;
|
|
365
|
+
const file = findWarmFileRecursive(entryPath, allowed, depth - 1, policy);
|
|
366
|
+
if (file) files.push(file);
|
|
367
|
+
}
|
|
368
|
+
return files;
|
|
369
|
+
}
|
|
370
|
+
|
|
237
371
|
function findWarmFileRecursive(
|
|
238
372
|
directory: string,
|
|
239
373
|
allowed: Set<string>,
|