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.
Files changed (92) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +2 -0
  3. package/clients/dispatch/dispatcher.ts +75 -91
  4. package/clients/dispatch/fact-provider-types.ts +22 -0
  5. package/clients/dispatch/fact-rule-runner.ts +22 -0
  6. package/clients/dispatch/fact-runner.ts +28 -0
  7. package/clients/dispatch/fact-scheduler.ts +78 -0
  8. package/clients/dispatch/fact-store.ts +67 -0
  9. package/clients/dispatch/facts/comment-facts.ts +59 -0
  10. package/clients/dispatch/facts/file-content.ts +20 -0
  11. package/clients/dispatch/facts/function-facts.ts +177 -0
  12. package/clients/dispatch/facts/try-catch-facts.ts +80 -0
  13. package/clients/dispatch/integration.ts +130 -24
  14. package/clients/dispatch/priorities.ts +22 -0
  15. package/clients/dispatch/rules/async-noise.ts +43 -0
  16. package/clients/dispatch/rules/error-obscuring.ts +40 -0
  17. package/clients/dispatch/rules/error-swallowing.ts +35 -0
  18. package/clients/dispatch/rules/pass-through-wrappers.ts +52 -0
  19. package/clients/dispatch/rules/placeholder-comments.ts +47 -0
  20. package/clients/dispatch/runners/architect.ts +2 -1
  21. package/clients/dispatch/runners/ast-grep-napi.ts +2 -1
  22. package/clients/dispatch/runners/biome-check.ts +40 -8
  23. package/clients/dispatch/runners/biome.ts +2 -1
  24. package/clients/dispatch/runners/eslint.ts +34 -6
  25. package/clients/dispatch/runners/go-vet.ts +2 -1
  26. package/clients/dispatch/runners/golangci-lint.ts +2 -1
  27. package/clients/dispatch/runners/index.ts +29 -27
  28. package/clients/dispatch/runners/lsp.ts +60 -4
  29. package/clients/dispatch/runners/oxlint.ts +2 -1
  30. package/clients/dispatch/runners/pyright.ts +2 -1
  31. package/clients/dispatch/runners/python-slop.ts +2 -1
  32. package/clients/dispatch/runners/rubocop.ts +2 -1
  33. package/clients/dispatch/runners/ruff.ts +2 -1
  34. package/clients/dispatch/runners/rust-clippy.ts +2 -1
  35. package/clients/dispatch/runners/shellcheck.ts +2 -1
  36. package/clients/dispatch/runners/similarity.ts +2 -1
  37. package/clients/dispatch/runners/spellcheck.ts +2 -1
  38. package/clients/dispatch/runners/sqlfluff.ts +2 -1
  39. package/clients/dispatch/runners/tree-sitter.ts +469 -1
  40. package/clients/dispatch/runners/ts-lsp.ts +2 -1
  41. package/clients/dispatch/runners/type-safety.ts +2 -1
  42. package/clients/dispatch/runners/yamllint.ts +2 -1
  43. package/clients/dispatch/tool-profile.ts +40 -0
  44. package/clients/dispatch/types.ts +3 -13
  45. package/clients/lsp/client.ts +366 -12
  46. package/clients/lsp/index.ts +374 -76
  47. package/clients/lsp/launch.ts +42 -2
  48. package/clients/lsp/server.ts +186 -12
  49. package/clients/pipeline.ts +2 -2
  50. package/clients/runtime-context.ts +2 -2
  51. package/clients/runtime-session.ts +43 -5
  52. package/clients/session-summary.ts +21 -0
  53. package/clients/tree-sitter-client.ts +162 -0
  54. package/clients/tree-sitter-logger.ts +47 -0
  55. package/clients/tree-sitter-query-loader.ts +13 -2
  56. package/index.ts +67 -17
  57. package/package.json +3 -1
  58. package/rules/rule-catalog.json +64 -0
  59. package/rules/tree-sitter-queries/go/go-bare-error.yml +19 -7
  60. package/rules/tree-sitter-queries/go/go-command-injection.yml +55 -0
  61. package/rules/tree-sitter-queries/go/go-direct-panic.yml +45 -0
  62. package/rules/tree-sitter-queries/go/go-empty-if-err.yml +47 -0
  63. package/rules/tree-sitter-queries/go/go-goroutine-loop-capture.yml +49 -0
  64. package/rules/tree-sitter-queries/go/go-ignored-call-result.yml +51 -0
  65. package/rules/tree-sitter-queries/go/go-insecure-random.yml +51 -0
  66. package/rules/tree-sitter-queries/go/go-log-fatal.yml +49 -0
  67. package/rules/tree-sitter-queries/go/go-path-traversal.yml +51 -0
  68. package/rules/tree-sitter-queries/go/go-shared-map-write-goroutine.yml +54 -0
  69. package/rules/tree-sitter-queries/go/go-sql-injection.yml +55 -0
  70. package/rules/tree-sitter-queries/go/go-weak-hash.yml +51 -0
  71. package/rules/tree-sitter-queries/python/python-command-injection.yml +63 -0
  72. package/rules/tree-sitter-queries/python/python-insecure-deserialization.yml +48 -0
  73. package/rules/tree-sitter-queries/python/python-insecure-random.yml +51 -0
  74. package/rules/tree-sitter-queries/python/python-path-traversal.yml +55 -0
  75. package/rules/tree-sitter-queries/python/python-sql-injection.yml +47 -0
  76. package/rules/tree-sitter-queries/python/python-ssrf.yml +50 -0
  77. package/rules/tree-sitter-queries/python/python-thread-global-write.yml +58 -0
  78. package/rules/tree-sitter-queries/python/python-weak-hash.yml +51 -0
  79. package/rules/tree-sitter-queries/ruby/ruby-command-injection.yml +56 -0
  80. package/rules/tree-sitter-queries/ruby/ruby-insecure-deserialization.yml +47 -0
  81. package/rules/tree-sitter-queries/ruby/ruby-insecure-random.yml +54 -0
  82. package/rules/tree-sitter-queries/ruby/ruby-weak-hash.yml +50 -0
  83. package/rules/tree-sitter-queries/rust/rust-lock-held-across-await.yml +59 -0
  84. package/rules/tree-sitter-queries/typescript/ts-command-injection.yml +60 -0
  85. package/rules/tree-sitter-queries/typescript/ts-detached-async-call.yml +56 -0
  86. package/rules/tree-sitter-queries/typescript/ts-insecure-random.yml +54 -0
  87. package/rules/tree-sitter-queries/typescript/ts-ssrf.yml +53 -0
  88. package/rules/tree-sitter-queries/typescript/ts-weak-hash.yml +54 -0
  89. package/scripts/validate-rule-catalog.mjs +227 -0
  90. package/skills/lsp-navigation/SKILL.md +15 -3
  91. package/tools/lsp-navigation.js +466 -79
  92. package/tools/lsp-navigation.ts +587 -85
@@ -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 = 150; // ms — waits for follow-up semantic diagnostics
145
- const INITIALIZE_TIMEOUT_MS = 15_000; // 15s — npx downloads are handled by ensureTool, not here
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(filePath);
355
+ const existingTimer = pendingDiagnostics.get(normalizedPath);
222
356
  if (existingTimer) clearTimeout(existingTimer);
223
357
 
224
358
  const timer = setTimeout(() => {
225
- diagnostics.set(filePath, newDiags);
226
- pendingDiagnostics.delete(filePath);
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", filePath);
363
+ diagnosticEmitter.emit("diagnostics", normalizedPath);
230
364
  }, DIAGNOSTICS_DEBOUNCE_MS);
231
365
 
232
- pendingDiagnostics.set(filePath, timer);
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
- async waitForDiagnostics(filePath, timeoutMs = 10000) {
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
- connection.end();
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
+ }