@mrclrchtr/supi-lsp 11.1.0 → 11.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -7
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +3 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/query-result.ts +9 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/child-web-tools.ts +110 -0
- package/node_modules/@mrclrchtr/supi-core/src/session.ts +11 -0
- package/node_modules/semver/LICENSE +15 -0
- package/node_modules/semver/README.md +680 -0
- package/node_modules/semver/bin/semver.js +195 -0
- package/node_modules/semver/classes/comparator.js +143 -0
- package/node_modules/semver/classes/index.js +7 -0
- package/node_modules/semver/classes/range.js +577 -0
- package/node_modules/semver/classes/semver.js +350 -0
- package/node_modules/semver/functions/clean.js +8 -0
- package/node_modules/semver/functions/cmp.js +54 -0
- package/node_modules/semver/functions/coerce.js +62 -0
- package/node_modules/semver/functions/compare-build.js +9 -0
- package/node_modules/semver/functions/compare-loose.js +5 -0
- package/node_modules/semver/functions/compare.js +7 -0
- package/node_modules/semver/functions/diff.js +60 -0
- package/node_modules/semver/functions/eq.js +5 -0
- package/node_modules/semver/functions/gt.js +5 -0
- package/node_modules/semver/functions/gte.js +5 -0
- package/node_modules/semver/functions/inc.js +21 -0
- package/node_modules/semver/functions/lt.js +5 -0
- package/node_modules/semver/functions/lte.js +5 -0
- package/node_modules/semver/functions/major.js +5 -0
- package/node_modules/semver/functions/minor.js +5 -0
- package/node_modules/semver/functions/neq.js +5 -0
- package/node_modules/semver/functions/parse.js +18 -0
- package/node_modules/semver/functions/patch.js +5 -0
- package/node_modules/semver/functions/prerelease.js +8 -0
- package/node_modules/semver/functions/rcompare.js +5 -0
- package/node_modules/semver/functions/rsort.js +5 -0
- package/node_modules/semver/functions/satisfies.js +12 -0
- package/node_modules/semver/functions/sort.js +5 -0
- package/node_modules/semver/functions/truncate.js +48 -0
- package/node_modules/semver/functions/valid.js +8 -0
- package/node_modules/semver/index.js +93 -0
- package/node_modules/semver/internal/constants.js +37 -0
- package/node_modules/semver/internal/debug.js +11 -0
- package/node_modules/semver/internal/identifiers.js +29 -0
- package/node_modules/semver/internal/lrucache.js +42 -0
- package/node_modules/semver/internal/parse-options.js +17 -0
- package/node_modules/semver/internal/re.js +223 -0
- package/node_modules/semver/package.json +78 -0
- package/node_modules/semver/preload.js +4 -0
- package/node_modules/semver/range.bnf +17 -0
- package/node_modules/semver/ranges/gtr.js +6 -0
- package/node_modules/semver/ranges/intersects.js +9 -0
- package/node_modules/semver/ranges/ltr.js +6 -0
- package/node_modules/semver/ranges/max-satisfying.js +27 -0
- package/node_modules/semver/ranges/min-satisfying.js +26 -0
- package/node_modules/semver/ranges/min-version.js +63 -0
- package/node_modules/semver/ranges/outside.js +82 -0
- package/node_modules/semver/ranges/simplify.js +49 -0
- package/node_modules/semver/ranges/subset.js +249 -0
- package/node_modules/semver/ranges/to-comparators.js +10 -0
- package/node_modules/semver/ranges/valid.js +13 -0
- package/package.json +5 -3
- package/src/client/client-diagnostic-collection.ts +11 -4
- package/src/client/client-diagnostics.ts +77 -6
- package/src/client/client-input-admission.ts +149 -0
- package/src/client/client-typescript-project.ts +40 -0
- package/src/client/client.ts +174 -49
- package/src/config/config.ts +53 -2
- package/src/config/operation-support.ts +19 -4
- package/src/config/server-config.ts +22 -0
- package/src/config/typescript-launch.ts +276 -0
- package/src/diagnostics/evidence.ts +2 -2
- package/src/manager/manager-project-info.ts +22 -10
- package/src/manager/manager-workspace-symbol.ts +157 -23
- package/src/manager/manager.ts +306 -75
- package/src/provider/refactor-planning.ts +4 -3
- package/src/session/runtime-controller.ts +2 -2
- package/src/session/runtime-registry.ts +19 -2
- package/src/session/runtime-rename.ts +1 -0
- package/src/session/scanner.ts +27 -15
- package/src/session/workspace-lsp-runtime.ts +5 -1
package/src/client/client.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type {
|
|
|
51
51
|
WorkspaceEdit,
|
|
52
52
|
WorkspaceSymbol,
|
|
53
53
|
} from "../config/types.ts";
|
|
54
|
+
import { resolveTypeScriptLaunch, type TypeScriptLaunch } from "../config/typescript-launch.ts";
|
|
54
55
|
import { boundCwd } from "../debug-telemetry.ts";
|
|
55
56
|
import type { DiagnosticEvidenceSummary } from "../diagnostics/evidence.ts";
|
|
56
57
|
import { raceRequestControl } from "../session/readiness.ts";
|
|
@@ -79,6 +80,11 @@ import type {
|
|
|
79
80
|
SemanticInputSnapshot,
|
|
80
81
|
} from "./client-semantic-input-barrier.ts";
|
|
81
82
|
import { SemanticInputSynchronizationError } from "./client-semantic-input-errors.ts";
|
|
83
|
+
import {
|
|
84
|
+
type ProjectMembership,
|
|
85
|
+
readTypeScriptProjectMembership,
|
|
86
|
+
typeScriptProjectInfoParams,
|
|
87
|
+
} from "./client-typescript-project.ts";
|
|
82
88
|
import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
|
|
83
89
|
|
|
84
90
|
const SHUTDOWN_TIMEOUT_MS = 5_000;
|
|
@@ -88,6 +94,12 @@ const CONTENT_MODIFIED_RETRY_DELAY_MS = 100;
|
|
|
88
94
|
|
|
89
95
|
/** Bounded stderr tail kept for startup-failure error messages. */
|
|
90
96
|
const STDERR_TAIL_MAX_CHARS = 4_000;
|
|
97
|
+
/**
|
|
98
|
+
* Bound for draining a failed startup's stdio. An initialize write can fail
|
|
99
|
+
* (`EPIPE`) before the exiting server's stderr is read; waiting for `close`
|
|
100
|
+
* lets the failure report the server's own reason instead of the transport error.
|
|
101
|
+
*/
|
|
102
|
+
const STARTUP_FAILURE_DRAIN_MS = 500;
|
|
91
103
|
|
|
92
104
|
interface SemanticRequestAttemptState {
|
|
93
105
|
inputRevision: number | undefined;
|
|
@@ -491,6 +503,20 @@ function killFailedStartupProcess(child: ChildProcess): void {
|
|
|
491
503
|
child.kill();
|
|
492
504
|
}
|
|
493
505
|
|
|
506
|
+
/** Resolve once `closed()` holds or the child emits `close`, bounded by `ms`. */
|
|
507
|
+
function waitForStdioClose(child: ChildProcess, closed: () => boolean, ms: number): Promise<void> {
|
|
508
|
+
if (closed()) return Promise.resolve();
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
const done = () => {
|
|
511
|
+
clearTimeout(timer);
|
|
512
|
+
child.off("close", done);
|
|
513
|
+
resolve();
|
|
514
|
+
};
|
|
515
|
+
const timer = setTimeout(done, ms);
|
|
516
|
+
child.once("close", done);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
494
520
|
// ── Types ─────────────────────────────────────────────────────────────
|
|
495
521
|
export type ClientStatus = "initializing" | "running" | "error" | "shutdown";
|
|
496
522
|
|
|
@@ -591,6 +617,13 @@ export class LspClient {
|
|
|
591
617
|
});
|
|
592
618
|
}
|
|
593
619
|
|
|
620
|
+
private launchSelection: TypeScriptLaunch | undefined;
|
|
621
|
+
|
|
622
|
+
/** Compiler used by this generation, never re-probed while it is healthy. */
|
|
623
|
+
get typescriptSelection() {
|
|
624
|
+
return this.launchSelection?.typescript;
|
|
625
|
+
}
|
|
626
|
+
|
|
594
627
|
get status(): ClientStatus {
|
|
595
628
|
return this._status;
|
|
596
629
|
}
|
|
@@ -670,8 +703,14 @@ export class LspClient {
|
|
|
670
703
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
671
704
|
/** Spawn the server process and perform the initialize handshake. */
|
|
672
705
|
async start(): Promise<void> {
|
|
673
|
-
|
|
674
|
-
|
|
706
|
+
try {
|
|
707
|
+
this.launchSelection = resolveTypeScriptLaunch(this.name, this.config, this.root);
|
|
708
|
+
} catch (error) {
|
|
709
|
+
this.handleProcessFailure(error instanceof Error ? error : new Error(String(error)));
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
const { command: cmd = this.config.command, args = this.config.args ?? [] } =
|
|
713
|
+
this.launchSelection;
|
|
675
714
|
this.startedAt = Date.now();
|
|
676
715
|
|
|
677
716
|
try {
|
|
@@ -712,19 +751,24 @@ export class LspClient {
|
|
|
712
751
|
});
|
|
713
752
|
this.rpc.onRequest((method, params) => this.handleServerRequest(method, params));
|
|
714
753
|
|
|
715
|
-
// Handle crashes
|
|
754
|
+
// Handle crashes. A failing initialize reports its own detailed failure.
|
|
755
|
+
let reportingStartupFailure = false;
|
|
716
756
|
this.process.on("exit", (_code) => {
|
|
717
|
-
this.handleProcessFailure(new Error("Client crashed"));
|
|
757
|
+
if (!reportingStartupFailure) this.handleProcessFailure(new Error("Client crashed"));
|
|
718
758
|
});
|
|
719
759
|
|
|
720
760
|
this.process.on("error", (_err) => {
|
|
721
|
-
this.handleProcessFailure(new Error("Client process error"));
|
|
761
|
+
if (!reportingStartupFailure) this.handleProcessFailure(new Error("Client process error"));
|
|
722
762
|
});
|
|
723
763
|
|
|
724
764
|
// Capture a bounded stderr tail (discarded otherwise) so startup
|
|
725
765
|
// failures can surface the server's own exit reason — expired builds,
|
|
726
766
|
// missing runtimes, or invalid flags. Stderr never reaches the agent.
|
|
727
767
|
let stderrTail = "";
|
|
768
|
+
let stdioClosed = false;
|
|
769
|
+
this.process.once("close", () => {
|
|
770
|
+
stdioClosed = true;
|
|
771
|
+
});
|
|
728
772
|
this.process.stderr?.on("data", (chunk: Buffer | string) => {
|
|
729
773
|
stderrTail = `${stderrTail}${chunk}`.slice(-STDERR_TAIL_MAX_CHARS);
|
|
730
774
|
});
|
|
@@ -741,7 +785,7 @@ export class LspClient {
|
|
|
741
785
|
},
|
|
742
786
|
],
|
|
743
787
|
capabilities: CLIENT_CAPABILITIES,
|
|
744
|
-
initializationOptions: this.
|
|
788
|
+
initializationOptions: this.launchSelection.initializationOptions,
|
|
745
789
|
})) as InitializeResult;
|
|
746
790
|
|
|
747
791
|
const positionEncoding = result.capabilities.positionEncoding ?? "utf-16";
|
|
@@ -763,12 +807,16 @@ export class LspClient {
|
|
|
763
807
|
|
|
764
808
|
this.armNoProgressTimer();
|
|
765
809
|
} catch (err) {
|
|
810
|
+
const child = this.process;
|
|
811
|
+
reportingStartupFailure = true;
|
|
812
|
+
killFailedStartupProcess(child);
|
|
813
|
+
// Let already-written stderr drain before reporting the failure.
|
|
814
|
+
await waitForStdioClose(child, () => stdioClosed, STARTUP_FAILURE_DRAIN_MS);
|
|
766
815
|
const stderrDetail = stderrTail.trim() ? `\n${this.name} stderr:\n${stderrTail.trim()}` : "";
|
|
767
816
|
const failure = new Error(`${this.name}: initialize failed: ${err}${stderrDetail}`, {
|
|
768
817
|
cause: err,
|
|
769
818
|
});
|
|
770
819
|
this.handleProcessFailure(failure);
|
|
771
|
-
killFailedStartupProcess(this.process);
|
|
772
820
|
throw failure;
|
|
773
821
|
}
|
|
774
822
|
}
|
|
@@ -865,6 +913,33 @@ export class LspClient {
|
|
|
865
913
|
}
|
|
866
914
|
|
|
867
915
|
// ── Document Synchronization and Diagnostics ────────────────────────
|
|
916
|
+
/**
|
|
917
|
+
* Admit a first open between semantic attempts, reading content after the wait.
|
|
918
|
+
* Semantic/refactor callers protect their original inputs; ordinary tracking
|
|
919
|
+
* can adopt fresh content after a known change instead.
|
|
920
|
+
*/
|
|
921
|
+
async openDocument(
|
|
922
|
+
filePath: string,
|
|
923
|
+
readContent: () => string,
|
|
924
|
+
control?: CodeRequestControl,
|
|
925
|
+
protectInputs = false,
|
|
926
|
+
): Promise<void> {
|
|
927
|
+
const trackedCount = this.openFiles.length;
|
|
928
|
+
await this.diagnostics.openDocument(filePath, readContent, control, protectInputs);
|
|
929
|
+
this.publishTrackedFileChange(trackedCount);
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/** Restore retained state without resurrecting a closed or replaced document. */
|
|
933
|
+
async restoreDocument(
|
|
934
|
+
filePath: string,
|
|
935
|
+
readContent: () => string,
|
|
936
|
+
isCurrent: () => boolean,
|
|
937
|
+
): Promise<void> {
|
|
938
|
+
const trackedCount = this.openFiles.length;
|
|
939
|
+
await this.diagnostics.restoreDocument(filePath, readContent, isCurrent);
|
|
940
|
+
this.publishTrackedFileChange(trackedCount);
|
|
941
|
+
}
|
|
942
|
+
|
|
868
943
|
/** Open a document, or update it when it is already open. */
|
|
869
944
|
didOpen(filePath: string, content: string): void {
|
|
870
945
|
const trackedCount = this.openFiles.length;
|
|
@@ -1147,6 +1222,23 @@ export class LspClient {
|
|
|
1147
1222
|
);
|
|
1148
1223
|
}
|
|
1149
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Report whether the server placed an open file in a configured project.
|
|
1227
|
+
* Only the TypeScript route can answer; other routes return `unknown`.
|
|
1228
|
+
*/
|
|
1229
|
+
async projectMembership(
|
|
1230
|
+
filePath: string,
|
|
1231
|
+
control?: CodeRequestControl,
|
|
1232
|
+
): Promise<ProjectMembership> {
|
|
1233
|
+
if (!this.hasTypeScriptRequestCommand()) return "unknown";
|
|
1234
|
+
const result = await this.query<unknown>(
|
|
1235
|
+
"workspace/executeCommand",
|
|
1236
|
+
typeScriptProjectInfoParams(fileToUri(filePath)),
|
|
1237
|
+
control,
|
|
1238
|
+
);
|
|
1239
|
+
return result.kind === "completed" ? readTypeScriptProjectMembership(result.data) : "unknown";
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1150
1242
|
async workspaceSymbol(
|
|
1151
1243
|
query: string,
|
|
1152
1244
|
control?: CodeRequestControl,
|
|
@@ -1244,6 +1336,7 @@ export class LspClient {
|
|
|
1244
1336
|
| undefined;
|
|
1245
1337
|
},
|
|
1246
1338
|
{
|
|
1339
|
+
documentUri: params.textDocument.uri,
|
|
1247
1340
|
inputRevision: options.inputRevision,
|
|
1248
1341
|
onVerifiedSnapshot: (snapshot) => {
|
|
1249
1342
|
inputRevision = snapshot.revision;
|
|
@@ -1273,8 +1366,10 @@ export class LspClient {
|
|
|
1273
1366
|
context: CodeActionContext,
|
|
1274
1367
|
control?: CodeRequestControl,
|
|
1275
1368
|
): Promise<CodeQueryResult<CodeAction[] | null>> {
|
|
1369
|
+
const backend = this.typescriptSelection?.backend;
|
|
1370
|
+
const server = `LSP server "${this.name}"${backend ? ` (${backend} backend)` : ""}`;
|
|
1276
1371
|
if (!this.supportsCodeActions) {
|
|
1277
|
-
return unavailableCodeQuery(
|
|
1372
|
+
return unavailableCodeQuery(`${server} does not advertise textDocument/codeAction.`);
|
|
1278
1373
|
}
|
|
1279
1374
|
if (typeof filePath !== "string" || filePath.length === 0 || !isRange(range)) {
|
|
1280
1375
|
return unavailableCodeQuery("Code actions require a valid file and range.");
|
|
@@ -1283,7 +1378,9 @@ export class LspClient {
|
|
|
1283
1378
|
return unavailableCodeQuery("Code actions require a valid context.");
|
|
1284
1379
|
}
|
|
1285
1380
|
if (context.only && !this.supportsCodeActionKinds(context.only)) {
|
|
1286
|
-
return unavailableCodeQuery(
|
|
1381
|
+
return unavailableCodeQuery(
|
|
1382
|
+
`${server} does not advertise the requested code-action kind: ${context.only.join(", ")}.`,
|
|
1383
|
+
);
|
|
1287
1384
|
}
|
|
1288
1385
|
|
|
1289
1386
|
const requestContext: CodeActionContext = {
|
|
@@ -1291,17 +1388,22 @@ export class LspClient {
|
|
|
1291
1388
|
triggerKind: context.triggerKind ?? CodeActionTriggerKind.Invoked,
|
|
1292
1389
|
...(context.only ? { only: [...context.only] } : {}),
|
|
1293
1390
|
};
|
|
1294
|
-
return this.runRefactorTransaction(
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1391
|
+
return this.runRefactorTransaction(
|
|
1392
|
+
"textDocument/codeAction",
|
|
1393
|
+
control,
|
|
1394
|
+
async (transaction) => {
|
|
1395
|
+
const raw = await transaction.rpc.sendRequest(
|
|
1396
|
+
"textDocument/codeAction",
|
|
1397
|
+
{ textDocument: { uri: fileToUri(filePath) }, range, context: requestContext },
|
|
1398
|
+
control,
|
|
1399
|
+
);
|
|
1400
|
+
if (raw === null || raw === undefined) return null;
|
|
1401
|
+
if (!Array.isArray(raw)) throw new Error("LSP server returned malformed code actions.");
|
|
1302
1402
|
|
|
1303
|
-
|
|
1304
|
-
|
|
1403
|
+
return this.resolveCodeActions(raw, transaction, control, requestContext.only);
|
|
1404
|
+
},
|
|
1405
|
+
{ documentUri: fileToUri(filePath) },
|
|
1406
|
+
);
|
|
1305
1407
|
}
|
|
1306
1408
|
|
|
1307
1409
|
async implementation(
|
|
@@ -1366,9 +1468,10 @@ export class LspClient {
|
|
|
1366
1468
|
control: CodeRequestControl | undefined,
|
|
1367
1469
|
operation: (transaction: RefactorTransaction) => Promise<T | null | undefined>,
|
|
1368
1470
|
options: {
|
|
1471
|
+
readonly documentUri: string;
|
|
1369
1472
|
readonly onVerifiedSnapshot?: (snapshot: SemanticInputSnapshot) => void;
|
|
1370
1473
|
readonly inputRevision?: number;
|
|
1371
|
-
}
|
|
1474
|
+
},
|
|
1372
1475
|
): Promise<CodeQueryResult<T | null>> {
|
|
1373
1476
|
throwIfCodeRequestInterrupted(control);
|
|
1374
1477
|
if (!this.rpc || this._status !== "running") {
|
|
@@ -1387,6 +1490,7 @@ export class LspClient {
|
|
|
1387
1490
|
retryGuard.assertCurrent();
|
|
1388
1491
|
const data = await this.runRefactorTransactionAttempt({
|
|
1389
1492
|
method,
|
|
1493
|
+
documentUri: options.documentUri,
|
|
1390
1494
|
control,
|
|
1391
1495
|
operation,
|
|
1392
1496
|
retryGuard,
|
|
@@ -1428,9 +1532,10 @@ export class LspClient {
|
|
|
1428
1532
|
}
|
|
1429
1533
|
}
|
|
1430
1534
|
|
|
1431
|
-
/**
|
|
1535
|
+
/** Admit one prepare/request/resolve attempt, not retries or rename's own enrollment. */
|
|
1432
1536
|
private async runRefactorTransactionAttempt<T>(options: {
|
|
1433
1537
|
method: string;
|
|
1538
|
+
documentUri: string;
|
|
1434
1539
|
control?: CodeRequestControl;
|
|
1435
1540
|
operation: (transaction: RefactorTransaction) => Promise<T | null | undefined>;
|
|
1436
1541
|
retryGuard: SemanticInputRequestRetryGuard;
|
|
@@ -1441,27 +1546,36 @@ export class LspClient {
|
|
|
1441
1546
|
throwIfCodeRequestInterrupted(control);
|
|
1442
1547
|
retryGuard.assertCurrent();
|
|
1443
1548
|
await this.getReady(control);
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
throwIfCodeRequestInterrupted(control);
|
|
1447
|
-
retryGuard.assertCurrent();
|
|
1448
|
-
const rpc = this.rpc;
|
|
1449
|
-
if (!rpc || this._status !== "running") {
|
|
1450
|
-
throw new Error(`LSP request ${method} is unavailable because the client is not running.`);
|
|
1451
|
-
}
|
|
1452
|
-
const data = await operation({
|
|
1453
|
-
rpc,
|
|
1454
|
-
assertCurrent: async () => {
|
|
1549
|
+
return this.diagnostics.withSemanticRequest(
|
|
1550
|
+
async () => {
|
|
1455
1551
|
retryGuard.assertCurrent();
|
|
1456
|
-
|
|
1552
|
+
const inputSnapshot = await this.diagnostics.synchronizeSemanticInputs(control);
|
|
1553
|
+
attempt.inputRevision = inputSnapshot.revision;
|
|
1554
|
+
throwIfCodeRequestInterrupted(control);
|
|
1555
|
+
retryGuard.assertCurrent();
|
|
1556
|
+
const rpc = this.rpc;
|
|
1557
|
+
if (!rpc || this._status !== "running") {
|
|
1558
|
+
throw new Error(
|
|
1559
|
+
`LSP request ${method} is unavailable because the client is not running.`,
|
|
1560
|
+
);
|
|
1561
|
+
}
|
|
1562
|
+
const data = await operation({
|
|
1563
|
+
rpc,
|
|
1564
|
+
assertCurrent: async () => {
|
|
1565
|
+
retryGuard.assertCurrent();
|
|
1566
|
+
return this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
const verifiedSnapshot = await this.diagnostics.assertSemanticInputsCurrent(
|
|
1570
|
+
inputSnapshot,
|
|
1571
|
+
control,
|
|
1572
|
+
);
|
|
1573
|
+
onVerifiedSnapshot?.(verifiedSnapshot);
|
|
1574
|
+
return data ?? null;
|
|
1457
1575
|
},
|
|
1458
|
-
});
|
|
1459
|
-
const verifiedSnapshot = await this.diagnostics.assertSemanticInputsCurrent(
|
|
1460
|
-
inputSnapshot,
|
|
1461
1576
|
control,
|
|
1577
|
+
options.documentUri,
|
|
1462
1578
|
);
|
|
1463
|
-
onVerifiedSnapshot?.(verifiedSnapshot);
|
|
1464
|
-
return data ?? null;
|
|
1465
1579
|
}
|
|
1466
1580
|
|
|
1467
1581
|
private async query<T>(
|
|
@@ -1633,17 +1747,28 @@ export class LspClient {
|
|
|
1633
1747
|
throwIfCodeRequestInterrupted(control);
|
|
1634
1748
|
retryGuard.assertCurrent();
|
|
1635
1749
|
await this.getReady(control);
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1750
|
+
// Local query methods construct the standard TextDocumentIdentifier envelope.
|
|
1751
|
+
const documentUri = (params as { textDocument?: { uri: string } } | null)?.textDocument?.uri;
|
|
1752
|
+
return this.diagnostics.withSemanticRequest(
|
|
1753
|
+
async () => {
|
|
1754
|
+
retryGuard.assertCurrent();
|
|
1755
|
+
const inputSnapshot = await this.diagnostics.synchronizeSemanticInputs(control);
|
|
1756
|
+
attempt.inputRevision = inputSnapshot.revision;
|
|
1757
|
+
throwIfCodeRequestInterrupted(control);
|
|
1758
|
+
retryGuard.assertCurrent();
|
|
1759
|
+
const rpc = this.rpc;
|
|
1760
|
+
if (!rpc || this._status !== "running") {
|
|
1761
|
+
throw new Error(
|
|
1762
|
+
`LSP request ${method} is unavailable because the client is not running.`,
|
|
1763
|
+
);
|
|
1764
|
+
}
|
|
1765
|
+
const data = (await rpc.sendRequest(method, params, control)) as T | null | undefined;
|
|
1766
|
+
await this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
|
|
1767
|
+
return data ?? null;
|
|
1768
|
+
},
|
|
1769
|
+
control,
|
|
1770
|
+
documentUri,
|
|
1771
|
+
);
|
|
1647
1772
|
}
|
|
1648
1773
|
|
|
1649
1774
|
private handleServerRequest(method: string, params: unknown): unknown {
|
package/src/config/config.ts
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getSupiConfigPath,
|
|
8
|
+
loadSupiConfigForScope,
|
|
9
|
+
loadSupiConfigSectionForScope,
|
|
10
|
+
} from "@mrclrchtr/supi-core/config";
|
|
7
11
|
import { truncateDebugIdentity } from "@mrclrchtr/supi-core/debug";
|
|
8
12
|
import type { JsonObject, JsonValue, LspConfig, ServerConfig } from "./types.ts";
|
|
9
13
|
|
|
14
|
+
import { isManualTypeScriptLaunch, isTypeScriptPackageName } from "./typescript-launch.ts";
|
|
15
|
+
|
|
10
16
|
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
11
17
|
|
|
12
18
|
// Load defaults at module level — resolve relative to this file.
|
|
@@ -65,11 +71,47 @@ export function loadConfig(cwd: string, options?: LoadConfigOptions): LspConfig
|
|
|
65
71
|
{ scope: "project" },
|
|
66
72
|
);
|
|
67
73
|
|
|
74
|
+
const fields: NonNullable<ServerConfig["typescriptExplicitFields"]> = {};
|
|
75
|
+
for (const [scope, servers] of [
|
|
76
|
+
["global", globalLsp.servers],
|
|
77
|
+
["project", projectLsp.servers],
|
|
78
|
+
] as const) {
|
|
79
|
+
captureTypeScriptOrigins(fields, servers?.typescript, getSupiConfigPath(scope, cwd, options));
|
|
80
|
+
}
|
|
68
81
|
const merged = mergeServerConfigs(defaults.servers, globalLsp.servers, projectLsp.servers);
|
|
69
|
-
|
|
82
|
+
if (merged.typescript) {
|
|
83
|
+
merged.typescript = { ...merged.typescript, typescriptExplicitFields: fields };
|
|
84
|
+
isManualTypeScriptLaunch(merged.typescript, cwd);
|
|
85
|
+
}
|
|
70
86
|
return { servers: merged };
|
|
71
87
|
}
|
|
72
88
|
|
|
89
|
+
/** Retain only effective explicit launch fields; initializationOptions replaces its inherited object. */
|
|
90
|
+
function captureTypeScriptOrigins(
|
|
91
|
+
fields: NonNullable<ServerConfig["typescriptExplicitFields"]>,
|
|
92
|
+
value: unknown,
|
|
93
|
+
origin: string,
|
|
94
|
+
): void {
|
|
95
|
+
if (!isRecord(value)) return;
|
|
96
|
+
if (
|
|
97
|
+
Object.hasOwn(value, "typescriptPackage") &&
|
|
98
|
+
!isTypeScriptPackageName(value.typescriptPackage)
|
|
99
|
+
) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Invalid lsp.servers.typescript.typescriptPackage in ${origin}: use an installed package name or alias, not a path.`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const { override } = validateServerOverride(value);
|
|
105
|
+
for (const field of ["command", "args", "typescriptPackage"] as const) {
|
|
106
|
+
if (Object.hasOwn(override, field)) fields[field] = origin;
|
|
107
|
+
}
|
|
108
|
+
if (!Object.hasOwn(override, "initializationOptions")) return;
|
|
109
|
+
delete fields.tsserverPath;
|
|
110
|
+
const init = override.initializationOptions;
|
|
111
|
+
if (isRecord(init) && isRecord(init.tsserver) && Object.hasOwn(init.tsserver, "path"))
|
|
112
|
+
fields.tsserverPath = origin;
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
/**
|
|
74
116
|
* Read effective explicit language disablement for the active LSP snapshot.
|
|
75
117
|
* Project values override global values, including an explicit re-enable.
|
|
@@ -205,6 +247,10 @@ function mergeSingleServer(options: {
|
|
|
205
247
|
}): ServerConfig | null {
|
|
206
248
|
const { name, base, value, builtIn, reportWarning } = options;
|
|
207
249
|
const { override, invalidFields } = validateServerOverride(value);
|
|
250
|
+
if (name !== "typescript" && override.typescriptPackage !== undefined) {
|
|
251
|
+
delete override.typescriptPackage;
|
|
252
|
+
invalidFields.push("typescriptPackage");
|
|
253
|
+
}
|
|
208
254
|
const missingFields = base
|
|
209
255
|
? []
|
|
210
256
|
: [
|
|
@@ -232,6 +278,11 @@ function mergeSingleServer(options: {
|
|
|
232
278
|
type ServerOverrideSetter = (override: Partial<ServerConfig>, candidate: unknown) => boolean;
|
|
233
279
|
|
|
234
280
|
const SERVER_OVERRIDE_SETTERS: Record<string, ServerOverrideSetter> = {
|
|
281
|
+
typescriptPackage(override, candidate) {
|
|
282
|
+
if (!isTypeScriptPackageName(candidate)) return false;
|
|
283
|
+
override.typescriptPackage = candidate;
|
|
284
|
+
return true;
|
|
285
|
+
},
|
|
235
286
|
command(override, candidate) {
|
|
236
287
|
if (typeof candidate !== "string" || candidate.trim().length === 0) return false;
|
|
237
288
|
override.command = candidate;
|
|
@@ -26,12 +26,15 @@ export type ServerOperationSupportMap = Partial<
|
|
|
26
26
|
Record<ServerOperationName, ServerOperationSupport>
|
|
27
27
|
>;
|
|
28
28
|
|
|
29
|
+
const REMOVE_UNUSED_KIND = "source.removeUnused";
|
|
30
|
+
const REMOVE_UNUSED_IMPORTS_KIND = "source.removeUnusedImports";
|
|
31
|
+
|
|
29
32
|
/** Code-action kinds requested by each operation-specific planner. */
|
|
30
33
|
export const REFACTOR_CODE_ACTION_KINDS = {
|
|
31
34
|
extract_function: ["refactor.extract.function"],
|
|
32
35
|
extract_variable: ["refactor.extract.constant", "refactor.extract.variable"],
|
|
33
36
|
update_imports: ["source.organizeImports"],
|
|
34
|
-
delete_dead_code: [
|
|
37
|
+
delete_dead_code: [REMOVE_UNUSED_KIND, REMOVE_UNUSED_IMPORTS_KIND],
|
|
35
38
|
} as const satisfies Record<
|
|
36
39
|
"extract_function" | "extract_variable" | "update_imports" | "delete_dead_code",
|
|
37
40
|
readonly string[]
|
|
@@ -143,6 +146,20 @@ function supportedOperation(
|
|
|
143
146
|
return { server, supi, ...(reason ? { reason } : {}) };
|
|
144
147
|
}
|
|
145
148
|
|
|
149
|
+
function deleteDeadCodeSupport(capabilities: ServerCapabilities | null): ServerOperationSupport {
|
|
150
|
+
const server = codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.delete_dead_code);
|
|
151
|
+
const importsOnly =
|
|
152
|
+
codeActionAdvertisement(capabilities, [REMOVE_UNUSED_KIND]) !== "advertised" &&
|
|
153
|
+
codeActionAdvertisement(capabilities, [REMOVE_UNUSED_IMPORTS_KIND]) === "advertised";
|
|
154
|
+
return importsOnly
|
|
155
|
+
? supportedOperation(
|
|
156
|
+
server,
|
|
157
|
+
"limited",
|
|
158
|
+
"Only unused-import removal is supported by this mapping, not arbitrary unused declarations.",
|
|
159
|
+
)
|
|
160
|
+
: supportedOperation(server);
|
|
161
|
+
}
|
|
162
|
+
|
|
146
163
|
/** Build the honest operation-support projection for one negotiated server. */
|
|
147
164
|
export function getServerOperationSupport(
|
|
148
165
|
capabilities: ServerCapabilities | null,
|
|
@@ -181,9 +198,7 @@ export function getServerOperationSupport(
|
|
|
181
198
|
update_imports: supportedOperation(
|
|
182
199
|
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.update_imports),
|
|
183
200
|
),
|
|
184
|
-
delete_dead_code:
|
|
185
|
-
codeActionAdvertisement(capabilities, REFACTOR_CODE_ACTION_KINDS.delete_dead_code),
|
|
186
|
-
),
|
|
201
|
+
delete_dead_code: deleteDeadCodeSupport(capabilities),
|
|
187
202
|
code_action_resolve: supportedOperation(resolveServer),
|
|
188
203
|
};
|
|
189
204
|
}
|
|
@@ -26,6 +26,20 @@ export interface ServerConfig {
|
|
|
26
26
|
initializationOptions?: unknown;
|
|
27
27
|
/** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
|
|
28
28
|
readinessTimeoutMs?: number;
|
|
29
|
+
/** TypeScript compiler package name or installed alias, resolved from the route root. */
|
|
30
|
+
typescriptPackage?: string;
|
|
31
|
+
/** @internal Effective explicit-field origins for TypeScript launch policy, not user configuration. */
|
|
32
|
+
typescriptExplicitFields?: Partial<
|
|
33
|
+
Record<"command" | "args" | "tsserverPath" | "typescriptPackage", string>
|
|
34
|
+
>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Compiler selection facts; absent versions are unknown, not inferred from the wrapper. */
|
|
38
|
+
export interface TypeScriptSelection {
|
|
39
|
+
backend: "tsserver" | "native";
|
|
40
|
+
packageName: string;
|
|
41
|
+
compilerVersion?: string;
|
|
42
|
+
wrapperVersion?: string;
|
|
29
43
|
}
|
|
30
44
|
|
|
31
45
|
/** LSP configuration keyed by language name (e.g. `typescript`, `python`). */
|
|
@@ -37,6 +51,10 @@ export interface DetectedProjectServer {
|
|
|
37
51
|
name: string;
|
|
38
52
|
root: string;
|
|
39
53
|
fileTypes: string[];
|
|
54
|
+
/** Resolved automatic compiler selection, when available. */
|
|
55
|
+
typescript?: TypeScriptSelection;
|
|
56
|
+
/** Actionable launch-selection failure, including its root and package. */
|
|
57
|
+
failureMessage?: string;
|
|
40
58
|
}
|
|
41
59
|
|
|
42
60
|
/** Structured reason for a route-level process-crash recovery state. */
|
|
@@ -64,4 +82,8 @@ export interface MissingServer {
|
|
|
64
82
|
command: string;
|
|
65
83
|
/** File extensions found in the project (subset of server.fileTypes). */
|
|
66
84
|
foundExtensions: string[];
|
|
85
|
+
/** Route root when a compiler-selection failure prevents launch. */
|
|
86
|
+
root?: string;
|
|
87
|
+
/** Compiler selection failure, distinct from a missing executable. */
|
|
88
|
+
failureMessage?: string;
|
|
67
89
|
}
|