@dyyz1993/pi-coding-agent 0.74.24 → 0.74.27
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/CHANGELOG.md +9 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +3 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/session-manager.d.ts +5 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +8 -0
- package/dist/core/session-manager.js.map +1 -1
- package/dist/extensions/agent-permissions/index.ts +235 -0
- package/dist/extensions/ask-tools/index.ts +115 -0
- package/dist/extensions/auto-memory/contract.d.ts +51 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -0
- package/dist/extensions/auto-memory/contract.js +2 -0
- package/dist/extensions/auto-memory/contract.js.map +1 -0
- package/dist/extensions/auto-memory/contract.ts +56 -0
- package/dist/extensions/auto-memory/index.ts +969 -0
- package/dist/extensions/auto-memory/prompts.ts +202 -0
- package/dist/extensions/auto-memory/skip-rules.ts +297 -0
- package/dist/extensions/auto-memory/utils.ts +208 -0
- package/dist/extensions/auto-session-title/index.ts +83 -0
- package/dist/extensions/bash-ext/contract.d.ts +79 -0
- package/dist/extensions/bash-ext/contract.d.ts.map +1 -0
- package/dist/extensions/bash-ext/contract.js +2 -0
- package/dist/extensions/bash-ext/contract.js.map +1 -0
- package/dist/extensions/bash-ext/contract.ts +69 -0
- package/dist/extensions/bash-ext/index.ts +858 -0
- package/dist/extensions/claude-hooks-compat/config-loader.ts +49 -0
- package/dist/extensions/claude-hooks-compat/handler-runner.ts +377 -0
- package/dist/extensions/claude-hooks-compat/if-parser.ts +53 -0
- package/dist/extensions/claude-hooks-compat/index.ts +178 -0
- package/dist/extensions/claude-hooks-compat/matcher.ts +17 -0
- package/dist/extensions/claude-hooks-compat/stdin-builder.ts +27 -0
- package/dist/extensions/claude-hooks-compat/types.ts +77 -0
- package/dist/extensions/compaction-manager/config.ts +47 -0
- package/dist/extensions/compaction-manager/context-fold.ts +63 -0
- package/dist/extensions/compaction-manager/index.ts +151 -0
- package/dist/extensions/compaction-manager/microcompact.ts +49 -0
- package/dist/extensions/compaction-manager/reactive.ts +9 -0
- package/dist/extensions/compaction-manager/session-memory.ts +48 -0
- package/dist/extensions/coordinator/INTEGRATION.md +376 -0
- package/dist/extensions/coordinator/handler.test.ts +277 -0
- package/dist/extensions/coordinator/handler.ts +189 -0
- package/dist/extensions/coordinator/index.ts +261 -0
- package/dist/extensions/coordinator/types.d.ts +100 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -0
- package/dist/extensions/coordinator/types.js +2 -0
- package/dist/extensions/coordinator/types.js.map +1 -0
- package/dist/extensions/coordinator/types.ts +72 -0
- package/dist/extensions/file-snapshot/index.ts +131 -0
- package/dist/extensions/file-time-guard/README.md +133 -0
- package/dist/extensions/file-time-guard/config.ts +13 -0
- package/dist/extensions/file-time-guard/index.ts +171 -0
- package/dist/extensions/hooks-engine/index.ts +117 -0
- package/dist/extensions/lsp/lsp/client/file-tracker.ts +70 -0
- package/dist/extensions/lsp/lsp/client/registry.ts +305 -0
- package/dist/extensions/lsp/lsp/client/runtime.ts +832 -0
- package/dist/extensions/lsp/lsp/config/resolver.ts +573 -0
- package/dist/extensions/lsp/lsp/contract.d.ts +101 -0
- package/dist/extensions/lsp/lsp/contract.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/contract.js +2 -0
- package/dist/extensions/lsp/lsp/contract.js.map +1 -0
- package/dist/extensions/lsp/lsp/contract.ts +103 -0
- package/dist/extensions/lsp/lsp/hooks/agent-end.ts +169 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts +10 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js +30 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.js.map +1 -0
- package/dist/extensions/lsp/lsp/hooks/diagnostics-mode.ts +41 -0
- package/dist/extensions/lsp/lsp/hooks/writethrough.ts +342 -0
- package/dist/extensions/lsp/lsp/index.ts +310 -0
- package/dist/extensions/lsp/lsp/lsp.test.ts +684 -0
- package/dist/extensions/lsp/lsp/monitoring/server-metrics.ts +176 -0
- package/dist/extensions/lsp/lsp/tools/lsp-tool.ts +402 -0
- package/dist/extensions/lsp/lsp/utils/dependency-resolver.ts +147 -0
- package/dist/extensions/lsp/lsp/utils/diagnostics-wait.ts +41 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts +20 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.d.ts.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js +64 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.js.map +1 -0
- package/dist/extensions/lsp/lsp/utils/lsp-helpers.ts +76 -0
- package/dist/extensions/message-bridge/GUIDE.md +210 -0
- package/dist/extensions/message-bridge/index.ts +222 -0
- package/dist/extensions/output-guard/index.ts +446 -0
- package/dist/extensions/preview/index.ts +278 -0
- package/dist/extensions/rules-engine/MATCH_HISTORY_RECONCILIATION.md +111 -0
- package/dist/extensions/rules-engine/RULES-ENGINE-GUIDE.md +470 -0
- package/dist/extensions/rules-engine/cache.js +232 -0
- package/dist/extensions/rules-engine/cache.ts +38 -0
- package/dist/extensions/rules-engine/config.js +63 -0
- package/dist/extensions/rules-engine/config.ts +70 -0
- package/dist/extensions/rules-engine/index.js +1530 -0
- package/dist/extensions/rules-engine/index.ts +552 -0
- package/dist/extensions/rules-engine/injector.js +68 -0
- package/dist/extensions/rules-engine/injector.ts +74 -0
- package/dist/extensions/rules-engine/loader.js +179 -0
- package/dist/extensions/rules-engine/loader.ts +205 -0
- package/dist/extensions/rules-engine/matcher.js +534 -0
- package/dist/extensions/rules-engine/matcher.ts +52 -0
- package/dist/extensions/rules-engine/types.d.ts +156 -0
- package/dist/extensions/rules-engine/types.d.ts.map +1 -0
- package/dist/extensions/rules-engine/types.js +2 -0
- package/dist/extensions/rules-engine/types.js.map +1 -0
- package/dist/extensions/rules-engine/types.ts +169 -0
- package/dist/extensions/session-supervisor/checker.ts +116 -0
- package/dist/extensions/session-supervisor/config.ts +45 -0
- package/dist/extensions/session-supervisor/index.ts +726 -0
- package/dist/extensions/session-supervisor/prompts.ts +132 -0
- package/dist/extensions/session-supervisor/scheduler.ts +69 -0
- package/dist/extensions/session-supervisor/types.ts +215 -0
- package/dist/extensions/subagent/README.md +172 -0
- package/dist/extensions/subagent/agents/explorer.md +25 -0
- package/dist/extensions/subagent/agents/guide.md +27 -0
- package/dist/extensions/subagent/agents/planner.md +37 -0
- package/dist/extensions/subagent/agents/reviewer.md +35 -0
- package/dist/extensions/subagent/agents/scout.md +50 -0
- package/dist/extensions/subagent/agents/verification.md +35 -0
- package/dist/extensions/subagent/agents/worker.md +24 -0
- package/dist/extensions/subagent/agents.ts +25 -0
- package/dist/extensions/subagent/index.ts +987 -0
- package/dist/extensions/subagent/prompts/implement-and-review.md +10 -0
- package/dist/extensions/subagent/prompts/implement.md +10 -0
- package/dist/extensions/subagent/prompts/scout-and-plan.md +9 -0
- package/dist/extensions/subagent-ext/contract.d.ts +2 -0
- package/dist/extensions/subagent-ext/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-ext/contract.js +2 -0
- package/dist/extensions/subagent-ext/contract.js.map +1 -0
- package/dist/extensions/subagent-ext/contract.ts +1 -0
- package/dist/extensions/subagent-ext/index.ts +347 -0
- package/dist/extensions/subagent-shared/contract.d.ts +25 -0
- package/dist/extensions/subagent-shared/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-shared/contract.js +2 -0
- package/dist/extensions/subagent-shared/contract.js.map +1 -0
- package/dist/extensions/subagent-shared/contract.ts +28 -0
- package/dist/extensions/subagent-shared/index.ts +4 -0
- package/dist/extensions/subagent-shared/render.ts +166 -0
- package/dist/extensions/subagent-shared/types.ts +35 -0
- package/dist/extensions/subagent-shared/utils.ts +112 -0
- package/dist/extensions/subagent-v2/contract.d.ts +2 -0
- package/dist/extensions/subagent-v2/contract.d.ts.map +1 -0
- package/dist/extensions/subagent-v2/contract.js +2 -0
- package/dist/extensions/subagent-v2/contract.js.map +1 -0
- package/dist/extensions/subagent-v2/contract.ts +1 -0
- package/dist/extensions/subagent-v2/index.ts +599 -0
- package/dist/extensions/todo-ext/contract.d.ts +27 -0
- package/dist/extensions/todo-ext/contract.d.ts.map +1 -0
- package/dist/extensions/todo-ext/contract.js +2 -0
- package/dist/extensions/todo-ext/contract.js.map +1 -0
- package/dist/extensions/todo-ext/contract.ts +30 -0
- package/dist/extensions/todo-ext/index.ts +419 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/package.json +6 -5
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
import { spawn as spawnChildProcess } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, existsSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, delimiter, join, resolve } from "node:path";
|
|
5
|
+
import { Readable } from "node:stream";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
export type LspRuntimeState = "inactive" | "starting" | "ready" | "error";
|
|
9
|
+
|
|
10
|
+
export type LspTransportMode = "direct" | "lspmux-configured" | "lspmux-auto" | "direct-fallback";
|
|
11
|
+
|
|
12
|
+
export interface LspDiagnosticPosition {
|
|
13
|
+
line: number;
|
|
14
|
+
character: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LspDiagnosticRange {
|
|
18
|
+
start: LspDiagnosticPosition;
|
|
19
|
+
end: LspDiagnosticPosition;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LspDiagnostic {
|
|
23
|
+
range: LspDiagnosticRange;
|
|
24
|
+
severity?: number;
|
|
25
|
+
code?: string | number;
|
|
26
|
+
source?: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface LspRuntimeStatus {
|
|
31
|
+
state: LspRuntimeState;
|
|
32
|
+
reason: string;
|
|
33
|
+
configuredCommand: string[] | undefined;
|
|
34
|
+
activeCommand: string[] | undefined;
|
|
35
|
+
transport: LspTransportMode | undefined;
|
|
36
|
+
lspmuxAvailable: boolean;
|
|
37
|
+
fallbackReason: string | undefined;
|
|
38
|
+
pid: number | undefined;
|
|
39
|
+
diagnosticsCount: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface LspSubprocess {
|
|
43
|
+
pid: number | undefined;
|
|
44
|
+
stdin: {
|
|
45
|
+
write(data: string | Uint8Array): unknown;
|
|
46
|
+
end(): unknown;
|
|
47
|
+
};
|
|
48
|
+
stdout: ReadableStream<Uint8Array> | null;
|
|
49
|
+
stderr: ReadableStream<Uint8Array> | null;
|
|
50
|
+
exited: Promise<number | null>;
|
|
51
|
+
kill(signal?: string | number): unknown;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface LspSpawnOptions {
|
|
55
|
+
cwd: string;
|
|
56
|
+
env: NodeJS.ProcessEnv;
|
|
57
|
+
stdin: "pipe";
|
|
58
|
+
stdout: "pipe";
|
|
59
|
+
stderr: "pipe";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type LspSpawn = (command: string[], options: LspSpawnOptions) => LspSubprocess;
|
|
63
|
+
|
|
64
|
+
interface PendingRpcRequest {
|
|
65
|
+
resolve: (value: unknown) => void;
|
|
66
|
+
reject: (error: Error) => void;
|
|
67
|
+
timeout: NodeJS.Timeout;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface LaunchPlan {
|
|
71
|
+
command: string[];
|
|
72
|
+
transport: LspTransportMode;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface PublishDiagnosticsParams {
|
|
76
|
+
uri?: string;
|
|
77
|
+
diagnostics?: unknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LspClientRuntimeOptions {
|
|
81
|
+
cwd?: string;
|
|
82
|
+
env?: NodeJS.ProcessEnv;
|
|
83
|
+
requestTimeoutMs?: number;
|
|
84
|
+
lspmuxPath?: string;
|
|
85
|
+
spawn?: LspSpawn;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface LspClientRuntime {
|
|
89
|
+
start(configuredCommand: string[] | undefined): Promise<void>;
|
|
90
|
+
stop(): Promise<void>;
|
|
91
|
+
reload(configuredCommand: string[] | undefined): Promise<void>;
|
|
92
|
+
request(method: string, params: unknown, timeoutMs?: number): Promise<unknown>;
|
|
93
|
+
notify(method: string, params: unknown): void;
|
|
94
|
+
getPublishedDiagnostics(filePath?: string): LspDiagnostic[];
|
|
95
|
+
getStatus(): LspRuntimeStatus;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
99
|
+
const MAX_OUTPUT_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
100
|
+
const MAX_FRAME_CONTENT_LENGTH = 4 * 1024 * 1024;
|
|
101
|
+
const LSPMUX_BINARY = "lspmux";
|
|
102
|
+
|
|
103
|
+
export function createLspClientRuntime(options: LspClientRuntimeOptions = {}): LspClientRuntime {
|
|
104
|
+
const cwd = options.cwd ?? process.cwd();
|
|
105
|
+
const env = options.env ?? process.env;
|
|
106
|
+
const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
107
|
+
const spawnProcess: LspSpawn = options.spawn ?? createDefaultSpawn();
|
|
108
|
+
|
|
109
|
+
let currentProcess: LspSubprocess | undefined;
|
|
110
|
+
let isStopping = false;
|
|
111
|
+
let nextRequestId = 1;
|
|
112
|
+
let outputBuffer = Buffer.alloc(0);
|
|
113
|
+
const pendingRequests = new Map<number, PendingRpcRequest>();
|
|
114
|
+
const diagnosticsByUri = new Map<string, LspDiagnostic[]>();
|
|
115
|
+
let _serverTag = "unknown";
|
|
116
|
+
|
|
117
|
+
const status: LspRuntimeStatus = {
|
|
118
|
+
state: "inactive",
|
|
119
|
+
reason: "LSP runtime has not started yet.",
|
|
120
|
+
configuredCommand: undefined,
|
|
121
|
+
activeCommand: undefined,
|
|
122
|
+
transport: undefined,
|
|
123
|
+
lspmuxAvailable: false,
|
|
124
|
+
fallbackReason: undefined,
|
|
125
|
+
pid: undefined,
|
|
126
|
+
diagnosticsCount: 0,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
async start(configuredCommand: string[] | undefined): Promise<void> {
|
|
131
|
+
_serverTag = (configuredCommand?.[0] ?? "unknown").split("/").pop() ?? "unknown";
|
|
132
|
+
status.configuredCommand = cloneCommand(configuredCommand);
|
|
133
|
+
status.fallbackReason = undefined;
|
|
134
|
+
status.lspmuxAvailable = false;
|
|
135
|
+
|
|
136
|
+
if (!configuredCommand || configuredCommand.length === 0) {
|
|
137
|
+
setInactive("No LSP server command configured.");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (currentProcess) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const launchPlans = buildLaunchPlans(configuredCommand, options.lspmuxPath, env);
|
|
146
|
+
status.lspmuxAvailable = launchPlans.some((plan) => plan.transport === "lspmux-auto");
|
|
147
|
+
|
|
148
|
+
let previousError: Error | undefined;
|
|
149
|
+
for (let index = 0; index < launchPlans.length; index++) {
|
|
150
|
+
const plan = launchPlans[index];
|
|
151
|
+
const isRetry = index > 0;
|
|
152
|
+
try {
|
|
153
|
+
await launch(plan);
|
|
154
|
+
if (isRetry && previousError) {
|
|
155
|
+
status.fallbackReason = previousError.message;
|
|
156
|
+
status.transport = "direct-fallback";
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
} catch (error) {
|
|
160
|
+
previousError = error instanceof Error ? error : new Error(String(error));
|
|
161
|
+
await terminateProcess();
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
status.state = "error";
|
|
166
|
+
status.reason = previousError?.message ?? "Failed to start LSP runtime.";
|
|
167
|
+
status.activeCommand = undefined;
|
|
168
|
+
status.transport = undefined;
|
|
169
|
+
status.pid = undefined;
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
async stop(): Promise<void> {
|
|
173
|
+
if (!currentProcess) {
|
|
174
|
+
setInactive("LSP runtime stopped.");
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
isStopping = true;
|
|
179
|
+
try {
|
|
180
|
+
await sendRequest("shutdown", null, 1_000).catch((err) => {
|
|
181
|
+
console.debug("[lsp] shutdown request failed:", err instanceof Error ? err.message : err);
|
|
182
|
+
return undefined;
|
|
183
|
+
});
|
|
184
|
+
sendNotification("exit", null);
|
|
185
|
+
} finally {
|
|
186
|
+
await terminateProcess();
|
|
187
|
+
setInactive("LSP runtime stopped.");
|
|
188
|
+
isStopping = false;
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
|
|
192
|
+
async reload(configuredCommand: string[] | undefined): Promise<void> {
|
|
193
|
+
await this.stop();
|
|
194
|
+
await this.start(configuredCommand);
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
request(method: string, params: unknown, timeoutMs = requestTimeoutMs): Promise<unknown> {
|
|
198
|
+
if (!currentProcess || status.state !== "ready") {
|
|
199
|
+
throw new Error("LSP runtime is not ready.");
|
|
200
|
+
}
|
|
201
|
+
return sendRequest(method, params, timeoutMs);
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
notify(method: string, params: unknown): void {
|
|
205
|
+
if (!currentProcess || status.state !== "ready") {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
sendNotification(method, params);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
getPublishedDiagnostics(filePath?: string): LspDiagnostic[] {
|
|
212
|
+
if (filePath) {
|
|
213
|
+
const uri = pathToFileURL(resolve(cwd, filePath)).href;
|
|
214
|
+
return diagnosticsByUri.get(uri) ?? [];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const allDiagnostics: LspDiagnostic[] = [];
|
|
218
|
+
for (const diagnostics of diagnosticsByUri.values()) {
|
|
219
|
+
allDiagnostics.push(...diagnostics);
|
|
220
|
+
}
|
|
221
|
+
return allDiagnostics;
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
getStatus(): LspRuntimeStatus {
|
|
225
|
+
return {
|
|
226
|
+
...status,
|
|
227
|
+
configuredCommand: cloneCommand(status.configuredCommand),
|
|
228
|
+
activeCommand: cloneCommand(status.activeCommand),
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
function setInactive(reason: string): void {
|
|
234
|
+
status.state = "inactive";
|
|
235
|
+
status.reason = reason;
|
|
236
|
+
status.activeCommand = undefined;
|
|
237
|
+
status.transport = undefined;
|
|
238
|
+
status.pid = undefined;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
async function launch(plan: LaunchPlan): Promise<void> {
|
|
242
|
+
status.state = "starting";
|
|
243
|
+
status.reason = `Starting LSP server via ${plan.transport}.`;
|
|
244
|
+
status.transport = plan.transport;
|
|
245
|
+
status.activeCommand = cloneCommand(plan.command);
|
|
246
|
+
status.pid = undefined;
|
|
247
|
+
outputBuffer = Buffer.alloc(0);
|
|
248
|
+
|
|
249
|
+
const processHandle = spawnProcess(plan.command, {
|
|
250
|
+
cwd,
|
|
251
|
+
env,
|
|
252
|
+
stdin: "pipe",
|
|
253
|
+
stdout: "pipe",
|
|
254
|
+
stderr: "pipe",
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
currentProcess = processHandle;
|
|
258
|
+
status.pid = processHandle.pid;
|
|
259
|
+
|
|
260
|
+
void readStream(processHandle.stdout, (chunk) => handleRpcOutput(chunk));
|
|
261
|
+
void readStream(processHandle.stderr, (chunk) => {
|
|
262
|
+
const msg = Buffer.from(chunk).toString("utf8").trim();
|
|
263
|
+
if (msg) status.reason = `${status.reason.split("|")[0].trim()} | stderr: ${msg.slice(-200)}`;
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
processHandle.exited.then((code) => {
|
|
267
|
+
if (processHandle !== currentProcess) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
if (isStopping) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
rejectPendingRequests(new Error("LSP process exited before requests completed."));
|
|
275
|
+
currentProcess = undefined;
|
|
276
|
+
status.state = "error";
|
|
277
|
+
status.reason = `LSP process exited with code ${code}.`;
|
|
278
|
+
status.pid = undefined;
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
await sendRequest("initialize", {
|
|
282
|
+
processId: process.pid,
|
|
283
|
+
rootUri: pathToFileURL(cwd).href,
|
|
284
|
+
capabilities: {
|
|
285
|
+
textDocument: {
|
|
286
|
+
publishDiagnostics: {
|
|
287
|
+
relatedInformation: true,
|
|
288
|
+
versionSupport: false,
|
|
289
|
+
tagSupport: { valueSet: [1, 2] },
|
|
290
|
+
},
|
|
291
|
+
synchronization: {
|
|
292
|
+
dynamicRegistration: false,
|
|
293
|
+
willSave: false,
|
|
294
|
+
willSaveWaitUntil: false,
|
|
295
|
+
didSave: true,
|
|
296
|
+
},
|
|
297
|
+
hover: { contentFormat: ["markdown", "plaintext"] },
|
|
298
|
+
definition: { linkSupport: true },
|
|
299
|
+
references: {},
|
|
300
|
+
documentSymbol: { hierarchicalDocumentSymbolSupport: true },
|
|
301
|
+
rename: {},
|
|
302
|
+
formatting: {},
|
|
303
|
+
diagnostic: {
|
|
304
|
+
dynamicRegistration: false,
|
|
305
|
+
relatedDocumentSupport: false,
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
workspace: {
|
|
309
|
+
symbol: {},
|
|
310
|
+
configuration: true,
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
clientInfo: {
|
|
314
|
+
name: "pi-lsp-scaffold",
|
|
315
|
+
version: "0.1.0",
|
|
316
|
+
},
|
|
317
|
+
});
|
|
318
|
+
sendNotification("initialized", {});
|
|
319
|
+
|
|
320
|
+
status.state = "ready";
|
|
321
|
+
status.reason = `LSP server ready via ${plan.transport}.`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function sendNotification(method: string, params: unknown): void {
|
|
325
|
+
sendMessage({
|
|
326
|
+
jsonrpc: "2.0",
|
|
327
|
+
method,
|
|
328
|
+
params,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function sendRequest(method: string, params: unknown, timeoutMs = requestTimeoutMs): Promise<unknown> {
|
|
333
|
+
const requestId = nextRequestId++;
|
|
334
|
+
return new Promise((resolveRequest, rejectRequest) => {
|
|
335
|
+
const timeout = setTimeout(() => {
|
|
336
|
+
pendingRequests.delete(requestId);
|
|
337
|
+
rejectRequest(new Error(`Timed out waiting for JSON-RPC response to ${method}.`));
|
|
338
|
+
}, timeoutMs);
|
|
339
|
+
|
|
340
|
+
pendingRequests.set(requestId, { resolve: resolveRequest, reject: rejectRequest, timeout });
|
|
341
|
+
sendMessage({
|
|
342
|
+
jsonrpc: "2.0",
|
|
343
|
+
id: requestId,
|
|
344
|
+
method,
|
|
345
|
+
params,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function sendMessage(message: unknown): void {
|
|
351
|
+
if (!currentProcess) {
|
|
352
|
+
throw new Error("LSP process is not running.");
|
|
353
|
+
}
|
|
354
|
+
const json = JSON.stringify(message);
|
|
355
|
+
const frame = `Content-Length: ${Buffer.byteLength(json, "utf8")}\r\n\r\n${json}`;
|
|
356
|
+
currentProcess.stdin.write(frame);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function handleRpcOutput(chunk: Uint8Array): void {
|
|
360
|
+
if (outputBuffer.length + chunk.length > MAX_OUTPUT_BUFFER_BYTES) {
|
|
361
|
+
rejectPendingRequests(new Error("LSP response buffer overflow."));
|
|
362
|
+
void terminateProcess();
|
|
363
|
+
status.state = "error";
|
|
364
|
+
status.reason = `LSP response buffer exceeded ${MAX_OUTPUT_BUFFER_BYTES} bytes.`;
|
|
365
|
+
outputBuffer = Buffer.alloc(0);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const nextChunk = Buffer.from(chunk);
|
|
370
|
+
outputBuffer = outputBuffer.length === 0 ? nextChunk : Buffer.concat([outputBuffer, nextChunk]);
|
|
371
|
+
|
|
372
|
+
while (true) {
|
|
373
|
+
const headerEnd = outputBuffer.indexOf("\r\n\r\n");
|
|
374
|
+
if (headerEnd === -1) {
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const header = outputBuffer.slice(0, headerEnd).toString("utf8");
|
|
379
|
+
const lengthMatch = /Content-Length:\s*(\d+)/i.exec(header);
|
|
380
|
+
if (!lengthMatch) {
|
|
381
|
+
outputBuffer = outputBuffer.slice(headerEnd + 4);
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const contentLength = Number.parseInt(lengthMatch[1], 10);
|
|
386
|
+
if (!Number.isFinite(contentLength) || contentLength < 0 || contentLength > MAX_FRAME_CONTENT_LENGTH) {
|
|
387
|
+
rejectPendingRequests(new Error(`Invalid LSP frame length: ${lengthMatch[1]}`));
|
|
388
|
+
void terminateProcess();
|
|
389
|
+
status.state = "error";
|
|
390
|
+
status.reason = `Invalid LSP frame length ${lengthMatch[1]}.`;
|
|
391
|
+
outputBuffer = Buffer.alloc(0);
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const frameEnd = headerEnd + 4 + contentLength;
|
|
396
|
+
if (outputBuffer.length < frameEnd) {
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const payload = outputBuffer.slice(headerEnd + 4, frameEnd).toString("utf8");
|
|
401
|
+
outputBuffer = outputBuffer.slice(frameEnd);
|
|
402
|
+
handleRpcMessage(payload);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function handleRpcMessage(payload: string): void {
|
|
407
|
+
let parsed: Record<string, unknown>;
|
|
408
|
+
try {
|
|
409
|
+
parsed = JSON.parse(payload) as Record<string, unknown>;
|
|
410
|
+
} catch (err) {
|
|
411
|
+
console.debug("[lsp] RPC message parse failed:", err instanceof Error ? err.message : err);
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const hasMethod = typeof parsed.method === "string";
|
|
416
|
+
const hasResult = "result" in parsed;
|
|
417
|
+
const hasError = "error" in parsed;
|
|
418
|
+
const rid = normalizeRequestId(parsed.id);
|
|
419
|
+
|
|
420
|
+
if (hasMethod && parsed.method === "textDocument/publishDiagnostics") {
|
|
421
|
+
const params = (parsed.params ?? {}) as PublishDiagnosticsParams;
|
|
422
|
+
if (typeof params.uri === "string") {
|
|
423
|
+
const diagnostics = normalizeDiagnostics(params.diagnostics);
|
|
424
|
+
diagnosticsByUri.set(params.uri, diagnostics);
|
|
425
|
+
status.diagnosticsCount = countDiagnostics(diagnosticsByUri);
|
|
426
|
+
}
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (hasMethod && rid !== undefined && !hasResult && !hasError) {
|
|
431
|
+
handleIncomingRequest(rid, parsed.method as string, parsed.params);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (hasMethod && rid === undefined) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (rid === undefined) {
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const pending = pendingRequests.get(rid);
|
|
444
|
+
if (!pending) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
clearTimeout(pending.timeout);
|
|
449
|
+
pendingRequests.delete(rid);
|
|
450
|
+
|
|
451
|
+
if (hasError) {
|
|
452
|
+
pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : JSON.stringify(parsed.error)));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
pending.resolve(parsed.result);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function handleIncomingRequest(id: number, method: string, params: unknown): void {
|
|
460
|
+
let result: unknown = null;
|
|
461
|
+
|
|
462
|
+
if (method === "eslint/confirmESLintExecution") {
|
|
463
|
+
result = 4;
|
|
464
|
+
} else if (
|
|
465
|
+
method === "eslint/noLibrary" ||
|
|
466
|
+
method === "eslint/noConfig" ||
|
|
467
|
+
method === "eslint/probeFailed" ||
|
|
468
|
+
method === "eslint/openDoc"
|
|
469
|
+
) {
|
|
470
|
+
result = {};
|
|
471
|
+
} else if (method === "window/showMessageRequest") {
|
|
472
|
+
result = null;
|
|
473
|
+
} else if (method === "workspace/configuration") {
|
|
474
|
+
result = handleWorkspaceConfiguration(params);
|
|
475
|
+
} else if (method === "client/registerCapability" || method === "client/unregisterCapability") {
|
|
476
|
+
result = null;
|
|
477
|
+
} else {
|
|
478
|
+
result = null;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
sendMessage({
|
|
482
|
+
jsonrpc: "2.0",
|
|
483
|
+
id,
|
|
484
|
+
result,
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function handleWorkspaceConfiguration(params: unknown): unknown[] {
|
|
489
|
+
let items: unknown[];
|
|
490
|
+
if (Array.isArray(params)) {
|
|
491
|
+
items = params;
|
|
492
|
+
} else if (params && typeof params === "object" && Array.isArray((params as Record<string, unknown>).items)) {
|
|
493
|
+
items = (params as Record<string, unknown>).items as unknown[];
|
|
494
|
+
} else {
|
|
495
|
+
items = [];
|
|
496
|
+
}
|
|
497
|
+
if (items.length === 0) {
|
|
498
|
+
return [buildEslintSettings()];
|
|
499
|
+
}
|
|
500
|
+
return items.map(() => buildEslintSettings());
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function buildEslintSettings(): Record<string, unknown> {
|
|
504
|
+
return {
|
|
505
|
+
validate: "on",
|
|
506
|
+
packageManager: "npm",
|
|
507
|
+
useESLintClass: false,
|
|
508
|
+
useFlatConfig: false,
|
|
509
|
+
experimental: { useFlatConfig: false },
|
|
510
|
+
codeActionOnSave: { enable: false, mode: "all" },
|
|
511
|
+
format: true,
|
|
512
|
+
quiet: false,
|
|
513
|
+
onIgnoredFiles: "off",
|
|
514
|
+
rulesCustomizations: [],
|
|
515
|
+
run: "onType",
|
|
516
|
+
problems: { shortenToSingleLine: false },
|
|
517
|
+
nodePath: "",
|
|
518
|
+
workingDirectory: { mode: "location" },
|
|
519
|
+
workspaceFolder: {
|
|
520
|
+
uri: pathToFileURL(cwd).href,
|
|
521
|
+
name: cwd.split("/").pop() ?? "workspace",
|
|
522
|
+
},
|
|
523
|
+
codeAction: {
|
|
524
|
+
disableRuleComment: { enable: true, location: "separateLine" },
|
|
525
|
+
showDocumentation: { enable: true },
|
|
526
|
+
},
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async function terminateProcess(): Promise<void> {
|
|
531
|
+
if (!currentProcess) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const processHandle = currentProcess;
|
|
536
|
+
currentProcess = undefined;
|
|
537
|
+
rejectPendingRequests(new Error("LSP process stopped."));
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
processHandle.stdin.end();
|
|
541
|
+
} catch (err) {
|
|
542
|
+
console.debug("[lsp] stdin end failed:", err instanceof Error ? err.message : err);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const exited = await Promise.race([
|
|
546
|
+
processHandle.exited,
|
|
547
|
+
new Promise<null>((resolvePromise) => setTimeout(() => resolvePromise(null), 1_000)),
|
|
548
|
+
]);
|
|
549
|
+
|
|
550
|
+
if (exited === null) {
|
|
551
|
+
try {
|
|
552
|
+
processHandle.kill("SIGKILL");
|
|
553
|
+
} catch (err) {
|
|
554
|
+
console.debug("[lsp] SIGKILL failed:", err instanceof Error ? err.message : err);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function rejectPendingRequests(error: Error): void {
|
|
560
|
+
for (const pending of pendingRequests.values()) {
|
|
561
|
+
clearTimeout(pending.timeout);
|
|
562
|
+
pending.reject(error);
|
|
563
|
+
}
|
|
564
|
+
pendingRequests.clear();
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function createDefaultSpawn(): LspSpawn {
|
|
569
|
+
const bunRuntime = getBunRuntime();
|
|
570
|
+
if (bunRuntime) {
|
|
571
|
+
return (command, spawnOptions) =>
|
|
572
|
+
bunRuntime.spawn(command, {
|
|
573
|
+
cwd: spawnOptions.cwd,
|
|
574
|
+
env: spawnOptions.env,
|
|
575
|
+
stdin: spawnOptions.stdin,
|
|
576
|
+
stdout: spawnOptions.stdout,
|
|
577
|
+
stderr: spawnOptions.stderr,
|
|
578
|
+
}) as unknown as LspSubprocess;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return (command, spawnOptions) => {
|
|
582
|
+
if (command.length === 0 || !command[0]) {
|
|
583
|
+
throw new Error("No LSP command provided.");
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
const [binary, ...args] = command;
|
|
587
|
+
const child = spawnChildProcess(binary, args, {
|
|
588
|
+
cwd: spawnOptions.cwd,
|
|
589
|
+
env: spawnOptions.env,
|
|
590
|
+
stdio: [spawnOptions.stdin, spawnOptions.stdout, spawnOptions.stderr],
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (!child.stdin || !child.stdout || !child.stderr) {
|
|
594
|
+
throw new Error(`Failed to start LSP process ${binary}: stdio pipes unavailable.`);
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return {
|
|
598
|
+
pid: child.pid ?? undefined,
|
|
599
|
+
stdin: child.stdin,
|
|
600
|
+
stdout: toWebReadable(child.stdout),
|
|
601
|
+
stderr: toWebReadable(child.stderr),
|
|
602
|
+
exited: new Promise<number | null>((resolve, reject) => {
|
|
603
|
+
child.once("error", (error) => reject(error));
|
|
604
|
+
child.once("exit", (code) => resolve(code));
|
|
605
|
+
}),
|
|
606
|
+
kill(signal?: string | number): unknown {
|
|
607
|
+
child.kill(signal as NodeJS.Signals | number | undefined);
|
|
608
|
+
return undefined;
|
|
609
|
+
},
|
|
610
|
+
};
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getBunRuntime():
|
|
615
|
+
| {
|
|
616
|
+
spawn(command: string[], options: LspSpawnOptions): unknown;
|
|
617
|
+
}
|
|
618
|
+
| undefined {
|
|
619
|
+
const candidate = (globalThis as { Bun?: unknown }).Bun;
|
|
620
|
+
if (!candidate || typeof candidate !== "object") {
|
|
621
|
+
return undefined;
|
|
622
|
+
}
|
|
623
|
+
const bunLike = candidate as {
|
|
624
|
+
spawn?: unknown;
|
|
625
|
+
};
|
|
626
|
+
if (typeof bunLike.spawn !== "function") {
|
|
627
|
+
return undefined;
|
|
628
|
+
}
|
|
629
|
+
return bunLike as {
|
|
630
|
+
spawn(command: string[], options: LspSpawnOptions): unknown;
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function buildLaunchPlans(
|
|
635
|
+
configuredCommand: string[],
|
|
636
|
+
explicitLspmuxPath: string | undefined,
|
|
637
|
+
env: NodeJS.ProcessEnv,
|
|
638
|
+
): LaunchPlan[] {
|
|
639
|
+
if (isLspmuxCommand(configuredCommand[0])) {
|
|
640
|
+
return [{ command: configuredCommand, transport: "lspmux-configured" }];
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const lspmuxPath = resolveLspmuxPath(explicitLspmuxPath, env);
|
|
644
|
+
if (!lspmuxPath) {
|
|
645
|
+
return [{ command: configuredCommand, transport: "direct" }];
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
return [
|
|
649
|
+
{
|
|
650
|
+
command: [lspmuxPath, "--", ...configuredCommand],
|
|
651
|
+
transport: "lspmux-auto",
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
command: configuredCommand,
|
|
655
|
+
transport: "direct-fallback",
|
|
656
|
+
},
|
|
657
|
+
];
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function resolveLspmuxPath(explicitPath: string | undefined, env: NodeJS.ProcessEnv): string | undefined {
|
|
661
|
+
if (explicitPath) {
|
|
662
|
+
return isExecutable(explicitPath) ? explicitPath : undefined;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path");
|
|
666
|
+
const pathValue = pathKey ? env[pathKey] : undefined;
|
|
667
|
+
if (!pathValue) {
|
|
668
|
+
return undefined;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
for (const directory of pathValue.split(delimiter)) {
|
|
672
|
+
if (!directory) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
for (const candidateName of executableCandidates(LSPMUX_BINARY)) {
|
|
676
|
+
const fullPath = join(directory, candidateName);
|
|
677
|
+
if (isExecutable(fullPath)) {
|
|
678
|
+
return fullPath;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const userFallback = join(homedir(), ".local", "bin", LSPMUX_BINARY);
|
|
684
|
+
return isExecutable(userFallback) ? userFallback : undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function executableCandidates(binary: string): string[] {
|
|
688
|
+
if (process.platform !== "win32") {
|
|
689
|
+
return [binary];
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const lower = binary.toLowerCase();
|
|
693
|
+
const candidates = [binary];
|
|
694
|
+
if (!lower.endsWith(".exe")) candidates.push(`${binary}.exe`);
|
|
695
|
+
if (!lower.endsWith(".cmd")) candidates.push(`${binary}.cmd`);
|
|
696
|
+
if (!lower.endsWith(".bat")) candidates.push(`${binary}.bat`);
|
|
697
|
+
return candidates;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
function isExecutable(filePath: string): boolean {
|
|
701
|
+
if (!existsSync(filePath)) {
|
|
702
|
+
return false;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (process.platform === "win32") {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
try {
|
|
710
|
+
accessSync(filePath, constants.X_OK);
|
|
711
|
+
return true;
|
|
712
|
+
} catch (err) {
|
|
713
|
+
console.debug("[lsp] executable check failed:", err instanceof Error ? err.message : err);
|
|
714
|
+
return false;
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function isLspmuxCommand(binary: string | undefined): boolean {
|
|
719
|
+
if (!binary) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
const name = basename(binary).toLowerCase();
|
|
723
|
+
return name === "lspmux" || name === "lspmux.exe" || name === "lspmux.cmd" || name === "lspmux.bat";
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function normalizeDiagnostics(rawDiagnostics: unknown): LspDiagnostic[] {
|
|
727
|
+
if (!Array.isArray(rawDiagnostics)) {
|
|
728
|
+
return [];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const diagnostics: LspDiagnostic[] = [];
|
|
732
|
+
for (const raw of rawDiagnostics) {
|
|
733
|
+
if (!raw || typeof raw !== "object") {
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const diagnostic = raw as Record<string, unknown>;
|
|
738
|
+
const message = typeof diagnostic.message === "string" ? diagnostic.message : undefined;
|
|
739
|
+
const range = normalizeRange(diagnostic.range);
|
|
740
|
+
if (!message || !range) {
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
diagnostics.push({
|
|
745
|
+
range,
|
|
746
|
+
severity: typeof diagnostic.severity === "number" ? diagnostic.severity : undefined,
|
|
747
|
+
code: typeof diagnostic.code === "string" || typeof diagnostic.code === "number" ? diagnostic.code : undefined,
|
|
748
|
+
source: typeof diagnostic.source === "string" ? diagnostic.source : undefined,
|
|
749
|
+
message,
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
return diagnostics;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
function normalizeRange(rawRange: unknown): LspDiagnosticRange | undefined {
|
|
757
|
+
if (!rawRange || typeof rawRange !== "object") {
|
|
758
|
+
return undefined;
|
|
759
|
+
}
|
|
760
|
+
const range = rawRange as Record<string, unknown>;
|
|
761
|
+
const start = normalizePosition(range.start);
|
|
762
|
+
const end = normalizePosition(range.end);
|
|
763
|
+
if (!start || !end) {
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
return { start, end };
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function normalizePosition(rawPosition: unknown): LspDiagnosticPosition | undefined {
|
|
770
|
+
if (!rawPosition || typeof rawPosition !== "object") {
|
|
771
|
+
return undefined;
|
|
772
|
+
}
|
|
773
|
+
const position = rawPosition as Record<string, unknown>;
|
|
774
|
+
if (typeof position.line !== "number" || typeof position.character !== "number") {
|
|
775
|
+
return undefined;
|
|
776
|
+
}
|
|
777
|
+
return {
|
|
778
|
+
line: position.line,
|
|
779
|
+
character: position.character,
|
|
780
|
+
};
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function normalizeRequestId(value: unknown): number | undefined {
|
|
784
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) {
|
|
785
|
+
return value;
|
|
786
|
+
}
|
|
787
|
+
if (typeof value === "string" && /^\d+$/.test(value)) {
|
|
788
|
+
const parsed = Number.parseInt(value, 10);
|
|
789
|
+
return Number.isSafeInteger(parsed) ? parsed : undefined;
|
|
790
|
+
}
|
|
791
|
+
return undefined;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function countDiagnostics(diagnosticsByUri: Map<string, LspDiagnostic[]>): number {
|
|
795
|
+
let total = 0;
|
|
796
|
+
for (const diagnostics of diagnosticsByUri.values()) {
|
|
797
|
+
total += diagnostics.length;
|
|
798
|
+
}
|
|
799
|
+
return total;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function cloneCommand(command: string[] | undefined): string[] | undefined {
|
|
803
|
+
return command ? [...command] : undefined;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function toWebReadable(stream: Readable): ReadableStream<Uint8Array> {
|
|
807
|
+
return Readable.toWeb(stream) as unknown as ReadableStream<Uint8Array>;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
async function readStream(
|
|
811
|
+
stream: ReadableStream<Uint8Array> | null,
|
|
812
|
+
onChunk: (chunk: Uint8Array) => void,
|
|
813
|
+
): Promise<void> {
|
|
814
|
+
if (!stream) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
const reader = stream.getReader();
|
|
819
|
+
try {
|
|
820
|
+
while (true) {
|
|
821
|
+
const { value, done } = await reader.read();
|
|
822
|
+
if (done) {
|
|
823
|
+
break;
|
|
824
|
+
}
|
|
825
|
+
if (value) {
|
|
826
|
+
onChunk(value);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
} finally {
|
|
830
|
+
reader.releaseLock();
|
|
831
|
+
}
|
|
832
|
+
}
|