pi-lens 3.1.2 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +16 -12
  3. package/clients/ast-grep-client.js +8 -1
  4. package/clients/ast-grep-client.ts +9 -1
  5. package/clients/biome-client.js +51 -38
  6. package/clients/biome-client.ts +60 -58
  7. package/clients/dependency-checker.js +30 -1
  8. package/clients/dependency-checker.ts +35 -1
  9. package/clients/dispatch/__tests__/runner-registration.test.ts +286 -282
  10. package/clients/dispatch/bus-dispatcher.js +15 -14
  11. package/clients/dispatch/bus-dispatcher.ts +32 -25
  12. package/clients/dispatch/dispatcher.js +18 -25
  13. package/clients/dispatch/dispatcher.test.ts +2 -1
  14. package/clients/dispatch/dispatcher.ts +17 -28
  15. package/clients/dispatch/plan.js +77 -32
  16. package/clients/dispatch/plan.ts +78 -32
  17. package/clients/dispatch/runners/ast-grep-napi.js +36 -376
  18. package/clients/dispatch/runners/ast-grep-napi.ts +60 -433
  19. package/clients/dispatch/runners/index.js +8 -4
  20. package/clients/dispatch/runners/index.ts +8 -4
  21. package/clients/dispatch/runners/lsp.js +65 -0
  22. package/clients/dispatch/runners/lsp.ts +125 -0
  23. package/clients/dispatch/runners/oxlint.js +2 -2
  24. package/clients/dispatch/runners/oxlint.ts +2 -2
  25. package/clients/dispatch/runners/pyright.js +24 -8
  26. package/clients/dispatch/runners/pyright.ts +28 -14
  27. package/clients/dispatch/runners/rust-clippy.js +2 -2
  28. package/clients/dispatch/runners/rust-clippy.ts +2 -4
  29. package/clients/dispatch/runners/tree-sitter.js +14 -2
  30. package/clients/dispatch/runners/tree-sitter.ts +15 -2
  31. package/clients/dispatch/runners/ts-lsp.js +3 -3
  32. package/clients/dispatch/runners/ts-lsp.ts +8 -5
  33. package/clients/dispatch/runners/yaml-rule-parser.js +292 -0
  34. package/clients/dispatch/runners/yaml-rule-parser.ts +338 -0
  35. package/clients/dispatch/types.js +3 -0
  36. package/clients/dispatch/types.ts +3 -0
  37. package/clients/formatters.js +67 -14
  38. package/clients/formatters.ts +68 -15
  39. package/clients/installer/index.js +78 -10
  40. package/clients/installer/index.ts +519 -426
  41. package/clients/jscpd-client.js +28 -0
  42. package/clients/jscpd-client.ts +41 -3
  43. package/clients/knip-client.js +30 -1
  44. package/clients/knip-client.ts +34 -2
  45. package/clients/lsp/__tests__/client.test.ts +64 -41
  46. package/clients/lsp/__tests__/config.test.ts +25 -17
  47. package/clients/lsp/__tests__/launch.test.ts +108 -43
  48. package/clients/lsp/__tests__/service.test.ts +76 -48
  49. package/clients/lsp/client.js +87 -2
  50. package/clients/lsp/client.ts +150 -6
  51. package/clients/lsp/config.js +8 -11
  52. package/clients/lsp/config.ts +24 -21
  53. package/clients/lsp/index.js +69 -0
  54. package/clients/lsp/index.ts +82 -0
  55. package/clients/lsp/interactive-install.js +19 -8
  56. package/clients/lsp/interactive-install.ts +52 -27
  57. package/clients/lsp/launch.js +182 -32
  58. package/clients/lsp/launch.ts +241 -38
  59. package/clients/lsp/path-utils.js +3 -46
  60. package/clients/lsp/path-utils.ts +11 -51
  61. package/clients/lsp/server.js +93 -71
  62. package/clients/lsp/server.ts +173 -131
  63. package/clients/path-utils.js +142 -0
  64. package/clients/path-utils.ts +153 -0
  65. package/clients/ruff-client.js +33 -4
  66. package/clients/ruff-client.ts +44 -13
  67. package/clients/safe-spawn.js +3 -1
  68. package/clients/safe-spawn.ts +3 -1
  69. package/clients/services/effect-integration.js +11 -7
  70. package/clients/services/effect-integration.ts +34 -26
  71. package/clients/sg-runner.js +51 -9
  72. package/clients/sg-runner.ts +58 -15
  73. package/clients/tree-sitter-client.js +12 -0
  74. package/clients/tree-sitter-client.ts +12 -0
  75. package/clients/typescript-client.js +6 -2
  76. package/clients/typescript-client.ts +9 -2
  77. package/commands/booboo.js +2 -4
  78. package/commands/booboo.ts +2 -4
  79. package/index.ts +377 -93
  80. package/package.json +2 -1
  81. package/rules/tree-sitter-queries/tsx/no-nested-links.yml +45 -0
  82. package/rules/tree-sitter-queries/typescript/constructor-super.yml +55 -0
  83. package/rules/tree-sitter-queries/typescript/debugger.yml +1 -1
  84. package/rules/tree-sitter-queries/typescript/no-dupe-class-members.yml +47 -0
  85. package/tsconfig.json +1 -1
  86. package/clients/__tests__/file-time.test.js +0 -216
  87. package/clients/__tests__/format-service.test.js +0 -245
  88. package/clients/__tests__/formatters.test.js +0 -271
  89. package/clients/agent-behavior-client.test.js +0 -94
  90. package/clients/ast-grep-client.test.js +0 -129
  91. package/clients/ast-grep-client.test.ts +0 -155
  92. package/clients/biome-client.test.js +0 -144
  93. package/clients/cache-manager.test.js +0 -197
  94. package/clients/complexity-client.test.js +0 -234
  95. package/clients/dependency-checker.test.js +0 -60
  96. package/clients/dispatch/__tests__/autofix-integration.test.js +0 -245
  97. package/clients/dispatch/__tests__/runner-registration.test.js +0 -236
  98. package/clients/dispatch/dispatcher.edge.test.js +0 -82
  99. package/clients/dispatch/dispatcher.format.test.js +0 -46
  100. package/clients/dispatch/dispatcher.inline.test.js +0 -74
  101. package/clients/dispatch/dispatcher.test.js +0 -115
  102. package/clients/dispatch/runners/architect.test.js +0 -138
  103. package/clients/dispatch/runners/ast-grep-napi.test.js +0 -106
  104. package/clients/dispatch/runners/oxlint.test.js +0 -230
  105. package/clients/dispatch/runners/pyright.test.js +0 -98
  106. package/clients/dispatch/runners/python-slop.test.js +0 -203
  107. package/clients/dispatch/runners/scan_codebase.test.js +0 -89
  108. package/clients/dispatch/runners/shellcheck.test.js +0 -98
  109. package/clients/dispatch/runners/spellcheck.test.js +0 -158
  110. package/clients/dispatch/runners/ts-slop.test.js +0 -180
  111. package/clients/dispatch/runners/ts-slop.test.ts +0 -230
  112. package/clients/dogfood.test.js +0 -201
  113. package/clients/file-kinds.test.js +0 -169
  114. package/clients/go-client.test.js +0 -127
  115. package/clients/jscpd-client.test.js +0 -127
  116. package/clients/knip-client.test.js +0 -112
  117. package/clients/lsp/__tests__/client.test.js +0 -325
  118. package/clients/lsp/__tests__/config.test.js +0 -166
  119. package/clients/lsp/__tests__/error-recovery.test.js +0 -213
  120. package/clients/lsp/__tests__/integration.test.js +0 -127
  121. package/clients/lsp/__tests__/launch.test.js +0 -260
  122. package/clients/lsp/__tests__/server.test.js +0 -259
  123. package/clients/lsp/__tests__/service.test.js +0 -417
  124. package/clients/metrics-client.test.js +0 -141
  125. package/clients/ruff-client.test.js +0 -132
  126. package/clients/rust-client.test.js +0 -108
  127. package/clients/sanitize.test.js +0 -177
  128. package/clients/secrets-scanner.test.js +0 -100
  129. package/clients/services/__tests__/effect-integration.test.js +0 -86
  130. package/clients/test-runner-client.test.js +0 -192
  131. package/clients/todo-scanner.test.js +0 -301
  132. package/clients/type-coverage-client.test.js +0 -105
  133. package/clients/typescript-client.codefix.test.js +0 -157
  134. package/clients/typescript-client.test.js +0 -105
  135. package/commands/clients/ast-grep-client.js +0 -250
  136. package/commands/clients/ast-grep-parser.js +0 -86
  137. package/commands/clients/ast-grep-rule-manager.js +0 -91
  138. package/commands/clients/ast-grep-types.js +0 -9
  139. package/commands/clients/biome-client.js +0 -380
  140. package/commands/clients/complexity-client.js +0 -667
  141. package/commands/clients/file-kinds.js +0 -177
  142. package/commands/clients/file-utils.js +0 -40
  143. package/commands/clients/jscpd-client.js +0 -169
  144. package/commands/clients/knip-client.js +0 -211
  145. package/commands/clients/ruff-client.js +0 -297
  146. package/commands/clients/safe-spawn.js +0 -88
  147. package/commands/clients/scan-utils.js +0 -83
  148. package/commands/clients/sg-runner.js +0 -190
  149. package/commands/clients/types.js +0 -11
  150. package/commands/clients/typescript-client.js +0 -505
  151. package/commands/rate.test.js +0 -119
  152. package/rules/ast-grep-rules/rules/no-dangerously-set-inner-html.yml +0 -13
  153. package/rules/ast-grep-rules/rules/no-debugger.yml +0 -12
  154. package/rules/ast-grep-rules/rules/no-eval.yml +0 -13
@@ -150,8 +150,10 @@ export async function createLSPClient(options) {
150
150
  },
151
151
  async change(filePath, content) {
152
152
  const uri = pathToFileURL(filePath).href;
153
- const version = (documentVersions.get(filePath) ?? 0) + 1;
154
- documentVersions.set(filePath, version);
153
+ // Normalize path for Windows case-insensitive lookup
154
+ const normalizedPath = normalizeMapKey(filePath);
155
+ const version = (documentVersions.get(normalizedPath) ?? 0) + 1;
156
+ documentVersions.set(normalizedPath, version);
155
157
  await connection.sendNotification("textDocument/didChange", {
156
158
  textDocument: { uri, version },
157
159
  contentChanges: [{ text: content }],
@@ -163,6 +165,10 @@ export async function createLSPClient(options) {
163
165
  const normalizedPath = normalizeMapKey(filePath);
164
166
  return diagnostics.get(normalizedPath) ?? [];
165
167
  },
168
+ getAllDiagnostics() {
169
+ // Return copy of all tracked diagnostics (for cascade checking)
170
+ return new Map(diagnostics);
171
+ },
166
172
  async waitForDiagnostics(filePath, timeoutMs = 10000) {
167
173
  const normalizedPath = normalizeMapKey(filePath);
168
174
  if (diagnostics.has(normalizedPath))
@@ -192,6 +198,85 @@ export async function createLSPClient(options) {
192
198
  }, timeoutMs);
193
199
  });
194
200
  },
201
+ async definition(filePath, line, character) {
202
+ const uri = pathToFileURL(filePath).href;
203
+ try {
204
+ const result = await connection.sendRequest("textDocument/definition", {
205
+ textDocument: { uri },
206
+ position: { line, character },
207
+ });
208
+ if (!result)
209
+ return [];
210
+ return Array.isArray(result) ? result : [result];
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ },
216
+ async references(filePath, line, character, includeDeclaration = true) {
217
+ const uri = pathToFileURL(filePath).href;
218
+ try {
219
+ const result = await connection.sendRequest("textDocument/references", {
220
+ textDocument: { uri },
221
+ position: { line, character },
222
+ context: { includeDeclaration },
223
+ });
224
+ return Array.isArray(result) ? result : [];
225
+ }
226
+ catch {
227
+ return [];
228
+ }
229
+ },
230
+ async hover(filePath, line, character) {
231
+ const uri = pathToFileURL(filePath).href;
232
+ try {
233
+ return (await connection.sendRequest("textDocument/hover", {
234
+ textDocument: { uri },
235
+ position: { line, character },
236
+ }));
237
+ }
238
+ catch {
239
+ return null;
240
+ }
241
+ },
242
+ async documentSymbol(filePath) {
243
+ const uri = pathToFileURL(filePath).href;
244
+ try {
245
+ const result = await connection.sendRequest("textDocument/documentSymbol", {
246
+ textDocument: { uri },
247
+ });
248
+ return Array.isArray(result) ? result : [];
249
+ }
250
+ catch {
251
+ return [];
252
+ }
253
+ },
254
+ async workspaceSymbol(query) {
255
+ try {
256
+ const result = await connection.sendRequest("workspace/symbol", {
257
+ query,
258
+ });
259
+ return Array.isArray(result) ? result : [];
260
+ }
261
+ catch {
262
+ return [];
263
+ }
264
+ },
265
+ async implementation(filePath, line, character) {
266
+ const uri = pathToFileURL(filePath).href;
267
+ try {
268
+ const result = await connection.sendRequest("textDocument/implementation", {
269
+ textDocument: { uri },
270
+ position: { line, character },
271
+ });
272
+ if (!result)
273
+ return [];
274
+ return Array.isArray(result) ? result : [result];
275
+ }
276
+ catch {
277
+ return [];
278
+ }
279
+ },
195
280
  async shutdown() {
196
281
  // Clear pending timers
197
282
  for (const timer of pendingDiagnostics.values()) {
@@ -32,6 +32,32 @@ export interface LSPDiagnostic {
32
32
  source?: string;
33
33
  }
34
34
 
35
+ export interface LSPLocation {
36
+ uri: string;
37
+ range: {
38
+ start: { line: number; character: number };
39
+ end: { line: number; character: number };
40
+ };
41
+ }
42
+
43
+ export interface LSPHover {
44
+ contents:
45
+ | string
46
+ | { kind: string; value: string }
47
+ | Array<string | { language: string; value: string }>;
48
+ range?: LSPLocation["range"];
49
+ }
50
+
51
+ export interface LSPSymbol {
52
+ name: string;
53
+ kind: number;
54
+ location?: LSPLocation;
55
+ range?: LSPLocation["range"];
56
+ selectionRange?: LSPLocation["range"];
57
+ detail?: string;
58
+ children?: LSPSymbol[];
59
+ }
60
+
35
61
  export interface LSPClientInfo {
36
62
  serverId: string;
37
63
  root: string;
@@ -42,6 +68,37 @@ export interface LSPClientInfo {
42
68
  };
43
69
  getDiagnostics(filePath: string): LSPDiagnostic[];
44
70
  waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
71
+ /** Get all tracked diagnostics (for cascade checking) */
72
+ getAllDiagnostics(): Map<string, LSPDiagnostic[]>;
73
+ /** Go to definition — returns Location[] */
74
+ definition(
75
+ filePath: string,
76
+ line: number,
77
+ character: number,
78
+ ): Promise<LSPLocation[]>;
79
+ /** Find all references */
80
+ references(
81
+ filePath: string,
82
+ line: number,
83
+ character: number,
84
+ includeDeclaration?: boolean,
85
+ ): Promise<LSPLocation[]>;
86
+ /** Hover info at position */
87
+ hover(
88
+ filePath: string,
89
+ line: number,
90
+ character: number,
91
+ ): Promise<LSPHover | null>;
92
+ /** Symbols in a document */
93
+ documentSymbol(filePath: string): Promise<LSPSymbol[]>;
94
+ /** Workspace-wide symbol search */
95
+ workspaceSymbol(query: string): Promise<LSPSymbol[]>;
96
+ /** Go to implementation */
97
+ implementation(
98
+ filePath: string,
99
+ line: number,
100
+ character: number,
101
+ ): Promise<LSPLocation[]>;
45
102
  shutdown(): Promise<void>;
46
103
  }
47
104
 
@@ -150,10 +207,7 @@ export async function createLSPClient(options: {
150
207
  workDoneProgress: true,
151
208
  },
152
209
  workspace: {
153
- workspaceFolders: {
154
- supported: true,
155
- changeNotifications: true,
156
- },
210
+ workspaceFolders: true, // Simple boolean for broader compatibility
157
211
  configuration: true,
158
212
  didChangeWatchedFiles: {
159
213
  dynamicRegistration: true,
@@ -222,8 +276,10 @@ export async function createLSPClient(options: {
222
276
 
223
277
  async change(filePath, content) {
224
278
  const uri = pathToFileURL(filePath).href;
225
- const version = (documentVersions.get(filePath) ?? 0) + 1;
226
- documentVersions.set(filePath, version);
279
+ // Normalize path for Windows case-insensitive lookup
280
+ const normalizedPath = normalizeMapKey(filePath);
281
+ const version = (documentVersions.get(normalizedPath) ?? 0) + 1;
282
+ documentVersions.set(normalizedPath, version);
227
283
 
228
284
  await connection.sendNotification("textDocument/didChange", {
229
285
  textDocument: { uri, version },
@@ -238,6 +294,11 @@ export async function createLSPClient(options: {
238
294
  return diagnostics.get(normalizedPath) ?? [];
239
295
  },
240
296
 
297
+ getAllDiagnostics() {
298
+ // Return copy of all tracked diagnostics (for cascade checking)
299
+ return new Map(diagnostics);
300
+ },
301
+
241
302
  async waitForDiagnostics(filePath, timeoutMs = 10000) {
242
303
  const normalizedPath = normalizeMapKey(filePath);
243
304
  if (diagnostics.has(normalizedPath)) return;
@@ -270,6 +331,89 @@ export async function createLSPClient(options: {
270
331
  });
271
332
  },
272
333
 
334
+ async definition(filePath, line, character) {
335
+ const uri = pathToFileURL(filePath).href;
336
+ try {
337
+ const result = await connection.sendRequest("textDocument/definition", {
338
+ textDocument: { uri },
339
+ position: { line, character },
340
+ });
341
+ if (!result) return [];
342
+ return Array.isArray(result) ? result : [result];
343
+ } catch {
344
+ return [];
345
+ }
346
+ },
347
+
348
+ async references(filePath, line, character, includeDeclaration = true) {
349
+ const uri = pathToFileURL(filePath).href;
350
+ try {
351
+ const result = await connection.sendRequest("textDocument/references", {
352
+ textDocument: { uri },
353
+ position: { line, character },
354
+ context: { includeDeclaration },
355
+ });
356
+ return Array.isArray(result) ? result : [];
357
+ } catch {
358
+ return [];
359
+ }
360
+ },
361
+
362
+ async hover(filePath, line, character) {
363
+ const uri = pathToFileURL(filePath).href;
364
+ try {
365
+ return (await connection.sendRequest("textDocument/hover", {
366
+ textDocument: { uri },
367
+ position: { line, character },
368
+ })) as LSPHover | null;
369
+ } catch {
370
+ return null;
371
+ }
372
+ },
373
+
374
+ async documentSymbol(filePath) {
375
+ const uri = pathToFileURL(filePath).href;
376
+ try {
377
+ const result = await connection.sendRequest(
378
+ "textDocument/documentSymbol",
379
+ {
380
+ textDocument: { uri },
381
+ },
382
+ );
383
+ return Array.isArray(result) ? result : [];
384
+ } catch {
385
+ return [];
386
+ }
387
+ },
388
+
389
+ async workspaceSymbol(query) {
390
+ try {
391
+ const result = await connection.sendRequest("workspace/symbol", {
392
+ query,
393
+ });
394
+ return Array.isArray(result) ? result : [];
395
+ } catch {
396
+ return [];
397
+ }
398
+ },
399
+
400
+ async implementation(filePath, line, character) {
401
+ const uri = pathToFileURL(filePath).href;
402
+ try {
403
+ const result = await connection.sendRequest(
404
+ "textDocument/implementation",
405
+ {
406
+ textDocument: { uri },
407
+ position: { line, character },
408
+ },
409
+ );
410
+ if (!result) return [];
411
+ return Array.isArray(result) ? result : [result];
412
+ } catch {
413
+ return [];
414
+ }
415
+ },
416
+
273
417
  async shutdown() {
274
418
  // Clear pending timers
275
419
  for (const timer of pendingDiagnostics.values()) {
@@ -18,18 +18,14 @@
18
18
  * }
19
19
  * }
20
20
  */
21
- import fs from "fs/promises";
22
- import path from "path";
23
- import { fileURLToPath } from "url";
24
- import { LSP_SERVERS, createRootDetector } from "./server.js";
21
+ import fs from "node:fs/promises";
22
+ import path from "node:path";
23
+ import { fileURLToPath } from "node:url";
25
24
  import { launchLSP } from "./launch.js";
25
+ import { createRootDetector, LSP_SERVERS, } from "./server.js";
26
26
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
27
  // --- Config Loading ---
28
- const CONFIG_PATHS = [
29
- ".pi-lens/lsp.json",
30
- ".pi-lens.json",
31
- "pi-lsp.json",
32
- ];
28
+ const CONFIG_PATHS = [".pi-lens/lsp.json", ".pi-lens.json", "pi-lsp.json"];
33
29
  /**
34
30
  * Load LSP configuration from file
35
31
  */
@@ -61,7 +57,7 @@ export function createCustomServer(config, id) {
61
57
  ? createRootDetector(config.rootMarkers)
62
58
  : async () => process.cwd(),
63
59
  async spawn(root) {
64
- const proc = launchLSP(config.command, config.args ?? ["--stdio"], {
60
+ const proc = await launchLSP(config.command, config.args ?? ["--stdio"], {
65
61
  cwd: root,
66
62
  env: config.env ? { ...process.env, ...config.env } : process.env,
67
63
  });
@@ -99,7 +95,7 @@ export async function initLSPConfig(cwd) {
99
95
  */
100
96
  export function getAllServers() {
101
97
  const all = [...LSP_SERVERS, ...customServers];
102
- return all.filter(s => !disabledServerIds.has(s.id));
98
+ return all.filter((s) => !disabledServerIds.has(s.id));
103
99
  }
104
100
  /**
105
101
  * Check if a server is disabled
@@ -107,6 +103,7 @@ export function getAllServers() {
107
103
  export function isServerDisabled(serverId) {
108
104
  return disabledServerIds.has(serverId);
109
105
  }
106
+ // --- Override getServersForFile to include custom servers
110
107
  export function getServersForFileWithConfig(filePath) {
111
108
  const ext = path.extname(filePath).toLowerCase();
112
109
  return getAllServers().filter((server) => server.extensions.includes(ext));
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * LSP Configuration for pi-lens
3
- *
3
+ *
4
4
  * Allows users to define custom LSP servers via configuration.
5
- *
5
+ *
6
6
  * Config file: .pi-lens/lsp.json
7
- *
7
+ *
8
8
  * Example:
9
9
  * {
10
10
  * "servers": {
@@ -19,11 +19,15 @@
19
19
  * }
20
20
  */
21
21
 
22
- import fs from "fs/promises";
23
- import path from "path";
24
- import { fileURLToPath } from "url";
25
- import { LSP_SERVERS, type LSPServerInfo, createRootDetector } from "./server.js";
22
+ import fs from "node:fs/promises";
23
+ import path from "node:path";
24
+ import { fileURLToPath } from "node:url";
26
25
  import { launchLSP } from "./launch.js";
26
+ import {
27
+ createRootDetector,
28
+ LSP_SERVERS,
29
+ type LSPServerInfo,
30
+ } from "./server.js";
27
31
 
28
32
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
29
33
 
@@ -45,11 +49,7 @@ export interface LSPConfig {
45
49
 
46
50
  // --- Config Loading ---
47
51
 
48
- const CONFIG_PATHS = [
49
- ".pi-lens/lsp.json",
50
- ".pi-lens.json",
51
- "pi-lsp.json",
52
- ];
52
+ const CONFIG_PATHS = [".pi-lens/lsp.json", ".pi-lens.json", "pi-lsp.json"];
53
53
 
54
54
  /**
55
55
  * Load LSP configuration from file
@@ -74,16 +74,19 @@ export async function loadLSPConfig(cwd: string): Promise<LSPConfig> {
74
74
  /**
75
75
  * Create LSPServerInfo from user configuration
76
76
  */
77
- export function createCustomServer(config: CustomServerConfig, id: string): LSPServerInfo {
77
+ export function createCustomServer(
78
+ config: CustomServerConfig,
79
+ id: string,
80
+ ): LSPServerInfo {
78
81
  return {
79
82
  id,
80
83
  name: config.name,
81
84
  extensions: config.extensions,
82
- root: config.rootMarkers
85
+ root: config.rootMarkers
83
86
  ? createRootDetector(config.rootMarkers)
84
87
  : async () => process.cwd(),
85
88
  async spawn(root) {
86
- const proc = launchLSP(config.command, config.args ?? ["--stdio"], {
89
+ const proc = await launchLSP(config.command, config.args ?? ["--stdio"], {
87
90
  cwd: root,
88
91
  env: config.env ? { ...process.env, ...config.env } : process.env,
89
92
  });
@@ -102,18 +105,20 @@ let disabledServerIds: Set<string> = new Set();
102
105
  */
103
106
  export async function initLSPConfig(cwd: string): Promise<void> {
104
107
  const config = await loadLSPConfig(cwd);
105
-
108
+
106
109
  // Clear previous custom servers
107
110
  customServers = [];
108
111
  disabledServerIds = new Set(config.disabledServers ?? []);
109
-
112
+
110
113
  // Register custom servers from config
111
114
  if (config.servers) {
112
115
  for (const [id, serverConfig] of Object.entries(config.servers)) {
113
116
  try {
114
117
  const server = createCustomServer(serverConfig, id);
115
118
  customServers.push(server);
116
- console.error(`[lsp-config] Registered custom server: ${id} (${serverConfig.name})`);
119
+ console.error(
120
+ `[lsp-config] Registered custom server: ${id} (${serverConfig.name})`,
121
+ );
117
122
  } catch (err) {
118
123
  console.error(`[lsp-config] Failed to register server ${id}:`, err);
119
124
  }
@@ -126,7 +131,7 @@ export async function initLSPConfig(cwd: string): Promise<void> {
126
131
  */
127
132
  export function getAllServers(): LSPServerInfo[] {
128
133
  const all = [...LSP_SERVERS, ...customServers];
129
- return all.filter(s => !disabledServerIds.has(s.id));
134
+ return all.filter((s) => !disabledServerIds.has(s.id));
130
135
  }
131
136
 
132
137
  /**
@@ -138,8 +143,6 @@ export function isServerDisabled(serverId: string): boolean {
138
143
 
139
144
  // --- Override getServersForFile to include custom servers
140
145
 
141
- import { getServersForFile as getBuiltinServersForFile } from "./server.js";
142
-
143
146
  export function getServersForFileWithConfig(filePath: string): LSPServerInfo[] {
144
147
  const ext = path.extname(filePath).toLowerCase();
145
148
  return getAllServers().filter((server) => server.extensions.includes(ext));
@@ -136,6 +136,75 @@ export class LSPService {
136
136
  await spawned.client.waitForDiagnostics(filePath, 3000);
137
137
  return spawned.client.getDiagnostics(filePath);
138
138
  }
139
+ /**
140
+ * Navigation: go to definition
141
+ */
142
+ async definition(filePath, line, character) {
143
+ const spawned = await this.getClientForFile(filePath);
144
+ if (!spawned)
145
+ return [];
146
+ return spawned.client.definition(filePath, line, character);
147
+ }
148
+ /**
149
+ * Navigation: find all references
150
+ */
151
+ async references(filePath, line, character, includeDeclaration = true) {
152
+ const spawned = await this.getClientForFile(filePath);
153
+ if (!spawned)
154
+ return [];
155
+ return spawned.client.references(filePath, line, character, includeDeclaration);
156
+ }
157
+ /**
158
+ * Navigation: hover info
159
+ */
160
+ async hover(filePath, line, character) {
161
+ const spawned = await this.getClientForFile(filePath);
162
+ if (!spawned)
163
+ return null;
164
+ return spawned.client.hover(filePath, line, character);
165
+ }
166
+ /**
167
+ * Navigation: symbols in document
168
+ */
169
+ async documentSymbol(filePath) {
170
+ const spawned = await this.getClientForFile(filePath);
171
+ if (!spawned)
172
+ return [];
173
+ return spawned.client.documentSymbol(filePath);
174
+ }
175
+ /**
176
+ * Navigation: workspace-wide symbol search
177
+ */
178
+ async workspaceSymbol(query) {
179
+ // Use the first active client for workspace-level queries
180
+ const clients = Array.from(this.state.clients.values());
181
+ if (clients.length === 0)
182
+ return [];
183
+ return clients[0].workspaceSymbol(query);
184
+ }
185
+ /**
186
+ * Navigation: go to implementation
187
+ */
188
+ async implementation(filePath, line, character) {
189
+ const spawned = await this.getClientForFile(filePath);
190
+ if (!spawned)
191
+ return [];
192
+ return spawned.client.implementation(filePath, line, character);
193
+ }
194
+ /**
195
+ * Get all diagnostics across all tracked files (for cascade checking)
196
+ */
197
+ async getAllDiagnostics() {
198
+ const all = new Map();
199
+ for (const [_key, client] of this.state.clients) {
200
+ const clientDiags = client.getAllDiagnostics();
201
+ for (const [filePath, diags] of clientDiags) {
202
+ const existing = all.get(filePath) ?? [];
203
+ all.set(filePath, [...existing, ...diags]);
204
+ }
205
+ }
206
+ return all;
207
+ }
139
208
  /**
140
209
  * Check if LSP is available for a file
141
210
  */
@@ -177,6 +177,88 @@ export class LSPService {
177
177
  return spawned.client.getDiagnostics(filePath);
178
178
  }
179
179
 
180
+ /**
181
+ * Navigation: go to definition
182
+ */
183
+ async definition(filePath: string, line: number, character: number) {
184
+ const spawned = await this.getClientForFile(filePath);
185
+ if (!spawned) return [];
186
+ return spawned.client.definition(filePath, line, character);
187
+ }
188
+
189
+ /**
190
+ * Navigation: find all references
191
+ */
192
+ async references(
193
+ filePath: string,
194
+ line: number,
195
+ character: number,
196
+ includeDeclaration = true,
197
+ ) {
198
+ const spawned = await this.getClientForFile(filePath);
199
+ if (!spawned) return [];
200
+ return spawned.client.references(
201
+ filePath,
202
+ line,
203
+ character,
204
+ includeDeclaration,
205
+ );
206
+ }
207
+
208
+ /**
209
+ * Navigation: hover info
210
+ */
211
+ async hover(filePath: string, line: number, character: number) {
212
+ const spawned = await this.getClientForFile(filePath);
213
+ if (!spawned) return null;
214
+ return spawned.client.hover(filePath, line, character);
215
+ }
216
+
217
+ /**
218
+ * Navigation: symbols in document
219
+ */
220
+ async documentSymbol(filePath: string) {
221
+ const spawned = await this.getClientForFile(filePath);
222
+ if (!spawned) return [];
223
+ return spawned.client.documentSymbol(filePath);
224
+ }
225
+
226
+ /**
227
+ * Navigation: workspace-wide symbol search
228
+ */
229
+ async workspaceSymbol(query: string) {
230
+ // Use the first active client for workspace-level queries
231
+ const clients = Array.from(this.state.clients.values());
232
+ if (clients.length === 0) return [];
233
+ return clients[0].workspaceSymbol(query);
234
+ }
235
+
236
+ /**
237
+ * Navigation: go to implementation
238
+ */
239
+ async implementation(filePath: string, line: number, character: number) {
240
+ const spawned = await this.getClientForFile(filePath);
241
+ if (!spawned) return [];
242
+ return spawned.client.implementation(filePath, line, character);
243
+ }
244
+
245
+ /**
246
+ * Get all diagnostics across all tracked files (for cascade checking)
247
+ */
248
+ async getAllDiagnostics(): Promise<
249
+ Map<string, import("./client.js").LSPDiagnostic[]>
250
+ > {
251
+ const all = new Map<string, import("./client.js").LSPDiagnostic[]>();
252
+ for (const [_key, client] of this.state.clients) {
253
+ const clientDiags = client.getAllDiagnostics();
254
+ for (const [filePath, diags] of clientDiags) {
255
+ const existing = all.get(filePath) ?? [];
256
+ all.set(filePath, [...existing, ...diags]);
257
+ }
258
+ }
259
+ return all;
260
+ }
261
+
180
262
  /**
181
263
  * Check if LSP is available for a file
182
264
  */
@@ -9,9 +9,9 @@
9
9
  * - User choice caching per project
10
10
  * - Only prompts for "common" languages (Go, Rust, YAML, JSON, Bash)
11
11
  */
12
- import * as fs from "fs/promises";
13
- import * as path from "path";
14
- import { spawn } from "child_process";
12
+ import { spawn } from "node:child_process";
13
+ import * as fs from "node:fs/promises";
14
+ import * as path from "node:path";
15
15
  // Languages that support interactive auto-install prompt
16
16
  const COMMON_LANGUAGES = {
17
17
  go: {
@@ -33,7 +33,7 @@ const COMMON_LANGUAGES = {
33
33
  packageName: "yaml-language-server",
34
34
  },
35
35
  json: {
36
- toolId: "vscode-json-languageserver",
36
+ toolId: "vscode-json-language-server",
37
37
  toolName: "JSON Language Server",
38
38
  installCommand: "npm install -g vscode-langservers-extracted",
39
39
  packageName: "vscode-langservers-extracted",
@@ -123,8 +123,8 @@ function promptUser(timeoutMs) {
123
123
  */
124
124
  function isAutoInstallEnabled() {
125
125
  // Check environment variable or process arguments
126
- return process.env.PI_LENS_AUTO_INSTALL === "1" ||
127
- process.argv.includes("--auto-install");
126
+ return (process.env.PI_LENS_AUTO_INSTALL === "1" ||
127
+ process.argv.includes("--auto-install"));
128
128
  }
129
129
  /**
130
130
  * Attempt to install a tool
@@ -173,9 +173,20 @@ export async function promptForInstall(language, cwd) {
173
173
  const thirtyDays = 30 * 24 * 60 * 60 * 1000;
174
174
  if (Date.now() - cached.timestamp < thirtyDays) {
175
175
  if (cached.choice === "yes" || cached.choice === "auto") {
176
- return true;
176
+ // Verify binary actually exists before trusting cache
177
+ try {
178
+ const { execSync } = await import("node:child_process");
179
+ execSync(`which ${config.toolId}`, { stdio: "ignore" });
180
+ return true; // Binary exists, cache is valid
181
+ }
182
+ catch {
183
+ // Binary not found, invalidate cache and continue to install
184
+ console.error(`[pi-lens] Cached ${config.toolId} not found, re-installing...`);
185
+ }
186
+ }
187
+ else {
188
+ return false; // User previously declined
177
189
  }
178
- return false;
179
190
  }
180
191
  }
181
192
  // Check auto-install flag