@mrclrchtr/supi-lsp 1.16.1 → 2.0.1
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 +44 -56
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +3 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +7 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +56 -2
- package/node_modules/@mrclrchtr/supi-code-runtime/src/evidence-badge.ts +40 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/index.ts +5 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +76 -3
- package/node_modules/@mrclrchtr/supi-code-runtime/src/workspace/runtime.ts +37 -2
- package/node_modules/@mrclrchtr/supi-core/package.json +6 -1
- package/node_modules/@mrclrchtr/supi-core/src/abort-utils.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/api.ts +8 -0
- package/node_modules/@mrclrchtr/supi-core/src/config/config-settings.ts +10 -4
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +1 -0
- package/node_modules/@mrclrchtr/supi-core/src/footer-registry.ts +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +6 -0
- package/node_modules/@mrclrchtr/supi-core/src/model-selection.ts +134 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +9 -2
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-ui.ts +109 -4
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +5 -1
- package/node_modules/@mrclrchtr/supi-core/src/spinner-frames.ts +11 -0
- package/node_modules/@mrclrchtr/supi-core/src/status-spinner.ts +68 -0
- package/package.json +4 -31
- package/src/api.ts +27 -0
- package/src/client/client-refresh.ts +5 -0
- package/src/client/client.ts +246 -1
- package/src/client/transport.ts +4 -0
- package/src/config/capabilities.ts +3 -0
- package/src/config/config.ts +4 -2
- package/src/config/lsp-settings.ts +102 -0
- package/src/config/server-actions.ts +59 -0
- package/src/config/server-config.ts +4 -0
- package/src/index.ts +11 -0
- package/src/manager/capability-index.ts +1 -1
- package/src/manager/manager-project-info.ts +2 -1
- package/src/manager/manager.ts +115 -26
- package/src/provider/lsp-semantic-provider.ts +177 -134
- package/src/provider/refactor-planning.ts +214 -0
- package/src/session/runtime-controller.ts +320 -0
- package/src/session/runtime-registration.ts +26 -5
- package/src/session/service-registry.ts +81 -4
- package/src/summary.ts +2 -0
- package/src/extension.ts +0 -1
- package/src/format.ts +0 -373
- package/src/handlers/diagnostic-injection.ts +0 -182
- package/src/handlers/session-lifecycle.ts +0 -149
- package/src/handlers/status-command.ts +0 -38
- package/src/handlers/workspace-recovery.ts +0 -92
- package/src/lsp.ts +0 -37
- package/src/session/lsp-state.ts +0 -93
- package/src/session/settings-registration.ts +0 -296
- package/src/session/tree-persist.ts +0 -75
- package/src/tool/guidance.ts +0 -69
- package/src/tool/names.ts +0 -27
- package/src/tool/overrides.ts +0 -204
- package/src/tool/register-tools.ts +0 -69
- package/src/tool/service-actions.ts +0 -399
- package/src/tool/tool-specs.ts +0 -248
- package/src/ui/renderer.ts +0 -120
- package/src/ui/ui.ts +0 -360
- package/src/workspace-change.ts +0 -40
package/src/client/client.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import { type ChildProcess, spawn } from "node:child_process";
|
|
6
6
|
import { existsSync } from "node:fs";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { recordDebugEvent } from "@mrclrchtr/supi-core/debug";
|
|
9
|
+
import type { ProgressToken } from "vscode-languageserver-protocol";
|
|
8
10
|
import { CLIENT_CAPABILITIES } from "../config/capabilities.ts";
|
|
9
11
|
import type {
|
|
10
12
|
CodeAction,
|
|
@@ -68,6 +70,15 @@ export class LspClient {
|
|
|
68
70
|
/** Listeners waiting for diagnostics on a specific uri */
|
|
69
71
|
private diagnosticWaiters = new Map<string, Array<() => void>>();
|
|
70
72
|
|
|
73
|
+
// ── Readiness (work-done-progress) ──────────────────────────────────
|
|
74
|
+
private trackedTokens = new Map<ProgressToken, "begin-seen" | "ended">();
|
|
75
|
+
private _readyPromise: Promise<void> | null = null;
|
|
76
|
+
private _readyResolve: (() => void) | undefined;
|
|
77
|
+
private _readyReject: ((err: Error) => void) | undefined;
|
|
78
|
+
private _isReady = false;
|
|
79
|
+
private noProgressTimer: ReturnType<typeof setTimeout> | null = null;
|
|
80
|
+
private tokenTimeouts = new Map<ProgressToken, ReturnType<typeof setTimeout>>();
|
|
81
|
+
|
|
71
82
|
constructor(
|
|
72
83
|
name: string,
|
|
73
84
|
private readonly config: ServerConfig,
|
|
@@ -89,6 +100,11 @@ export class LspClient {
|
|
|
89
100
|
return this.capabilities;
|
|
90
101
|
}
|
|
91
102
|
|
|
103
|
+
/** Whether the server is currently not indexing and ready to serve queries. */
|
|
104
|
+
get ready(): boolean {
|
|
105
|
+
return this._isReady;
|
|
106
|
+
}
|
|
107
|
+
|
|
92
108
|
// ── Lifecycle ───────────────────────────────────────────────────────
|
|
93
109
|
/** Spawn the server process and perform the initialize handshake. */
|
|
94
110
|
async start(): Promise<void> {
|
|
@@ -118,6 +134,8 @@ export class LspClient {
|
|
|
118
134
|
this.rpc.onNotification((method, params) => {
|
|
119
135
|
if (method === "textDocument/publishDiagnostics") {
|
|
120
136
|
this.handlePublishDiagnostics(params as PublishDiagnosticsParams);
|
|
137
|
+
} else if (method === "$/progress") {
|
|
138
|
+
this.handleProgress(params as { token: ProgressToken; value: { kind: string } });
|
|
121
139
|
}
|
|
122
140
|
});
|
|
123
141
|
this.rpc.onRequest((method, params) => this.handleServerRequest(method, params));
|
|
@@ -126,6 +144,8 @@ export class LspClient {
|
|
|
126
144
|
this.process.on("exit", (_code) => {
|
|
127
145
|
if (this._status !== "shutdown") {
|
|
128
146
|
this._status = "error";
|
|
147
|
+
this.cancelNoProgressTimer();
|
|
148
|
+
this.rejectReady(new Error("Client crashed"));
|
|
129
149
|
}
|
|
130
150
|
this.rpc?.dispose();
|
|
131
151
|
});
|
|
@@ -133,6 +153,7 @@ export class LspClient {
|
|
|
133
153
|
this.process.on("error", (_err) => {
|
|
134
154
|
if (this._status !== "shutdown") {
|
|
135
155
|
this._status = "error";
|
|
156
|
+
this.rejectReady(new Error("Client process error"));
|
|
136
157
|
}
|
|
137
158
|
});
|
|
138
159
|
|
|
@@ -151,6 +172,8 @@ export class LspClient {
|
|
|
151
172
|
this.capabilities = result.capabilities;
|
|
152
173
|
this.rpc.sendNotification("initialized", {});
|
|
153
174
|
this._status = "running";
|
|
175
|
+
|
|
176
|
+
this.armNoProgressTimer();
|
|
154
177
|
} catch (err) {
|
|
155
178
|
this._status = "error";
|
|
156
179
|
this.process.kill();
|
|
@@ -203,6 +226,15 @@ export class LspClient {
|
|
|
203
226
|
this.openDocs.clear();
|
|
204
227
|
this.diagnosticStore.clear();
|
|
205
228
|
this.releaseAllDiagnosticWaiters();
|
|
229
|
+
|
|
230
|
+
// Clear readiness state
|
|
231
|
+
if (this.noProgressTimer) {
|
|
232
|
+
clearTimeout(this.noProgressTimer);
|
|
233
|
+
this.noProgressTimer = null;
|
|
234
|
+
}
|
|
235
|
+
for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
|
|
236
|
+
this.tokenTimeouts.clear();
|
|
237
|
+
this.rejectReady(new Error("Client shutdown"));
|
|
206
238
|
}
|
|
207
239
|
|
|
208
240
|
// ── Document Synchronization ────────────────────────────────────────
|
|
@@ -468,6 +500,7 @@ export class LspClient {
|
|
|
468
500
|
private async request<T>(method: string, params: unknown): Promise<T | null> {
|
|
469
501
|
if (!this.rpc || this._status !== "running") return null;
|
|
470
502
|
try {
|
|
503
|
+
await this.getReady();
|
|
471
504
|
return (await this.rpc.sendRequest(method, params)) as T;
|
|
472
505
|
} catch {
|
|
473
506
|
return null;
|
|
@@ -482,8 +515,24 @@ export class LspClient {
|
|
|
482
515
|
return [{ uri: fileToUri(this.root), name: path.basename(this.root) || this.root }];
|
|
483
516
|
case "client/registerCapability":
|
|
484
517
|
case "client/unregisterCapability":
|
|
485
|
-
case "window/workDoneProgress/create":
|
|
486
518
|
return null;
|
|
519
|
+
case "window/workDoneProgress/create": {
|
|
520
|
+
const token = (params as { token: ProgressToken }).token;
|
|
521
|
+
this.trackedTokens.set(token, "begin-seen");
|
|
522
|
+
this.cancelNoProgressTimer();
|
|
523
|
+
this._isReady = false;
|
|
524
|
+
if (!this._readyPromise) {
|
|
525
|
+
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
526
|
+
this._readyResolve = resolve;
|
|
527
|
+
this._readyReject = reject;
|
|
528
|
+
});
|
|
529
|
+
// Prevent unhandled rejection when rejectReady fires before any
|
|
530
|
+
// consumer is actively awaiting this promise (e.g. during shutdown).
|
|
531
|
+
this._readyPromise.catch(() => {});
|
|
532
|
+
}
|
|
533
|
+
this.startTokenTimeout(token);
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
487
536
|
default:
|
|
488
537
|
throw new JsonRpcRequestError(-32601, `Method not found: ${method}`);
|
|
489
538
|
}
|
|
@@ -569,4 +618,200 @@ export class LspClient {
|
|
|
569
618
|
this.diagnosticWaiters.delete(uri);
|
|
570
619
|
for (const waiter of waiters) waiter();
|
|
571
620
|
}
|
|
621
|
+
|
|
622
|
+
// ── Readiness (work-done-progress) ──────────────────────────────────
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* Wait for the server to be ready to serve queries.
|
|
626
|
+
* Returns immediately if already ready; returns the ongoing promise
|
|
627
|
+
* if one is pending; creates and returns a new one otherwise.
|
|
628
|
+
*/
|
|
629
|
+
async getReady(): Promise<void> {
|
|
630
|
+
if (this._isReady) return;
|
|
631
|
+
if (this._readyPromise !== null) return this._readyPromise;
|
|
632
|
+
// If no progress timer was ever armed and no tokens are tracked,
|
|
633
|
+
// the server was either never started with a real process (test scenario)
|
|
634
|
+
// or completed before any progress tracking began. Resolve immediately
|
|
635
|
+
// only when the client is still running — a crash or shutdown clears
|
|
636
|
+
// both fields but must not report the client as ready.
|
|
637
|
+
if (this.noProgressTimer === null && this.trackedTokens.size === 0) {
|
|
638
|
+
if (this._status === "running") this._isReady = true;
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
642
|
+
this._readyResolve = resolve;
|
|
643
|
+
this._readyReject = reject;
|
|
644
|
+
});
|
|
645
|
+
// Prevent unhandled rejection when rejectReady fires before any
|
|
646
|
+
// consumer is actively awaiting this promise (e.g. during shutdown).
|
|
647
|
+
this._readyPromise.catch(() => {});
|
|
648
|
+
return this._readyPromise;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** Handle the $/progress notification from the server. */
|
|
652
|
+
private handleProgress(params: { token: ProgressToken; value: { kind: string } }): void {
|
|
653
|
+
const { token, value } = params;
|
|
654
|
+
// Defensive: guard against malformed notifications without a value.
|
|
655
|
+
if (!value || typeof value.kind !== "string") return;
|
|
656
|
+
|
|
657
|
+
if (value.kind === "begin") {
|
|
658
|
+
recordDebugEvent({
|
|
659
|
+
source: "lsp",
|
|
660
|
+
level: "debug",
|
|
661
|
+
category: "readiness.progress-begin",
|
|
662
|
+
message: `Readiness progress begin for token ${token}`,
|
|
663
|
+
data: { token },
|
|
664
|
+
});
|
|
665
|
+
// Cancel the 2s no-progress grace timer — a server that sends
|
|
666
|
+
// begin without a prior create is spec-deviant but valid.
|
|
667
|
+
this.cancelNoProgressTimer();
|
|
668
|
+
this.trackedTokens.set(token, "begin-seen");
|
|
669
|
+
this._isReady = false;
|
|
670
|
+
// Re-arm readiness promise if not already pending
|
|
671
|
+
if (!this._readyPromise) {
|
|
672
|
+
this._readyPromise = new Promise<void>((resolve, reject) => {
|
|
673
|
+
this._readyResolve = resolve;
|
|
674
|
+
this._readyReject = reject;
|
|
675
|
+
});
|
|
676
|
+
// Prevent unhandled rejection when rejectReady fires before any
|
|
677
|
+
// consumer is actively awaiting this promise (e.g. during shutdown).
|
|
678
|
+
this._readyPromise.catch(() => {});
|
|
679
|
+
}
|
|
680
|
+
this.startTokenTimeout(token);
|
|
681
|
+
} else if (value.kind === "end") {
|
|
682
|
+
recordDebugEvent({
|
|
683
|
+
source: "lsp",
|
|
684
|
+
level: "debug",
|
|
685
|
+
category: "readiness.progress-end",
|
|
686
|
+
message: `Readiness progress end for token ${token}`,
|
|
687
|
+
data: { token },
|
|
688
|
+
});
|
|
689
|
+
this.trackedTokens.set(token, "ended");
|
|
690
|
+
this.clearTokenTimeout(token);
|
|
691
|
+
this.checkAllTokensEnded();
|
|
692
|
+
}
|
|
693
|
+
// kind: "report" — intentionally no-op
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/** Check whether all tracked tokens have ended and resolve readiness. */
|
|
697
|
+
private checkAllTokensEnded(): void {
|
|
698
|
+
if (this.trackedTokens.size === 0) return;
|
|
699
|
+
for (const state of this.trackedTokens.values()) {
|
|
700
|
+
if (state !== "ended") return;
|
|
701
|
+
}
|
|
702
|
+
this.trackedTokens.clear();
|
|
703
|
+
this.resolveReady();
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Resolve the current readiness promise (if any) and mark the client ready. */
|
|
707
|
+
private resolveReady(): void {
|
|
708
|
+
if (this._readyResolve) {
|
|
709
|
+
this._readyResolve();
|
|
710
|
+
this._readyPromise = null;
|
|
711
|
+
this._readyResolve = undefined;
|
|
712
|
+
this._readyReject = undefined;
|
|
713
|
+
}
|
|
714
|
+
this._isReady = true;
|
|
715
|
+
recordDebugEvent({
|
|
716
|
+
source: "lsp",
|
|
717
|
+
level: "info",
|
|
718
|
+
category: "readiness.resolved",
|
|
719
|
+
message: `LSP client ${this.name} is ready (cwd: ${this.root})`,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* Reject the current readiness promise (if any) and mark the client
|
|
725
|
+
* not ready. Called on shutdown, crash, or restart.
|
|
726
|
+
*/
|
|
727
|
+
private rejectReady(reason: Error): void {
|
|
728
|
+
if (this._readyReject) {
|
|
729
|
+
this._readyReject(reason);
|
|
730
|
+
this._readyPromise = null;
|
|
731
|
+
this._readyResolve = undefined;
|
|
732
|
+
this._readyReject = undefined;
|
|
733
|
+
}
|
|
734
|
+
this._isReady = false;
|
|
735
|
+
this.trackedTokens.clear();
|
|
736
|
+
for (const timer of this.tokenTimeouts.values()) clearTimeout(timer);
|
|
737
|
+
this.tokenTimeouts.clear();
|
|
738
|
+
recordDebugEvent({
|
|
739
|
+
source: "lsp",
|
|
740
|
+
level: this._status === "shutdown" ? "debug" : "warning",
|
|
741
|
+
category: "readiness.rejected",
|
|
742
|
+
message: `LSP client ${this.name} readiness rejected: ${reason.message}`,
|
|
743
|
+
data: { status: this._status },
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Start a per-token timeout. If the token never receives an "end",
|
|
749
|
+
* force-end it after `readinessTimeoutMs` (default 10s).
|
|
750
|
+
*/
|
|
751
|
+
private startTokenTimeout(token: ProgressToken): void {
|
|
752
|
+
// Clear any existing timeout for this token (e.g., if both
|
|
753
|
+
// window/workDoneProgress/create and $/progress begin fire).
|
|
754
|
+
this.clearTokenTimeout(token);
|
|
755
|
+
const timeoutMs = this.config.readinessTimeoutMs ?? 10_000;
|
|
756
|
+
const timer = setTimeout(() => {
|
|
757
|
+
this.trackedTokens.set(token, "ended");
|
|
758
|
+
this.tokenTimeouts.delete(token);
|
|
759
|
+
recordDebugEvent({
|
|
760
|
+
source: "lsp",
|
|
761
|
+
level: "debug",
|
|
762
|
+
category: "readiness.token-timeout",
|
|
763
|
+
message: `Readiness per-token timeout fired for token ${token} after ${timeoutMs}ms`,
|
|
764
|
+
data: { token, timeoutMs },
|
|
765
|
+
});
|
|
766
|
+
this.checkAllTokensEnded();
|
|
767
|
+
}, timeoutMs);
|
|
768
|
+
this.tokenTimeouts.set(token, timer);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/** Clear the per-token timeout for a completed token. */
|
|
772
|
+
private clearTokenTimeout(token: ProgressToken): void {
|
|
773
|
+
const timer = this.tokenTimeouts.get(token);
|
|
774
|
+
if (timer) {
|
|
775
|
+
clearTimeout(timer);
|
|
776
|
+
this.tokenTimeouts.delete(token);
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/** Cancel the 2s no-progress grace timer. */
|
|
781
|
+
private cancelNoProgressTimer(): void {
|
|
782
|
+
if (this.noProgressTimer) {
|
|
783
|
+
clearTimeout(this.noProgressTimer);
|
|
784
|
+
this.noProgressTimer = null;
|
|
785
|
+
recordDebugEvent({
|
|
786
|
+
source: "lsp",
|
|
787
|
+
level: "debug",
|
|
788
|
+
category: "readiness.no-progress-cancelled",
|
|
789
|
+
message: `No-progress grace timer cancelled for ${this.name}`,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* Arm the 2s no-progress grace timer. If no server-initiated
|
|
796
|
+
* progress token arrives within this window, the server is treated
|
|
797
|
+
* as immediately ready (small project or non-progress-supporting server).
|
|
798
|
+
*
|
|
799
|
+
* A server that sends its first $/progress begin after 2s causes
|
|
800
|
+
* a brief false-ready window — the steady-state re-entrancy in
|
|
801
|
+
* handleProgress() flips isReady back to false when the begin arrives.
|
|
802
|
+
*/
|
|
803
|
+
private armNoProgressTimer(): void {
|
|
804
|
+
this.noProgressTimer = setTimeout(() => {
|
|
805
|
+
if (this.trackedTokens.size === 0 && this._status === "running") {
|
|
806
|
+
recordDebugEvent({
|
|
807
|
+
source: "lsp",
|
|
808
|
+
level: "debug",
|
|
809
|
+
category: "readiness.no-progress-resolved",
|
|
810
|
+
message: `No-progress grace timer resolved for ${this.name}`,
|
|
811
|
+
});
|
|
812
|
+
this.resolveReady();
|
|
813
|
+
}
|
|
814
|
+
this.noProgressTimer = null;
|
|
815
|
+
}, 2_000);
|
|
816
|
+
}
|
|
572
817
|
}
|
package/src/client/transport.ts
CHANGED
|
@@ -93,6 +93,10 @@ export class JsonRpcClient {
|
|
|
93
93
|
const timer = setTimeout(() => tokenSource.cancel(), timeoutMs);
|
|
94
94
|
|
|
95
95
|
const request = this.connection.sendRequest(method, params, tokenSource.token);
|
|
96
|
+
// Catch the raw request promise to prevent unhandled rejections when
|
|
97
|
+
// dispose() cancels the token without a preceding timeout (the raced
|
|
98
|
+
// promise below covers the timeout-then-dispose path separately).
|
|
99
|
+
request.catch(() => {});
|
|
96
100
|
|
|
97
101
|
// Race the request against a timeout so callers don't hang forever.
|
|
98
102
|
// The CancellationToken is also passed to sendRequest so the connection
|
package/src/config/config.ts
CHANGED
|
@@ -2,13 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import * as fs from "node:fs";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
|
|
6
7
|
import type { LspConfig, ServerConfig } from "./types.ts";
|
|
7
8
|
|
|
9
|
+
const CONFIG_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
8
11
|
// Load defaults at module level — resolve relative to this file.
|
|
9
|
-
// pi loads extensions via jiti, which always provides __dirname.
|
|
10
12
|
const DEFAULTS: LspConfig = JSON.parse(
|
|
11
|
-
fs.readFileSync(path.join(
|
|
13
|
+
fs.readFileSync(path.join(CONFIG_DIR, "defaults.json"), "utf-8"),
|
|
12
14
|
) as LspConfig;
|
|
13
15
|
|
|
14
16
|
// ── Public API ────────────────────────────────────────────────────────
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// LSP settings helpers — config loading for LSP session lifecycle.
|
|
2
|
+
//
|
|
3
|
+
// These are the non-UI config helpers extracted from settings-registration.ts
|
|
4
|
+
// so they can be consumed by the library-only runtime controller without
|
|
5
|
+
// importing pi-specific UI modules.
|
|
6
|
+
|
|
7
|
+
import { loadSupiConfig, loadSupiConfigForScope } from "@mrclrchtr/supi-core/config";
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
export interface LspSettings {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
severity: number;
|
|
14
|
+
active: string[];
|
|
15
|
+
exclude: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const LSP_DEFAULTS: LspSettings = {
|
|
19
|
+
enabled: true,
|
|
20
|
+
severity: 1,
|
|
21
|
+
active: [],
|
|
22
|
+
exclude: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// ── Config helpers ───────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load LSP settings from supi config for the given cwd.
|
|
29
|
+
*
|
|
30
|
+
* Merges project and global scopes with defaults, returning the
|
|
31
|
+
* effective LspSettings for session startup.
|
|
32
|
+
*/
|
|
33
|
+
export function loadLspSettings(cwd: string, homeDir?: string): LspSettings {
|
|
34
|
+
return loadSupiConfig("lsp", cwd, LSP_DEFAULTS, { homeDir });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Return a user-facing message explaining why LSP may appear disabled.
|
|
39
|
+
*
|
|
40
|
+
* **Since the always-on policy:** `lsp.enabled: false` is deprecated and
|
|
41
|
+
* ignored. This function is kept for backward compatibility with the
|
|
42
|
+
* settings UI and reports that LSP is always attempted.
|
|
43
|
+
*/
|
|
44
|
+
export function getLspDisabledMessage(cwd: string, homeDir?: string): string {
|
|
45
|
+
const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
|
|
46
|
+
const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
|
|
47
|
+
|
|
48
|
+
if (project.enabled === false) {
|
|
49
|
+
return "LSP is always attempted; `lsp.enabled: false` in project settings (.pi/supi/config.json) is deprecated and ignored";
|
|
50
|
+
}
|
|
51
|
+
if (global.enabled === false) {
|
|
52
|
+
return "LSP is always attempted; `lsp.enabled: false` in global settings (~/.pi/agent/supi/config.json) is deprecated and ignored";
|
|
53
|
+
}
|
|
54
|
+
return "LSP is always attempted";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── Deprecated-key detection (always-on policy) ────────────────
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of checking for deprecated LSP configuration keys.
|
|
61
|
+
*/
|
|
62
|
+
export interface DeprecatedLspKeys {
|
|
63
|
+
/** `lsp.enabled` presence in project config. */
|
|
64
|
+
projectEnabled: boolean;
|
|
65
|
+
/** `lsp.enabled` presence in global config. */
|
|
66
|
+
globalEnabled: boolean;
|
|
67
|
+
/** `lsp.active` presence in project config. */
|
|
68
|
+
projectActive: boolean;
|
|
69
|
+
/** `lsp.active` presence in global config. */
|
|
70
|
+
globalActive: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check whether deprecated `lsp.enabled` or `lsp.active` keys exist
|
|
75
|
+
* in project or global config. These keys are ignored by the always-on
|
|
76
|
+
* LSP policy but may be present in user config.
|
|
77
|
+
*/
|
|
78
|
+
export function getDeprecatedLspKeys(cwd: string, homeDir?: string): DeprecatedLspKeys {
|
|
79
|
+
const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
|
|
80
|
+
const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
projectEnabled: hasExplicitEnabledKey(project),
|
|
84
|
+
globalEnabled: hasExplicitEnabledKey(global),
|
|
85
|
+
projectActive: hasExplicitActiveKey(project),
|
|
86
|
+
globalActive: hasExplicitActiveKey(global),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Whether the loaded settings had an explicit `enabled` key (different from default). */
|
|
91
|
+
function hasExplicitEnabledKey(settings: Partial<{ enabled: boolean }>): boolean {
|
|
92
|
+
return settings.enabled === false;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Whether the loaded settings had an explicit `active` key. */
|
|
96
|
+
function hasExplicitActiveKey(settings: Partial<{ active: string[] }>): boolean {
|
|
97
|
+
return Array.isArray(settings.active) && settings.active.length > 0;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Alias for backwards compatibility with the RED test name
|
|
101
|
+
/** @deprecated Use {@link getDeprecatedLspKeys} instead. */
|
|
102
|
+
export const hasDeprecatedLspKeys: typeof getDeprecatedLspKeys = getDeprecatedLspKeys;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Server capability action label helpers — maps LSP capabilities to readable action labels.
|
|
2
|
+
|
|
3
|
+
import type { ServerCapabilities } from "../config/types.ts";
|
|
4
|
+
|
|
5
|
+
interface LspServerSupportedActionSpec {
|
|
6
|
+
label: string;
|
|
7
|
+
isSupported: (capabilities: ServerCapabilities | null | undefined) => boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const LSP_SERVER_SUPPORTED_ACTION_SPECS: readonly LspServerSupportedActionSpec[] = [
|
|
11
|
+
{
|
|
12
|
+
label: "diagnostics [optional file]",
|
|
13
|
+
isSupported: () => true,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
label: "hover(file,line,char)",
|
|
17
|
+
isSupported: (capabilities) => Boolean(capabilities?.hoverProvider),
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "definition(file,line,char)",
|
|
21
|
+
isSupported: (capabilities) => Boolean(capabilities?.definitionProvider),
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
label: "references(file,line,char)",
|
|
25
|
+
isSupported: (capabilities) => Boolean(capabilities?.referencesProvider),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: "implementation(file,line,char)",
|
|
29
|
+
isSupported: (capabilities) => Boolean(capabilities?.implementationProvider),
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
label: "symbols(file)",
|
|
33
|
+
isSupported: (capabilities) => Boolean(capabilities?.documentSymbolProvider),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "workspace_symbols(query)",
|
|
37
|
+
isSupported: (capabilities) => Boolean(capabilities?.workspaceSymbolProvider),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
label: "rename(file,line,char,newName)",
|
|
41
|
+
isSupported: (capabilities) => Boolean(capabilities?.renameProvider),
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
label: "code_actions(file,line,char)",
|
|
45
|
+
isSupported: (capabilities) => Boolean(capabilities?.codeActionProvider),
|
|
46
|
+
},
|
|
47
|
+
] as const;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the list of supported LSP server actions based on capabilities.
|
|
51
|
+
*/
|
|
52
|
+
export function getSupportedLspServerActions(
|
|
53
|
+
capabilities: ServerCapabilities | null | undefined,
|
|
54
|
+
): string[] {
|
|
55
|
+
if (!capabilities) return [];
|
|
56
|
+
return LSP_SERVER_SUPPORTED_ACTION_SPECS.filter((spec) => spec.isSupported(capabilities)).map(
|
|
57
|
+
(spec) => spec.label,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -8,6 +8,8 @@ export interface ServerConfig {
|
|
|
8
8
|
rootMarkers: string[];
|
|
9
9
|
enabled?: boolean;
|
|
10
10
|
initializationOptions?: unknown;
|
|
11
|
+
/** Maximum time to wait for a single $/progress cycle, in ms. Default: 10_000. */
|
|
12
|
+
readinessTimeoutMs?: number;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
/** LSP configuration keyed by language name (e.g. `typescript`, `python`). */
|
|
@@ -25,6 +27,8 @@ export interface ProjectServerInfo extends DetectedProjectServer {
|
|
|
25
27
|
status: "running" | "error" | "unavailable";
|
|
26
28
|
supportedActions: string[];
|
|
27
29
|
openFiles: string[];
|
|
30
|
+
/** Whether the LSP server is currently not indexing and ready to serve queries. */
|
|
31
|
+
ready: boolean;
|
|
28
32
|
}
|
|
29
33
|
|
|
30
34
|
/** A language whose source files are present but the server binary is missing. */
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
// Consumers should import the published API surface from
|
|
3
3
|
// `@mrclrchtr/supi-lsp/api`, not the package root.
|
|
4
4
|
|
|
5
|
+
export type { LspSettings } from "./config/lsp-settings.ts";
|
|
6
|
+
export {
|
|
7
|
+
getLspDisabledMessage,
|
|
8
|
+
loadLspSettings,
|
|
9
|
+
} from "./config/lsp-settings.ts";
|
|
5
10
|
export type {
|
|
6
11
|
CodeAction,
|
|
7
12
|
Diagnostic,
|
|
@@ -17,9 +22,15 @@ export type {
|
|
|
17
22
|
WorkspaceSymbol,
|
|
18
23
|
} from "./config/types.ts";
|
|
19
24
|
export { toLspPosition, toOneBasedPosition } from "./coordinates.ts";
|
|
25
|
+
export type {
|
|
26
|
+
LspControllerState,
|
|
27
|
+
LspStartResult,
|
|
28
|
+
} from "./session/runtime-controller.ts";
|
|
29
|
+
export { LspRuntimeController } from "./session/runtime-controller.ts";
|
|
20
30
|
export type {
|
|
21
31
|
OutstandingDiagnosticSummaryEntry,
|
|
22
32
|
RecoverDiagnosticsResult,
|
|
33
|
+
SemanticReadinessResult,
|
|
23
34
|
SessionLspServiceState,
|
|
24
35
|
WorkspaceDiagnosticSummaryEntry,
|
|
25
36
|
} from "./session/service-registry.ts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Capability index — indexes and queries server capabilities.
|
|
2
2
|
|
|
3
|
+
import { getSupportedLspServerActions } from "../config/server-actions.ts";
|
|
3
4
|
import type { ServerCapabilities } from "../config/types.ts";
|
|
4
|
-
import { getSupportedLspServerActions } from "../tool/tool-specs.ts";
|
|
5
5
|
import type { LspManager } from "./manager.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { LspClient } from "../client/client.ts";
|
|
2
|
+
import { getSupportedLspServerActions } from "../config/server-actions.ts";
|
|
2
3
|
import type { ProjectServerInfo } from "../config/server-config.ts";
|
|
3
4
|
import { displayRelativeFilePath } from "../summary.ts";
|
|
4
|
-
import { getSupportedLspServerActions } from "../tool/tool-specs.ts";
|
|
5
5
|
|
|
6
6
|
interface ProjectServerInfoInput {
|
|
7
7
|
serverName: string;
|
|
@@ -31,5 +31,6 @@ export function buildProjectServerInfo(
|
|
|
31
31
|
status,
|
|
32
32
|
supportedActions: getSupportedLspServerActions(input.client?.serverCapabilities),
|
|
33
33
|
openFiles: input.client?.openFiles.map((file) => displayRelativeFilePath(file, cwd)) ?? [],
|
|
34
|
+
ready: input.client?.ready ?? false,
|
|
34
35
|
};
|
|
35
36
|
}
|