@mrclrchtr/supi-code-intelligence 5.0.0 → 6.0.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 +2 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/package.json +2 -2
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
- package/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/README.md +34 -3
- 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 +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/package.json +3 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +12 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +67 -28
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +63 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +46 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +1 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-publication.ts +214 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +278 -79
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +29 -8
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +101 -19
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +19 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +49 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +73 -15
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +2 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/config/config.ts +5 -5
- package/node_modules/@mrclrchtr/supi-lsp/src/config/server-config.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-extends.ts +68 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-path.ts +8 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/config/tsconfig-scope.ts +176 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +4 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/workspace-sentinels.ts +46 -10
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +4 -3
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +65 -17
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +143 -21
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +43 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +31 -0
- 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 +2 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/api.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +0 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/index.ts +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +3 -3
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +58 -126
- package/src/analysis/health/file-scope.ts +184 -0
- package/src/analysis/health/recovery.ts +8 -1
- package/src/analysis/readiness.ts +4 -0
- package/src/analysis/search/ast-scan.ts +4 -1
- package/src/analysis/search/pattern-analysis.ts +10 -3
- package/src/analysis/search/pattern.ts +6 -1
- package/src/config.ts +35 -1
- package/src/extension.ts +57 -14
- package/src/overview/overview-data.ts +9 -2
- package/src/overview/overview.ts +49 -30
- package/src/overview/types.ts +2 -0
- package/src/session/find-types.ts +2 -2
- package/src/session/find-workflow.ts +3 -2
- package/src/session/health-refresh.ts +76 -49
- package/src/session/health-types.ts +7 -1
- package/src/session/health-workflow.ts +35 -18
- package/src/session/input/workflows.ts +2 -2
- package/src/session/inspect-workflow.ts +2 -1
- package/src/session/orientation/collect.ts +2 -2
- package/src/session/orientation-types.ts +2 -0
- package/src/session/orientation-workflow.ts +8 -2
- package/src/session/refactor-workflow.ts +2 -1
- package/src/session/session.ts +25 -1
- package/src/substrate/lsp/maintenance.ts +105 -23
- package/src/substrate/lsp/recovery.ts +11 -5
- package/src/tool/code_find/execute.ts +23 -0
- package/src/tool/code_find/guidance.ts +8 -0
- package/src/tool/{find → code_find}/render.ts +1 -1
- package/src/tool/{result/find.ts → code_find/result.ts} +36 -11
- package/src/tool/code_find/spec.ts +37 -0
- package/src/tool/code_graph/execute.ts +24 -0
- package/src/tool/code_graph/guidance.ts +6 -0
- package/src/tool/{graph → code_graph}/markdown.ts +1 -1
- package/src/tool/{result/graph.ts → code_graph/result.ts} +69 -4
- package/src/tool/code_graph/spec.ts +39 -0
- package/src/tool/{health → code_health}/execute.ts +2 -18
- package/src/tool/code_health/guidance.ts +8 -0
- package/src/tool/{health → code_health}/markdown.ts +68 -18
- package/src/tool/{result/health.ts → code_health/result.ts} +29 -4
- package/src/tool/code_health/spec.ts +41 -0
- package/src/tool/code_inspect/execute.ts +20 -0
- package/src/tool/code_inspect/guidance.ts +6 -0
- package/src/tool/{inspect → code_inspect}/markdown.ts +1 -1
- package/src/tool/{result/inspect.ts → code_inspect/result.ts} +32 -3
- package/src/tool/code_inspect/spec.ts +25 -0
- package/src/tool/code_orientation/execute.ts +25 -0
- package/src/tool/code_orientation/guidance.ts +9 -0
- package/src/tool/{orientation → code_orientation}/markdown.ts +1 -1
- package/src/tool/{result/orientation.ts → code_orientation/result.ts} +67 -3
- package/src/tool/code_orientation/spec.ts +25 -0
- package/src/tool/code_refactor_apply/execute.ts +17 -0
- package/src/tool/code_refactor_apply/guidance.ts +5 -0
- package/src/tool/{refactor-apply/execute.ts → code_refactor_apply/result.ts} +5 -11
- package/src/tool/code_refactor_apply/spec.ts +26 -0
- package/src/tool/code_refactor_plan/execute.ts +27 -0
- package/src/tool/code_refactor_plan/guidance.ts +8 -0
- package/src/tool/{refactor-plan/execute.ts → code_refactor_plan/result.ts} +9 -22
- package/src/tool/code_refactor_plan/spec.ts +25 -0
- package/src/tool/code_resolve/execute.ts +19 -0
- package/src/tool/code_resolve/guidance.ts +8 -0
- package/src/tool/{resolve → code_resolve}/markdown.ts +1 -1
- package/src/tool/{result/resolve.ts → code_resolve/result.ts} +37 -24
- package/src/tool/code_resolve/spec.ts +25 -0
- package/src/tool/guidance.ts +71 -52
- package/src/tool/infra/truncate.ts +40 -0
- package/src/tool/{refactor-plan/markdown.ts → refactor-markdown.ts} +4 -7
- package/src/tool/register.ts +9 -97
- package/src/tool/result/refactor.ts +1 -4
- package/src/tool/schemas.ts +26 -157
- package/src/tool/specs.ts +40 -126
- package/src/tool/find/execute.ts +0 -43
- package/src/tool/graph/execute.ts +0 -82
- package/src/tool/inspect/execute.ts +0 -40
- package/src/tool/orientation/execute.ts +0 -80
- package/src/tool/resolve/execute.ts +0 -53
- /package/src/tool/{find → code_find}/ast-kinds.ts +0 -0
- /package/src/tool/{find → code_find}/markdown.ts +0 -0
- /package/src/tool/{find → code_find}/modes.ts +0 -0
- /package/src/tool/{find → code_find}/tui.ts +0 -0
- /package/src/tool/{graph → code_graph}/tui.ts +0 -0
- /package/src/tool/{health → code_health}/refresh-status.ts +0 -0
- /package/src/tool/{health → code_health}/semantic-state.ts +0 -0
- /package/src/tool/{health → code_health}/tui.ts +0 -0
- /package/src/tool/{inspect → code_inspect}/tui.ts +0 -0
- /package/src/tool/{orientation → code_orientation}/tui.ts +0 -0
- /package/src/tool/{refactor-apply → code_refactor_apply}/tui.ts +0 -0
- /package/src/tool/{refactor-plan → code_refactor_plan}/tui.ts +0 -0
- /package/src/tool/{resolve → code_resolve}/tui.ts +0 -0
|
@@ -362,7 +362,7 @@ function recordRequestTiming(
|
|
|
362
362
|
method: observation.method,
|
|
363
363
|
methodClass: observation.methodClass,
|
|
364
364
|
outcome: observation.outcome,
|
|
365
|
-
...(identity.server !== undefined ? { server: identity.server } : {}),
|
|
365
|
+
...(identity.server !== undefined ? { server: truncateIdentity(identity.server) } : {}),
|
|
366
366
|
...(observation.errorCode !== undefined ? { errorCode: observation.errorCode } : {}),
|
|
367
367
|
},
|
|
368
368
|
}),
|
|
@@ -63,12 +63,12 @@ export const CLIENT_CAPABILITIES: ClientCapabilities = {
|
|
|
63
63
|
workDoneProgress: true,
|
|
64
64
|
},
|
|
65
65
|
workspace: {
|
|
66
|
-
workspaceFolders:
|
|
66
|
+
workspaceFolders: true,
|
|
67
67
|
workspaceEdit: {
|
|
68
68
|
documentChanges: true,
|
|
69
69
|
},
|
|
70
70
|
diagnostics: {
|
|
71
|
-
refreshSupport:
|
|
71
|
+
refreshSupport: true,
|
|
72
72
|
},
|
|
73
73
|
},
|
|
74
74
|
};
|
|
@@ -125,15 +125,15 @@ function mergeSingleServer(
|
|
|
125
125
|
override: Partial<ServerConfig>,
|
|
126
126
|
): ServerConfig | null {
|
|
127
127
|
if (!base) {
|
|
128
|
-
//
|
|
128
|
+
// A custom server needs a command and at least one file extension.
|
|
129
129
|
if (
|
|
130
|
-
override.command &&
|
|
130
|
+
typeof override.command === "string" &&
|
|
131
|
+
override.command.length > 0 &&
|
|
131
132
|
Array.isArray(override.fileTypes) &&
|
|
132
133
|
override.fileTypes.length > 0 &&
|
|
133
|
-
Array.isArray(override.rootMarkers)
|
|
134
|
-
override.rootMarkers.length > 0
|
|
134
|
+
(override.rootMarkers === undefined || Array.isArray(override.rootMarkers))
|
|
135
135
|
) {
|
|
136
|
-
return override as ServerConfig;
|
|
136
|
+
return { ...override, rootMarkers: override.rootMarkers ?? [] } as ServerConfig;
|
|
137
137
|
}
|
|
138
138
|
return null;
|
|
139
139
|
}
|
|
@@ -5,8 +5,11 @@ export interface ServerConfig {
|
|
|
5
5
|
command: string;
|
|
6
6
|
args?: string[];
|
|
7
7
|
fileTypes: string[];
|
|
8
|
+
/** Files that identify the project root. An empty list uses the session root. */
|
|
8
9
|
rootMarkers: string[];
|
|
9
10
|
enabled?: boolean;
|
|
11
|
+
/** Environment values added when the server process starts. */
|
|
12
|
+
env?: Record<string, string>;
|
|
10
13
|
initializationOptions?: unknown;
|
|
11
14
|
/** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
|
|
12
15
|
readinessTimeoutMs?: number;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { normalizeTsconfigPath as normalizePath } from "./tsconfig-path.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collect recognized local project configs declared by `extends`, including
|
|
7
|
+
* transitive dependencies and paths that do not exist yet.
|
|
8
|
+
*/
|
|
9
|
+
export function collectExtendedProjectConfigs(configPath: string): Set<string> {
|
|
10
|
+
const dependencies = new Set<string>();
|
|
11
|
+
const visited = new Set<string>();
|
|
12
|
+
|
|
13
|
+
const visit = (currentConfigPath: string): void => {
|
|
14
|
+
const normalizedConfigPath = normalizePath(currentConfigPath);
|
|
15
|
+
if (visited.has(normalizedConfigPath)) return;
|
|
16
|
+
visited.add(normalizedConfigPath);
|
|
17
|
+
|
|
18
|
+
const config = ts.readConfigFile(currentConfigPath, ts.sys.readFile).config;
|
|
19
|
+
if (!config || typeof config !== "object") return;
|
|
20
|
+
|
|
21
|
+
const extendsValues =
|
|
22
|
+
typeof config.extends === "string"
|
|
23
|
+
? [config.extends]
|
|
24
|
+
: Array.isArray(config.extends)
|
|
25
|
+
? config.extends.filter((value: unknown): value is string => typeof value === "string")
|
|
26
|
+
: [];
|
|
27
|
+
|
|
28
|
+
for (const extendsValue of extendsValues) {
|
|
29
|
+
const extendedConfigPath = resolveLocalExtendedConfigPath(currentConfigPath, extendsValue);
|
|
30
|
+
if (!extendedConfigPath) continue;
|
|
31
|
+
|
|
32
|
+
const normalizedExtendedConfigPath = normalizePath(extendedConfigPath);
|
|
33
|
+
dependencies.add(normalizedExtendedConfigPath);
|
|
34
|
+
visit(extendedConfigPath);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
visit(configPath);
|
|
39
|
+
return dependencies;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a local project-config extends entry.
|
|
44
|
+
*
|
|
45
|
+
* Package names are not local paths and are deliberately ignored. TypeScript
|
|
46
|
+
* uses the `.json` suffix for extensionless local config references. Only
|
|
47
|
+
* recognized project-config names are tracked; arbitrary local JSON files may
|
|
48
|
+
* still be parsed by TypeScript but are outside this invalidation contract.
|
|
49
|
+
*/
|
|
50
|
+
function resolveLocalExtendedConfigPath(configPath: string, extendsValue: string): string | null {
|
|
51
|
+
if (!path.isAbsolute(extendsValue) && !/^\.{1,2}(?:[\\/]|$)/.test(extendsValue)) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const resolved = path.resolve(path.dirname(configPath), extendsValue);
|
|
56
|
+
const candidate =
|
|
57
|
+
path.extname(resolved).toLowerCase() === ".json" ? resolved : `${resolved}.json`;
|
|
58
|
+
return isProjectConfigFileName(path.basename(candidate)) ? candidate : null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Whether a file name is a tsconfig.json, jsconfig.json, or tsconfig.*.json name. */
|
|
62
|
+
export function isProjectConfigFileName(name: string): boolean {
|
|
63
|
+
return (
|
|
64
|
+
name === "tsconfig.json" ||
|
|
65
|
+
name === "jsconfig.json" ||
|
|
66
|
+
(name.startsWith("tsconfig.") && name.endsWith(".json"))
|
|
67
|
+
);
|
|
68
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
|
|
4
|
+
/** Normalize a path for tsconfig cache keys and comparisons. */
|
|
5
|
+
export function normalizeTsconfigPath(target: string): string {
|
|
6
|
+
const resolved = path.resolve(target).replaceAll("\\", "/");
|
|
7
|
+
return ts.sys.useCaseSensitiveFileNames ? resolved : resolved.toLowerCase();
|
|
8
|
+
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import * as path from "node:path";
|
|
9
9
|
import ts from "typescript";
|
|
10
|
+
import { collectExtendedProjectConfigs } from "./tsconfig-extends.ts";
|
|
11
|
+
import { normalizeTsconfigPath as normalizePath } from "./tsconfig-path.ts";
|
|
12
|
+
|
|
13
|
+
export { isProjectConfigFileName } from "./tsconfig-extends.ts";
|
|
10
14
|
|
|
11
15
|
interface ParsedProjectConfig {
|
|
12
16
|
configPath: string;
|
|
@@ -19,8 +23,39 @@ interface ParsedProjectConfig {
|
|
|
19
23
|
usesDefaultInclude: boolean;
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
interface ParsedProjectConfigCacheEntry {
|
|
27
|
+
parsed: ParsedProjectConfig | null;
|
|
28
|
+
dependencies: Set<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Whether a file is inside the compilation scope of its nearest project config. */
|
|
32
|
+
export type FileScopeStatus = "included" | "excluded" | "no-config" | "out-of-tree";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The mechanism that produced a file scope decision.
|
|
36
|
+
*
|
|
37
|
+
* `extension` covers unsupported file extensions (checked before any pattern).
|
|
38
|
+
*/
|
|
39
|
+
export type ScopeDecisionBasis =
|
|
40
|
+
| "fileNames"
|
|
41
|
+
| "explicit"
|
|
42
|
+
| "include-pattern"
|
|
43
|
+
| "default-include"
|
|
44
|
+
| "exclude-pattern"
|
|
45
|
+
| "extension";
|
|
46
|
+
|
|
47
|
+
/** An explainable tsconfig scope decision for one file. */
|
|
48
|
+
export interface FileScopeDecision {
|
|
49
|
+
status: FileScopeStatus;
|
|
50
|
+
/** Decision mechanism; null when no decision applies (no-config, out-of-tree). */
|
|
51
|
+
basis: ScopeDecisionBasis | null;
|
|
52
|
+
/** Absolute path of the config that decided the file, when one exists. */
|
|
53
|
+
configPath: string | null;
|
|
54
|
+
caseSensitiveFileNames: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
22
57
|
const nearestConfigCache = new Map<string, string | null>();
|
|
23
|
-
const parsedConfigCache = new Map<string,
|
|
58
|
+
const parsedConfigCache = new Map<string, ParsedProjectConfigCacheEntry>();
|
|
24
59
|
|
|
25
60
|
const tsInternal = ts as typeof ts & {
|
|
26
61
|
getFileMatcherPatterns?: (
|
|
@@ -47,14 +82,63 @@ const tsInternal = ts as typeof ts & {
|
|
|
47
82
|
* @returns `true` if the file is excluded from compilation scope
|
|
48
83
|
*/
|
|
49
84
|
export function isFileExcludedByTsconfig(filePath: string, cwd: string): boolean {
|
|
85
|
+
// Legacy filter semantics: only a resolved "excluded" decision filters a
|
|
86
|
+
// file. Out-of-tree and no-config files stay unfiltered; the decision API
|
|
87
|
+
// still reports those statuses honestly for consumers that want them.
|
|
88
|
+
return getFileScopeDecision(filePath, cwd).status === "excluded";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compute the explainable tsconfig scope decision for one file.
|
|
93
|
+
*
|
|
94
|
+
* The decision carries the mechanism that produced it (the scope decision
|
|
95
|
+
* basis) so consumers can report why a file is inside or outside the nearest
|
|
96
|
+
* project config without re-deriving the scope logic.
|
|
97
|
+
*/
|
|
98
|
+
export function getFileScopeDecision(filePath: string, cwd: string): FileScopeDecision {
|
|
99
|
+
const caseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames;
|
|
50
100
|
const absolutePath = path.resolve(cwd, filePath);
|
|
101
|
+
if (isOutOfTree(cwd, absolutePath)) {
|
|
102
|
+
return {
|
|
103
|
+
status: "out-of-tree",
|
|
104
|
+
basis: null,
|
|
105
|
+
configPath: null,
|
|
106
|
+
caseSensitiveFileNames,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
51
110
|
const configPath = findNearestProjectConfig(path.dirname(absolutePath), cwd);
|
|
52
|
-
if (!configPath)
|
|
111
|
+
if (!configPath) {
|
|
112
|
+
return {
|
|
113
|
+
status: "no-config",
|
|
114
|
+
basis: null,
|
|
115
|
+
configPath: null,
|
|
116
|
+
caseSensitiveFileNames,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
53
119
|
|
|
54
120
|
const parsed = parseProjectConfig(configPath);
|
|
55
|
-
if (!parsed)
|
|
121
|
+
if (!parsed) {
|
|
122
|
+
return {
|
|
123
|
+
status: "no-config",
|
|
124
|
+
basis: null,
|
|
125
|
+
configPath,
|
|
126
|
+
caseSensitiveFileNames,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
56
129
|
|
|
57
|
-
|
|
130
|
+
const decision = decideFileScope(parsed, absolutePath);
|
|
131
|
+
return {
|
|
132
|
+
status: decision.included ? "included" : "excluded",
|
|
133
|
+
basis: decision.basis,
|
|
134
|
+
configPath: parsed.configPath,
|
|
135
|
+
caseSensitiveFileNames,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isOutOfTree(cwd: string, absolutePath: string): boolean {
|
|
140
|
+
const relative = path.relative(path.resolve(cwd), absolutePath);
|
|
141
|
+
return relative.startsWith(`..${path.sep}`) || relative === "..";
|
|
58
142
|
}
|
|
59
143
|
|
|
60
144
|
/**
|
|
@@ -104,11 +188,15 @@ function getLocalProjectConfig(directory: string): string | null {
|
|
|
104
188
|
function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
|
|
105
189
|
const normalizedConfigPath = normalizePath(configPath);
|
|
106
190
|
const cached = parsedConfigCache.get(normalizedConfigPath);
|
|
107
|
-
if (cached !== undefined) return cached;
|
|
191
|
+
if (cached !== undefined) return cached.parsed;
|
|
108
192
|
|
|
193
|
+
const cacheEntry: ParsedProjectConfigCacheEntry = {
|
|
194
|
+
parsed: null,
|
|
195
|
+
dependencies: collectExtendedProjectConfigs(configPath),
|
|
196
|
+
};
|
|
109
197
|
const parsed = ts.getParsedCommandLineOfConfigFile(configPath, {}, createParseConfigHost());
|
|
110
198
|
if (!parsed) {
|
|
111
|
-
parsedConfigCache.set(normalizedConfigPath,
|
|
199
|
+
parsedConfigCache.set(normalizedConfigPath, cacheEntry);
|
|
112
200
|
return null;
|
|
113
201
|
}
|
|
114
202
|
|
|
@@ -119,7 +207,10 @@ function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
|
|
|
119
207
|
configDir,
|
|
120
208
|
parsed.raw.include,
|
|
121
209
|
parsed.raw.exclude,
|
|
122
|
-
|
|
210
|
+
{
|
|
211
|
+
useDefaultInclude: usesDefaultInclude,
|
|
212
|
+
useCaseSensitiveFileNames: ts.sys.useCaseSensitiveFileNames,
|
|
213
|
+
},
|
|
123
214
|
);
|
|
124
215
|
const supportedExtensions = new Set(getSupportedExtensions(parsed.options));
|
|
125
216
|
if (parsed.options.resolveJsonModule) supportedExtensions.add(".json");
|
|
@@ -134,7 +225,8 @@ function parseProjectConfig(configPath: string): ParsedProjectConfig | null {
|
|
|
134
225
|
supportedExtensions,
|
|
135
226
|
usesDefaultInclude,
|
|
136
227
|
} satisfies ParsedProjectConfig;
|
|
137
|
-
|
|
228
|
+
cacheEntry.parsed = result;
|
|
229
|
+
parsedConfigCache.set(normalizedConfigPath, cacheEntry);
|
|
138
230
|
return result;
|
|
139
231
|
}
|
|
140
232
|
|
|
@@ -147,15 +239,21 @@ function extractExplicitFiles(rawFiles: unknown, configDir: string): Set<string>
|
|
|
147
239
|
);
|
|
148
240
|
}
|
|
149
241
|
|
|
242
|
+
interface FileMatcherOptions {
|
|
243
|
+
useDefaultInclude: boolean;
|
|
244
|
+
useCaseSensitiveFileNames: boolean;
|
|
245
|
+
}
|
|
246
|
+
|
|
150
247
|
function createFileMatchers(
|
|
151
248
|
configDir: string,
|
|
152
249
|
rawInclude: unknown,
|
|
153
250
|
rawExclude: unknown,
|
|
154
|
-
|
|
251
|
+
options: FileMatcherOptions,
|
|
155
252
|
): {
|
|
156
253
|
includeFilePattern: RegExp | null;
|
|
157
254
|
excludePattern: RegExp | null;
|
|
158
255
|
} {
|
|
256
|
+
const { useDefaultInclude, useCaseSensitiveFileNames } = options;
|
|
159
257
|
const includeSpecs = Array.isArray(rawInclude)
|
|
160
258
|
? rawInclude.filter((entry): entry is string => typeof entry === "string")
|
|
161
259
|
: undefined;
|
|
@@ -167,29 +265,45 @@ function createFileMatchers(
|
|
|
167
265
|
excludeSpecs,
|
|
168
266
|
useDefaultInclude ? ["**/*"] : includeSpecs,
|
|
169
267
|
);
|
|
268
|
+
// normalizePath() lowercases paths when the filesystem is case-insensitive,
|
|
269
|
+
// so the include/exclude regexes must match case-insensitively too. Otherwise
|
|
270
|
+
// files created after the parse (absent from fileNames, tested against the
|
|
271
|
+
// include pattern) are wrongly declared excluded.
|
|
272
|
+
const regexFlags = useCaseSensitiveFileNames ? undefined : "i";
|
|
170
273
|
|
|
171
274
|
return {
|
|
172
275
|
includeFilePattern: matcherPatterns.includeFilePattern
|
|
173
|
-
? new RegExp(matcherPatterns.includeFilePattern)
|
|
276
|
+
? new RegExp(matcherPatterns.includeFilePattern, regexFlags)
|
|
174
277
|
: null,
|
|
175
278
|
excludePattern: matcherPatterns.excludePattern
|
|
176
|
-
? new RegExp(matcherPatterns.excludePattern)
|
|
279
|
+
? new RegExp(matcherPatterns.excludePattern, regexFlags)
|
|
177
280
|
: null,
|
|
178
281
|
};
|
|
179
282
|
}
|
|
180
283
|
|
|
181
|
-
function
|
|
284
|
+
function decideFileScope(
|
|
285
|
+
parsed: ParsedProjectConfig,
|
|
286
|
+
absolutePath: string,
|
|
287
|
+
): { included: boolean; basis: ScopeDecisionBasis } {
|
|
182
288
|
const normalizedPath = normalizePath(absolutePath);
|
|
183
|
-
if (parsed.fileNames.has(normalizedPath)) return true;
|
|
289
|
+
if (parsed.fileNames.has(normalizedPath)) return { included: true, basis: "fileNames" };
|
|
184
290
|
|
|
185
291
|
const extension = path.extname(absolutePath).toLowerCase();
|
|
186
|
-
if (!parsed.supportedExtensions.has(extension)) return false;
|
|
292
|
+
if (!parsed.supportedExtensions.has(extension)) return { included: false, basis: "extension" };
|
|
187
293
|
|
|
188
|
-
if (parsed.explicitFiles)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
294
|
+
if (parsed.explicitFiles) {
|
|
295
|
+
return { included: parsed.explicitFiles.has(normalizedPath), basis: "explicit" };
|
|
296
|
+
}
|
|
297
|
+
// Include patterns are rooted at the config directory; a file outside it can
|
|
298
|
+
// only be in scope through the parse-time file set or an explicit files list.
|
|
299
|
+
if (!isWithinOrEqual(parsed.configDir, absolutePath)) {
|
|
300
|
+
return { included: false, basis: "include-pattern" };
|
|
301
|
+
}
|
|
302
|
+
if (parsed.excludePattern?.test(normalizedPath))
|
|
303
|
+
return { included: false, basis: "exclude-pattern" };
|
|
304
|
+
if (parsed.usesDefaultInclude) return { included: true, basis: "default-include" };
|
|
305
|
+
if (!parsed.includeFilePattern) return { included: false, basis: "include-pattern" };
|
|
306
|
+
return { included: parsed.includeFilePattern.test(normalizedPath), basis: "include-pattern" };
|
|
193
307
|
}
|
|
194
308
|
|
|
195
309
|
function getSupportedExtensions(options: ts.CompilerOptions): string[] {
|
|
@@ -229,11 +343,6 @@ function isWithinOrEqual(root: string, target: string): boolean {
|
|
|
229
343
|
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== "..");
|
|
230
344
|
}
|
|
231
345
|
|
|
232
|
-
function normalizePath(target: string): string {
|
|
233
|
-
const resolved = path.resolve(target).replaceAll("\\", "/");
|
|
234
|
-
return ts.sys.useCaseSensitiveFileNames ? resolved : resolved.toLowerCase();
|
|
235
|
-
}
|
|
236
|
-
|
|
237
346
|
/**
|
|
238
347
|
* Clear cached nearest-config lookups and parsed project config state.
|
|
239
348
|
* Useful for testing and after workspace file changes.
|
|
@@ -242,3 +351,46 @@ export function clearTsconfigCache(): void {
|
|
|
242
351
|
nearestConfigCache.clear();
|
|
243
352
|
parsedConfigCache.clear();
|
|
244
353
|
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Invalidate cached state for one config file that changed, was created, or
|
|
357
|
+
* was deleted.
|
|
358
|
+
*
|
|
359
|
+
* Removes the changed config and every cached root config that depends on it,
|
|
360
|
+
* so the next decision re-reads current config data. The wholesale
|
|
361
|
+
* {@link clearTsconfigCache} stays for lifecycle events; change signals use
|
|
362
|
+
* this targeted invalidation instead.
|
|
363
|
+
*/
|
|
364
|
+
export function invalidateTsconfigCacheForConfig(configPath: string): void {
|
|
365
|
+
const normalizedConfigPath = normalizePath(configPath);
|
|
366
|
+
const invalidated = new Set([normalizedConfigPath]);
|
|
367
|
+
|
|
368
|
+
for (const [cachedConfigPath, cacheEntry] of parsedConfigCache) {
|
|
369
|
+
if (cacheEntry.dependencies.has(normalizedConfigPath)) invalidated.add(cachedConfigPath);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
for (const cachedConfigPath of invalidated) {
|
|
373
|
+
parsedConfigCache.delete(cachedConfigPath);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
for (const [key, value] of nearestConfigCache) {
|
|
377
|
+
if (value !== null && invalidated.has(normalizePath(value))) nearestConfigCache.delete(key);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Invalidate nearest-config lookups that found nothing under `dir`.
|
|
383
|
+
*
|
|
384
|
+
* Used when a config is created: directories below the new config that
|
|
385
|
+
* previously resolved to no config may now resolve to it.
|
|
386
|
+
*/
|
|
387
|
+
export function invalidateTsconfigCacheForConfigDir(dir: string): void {
|
|
388
|
+
const normalizedDir = normalizePath(dir);
|
|
389
|
+
for (const [key, value] of nearestConfigCache) {
|
|
390
|
+
if (value !== null) continue;
|
|
391
|
+
const keyDir = key.split("::", 1)[0];
|
|
392
|
+
if (keyDir === normalizedDir || keyDir.startsWith(`${normalizedDir}/`)) {
|
|
393
|
+
nearestConfigCache.delete(key);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
/** Reason for a partial or unavailable single-file result when the push stays tentative. */
|
|
2
|
+
export const TENTATIVE_PUSH_UNAVAILABLE_REASON =
|
|
3
|
+
"The current push publication is tentative: a later diagnostic republish for the same document synchronization is required.";
|
|
4
|
+
|
|
1
5
|
/** A final evidence state for one tracked document. */
|
|
2
6
|
export type DiagnosticEvidenceStatus = "confirmed" | "unconfirmed" | "failed" | "removed";
|
|
3
7
|
|
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { FileChangeType, type FileEvent } from "../config/types.ts";
|
|
4
|
-
import { fileToUri } from "../utils.ts";
|
|
4
|
+
import { fileToUri, uriToFile } from "../utils.ts";
|
|
5
5
|
|
|
6
6
|
const IGNORED_DIRECTORIES = new Set(["node_modules", ".pnpm", ".git", "dist", "coverage"]);
|
|
7
7
|
const ROOT_LOCKFILES = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"];
|
|
8
8
|
|
|
9
|
+
export interface WorkspaceSentinelScanOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Also track every regular source file (not just sentinels). The workspace
|
|
12
|
+
* refresh path uses this to discover files created since the last refresh;
|
|
13
|
+
* the diff then reports them as `sourceChanges` so the caller can pull them.
|
|
14
|
+
*/
|
|
15
|
+
includeSourceFiles?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
9
18
|
/** Build a fresh snapshot of workspace sentinel files and their mtimes. */
|
|
10
|
-
export function scanWorkspaceSentinels(
|
|
19
|
+
export function scanWorkspaceSentinels(
|
|
20
|
+
cwd: string,
|
|
21
|
+
options: WorkspaceSentinelScanOptions = {},
|
|
22
|
+
): Map<string, number> {
|
|
11
23
|
const resolvedCwd = path.resolve(cwd);
|
|
12
24
|
const snapshot = new Map<string, number>();
|
|
13
25
|
|
|
14
26
|
if (!fs.existsSync(resolvedCwd)) return snapshot;
|
|
15
27
|
|
|
16
28
|
try {
|
|
17
|
-
walkWorkspace(resolvedCwd, resolvedCwd, snapshot);
|
|
29
|
+
walkWorkspace(resolvedCwd, resolvedCwd, snapshot, options.includeSourceFiles ?? false);
|
|
18
30
|
} catch {
|
|
19
31
|
return snapshot;
|
|
20
32
|
}
|
|
@@ -48,15 +60,34 @@ export function diffWorkspaceSentinelSnapshot(
|
|
|
48
60
|
return changes.sort((a, b) => a.uri.localeCompare(b.uri));
|
|
49
61
|
}
|
|
50
62
|
|
|
51
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Refresh a previous snapshot and return the new snapshot plus change events.
|
|
65
|
+
*
|
|
66
|
+
* When `includeSourceFiles` is set, the returned `changes` list keeps
|
|
67
|
+
* sentinel-file events only; source-file events are returned separately in
|
|
68
|
+
* `sourceChanges` so the caller can act on them without interpreting every
|
|
69
|
+
* regular file edit as a config-level workspace change.
|
|
70
|
+
*/
|
|
52
71
|
export function syncWorkspaceSentinelSnapshot(
|
|
53
72
|
cwd: string,
|
|
54
73
|
previous: Map<string, number>,
|
|
55
|
-
|
|
56
|
-
|
|
74
|
+
options: WorkspaceSentinelScanOptions = {},
|
|
75
|
+
): { snapshot: Map<string, number>; changes: FileEvent[]; sourceChanges: FileEvent[] } {
|
|
76
|
+
const snapshot = scanWorkspaceSentinels(cwd, options);
|
|
77
|
+
const changes = diffWorkspaceSentinelSnapshot(previous, snapshot);
|
|
78
|
+
if (!options.includeSourceFiles) return { snapshot, changes, sourceChanges: [] };
|
|
79
|
+
|
|
80
|
+
const sourceChanges = changes.filter((change) => {
|
|
81
|
+
const filePath = uriToFile(change.uri);
|
|
82
|
+
return !isWorkspaceSentinelPath(filePath, cwd);
|
|
83
|
+
});
|
|
57
84
|
return {
|
|
58
85
|
snapshot,
|
|
59
|
-
changes:
|
|
86
|
+
changes: changes.filter((change) => {
|
|
87
|
+
const filePath = uriToFile(change.uri);
|
|
88
|
+
return isWorkspaceSentinelPath(filePath, cwd);
|
|
89
|
+
}),
|
|
90
|
+
sourceChanges,
|
|
60
91
|
};
|
|
61
92
|
}
|
|
62
93
|
|
|
@@ -66,7 +97,12 @@ export function isWorkspaceRecoveryTrigger(filePath: string, cwd: string): boole
|
|
|
66
97
|
return isWorkspaceSentinelPath(path.resolve(root, filePath), root);
|
|
67
98
|
}
|
|
68
99
|
|
|
69
|
-
function walkWorkspace(
|
|
100
|
+
function walkWorkspace(
|
|
101
|
+
root: string,
|
|
102
|
+
directory: string,
|
|
103
|
+
snapshot: Map<string, number>,
|
|
104
|
+
includeSourceFiles: boolean,
|
|
105
|
+
): void {
|
|
70
106
|
let entries: fs.Dirent[];
|
|
71
107
|
try {
|
|
72
108
|
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
@@ -80,14 +116,14 @@ function walkWorkspace(root: string, directory: string, snapshot: Map<string, nu
|
|
|
80
116
|
for (const entry of entries) {
|
|
81
117
|
if (entry.isDirectory()) {
|
|
82
118
|
if (IGNORED_DIRECTORIES.has(entry.name)) continue;
|
|
83
|
-
walkWorkspace(root, path.join(directory, entry.name), snapshot);
|
|
119
|
+
walkWorkspace(root, path.join(directory, entry.name), snapshot, includeSourceFiles);
|
|
84
120
|
continue;
|
|
85
121
|
}
|
|
86
122
|
|
|
87
123
|
if (!entry.isFile()) continue;
|
|
88
124
|
|
|
89
125
|
const filePath = path.join(directory, entry.name);
|
|
90
|
-
if (!isWorkspaceSentinelPath(filePath, root)) continue;
|
|
126
|
+
if (!includeSourceFiles && !isWorkspaceSentinelPath(filePath, root)) continue;
|
|
91
127
|
try {
|
|
92
128
|
snapshot.set(filePath, fs.statSync(filePath).mtimeMs);
|
|
93
129
|
} catch {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
mapCodeQueryResult,
|
|
6
6
|
} from "@mrclrchtr/supi-code-runtime/api";
|
|
7
7
|
import type { LspClient } from "../client/client.ts";
|
|
8
|
+
import type { DiagnosticEntry } from "../client/client-document-state.ts";
|
|
8
9
|
import type { Diagnostic } from "../config/types.ts";
|
|
9
10
|
import { relativeFilePathFromUri } from "../diagnostics/diagnostic-summary.ts";
|
|
10
11
|
import { shouldIgnoreLspPath } from "../summary.ts";
|
|
@@ -27,15 +28,15 @@ export async function syncClientFileAndGetDiagnostics(
|
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export function collectOutstandingDiagnosticsDetailed(
|
|
30
|
-
|
|
31
|
+
clientEntries: Iterable<ReadonlyArray<DiagnosticEntry>>,
|
|
31
32
|
cwd: string,
|
|
32
33
|
excludePatterns: string[],
|
|
33
34
|
maxSeverity: number,
|
|
34
35
|
): Array<{ file: string; diagnostics: Diagnostic[] }> {
|
|
35
36
|
const fileDiags = new Map<string, Diagnostic[]>();
|
|
36
37
|
|
|
37
|
-
for (const
|
|
38
|
-
for (const entry of
|
|
38
|
+
for (const entries of clientEntries) {
|
|
39
|
+
for (const entry of entries) {
|
|
39
40
|
const file = relativeFilePathFromUri(entry.uri, cwd);
|
|
40
41
|
if (shouldIgnoreLspPath(file, cwd)) continue;
|
|
41
42
|
if (isExcludedByPattern(file, excludePatterns)) continue;
|