@mrclrchtr/supi-lsp 11.0.1 → 11.2.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 +43 -6
- 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-core/src/debug-registry.ts +41 -1
- package/node_modules/ignore/README.md +36 -10
- package/node_modules/ignore/index.d.ts +3 -2
- package/node_modules/ignore/index.js +57 -53
- package/node_modules/ignore/legacy.js +50 -46
- package/node_modules/ignore/package.json +1 -1
- 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/node_modules/vscode-jsonrpc/README.md +5 -0
- package/node_modules/vscode-jsonrpc/lib/browser/ril.js +8 -0
- package/node_modules/vscode-jsonrpc/lib/common/connection.js +7 -2
- package/node_modules/vscode-jsonrpc/lib/common/ral.d.ts +8 -0
- package/node_modules/vscode-jsonrpc/lib/node/main.d.ts +14 -0
- package/node_modules/vscode-jsonrpc/lib/node/main.js +6 -4
- package/node_modules/vscode-jsonrpc/lib/node/ril.js +5 -1
- package/node_modules/vscode-jsonrpc/package.json +4 -2
- package/node_modules/vscode-languageserver-protocol/package.json +4 -3
- package/node_modules/vscode-languageserver-types/lib/esm/main.js +2 -2
- package/node_modules/vscode-languageserver-types/lib/umd/main.js +2 -2
- package/node_modules/vscode-languageserver-types/package.json +2 -1
- package/package.json +8 -6
- package/src/client/client.ts +44 -5
- 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/manager/manager-project-info.ts +22 -10
- package/src/manager/manager.ts +53 -20
- package/src/provider/refactor-planning.ts +4 -3
- package/src/session/runtime-controller.ts +2 -2
- package/src/session/scanner.ts +27 -15
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { major, prerelease, valid } from "semver";
|
|
5
|
+
import type { ServerConfig, TypeScriptSelection } from "./server-config.ts";
|
|
6
|
+
|
|
7
|
+
interface CompilerPackage {
|
|
8
|
+
directory: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
version?: string;
|
|
11
|
+
bin?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Launch options and truthful compiler facts for one root; never cached across starts. */
|
|
15
|
+
export interface TypeScriptLaunch {
|
|
16
|
+
/** Native packages use their declared Node launcher, without a PATH dependency. */
|
|
17
|
+
command?: string;
|
|
18
|
+
args?: string[];
|
|
19
|
+
initializationOptions: unknown;
|
|
20
|
+
typescript?: TypeScriptSelection;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Retain already-known selection facts when validating launch files fails. */
|
|
24
|
+
class TypeScriptLaunchError extends Error {
|
|
25
|
+
readonly typescript: TypeScriptSelection | undefined;
|
|
26
|
+
constructor(message: string, options: ErrorOptions & { typescript?: TypeScriptSelection }) {
|
|
27
|
+
super(message, options);
|
|
28
|
+
this.typescript = options.typescript;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** True for an installed package name/alias, never a path, URL, or package subpath. */
|
|
33
|
+
export function isTypeScriptPackageName(value: unknown): value is string {
|
|
34
|
+
return (
|
|
35
|
+
typeof value === "string" &&
|
|
36
|
+
/^(?:@[a-z0-9._-]+\/)?[a-z0-9_][a-z0-9._-]*$/i.test(value) &&
|
|
37
|
+
![".", ".."].includes(value)
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
42
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
43
|
+
? (value as Record<string, unknown>)
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Resolve fresh package files using Node's root-relative node_modules search paths.
|
|
48
|
+
* Probing the package directory (rather than require.resolve's cached realpath) both
|
|
49
|
+
* detects broken selections and observes package-manager symlink changes on retry.
|
|
50
|
+
* Package exports need not expose package.json; no compiler code is executed.
|
|
51
|
+
*/
|
|
52
|
+
function findPackage(name: string, from: string): CompilerPackage | undefined {
|
|
53
|
+
const require = createRequire(path.join(from, "__supi_lsp_resolve__.cjs"));
|
|
54
|
+
for (const base of require.resolve.paths(name) ?? []) {
|
|
55
|
+
const directory = path.join(base, name);
|
|
56
|
+
try {
|
|
57
|
+
fs.lstatSync(directory);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") continue;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
return readCompilerPackage(fs.realpathSync(directory));
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readCompilerPackage(directory: string): CompilerPackage {
|
|
68
|
+
const manifest = record(
|
|
69
|
+
JSON.parse(fs.readFileSync(path.join(directory, "package.json"), "utf8")),
|
|
70
|
+
);
|
|
71
|
+
if (
|
|
72
|
+
!manifest ||
|
|
73
|
+
(manifest.name !== undefined && typeof manifest.name !== "string") ||
|
|
74
|
+
(manifest.version !== undefined &&
|
|
75
|
+
(typeof manifest.version !== "string" || !valid(manifest.version)))
|
|
76
|
+
) {
|
|
77
|
+
throw new Error(`Invalid package manifest at ${directory}`);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
directory,
|
|
81
|
+
name: typeof manifest.name === "string" ? manifest.name : undefined,
|
|
82
|
+
version: typeof manifest.version === "string" ? manifest.version : undefined,
|
|
83
|
+
bin: manifest.bin,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Apply the bounded TypeScript compiler policy without changing the settings snapshot.
|
|
88
|
+
* Programmatic configurations without loader provenance remain manual. Explicit
|
|
89
|
+
* launch fields bypass all package probing; a selector may not coexist with them.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveTypeScriptLaunch(
|
|
92
|
+
name: string,
|
|
93
|
+
config: ServerConfig,
|
|
94
|
+
root: string,
|
|
95
|
+
): TypeScriptLaunch {
|
|
96
|
+
const unchanged = { initializationOptions: config.initializationOptions };
|
|
97
|
+
if (name !== "typescript") return unchanged;
|
|
98
|
+
if (isManualTypeScriptLaunch(config, root)) return unchanged;
|
|
99
|
+
const selector = config.typescriptPackage;
|
|
100
|
+
let selected = selector ?? "typescript";
|
|
101
|
+
try {
|
|
102
|
+
if (!isTypeScriptPackageName(selected))
|
|
103
|
+
throw new Error("Expected an installed package name or alias, not a filesystem path.");
|
|
104
|
+
const probe = (installedName: string) => {
|
|
105
|
+
selected = installedName; // Retain the failing candidate's identity for the error.
|
|
106
|
+
return findPackage(installedName, root);
|
|
107
|
+
};
|
|
108
|
+
const selection =
|
|
109
|
+
selector === undefined ? selectAutomaticCompiler(probe) : { selected, pkg: probe(selected) };
|
|
110
|
+
const { pkg } = selection;
|
|
111
|
+
selected = selection.selected;
|
|
112
|
+
if (!pkg) {
|
|
113
|
+
if (selector !== undefined) throw new Error("Selected package is not installed.");
|
|
114
|
+
return unchanged; // Preserve TLS's legacy lookup when neither candidate exists.
|
|
115
|
+
}
|
|
116
|
+
return compilerLaunch(config.initializationOptions, pkg, selected);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
throw new TypeScriptLaunchError(
|
|
119
|
+
`TypeScript launch at ${root}, package "${selected}": ${error instanceof Error ? error.message : String(error)} Repair/install the selected TypeScript package, or use explicit launch configuration; then refresh health. Reload after changing launch settings.`,
|
|
120
|
+
{
|
|
121
|
+
cause: error,
|
|
122
|
+
typescript: error instanceof TypeScriptLaunchError ? error.typescript : undefined,
|
|
123
|
+
},
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** Short-circuit on native metadata; validate only the winner's launch files later. */
|
|
129
|
+
function selectAutomaticCompiler(probe: (name: string) => CompilerPackage | undefined): {
|
|
130
|
+
selected: string;
|
|
131
|
+
pkg?: CompilerPackage;
|
|
132
|
+
} {
|
|
133
|
+
const canonical = probe("typescript");
|
|
134
|
+
if (canonical && compilerBackend(canonical) === "native")
|
|
135
|
+
return { selected: "typescript", pkg: canonical };
|
|
136
|
+
const native = probe("@typescript/native");
|
|
137
|
+
if (native) {
|
|
138
|
+
if (compilerBackend(native) !== "native")
|
|
139
|
+
throw new Error("@typescript/native must alias the stable native typescript package.");
|
|
140
|
+
return { selected: "@typescript/native", pkg: native };
|
|
141
|
+
}
|
|
142
|
+
if (canonical) return { selected: "typescript", pkg: canonical };
|
|
143
|
+
return { selected: "@typescript/typescript6", pkg: probe("@typescript/typescript6") };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Determine launch authority and reject conflicting selectors without probing installations. */
|
|
147
|
+
export function isManualTypeScriptLaunch(config: ServerConfig, root: string): boolean {
|
|
148
|
+
const fields = config.typescriptExplicitFields ?? { command: "programmatic configuration" };
|
|
149
|
+
const tsserver = record(record(config.initializationOptions)?.tsserver);
|
|
150
|
+
const conflicts = [
|
|
151
|
+
...(fields.command ? [`command (${fields.command})`] : []),
|
|
152
|
+
...(fields.args ? [`args (${fields.args})`] : []),
|
|
153
|
+
...(tsserver && Object.hasOwn(tsserver, "path")
|
|
154
|
+
? [
|
|
155
|
+
`initializationOptions.tsserver.path (${fields.tsserverPath ?? "programmatic configuration"})`,
|
|
156
|
+
]
|
|
157
|
+
: []),
|
|
158
|
+
];
|
|
159
|
+
if (config.typescriptPackage !== undefined && conflicts.length) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`TypeScript launch at ${root}: typescriptPackage "${config.typescriptPackage}" (${fields.typescriptPackage ?? "programmatic configuration"}) conflicts with ${conflicts.join(", ")}. Remove the selector or the explicit launch fields and reload.`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
return conflicts.length > 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Classify consulted metadata before considering a lower-priority candidate. */
|
|
168
|
+
function compilerBackend(pkg: CompilerPackage): "native" | "tsserver" {
|
|
169
|
+
if (pkg.name === "@typescript/typescript6" && !pkg.version) return "tsserver";
|
|
170
|
+
if (pkg.name === "typescript" && !pkg.version)
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Selected implementation at ${pkg.directory} has no compiler version in package.json.`,
|
|
173
|
+
);
|
|
174
|
+
const version = pkg.version && !prerelease(pkg.version) ? major(pkg.version) : undefined;
|
|
175
|
+
if (pkg.name === "@typescript/typescript6" && version === 6) return "tsserver";
|
|
176
|
+
if (pkg.name === "typescript" && version !== undefined && version <= 7)
|
|
177
|
+
return version === 7 ? "native" : "tsserver";
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Unsupported automatic TypeScript backend ${pkg.name ?? "(unknown identity)"} ${pkg.version ?? "(unknown version)"}. Select an installed stable TS7 or legacy TypeScript package with typescriptPackage, or configure command/args manually.`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function compilerLaunch(
|
|
184
|
+
initializationOptions: unknown,
|
|
185
|
+
selectedPackage: CompilerPackage,
|
|
186
|
+
selected: string,
|
|
187
|
+
): TypeScriptLaunch {
|
|
188
|
+
let pkg = selectedPackage;
|
|
189
|
+
const backend = compilerBackend(pkg);
|
|
190
|
+
if (backend === "native") return nativeLaunch(initializationOptions, pkg, selected);
|
|
191
|
+
const wrapper = pkg.name === "@typescript/typescript6" ? pkg : undefined;
|
|
192
|
+
if (wrapper) {
|
|
193
|
+
const underlying = findPackage("@typescript/old", wrapper.directory);
|
|
194
|
+
if (!underlying)
|
|
195
|
+
throw new Error(`Wrapper at ${wrapper.directory} has no @typescript/old dependency.`);
|
|
196
|
+
pkg = underlying;
|
|
197
|
+
}
|
|
198
|
+
// TLS ignores an explicit path without implementation version metadata and
|
|
199
|
+
// tries other installations. Fail here rather than report a compiler it won't use.
|
|
200
|
+
if (!pkg.version)
|
|
201
|
+
throw new Error(
|
|
202
|
+
`Selected implementation at ${pkg.directory} has no compiler version in package.json; TLS would ignore this selection.`,
|
|
203
|
+
);
|
|
204
|
+
if (compilerBackend(pkg) !== "tsserver" || pkg.name !== "typescript")
|
|
205
|
+
throw new Error(`Wrapper implementation at ${pkg.directory} must be legacy TypeScript.`);
|
|
206
|
+
const serverPath = path.join(pkg.directory, "lib/tsserver.js");
|
|
207
|
+
if (!fs.statSync(serverPath).isFile()) throw new Error(`No tsserver.js at ${serverPath}.`);
|
|
208
|
+
const options = record(initializationOptions);
|
|
209
|
+
const tsserver = record(options?.tsserver);
|
|
210
|
+
return {
|
|
211
|
+
initializationOptions: { ...options, tsserver: { ...tsserver, path: serverPath } },
|
|
212
|
+
typescript: {
|
|
213
|
+
backend: "tsserver",
|
|
214
|
+
packageName: selected,
|
|
215
|
+
compilerVersion: pkg.version,
|
|
216
|
+
...(wrapper?.version ? { wrapperVersion: wrapper.version } : {}),
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function nativeLaunch(
|
|
222
|
+
initializationOptions: unknown,
|
|
223
|
+
pkg: CompilerPackage,
|
|
224
|
+
selected: string,
|
|
225
|
+
): TypeScriptLaunch {
|
|
226
|
+
const typescript: TypeScriptSelection = {
|
|
227
|
+
backend: "native",
|
|
228
|
+
packageName: selected,
|
|
229
|
+
compilerVersion: pkg.version,
|
|
230
|
+
};
|
|
231
|
+
try {
|
|
232
|
+
return {
|
|
233
|
+
command: process.execPath,
|
|
234
|
+
args: [nativeLauncher(pkg), "--lsp", "--stdio"],
|
|
235
|
+
initializationOptions,
|
|
236
|
+
typescript,
|
|
237
|
+
};
|
|
238
|
+
} catch (error) {
|
|
239
|
+
throw new TypeScriptLaunchError(error instanceof Error ? error.message : String(error), {
|
|
240
|
+
cause: error,
|
|
241
|
+
typescript,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function nativeLauncher(pkg: CompilerPackage): string {
|
|
247
|
+
const executable = record(pkg.bin)?.tsc;
|
|
248
|
+
if (typeof executable !== "string" || !executable || path.isAbsolute(executable))
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Native package at ${pkg.directory} must declare a relative bin.tsc executable.`,
|
|
251
|
+
);
|
|
252
|
+
const launcher = path.resolve(pkg.directory, executable);
|
|
253
|
+
const relative = path.relative(pkg.directory, launcher);
|
|
254
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || !fs.statSync(launcher).isFile())
|
|
255
|
+
throw new Error(`Invalid native tsc executable at ${launcher}.`);
|
|
256
|
+
return launcher;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Non-throwing selection probe shared by discovery and missing-server reporting. */
|
|
260
|
+
export function inspectTypeScriptLaunch(
|
|
261
|
+
name: string,
|
|
262
|
+
config: ServerConfig,
|
|
263
|
+
root: string,
|
|
264
|
+
): Pick<TypeScriptLaunch, "typescript"> & { command: string; failureMessage?: string } {
|
|
265
|
+
try {
|
|
266
|
+
const { command, typescript } = resolveTypeScriptLaunch(name, config, root);
|
|
267
|
+
return { command: command ?? config.command, ...(typescript ? { typescript } : {}) };
|
|
268
|
+
} catch (error) {
|
|
269
|
+
const typescript = error instanceof TypeScriptLaunchError ? error.typescript : undefined;
|
|
270
|
+
return {
|
|
271
|
+
command: typescript?.backend === "native" ? process.execPath : config.command,
|
|
272
|
+
...(typescript ? { typescript } : {}),
|
|
273
|
+
failureMessage: error instanceof Error ? error.message : String(error),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { LspClient } from "../client/client.ts";
|
|
2
2
|
import { getServerOperationSupport } from "../config/operation-support.ts";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ProjectServerInfo,
|
|
5
|
+
ProjectServerStatusReason,
|
|
6
|
+
TypeScriptSelection,
|
|
7
|
+
} from "../config/server-config.ts";
|
|
4
8
|
import { displayRelativeFilePath } from "../summary.ts";
|
|
5
9
|
|
|
6
10
|
interface ProjectServerInfoInput {
|
|
@@ -10,29 +14,37 @@ interface ProjectServerInfoInput {
|
|
|
10
14
|
client: LspClient | undefined;
|
|
11
15
|
unavailableReason?: "missing-command" | "start-failed" | "runtime-error";
|
|
12
16
|
statusReason?: ProjectServerStatusReason;
|
|
17
|
+
typescript?: TypeScriptSelection;
|
|
18
|
+
failureMessage?: string;
|
|
13
19
|
/** Filter files from ambient project status without changing explicit routing. */
|
|
14
20
|
includeOpenFile?: (file: string) => boolean;
|
|
15
21
|
}
|
|
16
22
|
|
|
23
|
+
function projectStatus(input: ProjectServerInfoInput): ProjectServerInfo["status"] {
|
|
24
|
+
if (input.statusReason || input.failureMessage) return "error";
|
|
25
|
+
if (input.client?.status === "running") return "running";
|
|
26
|
+
if (
|
|
27
|
+
input.client?.status === "error" ||
|
|
28
|
+
input.unavailableReason === "start-failed" ||
|
|
29
|
+
input.unavailableReason === "runtime-error"
|
|
30
|
+
)
|
|
31
|
+
return "error";
|
|
32
|
+
return "unavailable";
|
|
33
|
+
}
|
|
34
|
+
|
|
17
35
|
export function buildProjectServerInfo(
|
|
18
36
|
input: ProjectServerInfoInput,
|
|
19
37
|
cwd: string,
|
|
20
38
|
): ProjectServerInfo {
|
|
21
|
-
const status = input
|
|
22
|
-
? "error"
|
|
23
|
-
: input.client?.status === "running"
|
|
24
|
-
? "running"
|
|
25
|
-
: input.client?.status === "error" || input.unavailableReason === "start-failed"
|
|
26
|
-
? "error"
|
|
27
|
-
: input.unavailableReason === "runtime-error"
|
|
28
|
-
? "error"
|
|
29
|
-
: "unavailable";
|
|
39
|
+
const status = projectStatus(input);
|
|
30
40
|
|
|
31
41
|
return {
|
|
32
42
|
name: input.serverName,
|
|
33
43
|
root: input.root,
|
|
34
44
|
fileTypes: input.fileTypes,
|
|
35
45
|
status,
|
|
46
|
+
...(input.typescript ? { typescript: input.typescript } : {}),
|
|
47
|
+
...(input.failureMessage ? { failureMessage: input.failureMessage } : {}),
|
|
36
48
|
...(input.statusReason ? { statusReason: input.statusReason } : {}),
|
|
37
49
|
openFiles:
|
|
38
50
|
input.client?.openFiles
|
package/src/manager/manager.ts
CHANGED
|
@@ -48,6 +48,7 @@ import type {
|
|
|
48
48
|
WorkspaceSymbol,
|
|
49
49
|
} from "../config/types.ts";
|
|
50
50
|
import { FileChangeType } from "../config/types.ts";
|
|
51
|
+
import { inspectTypeScriptLaunch } from "../config/typescript-launch.ts";
|
|
51
52
|
import { boundCwd } from "../debug-telemetry.ts";
|
|
52
53
|
import {
|
|
53
54
|
accumulateOutstandingDiagnostics,
|
|
@@ -437,8 +438,12 @@ export class LspManager {
|
|
|
437
438
|
canServeFile(filePath: string): boolean {
|
|
438
439
|
const route = this.resolveFileRoute(filePath);
|
|
439
440
|
if (!route) return false;
|
|
440
|
-
if (this.getUnavailableReason(route.key, route.
|
|
441
|
-
|
|
441
|
+
if (this.getUnavailableReason(route.key, route.serverName, route.root)) return false;
|
|
442
|
+
if (this.clients.get(route.key)?.status === "running") return true;
|
|
443
|
+
const launch = inspectTypeScriptLaunch(route.serverName, route.serverConfig, route.root);
|
|
444
|
+
// A broken selection is a route failure, not an absent executable. Let
|
|
445
|
+
// startup record it so lazy readiness and health expose the actual error.
|
|
446
|
+
return Boolean(launch.failureMessage) || this.isServerCommandAvailable(launch.command);
|
|
442
447
|
}
|
|
443
448
|
|
|
444
449
|
/**
|
|
@@ -517,13 +522,21 @@ export class LspManager {
|
|
|
517
522
|
return available;
|
|
518
523
|
}
|
|
519
524
|
|
|
520
|
-
private getUnavailableReason(
|
|
525
|
+
private getUnavailableReason(
|
|
526
|
+
key: string,
|
|
527
|
+
serverName: string,
|
|
528
|
+
root: string,
|
|
529
|
+
): UnavailableReason | null {
|
|
521
530
|
const reason = this.unavailable.get(key);
|
|
522
531
|
if (!reason) return null;
|
|
523
532
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
533
|
+
const config = this.config.servers[serverName];
|
|
534
|
+
if (reason === "missing-command" && config) {
|
|
535
|
+
const launch = inspectTypeScriptLaunch(serverName, config, root);
|
|
536
|
+
if (!launch.failureMessage && this.isServerCommandAvailable(launch.command)) {
|
|
537
|
+
this.unavailable.delete(key);
|
|
538
|
+
return null;
|
|
539
|
+
}
|
|
527
540
|
}
|
|
528
541
|
|
|
529
542
|
return reason;
|
|
@@ -573,7 +586,7 @@ export class LspManager {
|
|
|
573
586
|
if (this.shouldBlockRetiredRoute(key, root, retryFailedStart)) return null;
|
|
574
587
|
const routeGeneration = this.getRouteRetirementGeneration(key);
|
|
575
588
|
if (retryFailedStart) this.unavailable.delete(key);
|
|
576
|
-
if (!retryFailedStart && this.getUnavailableReason(key,
|
|
589
|
+
if (!retryFailedStart && this.getUnavailableReason(key, serverName, root)) return null;
|
|
577
590
|
|
|
578
591
|
// Return existing client
|
|
579
592
|
const existing = this.clients.get(key);
|
|
@@ -1460,22 +1473,30 @@ export class LspManager {
|
|
|
1460
1473
|
}
|
|
1461
1474
|
}
|
|
1462
1475
|
|
|
1476
|
+
/** Record a root-aware selection/command failure without starting a client. */
|
|
1477
|
+
private recordUnavailableLaunch(route: FileRoute): boolean {
|
|
1478
|
+
const { serverName, serverConfig, root, key } = route;
|
|
1479
|
+
const launch = inspectTypeScriptLaunch(serverName, serverConfig, root);
|
|
1480
|
+
const command = launch.command;
|
|
1481
|
+
if (!launch.failureMessage && commandExists(command)) return false;
|
|
1482
|
+
this.unavailable.set(key, launch.failureMessage ? "start-failed" : "missing-command");
|
|
1483
|
+
this.failedStarts.set(key, {
|
|
1484
|
+
serverName,
|
|
1485
|
+
root,
|
|
1486
|
+
failureMessage:
|
|
1487
|
+
launch.failureMessage ?? `Configured server command is not available: ${command}`,
|
|
1488
|
+
});
|
|
1489
|
+
return true;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1463
1492
|
/**
|
|
1464
1493
|
* Perform the actual server start — extracted so the public method can
|
|
1465
1494
|
* deduplicate via pendingStarts without wrapping the entire body.
|
|
1466
1495
|
*/
|
|
1467
1496
|
private async performStart(route: FileRoute, routeGeneration: number): Promise<LspClient | null> {
|
|
1468
1497
|
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
|
-
}
|
|
1498
|
+
// Client.start resolves again at actual spawn time, including every retry.
|
|
1499
|
+
if (this.recordUnavailableLaunch(route)) return null;
|
|
1479
1500
|
|
|
1480
1501
|
// Spawn new client
|
|
1481
1502
|
const client = this.createClient(serverName, serverConfig, root, key);
|
|
@@ -1659,14 +1680,26 @@ export class LspManager {
|
|
|
1659
1680
|
|
|
1660
1681
|
getProjectServerInfo(serverName: string, root: string, fileTypes: string[]): ProjectServerInfo {
|
|
1661
1682
|
const key = clientKey(serverName, root);
|
|
1683
|
+
const client = this.clients.get(key);
|
|
1684
|
+
const config = this.config.servers[serverName];
|
|
1685
|
+
const selection: Pick<DetectedProjectServer, "typescript" | "failureMessage"> =
|
|
1686
|
+
client?.status === "running"
|
|
1687
|
+
? { typescript: client.typescriptSelection }
|
|
1688
|
+
: config
|
|
1689
|
+
? inspectTypeScriptLaunch(serverName, config, root)
|
|
1690
|
+
: {};
|
|
1662
1691
|
return buildProjectServerInfo(
|
|
1663
1692
|
{
|
|
1693
|
+
...selection,
|
|
1694
|
+
failureMessage:
|
|
1695
|
+
this.failedStarts.get(key)?.failureMessage ??
|
|
1696
|
+
this.processCrashRecoveries.get(key)?.failureMessage ??
|
|
1697
|
+
selection.failureMessage,
|
|
1664
1698
|
serverName,
|
|
1665
1699
|
root,
|
|
1666
1700
|
fileTypes,
|
|
1667
1701
|
client: this.clients.get(key),
|
|
1668
|
-
unavailableReason:
|
|
1669
|
-
this.getUnavailableReason(key, this.config.servers[serverName]?.command) ?? undefined,
|
|
1702
|
+
unavailableReason: this.getUnavailableReason(key, serverName, root) ?? undefined,
|
|
1670
1703
|
statusReason: this.processCrashRecoveries.get(key)?.statusReason,
|
|
1671
1704
|
includeOpenFile: (file) => this.isAutomaticScopePath(file),
|
|
1672
1705
|
},
|
|
@@ -2975,7 +3008,7 @@ export class LspManager {
|
|
|
2975
3008
|
control?: CodeRequestControl,
|
|
2976
3009
|
): Promise<{ kind: "enrolled" } | { kind: "unavailable"; reason: string }> {
|
|
2977
3010
|
throwIfCodeRequestInterrupted(control);
|
|
2978
|
-
const files = [...new Set(filePaths)].sort();
|
|
3011
|
+
const files = [...new Set(filePaths)].sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
|
2979
3012
|
const routeKey = clientKey(client.name, client.root);
|
|
2980
3013
|
if (this.clients.get(routeKey) !== client) {
|
|
2981
3014
|
return { kind: "unavailable", reason: "The semantic route changed during rename planning." };
|
|
@@ -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,
|
package/src/session/scanner.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
MissingServer,
|
|
8
8
|
ServerConfig,
|
|
9
9
|
} from "../config/types.ts";
|
|
10
|
+
import { inspectTypeScriptLaunch } from "../config/typescript-launch.ts";
|
|
10
11
|
import type { LspManager } from "../manager/manager.ts";
|
|
11
12
|
import { commandExists } from "../utils.ts";
|
|
12
13
|
import {
|
|
@@ -32,20 +33,16 @@ export function scanProjectCapabilities(
|
|
|
32
33
|
|
|
33
34
|
return Object.entries(config.servers)
|
|
34
35
|
.flatMap(([serverName, server]) => {
|
|
35
|
-
if (!commandExists(server.command)) return [];
|
|
36
36
|
const allRoots = new Set(markerMatches.get(serverName) ?? []);
|
|
37
37
|
if (extensionBasedServers.has(serverName)) {
|
|
38
38
|
allRoots.add(cwd);
|
|
39
39
|
}
|
|
40
40
|
const roots = dedupeTopmostRoots(Array.from(allRoots));
|
|
41
|
-
return roots.
|
|
42
|
-
(root)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
fileTypes: [...server.fileTypes],
|
|
47
|
-
}) satisfies DetectedProjectServer,
|
|
48
|
-
);
|
|
41
|
+
return roots.flatMap((root) => {
|
|
42
|
+
const { command, ...selection } = inspectTypeScriptLaunch(serverName, server, root);
|
|
43
|
+
if (!selection.failureMessage && !commandExists(command)) return [];
|
|
44
|
+
return [{ name: serverName, root, fileTypes: [...server.fileTypes], ...selection }];
|
|
45
|
+
});
|
|
49
46
|
})
|
|
50
47
|
.sort((a, b) => a.root.localeCompare(b.root) || a.name.localeCompare(b.name));
|
|
51
48
|
}
|
|
@@ -79,8 +76,6 @@ function inspectDirectory(
|
|
|
79
76
|
ctx: HintContext,
|
|
80
77
|
): void {
|
|
81
78
|
for (const [serverName, server] of Object.entries(servers)) {
|
|
82
|
-
if (!commandExists(server.command)) continue;
|
|
83
|
-
|
|
84
79
|
if (server.rootMarkers.length === 0) {
|
|
85
80
|
if (hasMatchingFile(directory, entryNames, server.fileTypes)) {
|
|
86
81
|
ctx.extensionBasedServers.add(serverName);
|
|
@@ -122,7 +117,7 @@ export async function startDetectedServers(
|
|
|
122
117
|
|
|
123
118
|
/**
|
|
124
119
|
* Scan the project for languages whose source files exist but whose LSP server
|
|
125
|
-
*
|
|
120
|
+
* executable or selected compiler is unavailable. Walks the project tree collecting
|
|
126
121
|
* file extensions, then checks each configured server.
|
|
127
122
|
*/
|
|
128
123
|
export function scanMissingServers(
|
|
@@ -142,14 +137,31 @@ export function scanMissingServers(
|
|
|
142
137
|
});
|
|
143
138
|
|
|
144
139
|
const missing: MissingServer[] = [];
|
|
140
|
+
const { markerMatches } = collectServerHints(config, cwd, maxDepth, policy);
|
|
145
141
|
|
|
146
142
|
for (const [name, server] of Object.entries(config.servers)) {
|
|
147
|
-
if (commandExists(server.command)) continue;
|
|
148
|
-
|
|
149
143
|
const matching = server.fileTypes.filter((ft) => foundExtensions.has(ft));
|
|
150
144
|
if (matching.length === 0) continue;
|
|
151
145
|
|
|
152
|
-
|
|
146
|
+
const roots = dedupeTopmostRoots(Array.from(markerMatches.get(name) ?? []));
|
|
147
|
+
for (const root of roots.length ? roots : [cwd]) {
|
|
148
|
+
const selection = inspectTypeScriptLaunch(name, server, root);
|
|
149
|
+
const command = selection.command;
|
|
150
|
+
if (selection.failureMessage) {
|
|
151
|
+
missing.push({
|
|
152
|
+
name,
|
|
153
|
+
command,
|
|
154
|
+
foundExtensions: matching,
|
|
155
|
+
root,
|
|
156
|
+
failureMessage: selection.failureMessage,
|
|
157
|
+
});
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!commandExists(command)) {
|
|
161
|
+
missing.push({ name, command, foundExtensions: matching });
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
return missing;
|