pi-lens 3.8.21 → 3.8.23
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 +28 -0
- package/README.md +2 -0
- package/clients/dispatch/dispatcher.ts +75 -91
- package/clients/dispatch/fact-provider-types.ts +22 -0
- package/clients/dispatch/fact-rule-runner.ts +22 -0
- package/clients/dispatch/fact-runner.ts +28 -0
- package/clients/dispatch/fact-scheduler.ts +78 -0
- package/clients/dispatch/fact-store.ts +67 -0
- package/clients/dispatch/facts/comment-facts.ts +59 -0
- package/clients/dispatch/facts/file-content.ts +20 -0
- package/clients/dispatch/facts/function-facts.ts +177 -0
- package/clients/dispatch/facts/try-catch-facts.ts +80 -0
- package/clients/dispatch/integration.ts +130 -24
- package/clients/dispatch/priorities.ts +22 -0
- package/clients/dispatch/rules/async-noise.ts +43 -0
- package/clients/dispatch/rules/error-obscuring.ts +40 -0
- package/clients/dispatch/rules/error-swallowing.ts +35 -0
- package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
- package/clients/dispatch/rules/placeholder-comments.ts +47 -0
- package/clients/dispatch/runners/architect.ts +2 -1
- package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
- package/clients/dispatch/runners/biome-check.ts +40 -8
- package/clients/dispatch/runners/biome.ts +2 -1
- package/clients/dispatch/runners/eslint.ts +34 -6
- package/clients/dispatch/runners/go-vet.ts +2 -1
- package/clients/dispatch/runners/golangci-lint.ts +2 -1
- package/clients/dispatch/runners/index.ts +29 -27
- package/clients/dispatch/runners/lsp.ts +60 -4
- package/clients/dispatch/runners/oxlint.ts +2 -1
- package/clients/dispatch/runners/pyright.ts +2 -1
- package/clients/dispatch/runners/python-slop.ts +2 -1
- package/clients/dispatch/runners/rubocop.ts +2 -1
- package/clients/dispatch/runners/ruff.ts +2 -1
- package/clients/dispatch/runners/rust-clippy.ts +2 -1
- package/clients/dispatch/runners/shellcheck.ts +2 -1
- package/clients/dispatch/runners/similarity.ts +2 -1
- package/clients/dispatch/runners/spellcheck.ts +2 -1
- package/clients/dispatch/runners/sqlfluff.ts +2 -1
- package/clients/dispatch/runners/tree-sitter.ts +469 -1
- package/clients/dispatch/runners/ts-lsp.ts +2 -1
- package/clients/dispatch/runners/type-safety.ts +2 -1
- package/clients/dispatch/runners/yamllint.ts +2 -1
- package/clients/dispatch/tool-profile.ts +40 -0
- package/clients/dispatch/types.ts +3 -13
- package/clients/lsp/client.ts +366 -12
- package/clients/lsp/index.ts +374 -76
- package/clients/lsp/launch.ts +42 -2
- package/clients/lsp/server.ts +186 -12
- package/clients/pipeline.ts +2 -2
- package/clients/runtime-context.ts +2 -2
- package/clients/runtime-session.ts +43 -5
- package/clients/session-summary.ts +21 -0
- package/clients/tree-sitter-client.ts +162 -0
- package/clients/tree-sitter-logger.ts +47 -0
- package/clients/tree-sitter-query-loader.ts +13 -2
- package/index.ts +67 -17
- package/package.json +3 -1
- package/rules/rule-catalog.json +64 -0
- package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
- package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
- package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
- package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
- package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
- package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
- package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
- package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
- package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
- package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
- package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
- package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
- package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
- package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
- package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
- package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
- package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
- package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
- package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
- package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
- package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
- package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
- package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
- package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
- package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
- package/scripts/validate-rule-catalog.mjs +227 -0
- package/skills/lsp-navigation/SKILL.md +15 -3
- package/tools/lsp-navigation.js +466 -79
- package/tools/lsp-navigation.ts +587 -85
package/clients/lsp/client.ts
CHANGED
|
@@ -49,6 +49,53 @@ export interface LSPHover {
|
|
|
49
49
|
range?: LSPLocation["range"];
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface LSPSignatureHelp {
|
|
53
|
+
signatures: Array<{
|
|
54
|
+
label: string;
|
|
55
|
+
documentation?: string | { kind: string; value: string };
|
|
56
|
+
parameters?: Array<{
|
|
57
|
+
label: string | [number, number];
|
|
58
|
+
documentation?: string | { kind: string; value: string };
|
|
59
|
+
}>;
|
|
60
|
+
}>;
|
|
61
|
+
activeSignature?: number;
|
|
62
|
+
activeParameter?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface LSPCodeAction {
|
|
66
|
+
title: string;
|
|
67
|
+
kind?: string;
|
|
68
|
+
diagnostics?: LSPDiagnostic[];
|
|
69
|
+
edit?: unknown;
|
|
70
|
+
command?: unknown;
|
|
71
|
+
data?: unknown;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface LSPWorkspaceEdit {
|
|
75
|
+
changes?: Record<string, unknown[]>;
|
|
76
|
+
documentChanges?: unknown[];
|
|
77
|
+
changeAnnotations?: Record<string, unknown>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface LSPWorkspaceDiagnosticsSupport {
|
|
81
|
+
advertised: boolean;
|
|
82
|
+
mode: "pull" | "push-only";
|
|
83
|
+
diagnosticProviderKind: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface LSPOperationSupport {
|
|
87
|
+
definition: boolean;
|
|
88
|
+
references: boolean;
|
|
89
|
+
hover: boolean;
|
|
90
|
+
signatureHelp: boolean;
|
|
91
|
+
documentSymbol: boolean;
|
|
92
|
+
workspaceSymbol: boolean;
|
|
93
|
+
codeAction: boolean;
|
|
94
|
+
rename: boolean;
|
|
95
|
+
implementation: boolean;
|
|
96
|
+
callHierarchy: boolean;
|
|
97
|
+
}
|
|
98
|
+
|
|
52
99
|
export interface LSPSymbol {
|
|
53
100
|
name: string;
|
|
54
101
|
kind: number;
|
|
@@ -93,6 +140,10 @@ export interface LSPClientInfo {
|
|
|
93
140
|
waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
|
|
94
141
|
/** Get all tracked diagnostics (for cascade checking) */
|
|
95
142
|
getAllDiagnostics(): Map<string, LSPDiagnostic[]>;
|
|
143
|
+
/** Capability snapshot for workspace diagnostics support */
|
|
144
|
+
getWorkspaceDiagnosticsSupport(): LSPWorkspaceDiagnosticsSupport;
|
|
145
|
+
/** Capability snapshot for navigation/edit operations */
|
|
146
|
+
getOperationSupport(): LSPOperationSupport;
|
|
96
147
|
/** Go to definition — returns Location[] */
|
|
97
148
|
definition(
|
|
98
149
|
filePath: string,
|
|
@@ -112,10 +163,31 @@ export interface LSPClientInfo {
|
|
|
112
163
|
line: number,
|
|
113
164
|
character: number,
|
|
114
165
|
): Promise<LSPHover | null>;
|
|
166
|
+
/** Signature help at position */
|
|
167
|
+
signatureHelp(
|
|
168
|
+
filePath: string,
|
|
169
|
+
line: number,
|
|
170
|
+
character: number,
|
|
171
|
+
): Promise<LSPSignatureHelp | null>;
|
|
115
172
|
/** Symbols in a document */
|
|
116
173
|
documentSymbol(filePath: string): Promise<LSPSymbol[]>;
|
|
117
174
|
/** Workspace-wide symbol search */
|
|
118
175
|
workspaceSymbol(query: string): Promise<LSPSymbol[]>;
|
|
176
|
+
/** Available code actions at a range */
|
|
177
|
+
codeAction(
|
|
178
|
+
filePath: string,
|
|
179
|
+
line: number,
|
|
180
|
+
character: number,
|
|
181
|
+
endLine: number,
|
|
182
|
+
endCharacter: number,
|
|
183
|
+
): Promise<LSPCodeAction[]>;
|
|
184
|
+
/** Rename symbol at position */
|
|
185
|
+
rename(
|
|
186
|
+
filePath: string,
|
|
187
|
+
line: number,
|
|
188
|
+
character: number,
|
|
189
|
+
newName: string,
|
|
190
|
+
): Promise<LSPWorkspaceEdit | null>;
|
|
119
191
|
/** Go to implementation */
|
|
120
192
|
implementation(
|
|
121
193
|
filePath: string,
|
|
@@ -141,8 +213,66 @@ export interface LSPClientInfo {
|
|
|
141
213
|
|
|
142
214
|
// --- Constants ---
|
|
143
215
|
|
|
144
|
-
const DIAGNOSTICS_DEBOUNCE_MS =
|
|
145
|
-
|
|
216
|
+
const DIAGNOSTICS_DEBOUNCE_MS = positiveIntFromEnv(
|
|
217
|
+
"PI_LENS_LSP_DIAGNOSTICS_DEBOUNCE_MS",
|
|
218
|
+
150,
|
|
219
|
+
); // ms — waits for follow-up semantic diagnostics
|
|
220
|
+
const INITIALIZE_TIMEOUT_MS = positiveIntFromEnv(
|
|
221
|
+
"PI_LENS_LSP_INIT_TIMEOUT_MS",
|
|
222
|
+
15_000,
|
|
223
|
+
); // 15s — npx downloads are handled by ensureTool, not here
|
|
224
|
+
const DIAGNOSTICS_WAIT_TIMEOUT_MS = positiveIntFromEnv(
|
|
225
|
+
"PI_LENS_LSP_DIAGNOSTICS_WAIT_MS",
|
|
226
|
+
10_000,
|
|
227
|
+
);
|
|
228
|
+
const PULL_DIAGNOSTICS_RETRY_BUDGET_MS = positiveIntFromEnv(
|
|
229
|
+
"PI_LENS_LSP_PULL_RETRY_BUDGET_MS",
|
|
230
|
+
1200,
|
|
231
|
+
);
|
|
232
|
+
const PULL_DIAGNOSTICS_RETRY_INTERVAL_MS = positiveIntFromEnv(
|
|
233
|
+
"PI_LENS_LSP_PULL_RETRY_INTERVAL_MS",
|
|
234
|
+
250,
|
|
235
|
+
);
|
|
236
|
+
|
|
237
|
+
const LSP_CRASH_CODES = new Set([
|
|
238
|
+
"ERR_STREAM_DESTROYED",
|
|
239
|
+
"ERR_STREAM_WRITE_AFTER_END",
|
|
240
|
+
"EPIPE",
|
|
241
|
+
"ECONNRESET",
|
|
242
|
+
]);
|
|
243
|
+
|
|
244
|
+
let crashGuardInstalled = false;
|
|
245
|
+
|
|
246
|
+
function isIgnorableLspRuntimeCrash(err: unknown): boolean {
|
|
247
|
+
if (!(err instanceof Error)) return false;
|
|
248
|
+
const code = (err as { code?: string }).code;
|
|
249
|
+
if (code && LSP_CRASH_CODES.has(code)) return true;
|
|
250
|
+
const msg = err.message.toLowerCase();
|
|
251
|
+
const stack = (err.stack ?? "").toLowerCase();
|
|
252
|
+
return (
|
|
253
|
+
msg.includes("stream") ||
|
|
254
|
+
msg.includes("write after end") ||
|
|
255
|
+
stack.includes("vscode-jsonrpc/lib/node/ril.js")
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function installCrashGuard(): void {
|
|
260
|
+
if (crashGuardInstalled) return;
|
|
261
|
+
crashGuardInstalled = true;
|
|
262
|
+
|
|
263
|
+
process.on("uncaughtException", (err) => {
|
|
264
|
+
if (isIgnorableLspRuntimeCrash(err)) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
throw err;
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
process.on("unhandledRejection", (reason) => {
|
|
271
|
+
if (isIgnorableLspRuntimeCrash(reason)) {
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
}
|
|
146
276
|
|
|
147
277
|
// --- Client Factory ---
|
|
148
278
|
|
|
@@ -152,6 +282,8 @@ export async function createLSPClient(options: {
|
|
|
152
282
|
root: string;
|
|
153
283
|
initialization?: Record<string, unknown>;
|
|
154
284
|
}): Promise<LSPClientInfo> {
|
|
285
|
+
installCrashGuard();
|
|
286
|
+
|
|
155
287
|
const { serverId, process: lspProcess, root, initialization } = options;
|
|
156
288
|
|
|
157
289
|
// Attach persistent 'error' listeners to all three stdio streams.
|
|
@@ -174,6 +306,7 @@ export async function createLSPClient(options: {
|
|
|
174
306
|
(label: string) => (err: Error & { code?: string }) => {
|
|
175
307
|
if (
|
|
176
308
|
err.code === "ERR_STREAM_DESTROYED" ||
|
|
309
|
+
err.code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
177
310
|
err.code === "EPIPE" ||
|
|
178
311
|
err.code === "ECONNRESET"
|
|
179
312
|
)
|
|
@@ -215,21 +348,22 @@ export async function createLSPClient(options: {
|
|
|
215
348
|
"textDocument/publishDiagnostics",
|
|
216
349
|
(params: { uri: string; diagnostics?: LSPDiagnostic[] }) => {
|
|
217
350
|
const filePath = uriToPath(params.uri);
|
|
351
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
218
352
|
const newDiags: LSPDiagnostic[] = params.diagnostics || [];
|
|
219
353
|
|
|
220
354
|
// Debounce: clear existing timer and set new one
|
|
221
|
-
const existingTimer = pendingDiagnostics.get(
|
|
355
|
+
const existingTimer = pendingDiagnostics.get(normalizedPath);
|
|
222
356
|
if (existingTimer) clearTimeout(existingTimer);
|
|
223
357
|
|
|
224
358
|
const timer = setTimeout(() => {
|
|
225
|
-
diagnostics.set(
|
|
226
|
-
pendingDiagnostics.delete(
|
|
359
|
+
diagnostics.set(normalizedPath, newDiags);
|
|
360
|
+
pendingDiagnostics.delete(normalizedPath);
|
|
227
361
|
|
|
228
362
|
// Signal any active waitForDiagnostics calls for this file.
|
|
229
|
-
diagnosticEmitter.emit("diagnostics",
|
|
363
|
+
diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
230
364
|
}, DIAGNOSTICS_DEBOUNCE_MS);
|
|
231
365
|
|
|
232
|
-
pendingDiagnostics.set(
|
|
366
|
+
pendingDiagnostics.set(normalizedPath, timer);
|
|
233
367
|
},
|
|
234
368
|
);
|
|
235
369
|
|
|
@@ -255,25 +389,39 @@ export async function createLSPClient(options: {
|
|
|
255
389
|
let isConnected = true;
|
|
256
390
|
let lastError: Error | undefined;
|
|
257
391
|
let isDestroyed = false;
|
|
392
|
+
let connectionDisposed = false;
|
|
393
|
+
|
|
394
|
+
function disposeConnection(): void {
|
|
395
|
+
if (connectionDisposed) return;
|
|
396
|
+
connectionDisposed = true;
|
|
397
|
+
try {
|
|
398
|
+
connection.dispose();
|
|
399
|
+
} catch {
|
|
400
|
+
// ignore
|
|
401
|
+
}
|
|
402
|
+
}
|
|
258
403
|
|
|
259
404
|
// Handle connection errors and close events
|
|
260
405
|
connection.onError((error) => {
|
|
261
406
|
lastError = error instanceof Error ? error : new Error(String(error));
|
|
262
407
|
isConnected = false;
|
|
263
408
|
isDestroyed = true;
|
|
409
|
+
disposeConnection();
|
|
264
410
|
console.error(`[lsp] ${serverId} connection error:`, lastError.message);
|
|
265
411
|
});
|
|
266
412
|
|
|
267
413
|
connection.onClose(() => {
|
|
268
414
|
isConnected = false;
|
|
269
415
|
isDestroyed = true;
|
|
416
|
+
disposeConnection();
|
|
270
417
|
});
|
|
271
418
|
|
|
272
419
|
// Also handle process exit to catch crashes immediately
|
|
273
420
|
lspProcess.process.on("exit", (code) => {
|
|
421
|
+
isConnected = false;
|
|
422
|
+
isDestroyed = true;
|
|
423
|
+
disposeConnection();
|
|
274
424
|
if (code !== 0 && code !== null) {
|
|
275
|
-
isConnected = false;
|
|
276
|
-
isDestroyed = true;
|
|
277
425
|
console.error(`[lsp] ${serverId} process exited with code ${code}`);
|
|
278
426
|
}
|
|
279
427
|
});
|
|
@@ -327,6 +475,9 @@ export async function createLSPClient(options: {
|
|
|
327
475
|
);
|
|
328
476
|
}
|
|
329
477
|
|
|
478
|
+
const workspaceDiagnosticsSupport = detectWorkspaceDiagnosticsSupport(initResult);
|
|
479
|
+
const operationSupport = detectOperationSupport(initResult);
|
|
480
|
+
|
|
330
481
|
// Send initialized notification
|
|
331
482
|
await safeSendNotification(connection, "initialized", {});
|
|
332
483
|
|
|
@@ -437,9 +588,41 @@ export async function createLSPClient(options: {
|
|
|
437
588
|
return new Map(diagnostics);
|
|
438
589
|
},
|
|
439
590
|
|
|
440
|
-
|
|
591
|
+
getWorkspaceDiagnosticsSupport() {
|
|
592
|
+
return workspaceDiagnosticsSupport;
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
getOperationSupport() {
|
|
596
|
+
return operationSupport;
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
async waitForDiagnostics(filePath, timeoutMs = DIAGNOSTICS_WAIT_TIMEOUT_MS) {
|
|
441
600
|
const normalizedPath = normalizeMapKey(filePath);
|
|
442
601
|
|
|
602
|
+
if (workspaceDiagnosticsSupport.mode === "pull") {
|
|
603
|
+
const firstPullCount = await requestPullDiagnostics(filePath);
|
|
604
|
+
if (firstPullCount > 0) return;
|
|
605
|
+
|
|
606
|
+
const retryBudgetMs = Math.min(
|
|
607
|
+
timeoutMs,
|
|
608
|
+
PULL_DIAGNOSTICS_RETRY_BUDGET_MS,
|
|
609
|
+
);
|
|
610
|
+
const startedAt = Date.now();
|
|
611
|
+
let latestCount = firstPullCount;
|
|
612
|
+
|
|
613
|
+
while (
|
|
614
|
+
latestCount === 0 &&
|
|
615
|
+
Date.now() - startedAt < retryBudgetMs
|
|
616
|
+
) {
|
|
617
|
+
await new Promise((resolve) =>
|
|
618
|
+
setTimeout(resolve, PULL_DIAGNOSTICS_RETRY_INTERVAL_MS),
|
|
619
|
+
);
|
|
620
|
+
latestCount = await requestPullDiagnostics(filePath);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (latestCount > 0) return;
|
|
624
|
+
}
|
|
625
|
+
|
|
443
626
|
// Fast path: diagnostics already available
|
|
444
627
|
if (diagnostics.has(normalizedPath)) return;
|
|
445
628
|
|
|
@@ -517,6 +700,20 @@ export async function createLSPClient(options: {
|
|
|
517
700
|
return result ?? null;
|
|
518
701
|
},
|
|
519
702
|
|
|
703
|
+
async signatureHelp(filePath, line, character) {
|
|
704
|
+
if (!isProcessAlive()) return null;
|
|
705
|
+
const uri = pathToFileURL(filePath).href;
|
|
706
|
+
const result = await safeSendRequest<LSPSignatureHelp>(
|
|
707
|
+
connection,
|
|
708
|
+
"textDocument/signatureHelp",
|
|
709
|
+
{
|
|
710
|
+
textDocument: { uri },
|
|
711
|
+
position: { line, character },
|
|
712
|
+
},
|
|
713
|
+
);
|
|
714
|
+
return result ?? null;
|
|
715
|
+
},
|
|
716
|
+
|
|
520
717
|
async documentSymbol(filePath) {
|
|
521
718
|
if (!isProcessAlive()) return [];
|
|
522
719
|
const uri = pathToFileURL(filePath).href;
|
|
@@ -542,6 +739,51 @@ export async function createLSPClient(options: {
|
|
|
542
739
|
return result ?? [];
|
|
543
740
|
},
|
|
544
741
|
|
|
742
|
+
async codeAction(
|
|
743
|
+
filePath,
|
|
744
|
+
line,
|
|
745
|
+
character,
|
|
746
|
+
endLine,
|
|
747
|
+
endCharacter,
|
|
748
|
+
) {
|
|
749
|
+
if (!isProcessAlive()) return [];
|
|
750
|
+
const uri = pathToFileURL(filePath).href;
|
|
751
|
+
const result = await safeSendRequest<unknown[]>(
|
|
752
|
+
connection,
|
|
753
|
+
"textDocument/codeAction",
|
|
754
|
+
{
|
|
755
|
+
textDocument: { uri },
|
|
756
|
+
range: {
|
|
757
|
+
start: { line, character },
|
|
758
|
+
end: { line: endLine, character: endCharacter },
|
|
759
|
+
},
|
|
760
|
+
context: {
|
|
761
|
+
diagnostics: diagnostics.get(normalizeMapKey(filePath)) ?? [],
|
|
762
|
+
},
|
|
763
|
+
},
|
|
764
|
+
);
|
|
765
|
+
if (!result || !Array.isArray(result)) return [];
|
|
766
|
+
return result.filter(
|
|
767
|
+
(item): item is LSPCodeAction =>
|
|
768
|
+
typeof item === "object" && item !== null && "title" in item,
|
|
769
|
+
);
|
|
770
|
+
},
|
|
771
|
+
|
|
772
|
+
async rename(filePath, line, character, newName) {
|
|
773
|
+
if (!isProcessAlive()) return null;
|
|
774
|
+
const uri = pathToFileURL(filePath).href;
|
|
775
|
+
const result = await safeSendRequest<LSPWorkspaceEdit>(
|
|
776
|
+
connection,
|
|
777
|
+
"textDocument/rename",
|
|
778
|
+
{
|
|
779
|
+
textDocument: { uri },
|
|
780
|
+
position: { line, character },
|
|
781
|
+
newName,
|
|
782
|
+
},
|
|
783
|
+
);
|
|
784
|
+
return result ?? null;
|
|
785
|
+
},
|
|
786
|
+
|
|
545
787
|
async implementation(filePath, line, character) {
|
|
546
788
|
if (!isProcessAlive()) return [];
|
|
547
789
|
const uri = pathToFileURL(filePath).href;
|
|
@@ -598,6 +840,7 @@ export async function createLSPClient(options: {
|
|
|
598
840
|
|
|
599
841
|
async shutdown() {
|
|
600
842
|
isConnected = false;
|
|
843
|
+
isDestroyed = true;
|
|
601
844
|
// Clear pending timers
|
|
602
845
|
for (const timer of pendingDiagnostics.values()) {
|
|
603
846
|
clearTimeout(timer);
|
|
@@ -620,11 +863,47 @@ export async function createLSPClient(options: {
|
|
|
620
863
|
/* ignore */
|
|
621
864
|
}
|
|
622
865
|
|
|
623
|
-
|
|
624
|
-
connection.dispose();
|
|
866
|
+
disposeConnection();
|
|
625
867
|
lspProcess.process.kill();
|
|
626
868
|
},
|
|
627
869
|
};
|
|
870
|
+
|
|
871
|
+
async function requestPullDiagnostics(filePath: string): Promise<number> {
|
|
872
|
+
if (!isProcessAlive()) return 0;
|
|
873
|
+
const uri = pathToFileURL(filePath).href;
|
|
874
|
+
try {
|
|
875
|
+
const report = await safeSendRequest<{
|
|
876
|
+
kind?: string;
|
|
877
|
+
items?: LSPDiagnostic[];
|
|
878
|
+
relatedDocuments?: Record<string, { items?: LSPDiagnostic[] }>;
|
|
879
|
+
}>(connection, "textDocument/diagnostic", {
|
|
880
|
+
textDocument: { uri },
|
|
881
|
+
});
|
|
882
|
+
|
|
883
|
+
if (!report) return 0;
|
|
884
|
+
|
|
885
|
+
const normalizedPath = normalizeMapKey(filePath);
|
|
886
|
+
const primaryItems = report.items ?? [];
|
|
887
|
+
diagnostics.set(normalizedPath, primaryItems);
|
|
888
|
+
let totalCount = primaryItems.length;
|
|
889
|
+
|
|
890
|
+
if (report.relatedDocuments) {
|
|
891
|
+
for (const [relatedUri, related] of Object.entries(
|
|
892
|
+
report.relatedDocuments,
|
|
893
|
+
)) {
|
|
894
|
+
const relatedPath = uriToPath(relatedUri);
|
|
895
|
+
const relatedItems = related?.items ?? [];
|
|
896
|
+
diagnostics.set(normalizeMapKey(relatedPath), relatedItems);
|
|
897
|
+
totalCount += relatedItems.length;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
diagnosticEmitter.emit("diagnostics", normalizedPath);
|
|
902
|
+
return totalCount;
|
|
903
|
+
} catch {
|
|
904
|
+
return 0;
|
|
905
|
+
}
|
|
906
|
+
}
|
|
628
907
|
}
|
|
629
908
|
|
|
630
909
|
// Helper to safely send notifications - catches stream destruction
|
|
@@ -681,6 +960,7 @@ function isStreamError(err: unknown): boolean {
|
|
|
681
960
|
msg.includes("disposed") ||
|
|
682
961
|
msg.includes("cancelled") ||
|
|
683
962
|
(err as { code?: string }).code === "ERR_STREAM_DESTROYED" ||
|
|
963
|
+
(err as { code?: string }).code === "ERR_STREAM_WRITE_AFTER_END" ||
|
|
684
964
|
(err as { code?: string }).code === "EPIPE"
|
|
685
965
|
);
|
|
686
966
|
}
|
|
@@ -722,3 +1002,77 @@ async function withTimeout<T>(
|
|
|
722
1002
|
),
|
|
723
1003
|
]);
|
|
724
1004
|
}
|
|
1005
|
+
|
|
1006
|
+
function positiveIntFromEnv(name: string, fallback: number): number {
|
|
1007
|
+
const raw = process.env[name];
|
|
1008
|
+
if (!raw) return fallback;
|
|
1009
|
+
const parsed = Number.parseInt(raw, 10);
|
|
1010
|
+
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
|
1011
|
+
return parsed;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function detectWorkspaceDiagnosticsSupport(
|
|
1015
|
+
initResult: unknown,
|
|
1016
|
+
): LSPWorkspaceDiagnosticsSupport {
|
|
1017
|
+
const capabilities =
|
|
1018
|
+
typeof initResult === "object" && initResult !== null
|
|
1019
|
+
? (initResult as { capabilities?: Record<string, unknown> }).capabilities
|
|
1020
|
+
: undefined;
|
|
1021
|
+
const diagnosticProvider = capabilities?.diagnosticProvider;
|
|
1022
|
+
if (!diagnosticProvider) {
|
|
1023
|
+
return {
|
|
1024
|
+
advertised: false,
|
|
1025
|
+
mode: "push-only",
|
|
1026
|
+
diagnosticProviderKind: "none",
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
if (typeof diagnosticProvider === "boolean") {
|
|
1031
|
+
return {
|
|
1032
|
+
advertised: diagnosticProvider,
|
|
1033
|
+
mode: diagnosticProvider ? "pull" : "push-only",
|
|
1034
|
+
diagnosticProviderKind: "boolean",
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
if (typeof diagnosticProvider === "object") {
|
|
1039
|
+
return {
|
|
1040
|
+
advertised: true,
|
|
1041
|
+
mode: "pull",
|
|
1042
|
+
diagnosticProviderKind: "object",
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
return {
|
|
1047
|
+
advertised: false,
|
|
1048
|
+
mode: "push-only",
|
|
1049
|
+
diagnosticProviderKind: typeof diagnosticProvider,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function detectOperationSupport(initResult: unknown): LSPOperationSupport {
|
|
1054
|
+
const capabilities =
|
|
1055
|
+
typeof initResult === "object" && initResult !== null
|
|
1056
|
+
? (initResult as { capabilities?: Record<string, unknown> }).capabilities
|
|
1057
|
+
: undefined;
|
|
1058
|
+
|
|
1059
|
+
const hasProvider = (key: string): boolean => {
|
|
1060
|
+
const value = capabilities?.[key];
|
|
1061
|
+
if (value === undefined || value === null) return false;
|
|
1062
|
+
if (typeof value === "boolean") return value;
|
|
1063
|
+
return true;
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
return {
|
|
1067
|
+
definition: hasProvider("definitionProvider"),
|
|
1068
|
+
references: hasProvider("referencesProvider"),
|
|
1069
|
+
hover: hasProvider("hoverProvider"),
|
|
1070
|
+
signatureHelp: hasProvider("signatureHelpProvider"),
|
|
1071
|
+
documentSymbol: hasProvider("documentSymbolProvider"),
|
|
1072
|
+
workspaceSymbol: hasProvider("workspaceSymbolProvider"),
|
|
1073
|
+
codeAction: hasProvider("codeActionProvider"),
|
|
1074
|
+
rename: hasProvider("renameProvider"),
|
|
1075
|
+
implementation: hasProvider("implementationProvider"),
|
|
1076
|
+
callHierarchy: hasProvider("callHierarchyProvider"),
|
|
1077
|
+
};
|
|
1078
|
+
}
|