@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
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether the identifier at `position` names a top-level declaration that only
|
|
6
|
+
* its own file can reference: the file is an ES module and the declaration is
|
|
7
|
+
* neither exported by a modifier nor by an export clause or assignment.
|
|
8
|
+
*
|
|
9
|
+
* Other TypeScript projects can then hold no references or implementations,
|
|
10
|
+
* so a project coverage gap cannot apply. Script files (no import or export),
|
|
11
|
+
* members, unreadable files, and non-declaration positions return false.
|
|
12
|
+
*
|
|
13
|
+
* @param position Zero-based LSP position.
|
|
14
|
+
*/
|
|
15
|
+
export function isFileLocalTopLevelDeclaration(
|
|
16
|
+
filePath: string,
|
|
17
|
+
position: { readonly line: number; readonly character: number },
|
|
18
|
+
): boolean {
|
|
19
|
+
let text: string;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(filePath, "utf8");
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
const sourceFile = ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true);
|
|
26
|
+
if (!ts.isExternalModule(sourceFile)) return false;
|
|
27
|
+
let offset: number;
|
|
28
|
+
try {
|
|
29
|
+
offset = ts.getPositionOfLineAndCharacter(sourceFile, position.line, position.character);
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const statement = sourceFile.statements.find(
|
|
34
|
+
(candidate) => candidate.getStart(sourceFile) <= offset && offset < candidate.end,
|
|
35
|
+
);
|
|
36
|
+
if (!statement) return false;
|
|
37
|
+
const name = topLevelDeclarationNameAt(statement, offset, sourceFile);
|
|
38
|
+
return name !== null && !hasExportModifier(statement) && !exportedByName(sourceFile, name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Name of the declaration `statement` declares at `offset`, or null. */
|
|
42
|
+
function topLevelDeclarationNameAt(
|
|
43
|
+
statement: ts.Statement,
|
|
44
|
+
offset: number,
|
|
45
|
+
sourceFile: ts.SourceFile,
|
|
46
|
+
): string | null {
|
|
47
|
+
const names: ts.Identifier[] = [];
|
|
48
|
+
if (
|
|
49
|
+
ts.isFunctionDeclaration(statement) ||
|
|
50
|
+
ts.isClassDeclaration(statement) ||
|
|
51
|
+
ts.isInterfaceDeclaration(statement) ||
|
|
52
|
+
ts.isTypeAliasDeclaration(statement) ||
|
|
53
|
+
ts.isEnumDeclaration(statement)
|
|
54
|
+
) {
|
|
55
|
+
if (statement.name) names.push(statement.name);
|
|
56
|
+
} else if (ts.isVariableStatement(statement)) {
|
|
57
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
58
|
+
if (ts.isIdentifier(declaration.name)) names.push(declaration.name);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const hit = names.find((name) => name.getStart(sourceFile) <= offset && offset <= name.end);
|
|
62
|
+
return hit ? hit.text : null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function hasExportModifier(statement: ts.Statement): boolean {
|
|
66
|
+
return (
|
|
67
|
+
ts.canHaveModifiers(statement) &&
|
|
68
|
+
(ts.getModifiers(statement) ?? []).some(
|
|
69
|
+
(modifier) =>
|
|
70
|
+
modifier.kind === ts.SyntaxKind.ExportKeyword ||
|
|
71
|
+
modifier.kind === ts.SyntaxKind.DeclareKeyword,
|
|
72
|
+
)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** `export { name }`, `export default name`, or `export = name` in this file. */
|
|
77
|
+
function exportedByName(sourceFile: ts.SourceFile, name: string): boolean {
|
|
78
|
+
return sourceFile.statements.some((statement) => {
|
|
79
|
+
if (ts.isExportAssignment(statement)) {
|
|
80
|
+
return ts.isIdentifier(statement.expression) && statement.expression.text === name;
|
|
81
|
+
}
|
|
82
|
+
if (!ts.isExportDeclaration(statement) || statement.moduleSpecifier) return false;
|
|
83
|
+
const clause = statement.exportClause;
|
|
84
|
+
if (!clause || !ts.isNamedExports(clause)) return false;
|
|
85
|
+
return clause.elements.some((element) => (element.propertyName ?? element.name).text === name);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -2,10 +2,11 @@ import { readFile } from "node:fs/promises";
|
|
|
2
2
|
import type {
|
|
3
3
|
CodeLocation,
|
|
4
4
|
CodeSymbol,
|
|
5
|
-
|
|
5
|
+
DeclarationLevel,
|
|
6
6
|
DocumentCodeSymbol,
|
|
7
7
|
SourceRange,
|
|
8
8
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
9
|
+
import { childDeclarationLevel } from "@mrclrchtr/supi-code-runtime/api";
|
|
9
10
|
import { uriToFile } from "@mrclrchtr/supi-core/path";
|
|
10
11
|
import type {
|
|
11
12
|
DocumentSymbol,
|
|
@@ -100,15 +101,26 @@ export async function mapDocumentSymbols(
|
|
|
100
101
|
): Promise<DocumentCodeSymbol[]> {
|
|
101
102
|
return flattenDocumentSymbols(symbols, filePath, null, {
|
|
102
103
|
sourceLines: await readSourceLines(filePath),
|
|
103
|
-
|
|
104
|
+
parent: null,
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
/** Level of a symbol reported inside `parent`, or top-level without one. */
|
|
109
|
+
function levelWithin(
|
|
110
|
+
parent: { readonly kind: string; readonly level: DeclarationLevel } | null,
|
|
111
|
+
kind: string,
|
|
112
|
+
): DeclarationLevel {
|
|
113
|
+
return parent ? childDeclarationLevel(parent, kind) : "top-level";
|
|
114
|
+
}
|
|
115
|
+
|
|
107
116
|
function flattenDocumentSymbols(
|
|
108
117
|
symbols: DocumentSymbol[] | SymbolInformation[],
|
|
109
118
|
filePath: string,
|
|
110
119
|
container: string | null,
|
|
111
|
-
context: {
|
|
120
|
+
context: {
|
|
121
|
+
sourceLines: readonly string[] | null;
|
|
122
|
+
parent: { readonly kind: string; readonly level: DeclarationLevel } | null;
|
|
123
|
+
},
|
|
112
124
|
): DocumentCodeSymbol[] {
|
|
113
125
|
const result: DocumentCodeSymbol[] = [];
|
|
114
126
|
for (const sym of symbols) {
|
|
@@ -116,15 +128,13 @@ function flattenDocumentSymbols(
|
|
|
116
128
|
let document: DocumentSymbol | null = null;
|
|
117
129
|
if (isSymbolInformation(sym)) information = sym;
|
|
118
130
|
else document = sym;
|
|
131
|
+
const kind = symbolKindName(sym.kind);
|
|
132
|
+
const level = information ? "unknown" : levelWithin(context.parent, kind);
|
|
133
|
+
const childContext = { sourceLines: context.sourceLines, parent: { kind, level } };
|
|
119
134
|
const declarationRange = toValidRange(document?.range ?? information?.location.range);
|
|
120
135
|
if (!declarationRange) {
|
|
121
136
|
if (document?.children?.length) {
|
|
122
|
-
result.push(
|
|
123
|
-
...flattenDocumentSymbols(document.children, filePath, sym.name, {
|
|
124
|
-
sourceLines: context.sourceLines,
|
|
125
|
-
nesting: "nested",
|
|
126
|
-
}),
|
|
127
|
-
);
|
|
137
|
+
result.push(...flattenDocumentSymbols(document.children, filePath, sym.name, childContext));
|
|
128
138
|
}
|
|
129
139
|
continue;
|
|
130
140
|
}
|
|
@@ -134,23 +144,18 @@ function flattenDocumentSymbols(
|
|
|
134
144
|
: null;
|
|
135
145
|
const symbol: DocumentCodeSymbol = {
|
|
136
146
|
name: sym.name,
|
|
137
|
-
kind
|
|
147
|
+
kind,
|
|
138
148
|
file: filePath,
|
|
139
149
|
declarationAnchor: { line: declStart.line + 1, character: declStart.character + 1 },
|
|
140
150
|
container: information?.containerName ?? container,
|
|
141
|
-
|
|
151
|
+
level,
|
|
142
152
|
};
|
|
143
153
|
if (nameStart) {
|
|
144
154
|
symbol.nameAnchor = { line: nameStart.line + 1, character: nameStart.character + 1 };
|
|
145
155
|
}
|
|
146
156
|
result.push(symbol);
|
|
147
157
|
if (document?.children?.length) {
|
|
148
|
-
result.push(
|
|
149
|
-
...flattenDocumentSymbols(document.children, filePath, sym.name, {
|
|
150
|
-
sourceLines: context.sourceLines,
|
|
151
|
-
nesting: "nested",
|
|
152
|
-
}),
|
|
153
|
-
);
|
|
158
|
+
result.push(...flattenDocumentSymbols(document.children, filePath, sym.name, childContext));
|
|
154
159
|
}
|
|
155
160
|
}
|
|
156
161
|
return result;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Shared session-scoped LSP service registry reused by peer extensions.
|
|
2
2
|
|
|
3
3
|
// biome-ignore lint/style/noExcessiveLinesPerFile: session registry, runtime recovery, and telemetry stay in one file; splitting would hide the recovery contract.
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import {
|
|
5
6
|
type CodeQueryResult,
|
|
6
7
|
type CodeRequestControl,
|
|
7
8
|
isCodeRequestInterruption,
|
|
8
9
|
mapCodeQueryResult,
|
|
10
|
+
partialCodeQuery,
|
|
9
11
|
throwIfCodeRequestInterrupted,
|
|
10
12
|
unavailableCodeQuery,
|
|
11
13
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
@@ -139,6 +141,8 @@ function recordScopeDecisionEvent(manager: LspManager): void {
|
|
|
139
141
|
}
|
|
140
142
|
}
|
|
141
143
|
|
|
144
|
+
const FILE_STARTUP_FAILURE_REASON = "The routed LSP client could not be started for this file";
|
|
145
|
+
|
|
142
146
|
class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
143
147
|
constructor(private readonly manager: LspManager) {}
|
|
144
148
|
|
|
@@ -157,6 +161,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
157
161
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
158
162
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
159
163
|
recoverProcessCrash: true,
|
|
164
|
+
protectInputs: true,
|
|
160
165
|
control,
|
|
161
166
|
});
|
|
162
167
|
if (!client) return unavailableFileQuery("hover", resolvedPath);
|
|
@@ -171,6 +176,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
171
176
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
172
177
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
173
178
|
recoverProcessCrash: true,
|
|
179
|
+
protectInputs: true,
|
|
174
180
|
control,
|
|
175
181
|
});
|
|
176
182
|
if (!client) return unavailableFileQuery("definition", resolvedPath);
|
|
@@ -185,12 +191,18 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
185
191
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
186
192
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
187
193
|
recoverProcessCrash: true,
|
|
194
|
+
protectInputs: true,
|
|
188
195
|
control,
|
|
189
196
|
});
|
|
190
197
|
if (!client) return unavailableFileQuery("references", resolvedPath);
|
|
191
|
-
return
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
return this.withProjectCoverage(
|
|
199
|
+
client,
|
|
200
|
+
{ file: resolvedPath, position },
|
|
201
|
+
"references",
|
|
202
|
+
mapCodeQueryResult(
|
|
203
|
+
await client.references(resolvedPath, position, control),
|
|
204
|
+
(data) => data ?? [],
|
|
205
|
+
),
|
|
194
206
|
);
|
|
195
207
|
}
|
|
196
208
|
|
|
@@ -202,10 +214,34 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
202
214
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
203
215
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
204
216
|
recoverProcessCrash: true,
|
|
217
|
+
protectInputs: true,
|
|
205
218
|
control,
|
|
206
219
|
});
|
|
207
220
|
if (!client) return unavailableFileQuery("implementation", resolvedPath);
|
|
208
|
-
return
|
|
221
|
+
return this.withProjectCoverage(
|
|
222
|
+
client,
|
|
223
|
+
{ file: resolvedPath, position },
|
|
224
|
+
"implementation",
|
|
225
|
+
await client.implementation(resolvedPath, position, control),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Downgrade a completed project-wide answer to partial when the TypeScript
|
|
231
|
+
* server may not have searched every relevant project.
|
|
232
|
+
*/
|
|
233
|
+
private withProjectCoverage<T extends Location[] | Location | LocationLink[] | null>(
|
|
234
|
+
client: NonNullable<Awaited<ReturnType<LspManager["ensureFileOpen"]>>>,
|
|
235
|
+
target: { readonly file: string; readonly position: Position },
|
|
236
|
+
request: "references" | "implementation",
|
|
237
|
+
result: CodeQueryResult<T>,
|
|
238
|
+
): CodeQueryResult<T> {
|
|
239
|
+
if (result.kind !== "completed") return result;
|
|
240
|
+
const gap = this.manager.typeScriptProjectCoverageGap(client, target.file, request, {
|
|
241
|
+
resultFiles: locationFiles(result.data),
|
|
242
|
+
position: target.position,
|
|
243
|
+
});
|
|
244
|
+
return gap ? partialCodeQuery(result.data, gap) : result;
|
|
209
245
|
}
|
|
210
246
|
|
|
211
247
|
async documentSymbols(
|
|
@@ -215,6 +251,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
215
251
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
216
252
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
217
253
|
recoverProcessCrash: true,
|
|
254
|
+
protectInputs: true,
|
|
218
255
|
control,
|
|
219
256
|
});
|
|
220
257
|
if (!client) return unavailableFileQuery("document symbols", resolvedPath);
|
|
@@ -260,6 +297,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
260
297
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
261
298
|
const client = await this.manager.ensureFileOpen(resolvedPath, {
|
|
262
299
|
recoverProcessCrash: true,
|
|
300
|
+
protectInputs: true,
|
|
263
301
|
control,
|
|
264
302
|
});
|
|
265
303
|
if (!client) return null;
|
|
@@ -309,7 +347,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
309
347
|
const processCrashRecovery = this.manager.getProcessCrashRecoveryReportForFile(resolvedPath);
|
|
310
348
|
return {
|
|
311
349
|
kind: "unavailable",
|
|
312
|
-
reason: "No LSP client can serve this file",
|
|
350
|
+
reason: this.fileUnavailableReason(resolvedPath, "No LSP client can serve this file"),
|
|
313
351
|
...(processCrashRecovery ? { processCrashRecovery } : {}),
|
|
314
352
|
};
|
|
315
353
|
}
|
|
@@ -347,7 +385,7 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
347
385
|
if (!client) {
|
|
348
386
|
return {
|
|
349
387
|
kind: "unavailable",
|
|
350
|
-
reason:
|
|
388
|
+
reason: this.fileUnavailableReason(resolvedPath, FILE_STARTUP_FAILURE_REASON),
|
|
351
389
|
...startup,
|
|
352
390
|
...recovery,
|
|
353
391
|
};
|
|
@@ -355,6 +393,15 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
355
393
|
return { kind: "ready", ...startup, ...recovery };
|
|
356
394
|
}
|
|
357
395
|
|
|
396
|
+
private fileUnavailableReason(filePath: string, configuredRouteReason: string): string {
|
|
397
|
+
if (!this.manager.hasConfiguredServerForFile(filePath)) {
|
|
398
|
+
return "No compatible LSP server is configured for this file";
|
|
399
|
+
}
|
|
400
|
+
return this.manager.hasFailedStartupForFile(filePath)
|
|
401
|
+
? FILE_STARTUP_FAILURE_REASON
|
|
402
|
+
: configuredRouteReason;
|
|
403
|
+
}
|
|
404
|
+
|
|
358
405
|
/**
|
|
359
406
|
* Wait until all started LSP clients are ready for semantic queries.
|
|
360
407
|
* Performs one representative warm-up probe per client/root.
|
|
@@ -588,6 +635,21 @@ export async function waitForWorkspaceLspRuntime(
|
|
|
588
635
|
return state;
|
|
589
636
|
}
|
|
590
637
|
|
|
638
|
+
/** Local file paths of LSP locations or location links; non-file URIs are skipped. */
|
|
639
|
+
function locationFiles(data: Location[] | Location | LocationLink[] | null): string[] {
|
|
640
|
+
const items: (Location | LocationLink)[] =
|
|
641
|
+
data === null ? [] : Array.isArray(data) ? data : [data];
|
|
642
|
+
return items.flatMap((item) => {
|
|
643
|
+
const uri = "targetUri" in item ? item.targetUri : item.uri;
|
|
644
|
+
if (!uri.startsWith("file:")) return [];
|
|
645
|
+
try {
|
|
646
|
+
return [fileURLToPath(uri)];
|
|
647
|
+
} catch {
|
|
648
|
+
return [];
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
|
|
591
653
|
/** Remove the LSP service state for a session cwd. */
|
|
592
654
|
export function clearWorkspaceLspRuntime(cwd: string): void {
|
|
593
655
|
registry.clear(cwd);
|
|
@@ -21,6 +21,7 @@ export async function runRoutedRename(options: {
|
|
|
21
21
|
const { manager, file, position, newName, control } = options;
|
|
22
22
|
const client = await manager.ensureFileOpen(file, {
|
|
23
23
|
recoverProcessCrash: true,
|
|
24
|
+
protectInputs: true,
|
|
24
25
|
control,
|
|
25
26
|
});
|
|
26
27
|
if (!client) return null;
|
|
@@ -143,7 +143,11 @@ export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
|
|
|
143
143
|
filePath: string,
|
|
144
144
|
control?: CodeRequestControl,
|
|
145
145
|
): Promise<CodeQueryResult<DocumentSymbol[] | SymbolInformation[]>>;
|
|
146
|
-
/**
|
|
146
|
+
/**
|
|
147
|
+
* Collect symbols from contributing routes; completeness is relative to scopes.
|
|
148
|
+
* Data may include out-of-scope rows, which callers must filter themselves.
|
|
149
|
+
* Only policy exclusions proven outside every scope are irrelevant to completeness.
|
|
150
|
+
*/
|
|
147
151
|
workspaceSymbol(
|
|
148
152
|
query: string,
|
|
149
153
|
control?: CodeRequestControl,
|