@mrclrchtr/supi-code-intelligence 6.3.0 → 6.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/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +4 -32
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sources.ts +108 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +165 -14
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +17 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +32 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/package.json +5 -5
- package/src/session/health-refresh.ts +15 -10
- package/src/session/health-types.ts +5 -0
- package/src/session/health-workflow.ts +7 -3
- package/src/session/session.ts +12 -5
- package/src/substrate/lsp/maintenance.ts +53 -47
- package/src/substrate/lsp/source-tracking.ts +247 -0
- package/src/tool/code_find/guidance.ts +3 -3
- package/src/tool/code_graph/guidance.ts +2 -2
- package/src/tool/code_health/guidance.ts +3 -3
- package/src/tool/code_health/markdown.ts +21 -5
- package/src/tool/code_health/refresh-outcome.ts +11 -0
- package/src/tool/code_health/refresh-status.ts +25 -4
- package/src/tool/code_health/tui.ts +4 -0
- package/src/tool/code_inspect/guidance.ts +5 -3
- package/src/tool/code_orientation/guidance.ts +3 -4
- package/src/tool/code_refactor_apply/guidance.ts +1 -1
- package/src/tool/code_refactor_plan/guidance.ts +3 -5
- package/src/tool/code_resolve/guidance.ts +3 -3
- package/src/tool/guidance.ts +11 -9
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrclrchtr/supi-lsp",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
4
4
|
"description": "Language Server Protocol runtime for SuPi code intelligence",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"vscode-jsonrpc": "^9.0.0",
|
|
38
38
|
"vscode-languageserver-protocol": "^3.18.0",
|
|
39
39
|
"vscode-languageserver-types": "^3.18.0",
|
|
40
|
-
"@mrclrchtr/supi-code-runtime": "6.
|
|
41
|
-
"@mrclrchtr/supi-core": "6.
|
|
40
|
+
"@mrclrchtr/supi-code-runtime": "6.4.0",
|
|
41
|
+
"@mrclrchtr/supi-core": "6.4.0"
|
|
42
42
|
},
|
|
43
43
|
"bundledDependencies": [
|
|
44
44
|
"@mrclrchtr/supi-code-runtime",
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Public API surface for the LSP session-scoped service.
|
|
2
2
|
|
|
3
|
+
export { isMissingFileError } from "./client/client-file-state.ts";
|
|
3
4
|
export { type LoadConfigOptions, loadConfig } from "./config/config.ts";
|
|
4
5
|
export type { LspSettings } from "./config/lsp-settings.ts";
|
|
5
6
|
export { loadLspSettings } from "./config/lsp-settings.ts";
|
|
@@ -41,6 +42,12 @@ export {
|
|
|
41
42
|
type WorkspaceSentinelScanOptions,
|
|
42
43
|
type WorkspaceSentinelSyncResult,
|
|
43
44
|
} from "./diagnostics/workspace-sentinels.ts";
|
|
45
|
+
export {
|
|
46
|
+
scanWorkspaceSources,
|
|
47
|
+
type WorkspaceSourceInventory,
|
|
48
|
+
type WorkspaceSourceInventoryReason,
|
|
49
|
+
type WorkspaceSourceScanOptions,
|
|
50
|
+
} from "./diagnostics/workspace-sources.ts";
|
|
44
51
|
export { raceReadinessValue, raceRequestControl } from "./session/readiness.ts";
|
|
45
52
|
export type {
|
|
46
53
|
LspControllerState,
|
|
@@ -51,6 +58,8 @@ export type {
|
|
|
51
58
|
} from "./session/runtime-controller.ts";
|
|
52
59
|
export { LspRuntimeController } from "./session/runtime-controller.ts";
|
|
53
60
|
export type {
|
|
61
|
+
BulkTrackFileOutcome,
|
|
62
|
+
BulkTrackFilesResult,
|
|
54
63
|
DiagnosticEvidenceDocument,
|
|
55
64
|
DiagnosticEvidenceStatus,
|
|
56
65
|
DiagnosticEvidenceSummary,
|
|
@@ -79,6 +88,7 @@ export {
|
|
|
79
88
|
waitForWorkspaceLspRuntime,
|
|
80
89
|
} from "./session/runtime-registry.ts";
|
|
81
90
|
export { scanMissingServers } from "./session/scanner.ts";
|
|
91
|
+
export { MAX_BULK_TRACK_FILES } from "./session/workspace-lsp-runtime.ts";
|
|
82
92
|
export {
|
|
83
93
|
AUTOMATIC_LSP_EXCLUDED_DIRECTORIES,
|
|
84
94
|
type AutomaticLspPathPolicy,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { fileToUri
|
|
3
|
+
import { fileToUri } from "@mrclrchtr/supi-core/path";
|
|
4
4
|
import { FileChangeType, type FileEvent } from "../config/types.ts";
|
|
5
5
|
import {
|
|
6
6
|
type AutomaticLspPathPolicy,
|
|
@@ -11,12 +11,6 @@ import {
|
|
|
11
11
|
const ROOT_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
|
|
12
12
|
|
|
13
13
|
export interface WorkspaceSentinelScanOptions {
|
|
14
|
-
/**
|
|
15
|
-
* Also track every regular source file (not just sentinels). The workspace
|
|
16
|
-
* refresh path uses this to discover files created since the last refresh;
|
|
17
|
-
* the diff then reports them as `sourceChanges` so the caller can pull them.
|
|
18
|
-
*/
|
|
19
|
-
includeSourceFiles?: boolean;
|
|
20
14
|
/** Runtime-owned automatic path policy. */
|
|
21
15
|
policy?: AutomaticLspPathPolicy;
|
|
22
16
|
}
|
|
@@ -36,7 +30,7 @@ export function scanWorkspaceSentinels(
|
|
|
36
30
|
for (const entry of entries) {
|
|
37
31
|
if (!entry.isFile() && !entry.isSymbolicLink()) continue;
|
|
38
32
|
const filePath = path.join(directory, entry.name);
|
|
39
|
-
if (!
|
|
33
|
+
if (!isWorkspaceSentinelPath(filePath, resolvedCwd)) continue;
|
|
40
34
|
try {
|
|
41
35
|
snapshot.set(filePath, fs.statSync(filePath).mtimeMs);
|
|
42
36
|
} catch {
|
|
@@ -78,38 +72,16 @@ export function diffWorkspaceSentinelSnapshot(
|
|
|
78
72
|
export interface WorkspaceSentinelSyncResult {
|
|
79
73
|
snapshot: Map<string, number>;
|
|
80
74
|
changes: FileEvent[];
|
|
81
|
-
sourceChanges: FileEvent[];
|
|
82
75
|
}
|
|
83
76
|
|
|
84
|
-
/**
|
|
85
|
-
* Refresh a previous snapshot and return the new snapshot plus change events.
|
|
86
|
-
*
|
|
87
|
-
* When `includeSourceFiles` is set, the returned `changes` list keeps
|
|
88
|
-
* sentinel-file events only; source-file events are returned separately in
|
|
89
|
-
* `sourceChanges` so the caller can act on them without interpreting every
|
|
90
|
-
* regular file edit as a config-level workspace change.
|
|
91
|
-
*/
|
|
77
|
+
/** Refresh a previous sentinel snapshot and return its change events. */
|
|
92
78
|
export function syncWorkspaceSentinelSnapshot(
|
|
93
79
|
cwd: string,
|
|
94
80
|
previous: Map<string, number>,
|
|
95
81
|
options: WorkspaceSentinelScanOptions = {},
|
|
96
82
|
): WorkspaceSentinelSyncResult {
|
|
97
83
|
const snapshot = scanWorkspaceSentinels(cwd, options);
|
|
98
|
-
|
|
99
|
-
if (!options.includeSourceFiles) return { snapshot, changes, sourceChanges: [] };
|
|
100
|
-
|
|
101
|
-
const sourceChanges = changes.filter((change) => {
|
|
102
|
-
const filePath = uriToFile(change.uri);
|
|
103
|
-
return !isWorkspaceSentinelPath(filePath, cwd);
|
|
104
|
-
});
|
|
105
|
-
return {
|
|
106
|
-
snapshot,
|
|
107
|
-
changes: changes.filter((change) => {
|
|
108
|
-
const filePath = uriToFile(change.uri);
|
|
109
|
-
return isWorkspaceSentinelPath(filePath, cwd);
|
|
110
|
-
}),
|
|
111
|
-
sourceChanges,
|
|
112
|
-
};
|
|
84
|
+
return { snapshot, changes: diffWorkspaceSentinelSnapshot(previous, snapshot) };
|
|
113
85
|
}
|
|
114
86
|
|
|
115
87
|
function isWorkspaceSentinelPath(filePath: string, root: string): boolean {
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { type Dirent, promises as fs } from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type CodeRequestControl,
|
|
5
|
+
isCodeRequestInterruption,
|
|
6
|
+
throwIfCodeRequestInterrupted,
|
|
7
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
8
|
+
import { raceRequestControl } from "../session/readiness.ts";
|
|
9
|
+
import type { AutomaticLspPathPolicy } from "../workspace-path-policy.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_SOURCE_INVENTORY_FILES = 50_000;
|
|
12
|
+
|
|
13
|
+
export type WorkspaceSourceInventoryReason = "file-limit" | "filesystem-error";
|
|
14
|
+
|
|
15
|
+
/** Complete or safety-limited automatic source inventory. */
|
|
16
|
+
export interface WorkspaceSourceInventory {
|
|
17
|
+
readonly status: "complete" | "limited";
|
|
18
|
+
/** The reason is null only when the inventory is complete. */
|
|
19
|
+
readonly reason: WorkspaceSourceInventoryReason | null;
|
|
20
|
+
/** Number of configured-extension files observed before the scan stopped. */
|
|
21
|
+
readonly observedFileCount: number;
|
|
22
|
+
/** Paths are present only as a usable complete inventory. */
|
|
23
|
+
readonly files: readonly string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Inputs for one automatic source inventory. */
|
|
27
|
+
export interface WorkspaceSourceScanOptions {
|
|
28
|
+
readonly fileTypes: readonly string[];
|
|
29
|
+
readonly policy: AutomaticLspPathPolicy;
|
|
30
|
+
readonly control?: CodeRequestControl;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Scan one workspace for automatic source paths.
|
|
35
|
+
*
|
|
36
|
+
* The scan uses the runtime path policy and configured LSP extensions. A
|
|
37
|
+
* limited result is not a source baseline and must not be diffed. Caller
|
|
38
|
+
* interruption is rethrown so a health call does not retain partial state.
|
|
39
|
+
*/
|
|
40
|
+
export async function scanWorkspaceSources(
|
|
41
|
+
cwd: string,
|
|
42
|
+
options: WorkspaceSourceScanOptions,
|
|
43
|
+
): Promise<WorkspaceSourceInventory> {
|
|
44
|
+
const extensionSet = new Set(
|
|
45
|
+
options.fileTypes.map((fileType) => fileType.replace(/^\./, "").toLowerCase()),
|
|
46
|
+
);
|
|
47
|
+
const files: string[] = [];
|
|
48
|
+
let observedFileCount = 0;
|
|
49
|
+
let reason: WorkspaceSourceInventoryReason | null = null;
|
|
50
|
+
|
|
51
|
+
const readEntries = async (directory: string): Promise<Dirent[] | null> => {
|
|
52
|
+
try {
|
|
53
|
+
return await raceRequestControl(
|
|
54
|
+
fs.readdir(directory, { withFileTypes: true }) as Promise<Dirent[]>,
|
|
55
|
+
options.control,
|
|
56
|
+
);
|
|
57
|
+
} catch (error) {
|
|
58
|
+
if (isCodeRequestInterruption(error, options.control)) throw error;
|
|
59
|
+
reason = "filesystem-error";
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const visitEntry = async (directory: string, entry: Dirent): Promise<void> => {
|
|
65
|
+
const filePath = path.join(directory, entry.name);
|
|
66
|
+
const kind = entry.isDirectory() ? "directory" : "file";
|
|
67
|
+
if (!options.policy.isEligible(filePath, kind)) return;
|
|
68
|
+
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
await walk(filePath);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!entry.isFile() && !entry.isSymbolicLink()) return;
|
|
74
|
+
if (!extensionSet.has(path.extname(entry.name).slice(1).toLowerCase())) return;
|
|
75
|
+
|
|
76
|
+
observedFileCount++;
|
|
77
|
+
if (observedFileCount > MAX_SOURCE_INVENTORY_FILES) {
|
|
78
|
+
reason = "file-limit";
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
files.push(filePath);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const walk = async (directory: string): Promise<void> => {
|
|
85
|
+
if (reason !== null) return;
|
|
86
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
87
|
+
if (!options.policy.isEligible(directory, "directory")) return;
|
|
88
|
+
|
|
89
|
+
const entries = await readEntries(directory);
|
|
90
|
+
if (!entries) return;
|
|
91
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
92
|
+
for (const entry of entries) {
|
|
93
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
94
|
+
await visitEntry(directory, entry);
|
|
95
|
+
if (reason !== null) return;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
await walk(path.resolve(cwd));
|
|
100
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
status: reason === null ? "complete" : "limited",
|
|
104
|
+
reason,
|
|
105
|
+
observedFileCount,
|
|
106
|
+
files: reason === null ? files.sort((a, b) => a.localeCompare(b)) : [],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
truncateDebugIdentity as truncateIdentity,
|
|
27
27
|
} from "@mrclrchtr/supi-core/debug";
|
|
28
28
|
import { resolveToolPath as resolveSessionPath, uriToFile } from "@mrclrchtr/supi-core/path";
|
|
29
|
+
import { isMissingFileError } from "../client/client-file-state.ts";
|
|
29
30
|
import { getServerForFile } from "../config/config.ts";
|
|
30
31
|
import {
|
|
31
32
|
getFileScopeDecision,
|
|
@@ -63,6 +64,10 @@ import {
|
|
|
63
64
|
type WorkspaceSentinelScanOptions,
|
|
64
65
|
type WorkspaceSentinelSyncResult,
|
|
65
66
|
} from "../diagnostics/workspace-sentinels.ts";
|
|
67
|
+
import {
|
|
68
|
+
scanWorkspaceSources as scanAutomaticWorkspaceSources,
|
|
69
|
+
type WorkspaceSourceInventory,
|
|
70
|
+
} from "../diagnostics/workspace-sources.ts";
|
|
66
71
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
67
72
|
import {
|
|
68
73
|
emptyProcessCrashRecoveryReport,
|
|
@@ -71,6 +76,11 @@ import {
|
|
|
71
76
|
type ScopeDecisionSummary,
|
|
72
77
|
type WorkspaceDiagnosticReport,
|
|
73
78
|
} from "../session/runtime-diagnostics.ts";
|
|
79
|
+
import {
|
|
80
|
+
type BulkTrackFileOutcome,
|
|
81
|
+
type BulkTrackFilesResult,
|
|
82
|
+
MAX_BULK_TRACK_FILES,
|
|
83
|
+
} from "../session/workspace-lsp-runtime.ts";
|
|
74
84
|
import {
|
|
75
85
|
displayRelativeFilePath,
|
|
76
86
|
formatCoverageSummaryText,
|
|
@@ -124,6 +134,7 @@ type UnavailableReason = "missing-command" | "start-failed" | "runtime-error";
|
|
|
124
134
|
|
|
125
135
|
/** Maximum tracked-file entries retained in one scope-decision telemetry summary. */
|
|
126
136
|
const SCOPE_DECISION_MAX_ENTRIES = 24;
|
|
137
|
+
const MAX_CONCURRENT_BULK_TRACKS = 4;
|
|
127
138
|
|
|
128
139
|
interface ScopeDecisionAccumulator {
|
|
129
140
|
entries: ScopeDecisionEntry[];
|
|
@@ -286,6 +297,9 @@ export class LspManager {
|
|
|
286
297
|
private handledCrashGenerations = new Map<string, number>();
|
|
287
298
|
/** Prevent late startup and lifecycle callbacks from republishing after shutdown. */
|
|
288
299
|
private shuttingDown = false;
|
|
300
|
+
/** Coalesce per-file tracking transitions into one bulk transition. */
|
|
301
|
+
private trackedFilesBatchDepth = 0;
|
|
302
|
+
private trackedFilesBatchChanged = false;
|
|
289
303
|
constructor(
|
|
290
304
|
private readonly config: LspConfig,
|
|
291
305
|
private readonly cwd: string,
|
|
@@ -343,7 +357,7 @@ export class LspManager {
|
|
|
343
357
|
return this.canServeFile(filePath);
|
|
344
358
|
}
|
|
345
359
|
|
|
346
|
-
/** Inventory automatic workspace
|
|
360
|
+
/** Inventory automatic workspace sentinels with the runtime policy. */
|
|
347
361
|
scanWorkspaceSentinels(options: WorkspaceSentinelScanOptions = {}): Map<string, number> {
|
|
348
362
|
return scanAutomaticWorkspaceSentinels(this.cwd, {
|
|
349
363
|
...options,
|
|
@@ -351,7 +365,16 @@ export class LspManager {
|
|
|
351
365
|
});
|
|
352
366
|
}
|
|
353
367
|
|
|
354
|
-
/**
|
|
368
|
+
/** Scan configured LSP source extensions with the runtime path policy. */
|
|
369
|
+
scanWorkspaceSources(control?: CodeRequestControl): Promise<WorkspaceSourceInventory> {
|
|
370
|
+
return scanAutomaticWorkspaceSources(this.cwd, {
|
|
371
|
+
fileTypes: Object.values(this.config.servers).flatMap((server) => server.fileTypes),
|
|
372
|
+
policy: this.automaticPathPolicy,
|
|
373
|
+
control,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/** Refresh the automatic workspace sentinel with the runtime policy. */
|
|
355
378
|
syncWorkspaceSentinelSnapshot(
|
|
356
379
|
previous: Map<string, number>,
|
|
357
380
|
options: WorkspaceSentinelScanOptions = {},
|
|
@@ -417,9 +440,14 @@ export class LspManager {
|
|
|
417
440
|
// A recovered route keeps its consumed budget but has no active reason.
|
|
418
441
|
// Passive callers can use its running client normally.
|
|
419
442
|
|
|
420
|
-
return this.startServerForRoot(route.serverName, route.root);
|
|
443
|
+
return this.startServerForRoot(route.serverName, route.root, options.control);
|
|
421
444
|
}
|
|
422
|
-
async startServerForRoot(
|
|
445
|
+
async startServerForRoot(
|
|
446
|
+
serverName: string,
|
|
447
|
+
root: string,
|
|
448
|
+
control?: CodeRequestControl,
|
|
449
|
+
): Promise<LspClient | null> {
|
|
450
|
+
throwIfCodeRequestInterrupted(control);
|
|
423
451
|
if (this.shuttingDown) return null;
|
|
424
452
|
const serverConfig = this.config.servers[serverName];
|
|
425
453
|
if (!serverConfig) return null;
|
|
@@ -450,17 +478,18 @@ export class LspManager {
|
|
|
450
478
|
// This prevents spawning duplicate server processes when two
|
|
451
479
|
// callers race through getClientForFile before either await yields.
|
|
452
480
|
const pending = this.pendingStarts.get(key);
|
|
453
|
-
if (pending) return pending;
|
|
481
|
+
if (pending) return raceRequestControl(pending, control);
|
|
454
482
|
|
|
455
483
|
const startPromise = this.performStart(serverName, serverConfig, root, key);
|
|
456
484
|
this.pendingStarts.set(key, startPromise);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
this.pendingStarts.delete(key);
|
|
462
|
-
}
|
|
463
|
-
|
|
485
|
+
// Keep the shared startup registered until it settles. A cancelled
|
|
486
|
+
// caller only stops waiting; it must not allow a duplicate route start.
|
|
487
|
+
void startPromise
|
|
488
|
+
.finally(() => {
|
|
489
|
+
if (this.pendingStarts.get(key) === startPromise) this.pendingStarts.delete(key);
|
|
490
|
+
})
|
|
491
|
+
.catch(() => {});
|
|
492
|
+
return raceRequestControl(startPromise, control);
|
|
464
493
|
}
|
|
465
494
|
|
|
466
495
|
private createClient(
|
|
@@ -505,6 +534,11 @@ export class LspManager {
|
|
|
505
534
|
}
|
|
506
535
|
}
|
|
507
536
|
|
|
537
|
+
if (kind === "tracked-files" && this.trackedFilesBatchDepth > 0) {
|
|
538
|
+
this.trackedFilesBatchChanged = true;
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
|
|
508
542
|
const aggregateKind =
|
|
509
543
|
kind === "readiness" && generation > 1 && client.ready ? "recovery" : kind;
|
|
510
544
|
this.publishLifecycle(aggregateKind);
|
|
@@ -1387,6 +1421,118 @@ export class LspManager {
|
|
|
1387
1421
|
return unavailableCodeQuery(`Diagnostic collection failed for ${resolvedPath}: ${detail}`);
|
|
1388
1422
|
}
|
|
1389
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Track a bounded set of automatic source files with shared route starts.
|
|
1426
|
+
* Inputs after the first 256 unique paths are ignored.
|
|
1427
|
+
*/
|
|
1428
|
+
async bulkTrackFiles(
|
|
1429
|
+
filePaths: readonly string[],
|
|
1430
|
+
control?: CodeRequestControl,
|
|
1431
|
+
): Promise<BulkTrackFilesResult> {
|
|
1432
|
+
throwIfCodeRequestInterrupted(control);
|
|
1433
|
+
const selected = Array.from(
|
|
1434
|
+
new Set(filePaths.map((filePath) => resolveSessionPath(this.cwd, filePath))),
|
|
1435
|
+
).slice(0, MAX_BULK_TRACK_FILES);
|
|
1436
|
+
if (selected.length === 0) return { outcomes: [] };
|
|
1437
|
+
|
|
1438
|
+
this.trackedFilesBatchDepth++;
|
|
1439
|
+
const outcomes = new Array<BulkTrackFileOutcome>(selected.length);
|
|
1440
|
+
const workerCount = Math.min(MAX_CONCURRENT_BULK_TRACKS, selected.length);
|
|
1441
|
+
let nextIndex = 0;
|
|
1442
|
+
const worker = async (): Promise<void> => {
|
|
1443
|
+
for (;;) {
|
|
1444
|
+
throwIfCodeRequestInterrupted(control);
|
|
1445
|
+
const index = nextIndex++;
|
|
1446
|
+
if (index >= selected.length) return;
|
|
1447
|
+
outcomes[index] = await this.trackBulkFile(selected[index], control);
|
|
1448
|
+
}
|
|
1449
|
+
};
|
|
1450
|
+
const workers = Array.from({ length: workerCount }, () => worker());
|
|
1451
|
+
try {
|
|
1452
|
+
await Promise.all(workers);
|
|
1453
|
+
return { outcomes };
|
|
1454
|
+
} catch (error) {
|
|
1455
|
+
// Let every worker observe cancellation before the batch transition is
|
|
1456
|
+
// published. Shared route startup may continue, but no worker may open
|
|
1457
|
+
// another document after this caller stops waiting.
|
|
1458
|
+
await Promise.allSettled(workers);
|
|
1459
|
+
throw error;
|
|
1460
|
+
} finally {
|
|
1461
|
+
this.trackedFilesBatchDepth--;
|
|
1462
|
+
if (this.trackedFilesBatchDepth === 0 && this.trackedFilesBatchChanged) {
|
|
1463
|
+
this.trackedFilesBatchChanged = false;
|
|
1464
|
+
this.publishLifecycle("tracked-files");
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private async trackBulkFile(
|
|
1470
|
+
filePath: string,
|
|
1471
|
+
control?: CodeRequestControl,
|
|
1472
|
+
): Promise<BulkTrackFileOutcome> {
|
|
1473
|
+
const resolvedPath = resolveSessionPath(this.cwd, filePath);
|
|
1474
|
+
if (!this.isConfiguredAutomaticSourceFile(resolvedPath)) {
|
|
1475
|
+
return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
|
|
1476
|
+
}
|
|
1477
|
+
const fileStatus = this.inspectSourceFile(resolvedPath);
|
|
1478
|
+
if (fileStatus !== "regular") {
|
|
1479
|
+
if (fileStatus === "missing") {
|
|
1480
|
+
return { file: resolvedPath, kind: "unsupported", reason: "missing" };
|
|
1481
|
+
}
|
|
1482
|
+
if (fileStatus === "not-automatic-source") {
|
|
1483
|
+
return { file: resolvedPath, kind: "unsupported", reason: "not-automatic-source" };
|
|
1484
|
+
}
|
|
1485
|
+
return {
|
|
1486
|
+
file: resolvedPath,
|
|
1487
|
+
kind: "unavailable",
|
|
1488
|
+
reason: "The source file could not be inspected.",
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
if (this.isFileTracked(resolvedPath)) {
|
|
1492
|
+
return { file: resolvedPath, kind: "already-tracked" };
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
throwIfCodeRequestInterrupted(control);
|
|
1496
|
+
// Recheck the automatic source-support rule after the inventory scan. A
|
|
1497
|
+
// config, exclusion, or route can change while a refresh is in flight.
|
|
1498
|
+
if (!this.isSupportedSourceFile(resolvedPath)) {
|
|
1499
|
+
return {
|
|
1500
|
+
file: resolvedPath,
|
|
1501
|
+
kind: "unavailable",
|
|
1502
|
+
reason: "No available LSP route can serve this source file.",
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
const client = await this.ensureFileOpen(resolvedPath, { control });
|
|
1507
|
+
if (!client) {
|
|
1508
|
+
return {
|
|
1509
|
+
file: resolvedPath,
|
|
1510
|
+
kind: "unavailable",
|
|
1511
|
+
reason: "The LSP route could not open this source file.",
|
|
1512
|
+
};
|
|
1513
|
+
}
|
|
1514
|
+
return { file: resolvedPath, kind: "tracked" };
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
private isConfiguredAutomaticSourceFile(filePath: string): boolean {
|
|
1518
|
+
return this.isAutomaticScopePath(filePath) && getServerForFile(this.config, filePath) !== null;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
private inspectSourceFile(
|
|
1522
|
+
filePath: string,
|
|
1523
|
+
): "regular" | "missing" | "not-automatic-source" | "unavailable" {
|
|
1524
|
+
try {
|
|
1525
|
+
return fs.statSync(filePath).isFile() ? "regular" : "not-automatic-source";
|
|
1526
|
+
} catch (error) {
|
|
1527
|
+
if (isMissingFileError(error)) return "missing";
|
|
1528
|
+
return "unavailable";
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
private isFileTracked(filePath: string): boolean {
|
|
1533
|
+
return this.getExistingClientForFile(filePath)?.openFiles.includes(filePath) ?? false;
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1390
1536
|
/** Close a file across any active LSP clients and clear its cached diagnostics. */
|
|
1391
1537
|
closeFile(filePath: string): void {
|
|
1392
1538
|
closeFileAcrossClients(this.clients.values(), resolveSessionPath(this.cwd, filePath));
|
|
@@ -1994,9 +2140,14 @@ export class LspManager {
|
|
|
1994
2140
|
const client = await this.getClientForFile(resolvedPath, options);
|
|
1995
2141
|
if (!client) return null;
|
|
1996
2142
|
try {
|
|
1997
|
-
|
|
2143
|
+
const content = fs.readFileSync(resolvedPath, "utf-8");
|
|
2144
|
+
// Route startup can be shared. The document open belongs to this
|
|
2145
|
+
// caller, so check again after the shared wait before sending didOpen.
|
|
2146
|
+
throwIfCodeRequestInterrupted(options.control);
|
|
2147
|
+
client.didOpen(resolvedPath, content);
|
|
1998
2148
|
return client;
|
|
1999
|
-
} catch {
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
if (isCodeRequestInterruption(error, options.control)) throw error;
|
|
2000
2151
|
this.closeFile(resolvedPath);
|
|
2001
2152
|
return null;
|
|
2002
2153
|
}
|
|
@@ -43,6 +43,7 @@ import type {
|
|
|
43
43
|
RecoverDiagnosticsResult,
|
|
44
44
|
} from "./runtime-diagnostics.ts";
|
|
45
45
|
import type {
|
|
46
|
+
BulkTrackFilesResult,
|
|
46
47
|
RoutedMutationResponse,
|
|
47
48
|
SemanticReadinessResult,
|
|
48
49
|
WorkspaceLspRuntime,
|
|
@@ -70,6 +71,8 @@ export {
|
|
|
70
71
|
type WorkspaceDiagnosticSummaryEntry,
|
|
71
72
|
} from "./runtime-diagnostics.ts";
|
|
72
73
|
export type {
|
|
74
|
+
BulkTrackFileOutcome,
|
|
75
|
+
BulkTrackFilesResult,
|
|
73
76
|
RoutedMutationResponse,
|
|
74
77
|
SemanticReadinessResult,
|
|
75
78
|
WorkspaceLspRuntime,
|
|
@@ -345,7 +348,12 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
345
348
|
return this.manager.scanWorkspaceSentinels(options);
|
|
346
349
|
}
|
|
347
350
|
|
|
348
|
-
/**
|
|
351
|
+
/** Scan configured source extensions under the automatic path policy. */
|
|
352
|
+
scanWorkspaceSources(control?: CodeRequestControl) {
|
|
353
|
+
return this.manager.scanWorkspaceSources(control);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Refresh the automatic workspace sentinel. */
|
|
349
357
|
syncWorkspaceSentinelSnapshot(
|
|
350
358
|
previous: Map<string, number>,
|
|
351
359
|
options: WorkspaceSentinelScanOptions = {},
|
|
@@ -353,6 +361,14 @@ class DefaultWorkspaceLspRuntime implements WorkspaceLspRuntime {
|
|
|
353
361
|
return this.manager.syncWorkspaceSentinelSnapshot(previous, options);
|
|
354
362
|
}
|
|
355
363
|
|
|
364
|
+
/** Track a bounded set of source files without exposing clients. */
|
|
365
|
+
bulkTrackFiles(
|
|
366
|
+
filePaths: readonly string[],
|
|
367
|
+
control?: CodeRequestControl,
|
|
368
|
+
): Promise<BulkTrackFilesResult> {
|
|
369
|
+
return this.manager.bulkTrackFiles(filePaths, control);
|
|
370
|
+
}
|
|
371
|
+
|
|
356
372
|
/** Track a file in its routed client without exposing that client. */
|
|
357
373
|
async trackFile(filePath: string): Promise<boolean> {
|
|
358
374
|
const resolvedPath = this.resolveFilePath(filePath);
|
|
@@ -17,9 +17,13 @@ import type {
|
|
|
17
17
|
WorkspaceSentinelScanOptions,
|
|
18
18
|
WorkspaceSentinelSyncResult,
|
|
19
19
|
} from "../diagnostics/workspace-sentinels.ts";
|
|
20
|
+
import type { WorkspaceSourceInventory } from "../diagnostics/workspace-sources.ts";
|
|
20
21
|
import type { WorkspaceLspDiagnosticSurface } from "./runtime-diagnostic-surface.ts";
|
|
21
22
|
import type { ProcessCrashRecoveryReport } from "./runtime-diagnostics.ts";
|
|
22
23
|
|
|
24
|
+
/** Maximum number of files processed by one automatic bulk-tracking call. */
|
|
25
|
+
export const MAX_BULK_TRACK_FILES = 256;
|
|
26
|
+
|
|
23
27
|
export type WorkspaceLspRuntimeState =
|
|
24
28
|
| { kind: "ready"; runtime: WorkspaceLspRuntime }
|
|
25
29
|
| { kind: "inactive"; runtime: WorkspaceLspRuntime }
|
|
@@ -53,6 +57,22 @@ export interface RoutedMutationResponse<T> {
|
|
|
53
57
|
readonly authorizedMutationRoots: readonly string[];
|
|
54
58
|
}
|
|
55
59
|
|
|
60
|
+
/** Outcome for one path selected by a bounded automatic tracking batch. */
|
|
61
|
+
export type BulkTrackFileOutcome =
|
|
62
|
+
| { readonly file: string; readonly kind: "tracked" }
|
|
63
|
+
| { readonly file: string; readonly kind: "already-tracked" }
|
|
64
|
+
| {
|
|
65
|
+
readonly file: string;
|
|
66
|
+
readonly kind: "unsupported";
|
|
67
|
+
readonly reason: "missing" | "not-automatic-source";
|
|
68
|
+
}
|
|
69
|
+
| { readonly file: string; readonly kind: "unavailable"; readonly reason: string };
|
|
70
|
+
|
|
71
|
+
/** Result of one bounded automatic tracking batch. */
|
|
72
|
+
export interface BulkTrackFilesResult {
|
|
73
|
+
readonly outcomes: readonly BulkTrackFileOutcome[];
|
|
74
|
+
}
|
|
75
|
+
|
|
56
76
|
/**
|
|
57
77
|
* Workspace-scoped LSP interface that owns routing, readiness, semantic operations,
|
|
58
78
|
* diagnostics, and recovery without exposing clients or the mutable manager.
|
|
@@ -116,13 +136,23 @@ export interface WorkspaceLspRuntime extends WorkspaceLspDiagnosticSurface {
|
|
|
116
136
|
getProjectServers(): ProjectServerInfo[];
|
|
117
137
|
/** Check whether automatic LSP work can use and serve the source file. */
|
|
118
138
|
isSupportedSourceFile(filePath: string): boolean;
|
|
119
|
-
/** Inventory policy-eligible workspace sentinels
|
|
139
|
+
/** Inventory policy-eligible workspace sentinels. */
|
|
120
140
|
scanWorkspaceSentinels(options?: WorkspaceSentinelScanOptions): Map<string, number>;
|
|
121
|
-
/**
|
|
141
|
+
/** Scan configured source extensions under the automatic path policy. */
|
|
142
|
+
scanWorkspaceSources(control?: CodeRequestControl): Promise<WorkspaceSourceInventory>;
|
|
143
|
+
/** Refresh one policy-eligible workspace sentinel. */
|
|
122
144
|
syncWorkspaceSentinelSnapshot(
|
|
123
145
|
previous: Map<string, number>,
|
|
124
146
|
options?: WorkspaceSentinelScanOptions,
|
|
125
147
|
): WorkspaceSentinelSyncResult;
|
|
148
|
+
/**
|
|
149
|
+
* Track a bounded, deduplicated set of automatic source files. Inputs after
|
|
150
|
+
* the first 256 unique paths are ignored; callers must retain those paths.
|
|
151
|
+
*/
|
|
152
|
+
bulkTrackFiles(
|
|
153
|
+
filePaths: readonly string[],
|
|
154
|
+
control?: CodeRequestControl,
|
|
155
|
+
): Promise<BulkTrackFilesResult>;
|
|
126
156
|
trackFile(filePath: string): Promise<boolean>;
|
|
127
157
|
closeFile(filePath: string): void;
|
|
128
158
|
pruneMissingFiles(): readonly string[];
|