@mrclrchtr/supi-lsp 11.1.0 → 11.3.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 +81 -7
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +9 -0
- 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/LICENSE +15 -0
- package/node_modules/semver/README.md +680 -0
- package/node_modules/semver/bin/semver.js +195 -0
- package/node_modules/semver/classes/comparator.js +143 -0
- package/node_modules/semver/classes/index.js +7 -0
- package/node_modules/semver/classes/range.js +577 -0
- package/node_modules/semver/classes/semver.js +350 -0
- package/node_modules/semver/functions/clean.js +8 -0
- package/node_modules/semver/functions/cmp.js +54 -0
- package/node_modules/semver/functions/coerce.js +62 -0
- package/node_modules/semver/functions/compare-build.js +9 -0
- package/node_modules/semver/functions/compare-loose.js +5 -0
- package/node_modules/semver/functions/compare.js +7 -0
- package/node_modules/semver/functions/diff.js +60 -0
- package/node_modules/semver/functions/eq.js +5 -0
- package/node_modules/semver/functions/gt.js +5 -0
- package/node_modules/semver/functions/gte.js +5 -0
- package/node_modules/semver/functions/inc.js +21 -0
- package/node_modules/semver/functions/lt.js +5 -0
- package/node_modules/semver/functions/lte.js +5 -0
- package/node_modules/semver/functions/major.js +5 -0
- package/node_modules/semver/functions/minor.js +5 -0
- package/node_modules/semver/functions/neq.js +5 -0
- package/node_modules/semver/functions/parse.js +18 -0
- package/node_modules/semver/functions/patch.js +5 -0
- package/node_modules/semver/functions/prerelease.js +8 -0
- package/node_modules/semver/functions/rcompare.js +5 -0
- package/node_modules/semver/functions/rsort.js +5 -0
- package/node_modules/semver/functions/satisfies.js +12 -0
- package/node_modules/semver/functions/sort.js +5 -0
- package/node_modules/semver/functions/truncate.js +48 -0
- package/node_modules/semver/functions/valid.js +8 -0
- package/node_modules/semver/index.js +93 -0
- package/node_modules/semver/internal/constants.js +37 -0
- package/node_modules/semver/internal/debug.js +11 -0
- package/node_modules/semver/internal/identifiers.js +29 -0
- package/node_modules/semver/internal/lrucache.js +42 -0
- package/node_modules/semver/internal/parse-options.js +17 -0
- package/node_modules/semver/internal/re.js +223 -0
- package/node_modules/semver/package.json +78 -0
- package/node_modules/semver/preload.js +4 -0
- package/node_modules/semver/range.bnf +17 -0
- package/node_modules/semver/ranges/gtr.js +6 -0
- package/node_modules/semver/ranges/intersects.js +9 -0
- package/node_modules/semver/ranges/ltr.js +6 -0
- package/node_modules/semver/ranges/max-satisfying.js +27 -0
- package/node_modules/semver/ranges/min-satisfying.js +26 -0
- package/node_modules/semver/ranges/min-version.js +63 -0
- package/node_modules/semver/ranges/outside.js +82 -0
- package/node_modules/semver/ranges/simplify.js +49 -0
- package/node_modules/semver/ranges/subset.js +249 -0
- package/node_modules/semver/ranges/to-comparators.js +10 -0
- package/node_modules/semver/ranges/valid.js +13 -0
- package/package.json +5 -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 +174 -49
- package/src/config/config.ts +53 -2
- package/src/config/operation-support.ts +19 -4
- package/src/config/server-config.ts +22 -0
- package/src/config/typescript-launch.ts +276 -0
- package/src/diagnostics/evidence.ts +2 -2
- package/src/manager/manager-project-info.ts +22 -10
- package/src/manager/manager-workspace-symbol.ts +157 -23
- package/src/manager/manager.ts +306 -75
- package/src/provider/refactor-planning.ts +4 -3
- package/src/session/runtime-controller.ts +2 -2
- package/src/session/runtime-registry.ts +19 -2
- package/src/session/runtime-rename.ts +1 -0
- package/src/session/scanner.ts +27 -15
- package/src/session/workspace-lsp-runtime.ts +5 -1
package/src/manager/manager.ts
CHANGED
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
withTimeout,
|
|
20
20
|
} from "../client/client.ts";
|
|
21
21
|
import type { ClientDiagnosticSnapshot } from "../client/client-document-state.ts";
|
|
22
|
+
import type { ProjectMembership } from "../client/client-typescript-project.ts";
|
|
22
23
|
|
|
23
24
|
export { RECOVERY_CLIENT_STARTUP_BOUND_MS } from "../client/client.ts";
|
|
24
25
|
|
|
@@ -48,6 +49,7 @@ import type {
|
|
|
48
49
|
WorkspaceSymbol,
|
|
49
50
|
} from "../config/types.ts";
|
|
50
51
|
import { FileChangeType } from "../config/types.ts";
|
|
52
|
+
import { inspectTypeScriptLaunch } from "../config/typescript-launch.ts";
|
|
51
53
|
import { boundCwd } from "../debug-telemetry.ts";
|
|
52
54
|
import {
|
|
53
55
|
accumulateOutstandingDiagnostics,
|
|
@@ -134,8 +136,10 @@ import {
|
|
|
134
136
|
collectWorkspaceSymbols,
|
|
135
137
|
findWorkspaceSymbolWarmTargets,
|
|
136
138
|
getWorkspaceSymbolWarmPosition,
|
|
139
|
+
isWithinAnyScope,
|
|
137
140
|
type WorkspaceSymbolCollection,
|
|
138
141
|
workspaceSymbolCollectionResult,
|
|
142
|
+
workspaceSymbolCoverageGap,
|
|
139
143
|
} from "./manager-workspace-symbol.ts";
|
|
140
144
|
|
|
141
145
|
type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
@@ -250,6 +254,8 @@ export interface ManagerLifecycleTransition {
|
|
|
250
254
|
type ManagerLifecycleListener = (transition: ManagerLifecycleTransition) => void;
|
|
251
255
|
|
|
252
256
|
type FileClientRequestOptions = {
|
|
257
|
+
/** Reject known input changes while this call's first open waits to dispatch original parameters. */
|
|
258
|
+
protectInputs?: boolean;
|
|
253
259
|
/** Allow this request to start the route's one process-crash recovery. */
|
|
254
260
|
recoverProcessCrash?: boolean;
|
|
255
261
|
/** Control only this caller's wait for a shared replacement. */
|
|
@@ -396,8 +402,12 @@ export class LspManager {
|
|
|
396
402
|
private invalidationEpoch = 0;
|
|
397
403
|
/** Invalidation generation in which each route last received a recovery restart. */
|
|
398
404
|
private recoveryRestartEpochs = new Map<string, number>();
|
|
399
|
-
/**
|
|
400
|
-
|
|
405
|
+
/**
|
|
406
|
+
* Files already probed for workspace-symbol warm-up, with the project
|
|
407
|
+
* membership the server reported. `inferred` probes stay recorded so they
|
|
408
|
+
* are not reopened, but they do not prove project coverage.
|
|
409
|
+
*/
|
|
410
|
+
private warmedWorkspaceSymbolFiles = new Map<string, ProjectMembership>();
|
|
401
411
|
/** Project roots whose semantic state was warmed with a readiness probe. */
|
|
402
412
|
private warmedSemanticProjects = new Set<string>();
|
|
403
413
|
/** In-flight warm-up probes keyed by project key so concurrent callers share one probe. */
|
|
@@ -433,12 +443,29 @@ export class LspManager {
|
|
|
433
443
|
detected.filter((entry) => this.automaticPathPolicy.isEligible(entry.root, "directory")),
|
|
434
444
|
);
|
|
435
445
|
}
|
|
446
|
+
/** Check configured routing only, independently of executable availability or startup state. */
|
|
447
|
+
hasConfiguredServerForFile(filePath: string): boolean {
|
|
448
|
+
return getServerForFile(this.config, resolveSessionPath(this.cwd, filePath)) !== null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/** Observe a retained initial-start failure without retrying or conflating process-crash recovery. */
|
|
452
|
+
hasFailedStartupForFile(filePath: string): boolean {
|
|
453
|
+
const route = this.resolveFileRoute(filePath);
|
|
454
|
+
if (!route || this.processCrashRecoveries.has(route.key)) return false;
|
|
455
|
+
const reason = this.unavailable.get(route.key);
|
|
456
|
+
return reason === "start-failed" || reason === "missing-command";
|
|
457
|
+
}
|
|
458
|
+
|
|
436
459
|
/** Check whether a file path has an available LSP server for explicit semantic operations. */
|
|
437
460
|
canServeFile(filePath: string): boolean {
|
|
438
461
|
const route = this.resolveFileRoute(filePath);
|
|
439
462
|
if (!route) return false;
|
|
440
|
-
if (this.getUnavailableReason(route.key, route.
|
|
441
|
-
|
|
463
|
+
if (this.getUnavailableReason(route.key, route.serverName, route.root)) return false;
|
|
464
|
+
if (this.clients.get(route.key)?.status === "running") return true;
|
|
465
|
+
const launch = inspectTypeScriptLaunch(route.serverName, route.serverConfig, route.root);
|
|
466
|
+
// A broken selection is a route failure, not an absent executable. Let
|
|
467
|
+
// startup record it so lazy readiness and health expose the actual error.
|
|
468
|
+
return Boolean(launch.failureMessage) || this.isServerCommandAvailable(launch.command);
|
|
442
469
|
}
|
|
443
470
|
|
|
444
471
|
/**
|
|
@@ -517,13 +544,21 @@ export class LspManager {
|
|
|
517
544
|
return available;
|
|
518
545
|
}
|
|
519
546
|
|
|
520
|
-
private getUnavailableReason(
|
|
547
|
+
private getUnavailableReason(
|
|
548
|
+
key: string,
|
|
549
|
+
serverName: string,
|
|
550
|
+
root: string,
|
|
551
|
+
): UnavailableReason | null {
|
|
521
552
|
const reason = this.unavailable.get(key);
|
|
522
553
|
if (!reason) return null;
|
|
523
554
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
555
|
+
const config = this.config.servers[serverName];
|
|
556
|
+
if (reason === "missing-command" && config) {
|
|
557
|
+
const launch = inspectTypeScriptLaunch(serverName, config, root);
|
|
558
|
+
if (!launch.failureMessage && this.isServerCommandAvailable(launch.command)) {
|
|
559
|
+
this.unavailable.delete(key);
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
527
562
|
}
|
|
528
563
|
|
|
529
564
|
return reason;
|
|
@@ -573,7 +608,7 @@ export class LspManager {
|
|
|
573
608
|
if (this.shouldBlockRetiredRoute(key, root, retryFailedStart)) return null;
|
|
574
609
|
const routeGeneration = this.getRouteRetirementGeneration(key);
|
|
575
610
|
if (retryFailedStart) this.unavailable.delete(key);
|
|
576
|
-
if (!retryFailedStart && this.getUnavailableReason(key,
|
|
611
|
+
if (!retryFailedStart && this.getUnavailableReason(key, serverName, root)) return null;
|
|
577
612
|
|
|
578
613
|
// Return existing client
|
|
579
614
|
const existing = this.clients.get(key);
|
|
@@ -1301,7 +1336,13 @@ export class LspManager {
|
|
|
1301
1336
|
recovery.statusReason = undefined;
|
|
1302
1337
|
recovery.failureMessage = undefined;
|
|
1303
1338
|
this.publishLifecycle("recovery");
|
|
1304
|
-
this.restoreTrackedDocuments(started, recovery.files);
|
|
1339
|
+
await this.restoreTrackedDocuments(started, recovery.files);
|
|
1340
|
+
if (
|
|
1341
|
+
this.isProcessCrashRecoveryCancelled(recovery, key) ||
|
|
1342
|
+
this.clients.get(key) !== started ||
|
|
1343
|
+
started.status !== "running"
|
|
1344
|
+
)
|
|
1345
|
+
return null;
|
|
1305
1346
|
this.recordProcessCrashRecoveryEvent(
|
|
1306
1347
|
route.serverName,
|
|
1307
1348
|
route.root,
|
|
@@ -1386,20 +1427,16 @@ export class LspManager {
|
|
|
1386
1427
|
}
|
|
1387
1428
|
|
|
1388
1429
|
/** Restore route documents from disk and retain failed-document evidence. */
|
|
1389
|
-
private restoreTrackedDocuments(
|
|
1390
|
-
|
|
1430
|
+
private async restoreTrackedDocuments(
|
|
1431
|
+
client: LspClient,
|
|
1432
|
+
files: readonly string[],
|
|
1433
|
+
): Promise<void> {
|
|
1434
|
+
const key = clientKey(client.name, client.root);
|
|
1435
|
+
const isCurrent = () => !this.shuttingDown && this.clients.get(key) === client;
|
|
1391
1436
|
for (const filePath of files) {
|
|
1392
|
-
if (!
|
|
1393
|
-
|
|
1394
|
-
continue;
|
|
1395
|
-
}
|
|
1396
|
-
try {
|
|
1397
|
-
client.didOpen(filePath, fs.readFileSync(filePath, "utf-8"));
|
|
1398
|
-
} catch {
|
|
1399
|
-
failedFiles.push(filePath);
|
|
1400
|
-
}
|
|
1437
|
+
if (!isCurrent()) return;
|
|
1438
|
+
await client.restoreDocument(filePath, () => fs.readFileSync(filePath, "utf-8"), isCurrent);
|
|
1401
1439
|
}
|
|
1402
|
-
for (const filePath of failedFiles) client.markFailedFile(filePath);
|
|
1403
1440
|
}
|
|
1404
1441
|
|
|
1405
1442
|
private markProcessCrashRecoveryFailed(
|
|
@@ -1460,22 +1497,30 @@ export class LspManager {
|
|
|
1460
1497
|
}
|
|
1461
1498
|
}
|
|
1462
1499
|
|
|
1500
|
+
/** Record a root-aware selection/command failure without starting a client. */
|
|
1501
|
+
private recordUnavailableLaunch(route: FileRoute): boolean {
|
|
1502
|
+
const { serverName, serverConfig, root, key } = route;
|
|
1503
|
+
const launch = inspectTypeScriptLaunch(serverName, serverConfig, root);
|
|
1504
|
+
const command = launch.command;
|
|
1505
|
+
if (!launch.failureMessage && commandExists(command)) return false;
|
|
1506
|
+
this.unavailable.set(key, launch.failureMessage ? "start-failed" : "missing-command");
|
|
1507
|
+
this.failedStarts.set(key, {
|
|
1508
|
+
serverName,
|
|
1509
|
+
root,
|
|
1510
|
+
failureMessage:
|
|
1511
|
+
launch.failureMessage ?? `Configured server command is not available: ${command}`,
|
|
1512
|
+
});
|
|
1513
|
+
return true;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1463
1516
|
/**
|
|
1464
1517
|
* Perform the actual server start — extracted so the public method can
|
|
1465
1518
|
* deduplicate via pendingStarts without wrapping the entire body.
|
|
1466
1519
|
*/
|
|
1467
1520
|
private async performStart(route: FileRoute, routeGeneration: number): Promise<LspClient | null> {
|
|
1468
1521
|
const { serverName, serverConfig, root, key } = route;
|
|
1469
|
-
//
|
|
1470
|
-
if (
|
|
1471
|
-
this.unavailable.set(key, "missing-command");
|
|
1472
|
-
this.failedStarts.set(key, {
|
|
1473
|
-
serverName,
|
|
1474
|
-
root,
|
|
1475
|
-
failureMessage: `Configured server command is not available: ${serverConfig.command}`,
|
|
1476
|
-
});
|
|
1477
|
-
return null;
|
|
1478
|
-
}
|
|
1522
|
+
// Client.start resolves again at actual spawn time, including every retry.
|
|
1523
|
+
if (this.recordUnavailableLaunch(route)) return null;
|
|
1479
1524
|
|
|
1480
1525
|
// Spawn new client
|
|
1481
1526
|
const client = this.createClient(serverName, serverConfig, root, key);
|
|
@@ -1635,7 +1680,15 @@ export class LspManager {
|
|
|
1635
1680
|
void replacement.forceKill().catch(() => {});
|
|
1636
1681
|
return { files, restarted: false };
|
|
1637
1682
|
}
|
|
1638
|
-
this.restoreTrackedDocuments(replacement, files);
|
|
1683
|
+
await this.restoreTrackedDocuments(replacement, files);
|
|
1684
|
+
if (
|
|
1685
|
+
this.clients.get(key) !== replacement ||
|
|
1686
|
+
this.shuttingDown ||
|
|
1687
|
+
replacement.status !== "running"
|
|
1688
|
+
) {
|
|
1689
|
+
void replacement.forceKill().catch(() => {});
|
|
1690
|
+
return { files, restarted: false };
|
|
1691
|
+
}
|
|
1639
1692
|
this.clearUnconsumedProcessCrashState(key, client);
|
|
1640
1693
|
return { files, restarted: true };
|
|
1641
1694
|
} catch {
|
|
@@ -1659,14 +1712,26 @@ export class LspManager {
|
|
|
1659
1712
|
|
|
1660
1713
|
getProjectServerInfo(serverName: string, root: string, fileTypes: string[]): ProjectServerInfo {
|
|
1661
1714
|
const key = clientKey(serverName, root);
|
|
1715
|
+
const client = this.clients.get(key);
|
|
1716
|
+
const config = this.config.servers[serverName];
|
|
1717
|
+
const selection: Pick<DetectedProjectServer, "typescript" | "failureMessage"> =
|
|
1718
|
+
client?.status === "running"
|
|
1719
|
+
? { typescript: client.typescriptSelection }
|
|
1720
|
+
: config
|
|
1721
|
+
? inspectTypeScriptLaunch(serverName, config, root)
|
|
1722
|
+
: {};
|
|
1662
1723
|
return buildProjectServerInfo(
|
|
1663
1724
|
{
|
|
1725
|
+
...selection,
|
|
1726
|
+
failureMessage:
|
|
1727
|
+
this.failedStarts.get(key)?.failureMessage ??
|
|
1728
|
+
this.processCrashRecoveries.get(key)?.failureMessage ??
|
|
1729
|
+
selection.failureMessage,
|
|
1664
1730
|
serverName,
|
|
1665
1731
|
root,
|
|
1666
1732
|
fileTypes,
|
|
1667
1733
|
client: this.clients.get(key),
|
|
1668
|
-
unavailableReason:
|
|
1669
|
-
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
1734
|
+
unavailableReason: this.getUnavailableReason(key, serverName, root) ?? undefined,
|
|
1670
1735
|
statusReason: this.processCrashRecoveries.get(key)?.statusReason,
|
|
1671
1736
|
includeOpenFile: (file) => this.isAutomaticScopePath(file),
|
|
1672
1737
|
},
|
|
@@ -1733,7 +1798,9 @@ export class LspManager {
|
|
|
1733
1798
|
if (options.retryFailedRoute) {
|
|
1734
1799
|
this.retireMissingProjectRoots();
|
|
1735
1800
|
const route = this.resolveFileRoute(resolvedPath);
|
|
1736
|
-
|
|
1801
|
+
// Exact-file demand, including refresh, bypasses automatic discovery exclusions.
|
|
1802
|
+
// Keep the original route identity for document synchronization and bounded retries.
|
|
1803
|
+
if (!route) {
|
|
1737
1804
|
return {
|
|
1738
1805
|
client: null,
|
|
1739
1806
|
processCrashRecovery: emptyProcessCrashRecoveryReport(),
|
|
@@ -2017,13 +2084,19 @@ export class LspManager {
|
|
|
2017
2084
|
|
|
2018
2085
|
const serverConfig = this.config.servers[firstReady.name];
|
|
2019
2086
|
if (serverConfig) {
|
|
2020
|
-
|
|
2087
|
+
// Candidates for the best-ranked marker root; later ones replace a probe
|
|
2088
|
+
// that lands in an inferred project outside the configured program.
|
|
2089
|
+
const targets = findWorkspaceSymbolWarmTargets(
|
|
2021
2090
|
firstReady.root,
|
|
2022
2091
|
serverConfig.rootMarkers,
|
|
2023
2092
|
serverConfig.fileTypes,
|
|
2024
2093
|
{ policy: this.automaticPathPolicy },
|
|
2025
|
-
)
|
|
2026
|
-
|
|
2094
|
+
);
|
|
2095
|
+
const primaryRoot = targets[0]?.projectRoot;
|
|
2096
|
+
const files = targets
|
|
2097
|
+
.filter((target) => target.projectRoot === primaryRoot)
|
|
2098
|
+
.map((target) => target.file);
|
|
2099
|
+
if (files.length > 0) await this.warmSemanticProject(firstReady, files, false, control);
|
|
2027
2100
|
// The warm-up request can start server indexing after the initial
|
|
2028
2101
|
// readiness wait resolves. Observe the resulting progress before
|
|
2029
2102
|
// reporting the workspace as ready.
|
|
@@ -2866,7 +2939,20 @@ export class LspManager {
|
|
|
2866
2939
|
return this.filterWorkspaceSymbolCollection(collection, resolvedScopes);
|
|
2867
2940
|
};
|
|
2868
2941
|
const initial = await collect(this.clients.values(), query);
|
|
2869
|
-
|
|
2942
|
+
// Allowed out-of-scope rows do not show that the server knows the scope yet.
|
|
2943
|
+
const hasScopedResult = (collection: WorkspaceSymbolCollection) =>
|
|
2944
|
+
collection.hasSupport &&
|
|
2945
|
+
collection.results.some(
|
|
2946
|
+
(result) =>
|
|
2947
|
+
!resolvedScopes?.length ||
|
|
2948
|
+
isWithinAnyScope(uriToFile(result.location.uri), resolvedScopes),
|
|
2949
|
+
);
|
|
2950
|
+
// Rows from a route whose warm-up reached only inferred projects may come
|
|
2951
|
+
// from those files alone, so they cannot end the search early.
|
|
2952
|
+
if (
|
|
2953
|
+
!initial.hasSupport ||
|
|
2954
|
+
(hasScopedResult(initial) && !this.hasInferredOnlyWarmRoute(routeMatches))
|
|
2955
|
+
) {
|
|
2870
2956
|
return workspaceSymbolCollectionResult(initial);
|
|
2871
2957
|
}
|
|
2872
2958
|
|
|
@@ -2878,11 +2964,32 @@ export class LspManager {
|
|
|
2878
2964
|
}),
|
|
2879
2965
|
getWarmPosition: getWorkspaceSymbolWarmPosition,
|
|
2880
2966
|
collect,
|
|
2967
|
+
hasResult: hasScopedResult,
|
|
2881
2968
|
routeMatches,
|
|
2882
2969
|
query,
|
|
2883
2970
|
control,
|
|
2884
2971
|
});
|
|
2885
|
-
|
|
2972
|
+
const collection = warmed.collection ?? initial;
|
|
2973
|
+
return workspaceSymbolCollectionResult(
|
|
2974
|
+
warmed.coverageConfirmed
|
|
2975
|
+
? collection
|
|
2976
|
+
: workspaceSymbolCoverageGap(collection, warmed.inferredProbeCount),
|
|
2977
|
+
);
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2980
|
+
/** True when a matching route has inferred warm-up probes and no other probe. */
|
|
2981
|
+
private hasInferredOnlyWarmRoute(routeMatches: (client: LspClient) => boolean): boolean {
|
|
2982
|
+
return Array.from(this.clients.values()).some((client) => {
|
|
2983
|
+
if (!routeMatches(client)) return false;
|
|
2984
|
+
const prefix = `${client.name}:${path.resolve(client.root)}`;
|
|
2985
|
+
let inferred = false;
|
|
2986
|
+
for (const [key, membership] of this.warmedWorkspaceSymbolFiles) {
|
|
2987
|
+
if (key !== prefix && !key.startsWith(`${prefix}${path.sep}`)) continue;
|
|
2988
|
+
if (membership !== "inferred") return false;
|
|
2989
|
+
inferred = true;
|
|
2990
|
+
}
|
|
2991
|
+
return inferred;
|
|
2992
|
+
});
|
|
2886
2993
|
}
|
|
2887
2994
|
|
|
2888
2995
|
private addWorkspaceSymbolDemandEvidence(
|
|
@@ -2905,6 +3012,7 @@ export class LspManager {
|
|
|
2905
3012
|
const results = collection.results.filter((result) => {
|
|
2906
3013
|
const file = uriToFile(result.location.uri);
|
|
2907
3014
|
if (this.isWorkspaceSymbolResultAllowed(file, scopes)) return true;
|
|
3015
|
+
if (!this.isWorkspaceSymbolExclusionRelevant(file, scopes)) return false;
|
|
2908
3016
|
excludedCount++;
|
|
2909
3017
|
const example = displayRelativeFilePath(file, this.cwd);
|
|
2910
3018
|
if (excludedExamples.size < 5) excludedExamples.add(example);
|
|
@@ -2918,6 +3026,26 @@ export class LspManager {
|
|
|
2918
3026
|
};
|
|
2919
3027
|
}
|
|
2920
3028
|
|
|
3029
|
+
/** Only proven-disjoint exclusions can be omitted from scoped completeness. */
|
|
3030
|
+
private isWorkspaceSymbolExclusionRelevant(
|
|
3031
|
+
filePath: string,
|
|
3032
|
+
scopes: readonly string[] | undefined,
|
|
3033
|
+
): boolean {
|
|
3034
|
+
if (!scopes?.length || !path.isAbsolute(filePath)) return true;
|
|
3035
|
+
if (scopes.some((scope) => projectRoots.isWithinOrEqual(scope, filePath))) return true;
|
|
3036
|
+
try {
|
|
3037
|
+
// Observed spellings and physical identities both matter: an excluded
|
|
3038
|
+
// alias may point into scope even when its spelling is outside it.
|
|
3039
|
+
const physicalFile = fs.realpathSync(filePath);
|
|
3040
|
+
return scopes.some((scope) =>
|
|
3041
|
+
projectRoots.isWithinOrEqual(fs.realpathSync(scope), physicalFile),
|
|
3042
|
+
);
|
|
3043
|
+
} catch {
|
|
3044
|
+
// Missing, unreadable or otherwise unresolved paths cannot prove absence.
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
}
|
|
3048
|
+
|
|
2921
3049
|
/** Keep explicitly selected excluded scopes inspectable while filtering broad results. */
|
|
2922
3050
|
private isWorkspaceSymbolResultAllowed(
|
|
2923
3051
|
filePath: string,
|
|
@@ -3012,15 +3140,22 @@ export class LspManager {
|
|
|
3012
3140
|
const client = await this.getClientForFile(resolvedPath, options);
|
|
3013
3141
|
if (!client) return null;
|
|
3014
3142
|
try {
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3143
|
+
await client.openDocument(
|
|
3144
|
+
resolvedPath,
|
|
3145
|
+
() => {
|
|
3146
|
+
try {
|
|
3147
|
+
return fs.readFileSync(resolvedPath, "utf-8");
|
|
3148
|
+
} catch (error) {
|
|
3149
|
+
this.closeFile(resolvedPath);
|
|
3150
|
+
throw error;
|
|
3151
|
+
}
|
|
3152
|
+
},
|
|
3153
|
+
options.control,
|
|
3154
|
+
options.protectInputs,
|
|
3155
|
+
);
|
|
3020
3156
|
return client;
|
|
3021
3157
|
} catch (error) {
|
|
3022
3158
|
if (isCodeRequestInterruption(error, options.control)) throw error;
|
|
3023
|
-
this.closeFile(resolvedPath);
|
|
3024
3159
|
return null;
|
|
3025
3160
|
}
|
|
3026
3161
|
}
|
|
@@ -3036,13 +3171,29 @@ export class LspManager {
|
|
|
3036
3171
|
symbols: import("../config/types.ts").DocumentSymbol[] | SymbolInformation[] | null,
|
|
3037
3172
|
) => import("../config/types.ts").Position | null;
|
|
3038
3173
|
collect: (clients: Iterable<LspClient>, query: string) => Promise<WorkspaceSymbolCollection>;
|
|
3174
|
+
/** Stop warming once a collection contains a row that answers the request. */
|
|
3175
|
+
hasResult: (collection: WorkspaceSymbolCollection) => boolean;
|
|
3039
3176
|
routeMatches?: (client: LspClient) => boolean;
|
|
3040
3177
|
query: string;
|
|
3041
3178
|
control?: CodeRequestControl;
|
|
3042
|
-
}): Promise<{
|
|
3043
|
-
|
|
3044
|
-
|
|
3179
|
+
}): Promise<{
|
|
3180
|
+
collection: WorkspaceSymbolCollection | null;
|
|
3181
|
+
/** False only when every probe the server classified landed in an inferred project. */
|
|
3182
|
+
coverageConfirmed: boolean;
|
|
3183
|
+
inferredProbeCount: number;
|
|
3184
|
+
}> {
|
|
3185
|
+
const { findWarmTargets, getWarmPosition, collect, hasResult, routeMatches, query, control } =
|
|
3186
|
+
options;
|
|
3045
3187
|
let latestCollection: WorkspaceSymbolCollection | null = null;
|
|
3188
|
+
// A route with no inferred probe keeps today's evidence: non-TypeScript
|
|
3189
|
+
// servers index the workspace, and `unknown` cannot prove a gap.
|
|
3190
|
+
const routeCoverage: boolean[] = [];
|
|
3191
|
+
let inferredProbeCount = 0;
|
|
3192
|
+
const outcome = () => ({
|
|
3193
|
+
collection: latestCollection,
|
|
3194
|
+
coverageConfirmed: routeCoverage.every(Boolean),
|
|
3195
|
+
inferredProbeCount,
|
|
3196
|
+
});
|
|
3046
3197
|
|
|
3047
3198
|
for (const client of Array.from(this.clients.values())) {
|
|
3048
3199
|
if (routeMatches && !routeMatches(client)) continue;
|
|
@@ -3058,13 +3209,29 @@ export class LspManager {
|
|
|
3058
3209
|
serverConfig.fileTypes,
|
|
3059
3210
|
).slice(0, 24);
|
|
3060
3211
|
|
|
3212
|
+
// Marker roots whose program an earlier candidate already loaded.
|
|
3213
|
+
const coveredRoots = new Set<string>();
|
|
3214
|
+
let routeInferred = false;
|
|
3061
3215
|
for (const target of warmTargets) {
|
|
3216
|
+
if (coveredRoots.has(target.projectRoot)) continue;
|
|
3062
3217
|
// A file probe does not load every program under the route root.
|
|
3063
3218
|
const fileKey = `${client.name}:${path.resolve(target.file)}`;
|
|
3064
|
-
|
|
3219
|
+
const previous = this.warmedWorkspaceSymbolFiles.get(fileKey);
|
|
3220
|
+
if (previous === "inferred") {
|
|
3221
|
+
routeInferred = true;
|
|
3222
|
+
inferredProbeCount++;
|
|
3223
|
+
continue;
|
|
3224
|
+
}
|
|
3225
|
+
if (previous !== undefined) {
|
|
3226
|
+
coveredRoots.add(target.projectRoot);
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3065
3229
|
|
|
3066
|
-
const openedClient = await this.ensureFileOpen(target.file);
|
|
3067
|
-
if (!openedClient)
|
|
3230
|
+
const openedClient = await this.ensureFileOpen(target.file, { control });
|
|
3231
|
+
if (!openedClient) {
|
|
3232
|
+
this.recordWarmProbeEvent(client.name, target.file, "open-failed", control);
|
|
3233
|
+
continue;
|
|
3234
|
+
}
|
|
3068
3235
|
|
|
3069
3236
|
// A cancelled pass must not mark projects warmed on its way out.
|
|
3070
3237
|
throwIfCodeRequestInterrupted(control);
|
|
@@ -3074,32 +3241,84 @@ export class LspManager {
|
|
|
3074
3241
|
getWarmPosition,
|
|
3075
3242
|
control,
|
|
3076
3243
|
);
|
|
3077
|
-
if (available) {
|
|
3078
|
-
this.
|
|
3079
|
-
|
|
3244
|
+
if (!available) {
|
|
3245
|
+
this.recordWarmProbeEvent(client.name, target.file, "probe-unavailable", control);
|
|
3246
|
+
continue;
|
|
3247
|
+
}
|
|
3248
|
+
const membership = await openedClient.projectMembership(target.file, control);
|
|
3249
|
+
throwIfCodeRequestInterrupted(control);
|
|
3250
|
+
this.warmedWorkspaceSymbolFiles.set(fileKey, membership);
|
|
3251
|
+
this.recordWarmProbeEvent(client.name, target.file, membership, control);
|
|
3252
|
+
if (membership === "inferred") {
|
|
3253
|
+
// The file loaded only its own inferred project; workspace/symbol
|
|
3254
|
+
// still cannot see the configured program, so try the next file
|
|
3255
|
+
// without collecting: a hit here could come from that inferred
|
|
3256
|
+
// project alone and would hide the configured program's rows.
|
|
3257
|
+
routeInferred = true;
|
|
3258
|
+
inferredProbeCount++;
|
|
3259
|
+
continue;
|
|
3080
3260
|
}
|
|
3081
3261
|
|
|
3262
|
+
coveredRoots.add(target.projectRoot);
|
|
3082
3263
|
latestCollection = await collect(this.clients.values(), query);
|
|
3083
|
-
if (latestCollection
|
|
3084
|
-
return {
|
|
3264
|
+
if (hasResult(latestCollection)) {
|
|
3265
|
+
return { ...outcome(), coverageConfirmed: true };
|
|
3085
3266
|
}
|
|
3086
3267
|
}
|
|
3268
|
+
routeCoverage.push(coveredRoots.size > 0 || !routeInferred);
|
|
3087
3269
|
}
|
|
3088
3270
|
|
|
3271
|
+
if (inferredProbeCount > 0 && latestCollection === null) {
|
|
3272
|
+
latestCollection = await collect(this.clients.values(), query);
|
|
3273
|
+
}
|
|
3089
3274
|
// A completed-empty retry replaces an earlier failure too.
|
|
3090
|
-
return
|
|
3275
|
+
return outcome();
|
|
3276
|
+
}
|
|
3277
|
+
|
|
3278
|
+
/**
|
|
3279
|
+
* Record one workspace-symbol warm-up probe: which file was opened and
|
|
3280
|
+
* whether the server placed it in a configured project. `unknown` means the
|
|
3281
|
+
* route cannot report membership (non-TypeScript or native TS7).
|
|
3282
|
+
*/
|
|
3283
|
+
private recordWarmProbeEvent(
|
|
3284
|
+
serverName: string,
|
|
3285
|
+
file: string,
|
|
3286
|
+
outcome: ProjectMembership | "open-failed" | "probe-unavailable",
|
|
3287
|
+
control?: CodeRequestControl,
|
|
3288
|
+
): void {
|
|
3289
|
+
try {
|
|
3290
|
+
recordDebugEvent({
|
|
3291
|
+
operationId: control?.operationId,
|
|
3292
|
+
source: "lsp",
|
|
3293
|
+
level: "debug",
|
|
3294
|
+
category: "workspace-symbol.warm-probe",
|
|
3295
|
+
message: `Workspace-symbol warm-up probe: ${outcome}`,
|
|
3296
|
+
cwd: boundCwd(this.cwd),
|
|
3297
|
+
data: {
|
|
3298
|
+
server: truncateIdentity(serverName),
|
|
3299
|
+
file: truncateIdentity(displayRelativeFilePath(file, this.cwd)),
|
|
3300
|
+
outcome,
|
|
3301
|
+
},
|
|
3302
|
+
});
|
|
3303
|
+
} catch {
|
|
3304
|
+
// Warm-up telemetry must never change query behavior.
|
|
3305
|
+
}
|
|
3091
3306
|
}
|
|
3092
3307
|
|
|
3093
3308
|
private async warmSemanticProject(
|
|
3094
3309
|
client: LspClient,
|
|
3095
|
-
filePath: string,
|
|
3310
|
+
filePath: string | readonly string[],
|
|
3096
3311
|
preferExactFile: boolean = false,
|
|
3097
3312
|
control?: CodeRequestControl,
|
|
3098
3313
|
): Promise<void> {
|
|
3099
3314
|
const serverConfig = this.config.servers[client.name];
|
|
3100
3315
|
if (!serverConfig) return;
|
|
3101
3316
|
|
|
3102
|
-
const
|
|
3317
|
+
const candidates = (typeof filePath === "string" ? [filePath] : filePath).map((file) =>
|
|
3318
|
+
resolveSessionPath(this.cwd, file),
|
|
3319
|
+
);
|
|
3320
|
+
const resolvedFile = candidates[0];
|
|
3321
|
+
if (!resolvedFile) return;
|
|
3103
3322
|
const projectRoot = preferExactFile
|
|
3104
3323
|
? resolveRootForFile(resolvedFile, client.name, serverConfig.rootMarkers, {
|
|
3105
3324
|
knownRoots: this.knownRoots,
|
|
@@ -3117,7 +3336,7 @@ export class LspManager {
|
|
|
3117
3336
|
return;
|
|
3118
3337
|
}
|
|
3119
3338
|
|
|
3120
|
-
const probe = this.performWarmProbe(client,
|
|
3339
|
+
const probe = this.performWarmProbe(client, candidates, projectKey, control);
|
|
3121
3340
|
this.pendingWarmProbes.set(projectKey, probe);
|
|
3122
3341
|
try {
|
|
3123
3342
|
await raceRequestControl(probe, control);
|
|
@@ -3128,28 +3347,40 @@ export class LspManager {
|
|
|
3128
3347
|
}
|
|
3129
3348
|
}
|
|
3130
3349
|
|
|
3350
|
+
/**
|
|
3351
|
+
* Probe candidates in order until one loads a non-inferred project.
|
|
3352
|
+
* The project is marked warmed after any successful probe so files that
|
|
3353
|
+
* really sit in inferred projects are not reprobed on every request.
|
|
3354
|
+
*/
|
|
3131
3355
|
private async performWarmProbe(
|
|
3132
3356
|
client: LspClient,
|
|
3133
|
-
|
|
3357
|
+
candidates: readonly string[],
|
|
3134
3358
|
projectKey: string,
|
|
3135
3359
|
control?: CodeRequestControl,
|
|
3136
3360
|
): Promise<void> {
|
|
3137
3361
|
const key = clientKey(client.name, client.root);
|
|
3138
|
-
if (this.clients.get(key) !== client) return;
|
|
3139
|
-
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
3140
|
-
if (!openedClient || openedClient !== client || this.clients.get(key) !== client) return;
|
|
3141
|
-
|
|
3142
3362
|
// The probe is shared state for concurrent callers. One caller's
|
|
3143
3363
|
// cancellation must not cancel the shared in-flight queries, so only the
|
|
3144
3364
|
// Debug Operation ID is forwarded; each caller races its own wait above.
|
|
3145
3365
|
const sharedControl = { operationId: control?.operationId };
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3152
|
-
|
|
3366
|
+
let warmed = false;
|
|
3367
|
+
for (const [index, resolvedFile] of candidates.entries()) {
|
|
3368
|
+
if (this.clients.get(key) !== client) return;
|
|
3369
|
+
const openedClient = await this.ensureFileOpen(resolvedFile);
|
|
3370
|
+
if (!openedClient || openedClient !== client || this.clients.get(key) !== client) continue;
|
|
3371
|
+
const available = await this.probeDocumentSymbols(
|
|
3372
|
+
openedClient,
|
|
3373
|
+
resolvedFile,
|
|
3374
|
+
getWorkspaceSymbolWarmPosition,
|
|
3375
|
+
sharedControl,
|
|
3376
|
+
);
|
|
3377
|
+
if (!available) continue;
|
|
3378
|
+
warmed = true;
|
|
3379
|
+
if (index === candidates.length - 1) break;
|
|
3380
|
+
const membership = await openedClient.projectMembership(resolvedFile, sharedControl);
|
|
3381
|
+
if (membership !== "inferred") break;
|
|
3382
|
+
}
|
|
3383
|
+
if (warmed && this.clients.get(key) === client) {
|
|
3153
3384
|
this.warmedSemanticProjects.add(projectKey);
|
|
3154
3385
|
}
|
|
3155
3386
|
}
|
|
@@ -3180,7 +3411,7 @@ export class LspManager {
|
|
|
3180
3411
|
|
|
3181
3412
|
private clearWarmedWorkspaceSymbolFiles(serverName: string, root: string): void {
|
|
3182
3413
|
const prefix = `${serverName}:${path.resolve(root)}`;
|
|
3183
|
-
for (const key of Array.from(this.warmedWorkspaceSymbolFiles)) {
|
|
3414
|
+
for (const key of Array.from(this.warmedWorkspaceSymbolFiles.keys())) {
|
|
3184
3415
|
if (key === prefix || key.startsWith(`${prefix}${path.sep}`)) {
|
|
3185
3416
|
this.warmedWorkspaceSymbolFiles.delete(key);
|
|
3186
3417
|
}
|
|
@@ -87,8 +87,7 @@ export async function runFilteredCodeActionRefactor(options: {
|
|
|
87
87
|
if (!response || response.reason) {
|
|
88
88
|
return {
|
|
89
89
|
kind: "unavailable",
|
|
90
|
-
reason:
|
|
91
|
-
response?.reason ?? `No code actions are available for refactor operation "${operation}".`,
|
|
90
|
+
reason: `Refactor operation "${operation}" is unavailable: ${response?.reason ?? "No code actions are available."}`,
|
|
92
91
|
};
|
|
93
92
|
}
|
|
94
93
|
const actions = response.value;
|
|
@@ -136,7 +135,9 @@ export function isUpdateImportsCodeAction(action: CodeAction): boolean {
|
|
|
136
135
|
|
|
137
136
|
export function isDeleteDeadCodeCodeAction(action: CodeAction): boolean {
|
|
138
137
|
const kind = typeof action?.kind === "string" ? action.kind : "";
|
|
139
|
-
return
|
|
138
|
+
return codeActionKindsForOperation("delete_dead_code").some(
|
|
139
|
+
(requested) => kind === requested || kind.startsWith(`${requested}.`),
|
|
140
|
+
);
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
interface PreciseCodeActionPlan {
|
|
@@ -519,8 +519,8 @@ export class LspRuntimeController {
|
|
|
519
519
|
return this.#configurationError;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
/**
|
|
523
|
-
getMissingServers():
|
|
522
|
+
/** Report missing executables and root-specific compiler selection failures. */
|
|
523
|
+
getMissingServers(): ReturnType<typeof scanMissingServers> {
|
|
524
524
|
if (this.#state.kind !== "ready") return [];
|
|
525
525
|
return scanMissingServers(
|
|
526
526
|
this.#state.config,
|