pi-lens 2.2.9 → 3.0.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 (304) hide show
  1. package/CHANGELOG.md +198 -0
  2. package/README.md +709 -519
  3. package/clients/__tests__/file-time.test.js +216 -0
  4. package/clients/__tests__/file-time.test.ts +276 -0
  5. package/clients/__tests__/format-service.test.js +245 -0
  6. package/clients/__tests__/format-service.test.ts +339 -0
  7. package/clients/__tests__/formatters.test.js +271 -0
  8. package/clients/__tests__/formatters.test.ts +401 -0
  9. package/clients/amain-types.js +164 -0
  10. package/clients/amain-types.ts +165 -0
  11. package/clients/architect-client.js +56 -12
  12. package/clients/architect-client.ts +81 -16
  13. package/clients/ast-grep-client.js +2 -2
  14. package/clients/ast-grep-client.ts +14 -39
  15. package/clients/ast-grep-parser.ts +1 -1
  16. package/clients/ast-grep-rule-manager.js +8 -0
  17. package/clients/ast-grep-rule-manager.ts +10 -1
  18. package/clients/ast-grep-types.js +9 -0
  19. package/clients/ast-grep-types.ts +106 -0
  20. package/clients/auto-loop.js +10 -0
  21. package/clients/auto-loop.ts +14 -1
  22. package/clients/biome-client.js +81 -19
  23. package/clients/biome-client.ts +103 -22
  24. package/clients/bus/bus.js +191 -0
  25. package/clients/bus/bus.ts +251 -0
  26. package/clients/bus/events.js +214 -0
  27. package/clients/bus/events.ts +279 -0
  28. package/clients/bus/index.js +8 -0
  29. package/clients/bus/index.ts +9 -0
  30. package/clients/bus/integration.js +158 -0
  31. package/clients/bus/integration.ts +214 -0
  32. package/clients/complexity-client.js +13 -7
  33. package/clients/complexity-client.ts +13 -7
  34. package/clients/config-validator.js +465 -0
  35. package/clients/config-validator.ts +558 -0
  36. package/clients/dependency-checker.js +4 -10
  37. package/clients/dependency-checker.ts +4 -10
  38. package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
  39. package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
  40. package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
  41. package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
  42. package/clients/dispatch/bus-dispatcher.js +177 -0
  43. package/clients/dispatch/bus-dispatcher.ts +251 -0
  44. package/clients/dispatch/dispatcher.edge.test.js +82 -0
  45. package/clients/dispatch/dispatcher.edge.test.ts +100 -0
  46. package/clients/dispatch/dispatcher.format.test.js +46 -0
  47. package/clients/dispatch/dispatcher.format.test.ts +58 -0
  48. package/clients/dispatch/dispatcher.inline.test.js +74 -0
  49. package/clients/dispatch/dispatcher.inline.test.ts +93 -0
  50. package/clients/dispatch/dispatcher.js +19 -53
  51. package/clients/dispatch/dispatcher.ts +20 -67
  52. package/clients/dispatch/plan.js +9 -4
  53. package/clients/dispatch/plan.ts +9 -4
  54. package/clients/dispatch/runners/architect.js +21 -7
  55. package/clients/dispatch/runners/architect.test.js +138 -0
  56. package/clients/dispatch/runners/architect.test.ts +162 -0
  57. package/clients/dispatch/runners/architect.ts +22 -7
  58. package/clients/dispatch/runners/ast-grep-napi.js +462 -0
  59. package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
  60. package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
  61. package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
  62. package/clients/dispatch/runners/ast-grep.js +62 -19
  63. package/clients/dispatch/runners/ast-grep.ts +70 -18
  64. package/clients/dispatch/runners/biome.js +29 -53
  65. package/clients/dispatch/runners/biome.ts +29 -63
  66. package/clients/dispatch/runners/config-validation.js +67 -0
  67. package/clients/dispatch/runners/config-validation.ts +82 -0
  68. package/clients/dispatch/runners/go-vet.js +4 -28
  69. package/clients/dispatch/runners/go-vet.ts +4 -32
  70. package/clients/dispatch/runners/index.js +30 -10
  71. package/clients/dispatch/runners/index.ts +30 -10
  72. package/clients/dispatch/runners/oxlint.js +141 -0
  73. package/clients/dispatch/runners/oxlint.test.js +230 -0
  74. package/clients/dispatch/runners/oxlint.test.ts +303 -0
  75. package/clients/dispatch/runners/oxlint.ts +175 -0
  76. package/clients/dispatch/runners/pyright.js +40 -70
  77. package/clients/dispatch/runners/pyright.test.js +16 -2
  78. package/clients/dispatch/runners/pyright.test.ts +14 -2
  79. package/clients/dispatch/runners/pyright.ts +48 -91
  80. package/clients/dispatch/runners/python-slop.js +97 -0
  81. package/clients/dispatch/runners/python-slop.test.js +203 -0
  82. package/clients/dispatch/runners/python-slop.test.ts +298 -0
  83. package/clients/dispatch/runners/python-slop.ts +124 -0
  84. package/clients/dispatch/runners/ruff.js +18 -71
  85. package/clients/dispatch/runners/ruff.ts +19 -79
  86. package/clients/dispatch/runners/rust-clippy.js +28 -32
  87. package/clients/dispatch/runners/rust-clippy.ts +29 -31
  88. package/clients/dispatch/runners/scan_codebase.test.js +89 -0
  89. package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
  90. package/clients/dispatch/runners/shellcheck.js +147 -0
  91. package/clients/dispatch/runners/shellcheck.test.js +98 -0
  92. package/clients/dispatch/runners/shellcheck.test.ts +129 -0
  93. package/clients/dispatch/runners/shellcheck.ts +188 -0
  94. package/clients/dispatch/runners/similarity.js +230 -0
  95. package/clients/dispatch/runners/similarity.ts +339 -0
  96. package/clients/dispatch/runners/spellcheck.js +106 -0
  97. package/clients/dispatch/runners/spellcheck.test.js +158 -0
  98. package/clients/dispatch/runners/spellcheck.test.ts +214 -0
  99. package/clients/dispatch/runners/spellcheck.ts +136 -0
  100. package/clients/dispatch/runners/tree-sitter.js +107 -0
  101. package/clients/dispatch/runners/tree-sitter.ts +135 -0
  102. package/clients/dispatch/runners/ts-lsp.js +104 -33
  103. package/clients/dispatch/runners/ts-lsp.ts +120 -38
  104. package/clients/dispatch/runners/ts-slop.js +113 -0
  105. package/clients/dispatch/runners/ts-slop.test.js +180 -0
  106. package/clients/dispatch/runners/ts-slop.test.ts +230 -0
  107. package/clients/dispatch/runners/ts-slop.ts +142 -0
  108. package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
  109. package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
  110. package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
  111. package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
  112. package/clients/dispatch/runners/utils.js +2 -4
  113. package/clients/dispatch/runners/utils.ts +2 -4
  114. package/clients/dispatch/types.ts +1 -1
  115. package/clients/dispatch/utils/format-utils.js +49 -0
  116. package/clients/dispatch/utils/format-utils.ts +60 -0
  117. package/clients/dogfood.test.js +201 -0
  118. package/clients/dogfood.test.ts +269 -0
  119. package/clients/file-time.js +152 -0
  120. package/clients/file-time.ts +208 -0
  121. package/clients/file-utils.js +40 -0
  122. package/clients/file-utils.ts +44 -0
  123. package/clients/fix-scanners.js +10 -20
  124. package/clients/fix-scanners.ts +10 -22
  125. package/clients/format-service.js +172 -0
  126. package/clients/format-service.ts +254 -0
  127. package/clients/formatters.js +435 -0
  128. package/clients/formatters.ts +508 -0
  129. package/clients/go-client.js +5 -14
  130. package/clients/go-client.ts +5 -13
  131. package/clients/installer/index.js +356 -0
  132. package/clients/installer/index.ts +426 -0
  133. package/clients/jscpd-client.js +11 -9
  134. package/clients/jscpd-client.ts +12 -8
  135. package/clients/knip-client.js +3 -7
  136. package/clients/knip-client.ts +3 -6
  137. package/clients/lsp/__tests__/client.test.js +325 -0
  138. package/clients/lsp/__tests__/client.test.ts +434 -0
  139. package/clients/lsp/__tests__/config.test.js +166 -0
  140. package/clients/lsp/__tests__/config.test.ts +209 -0
  141. package/clients/lsp/__tests__/error-recovery.test.js +213 -0
  142. package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
  143. package/clients/lsp/__tests__/integration.test.js +127 -0
  144. package/clients/lsp/__tests__/integration.test.ts +160 -0
  145. package/clients/lsp/__tests__/launch.test.js +260 -0
  146. package/clients/lsp/__tests__/launch.test.ts +329 -0
  147. package/clients/lsp/__tests__/server.test.js +259 -0
  148. package/clients/lsp/__tests__/server.test.ts +332 -0
  149. package/clients/lsp/__tests__/service.test.js +417 -0
  150. package/clients/lsp/__tests__/service.test.ts +499 -0
  151. package/clients/lsp/client.js +235 -0
  152. package/clients/lsp/client.ts +328 -0
  153. package/clients/lsp/config.js +115 -0
  154. package/clients/lsp/config.ts +149 -0
  155. package/clients/lsp/index.js +222 -0
  156. package/clients/lsp/index.ts +280 -0
  157. package/clients/lsp/installer/index.js +391 -0
  158. package/clients/lsp/interactive-install.js +210 -0
  159. package/clients/lsp/interactive-install.ts +251 -0
  160. package/clients/lsp/language.js +170 -0
  161. package/clients/lsp/language.ts +216 -0
  162. package/clients/lsp/launch.js +174 -0
  163. package/clients/lsp/launch.ts +240 -0
  164. package/clients/lsp/lsp/launch.js +116 -0
  165. package/clients/lsp/lsp/server.js +532 -0
  166. package/clients/lsp/lsp-index.js +10 -0
  167. package/clients/lsp/lsp-index.ts +11 -0
  168. package/clients/lsp/path-utils.js +48 -0
  169. package/clients/lsp/path-utils.ts +52 -0
  170. package/clients/lsp/server.js +615 -0
  171. package/clients/lsp/server.ts +800 -0
  172. package/clients/lsp/test-py-spawn/requirements.txt +1 -0
  173. package/clients/lsp/test-py-spawn/test.py +3 -0
  174. package/clients/lsp/test-py-svc/requirements.txt +1 -0
  175. package/clients/lsp/test-py-svc/test.py +3 -0
  176. package/clients/lsp/test-python-project/requirements.txt +1 -0
  177. package/clients/lsp/test-python-project/test.py +5 -0
  178. package/clients/metrics-history.js +2 -2
  179. package/clients/metrics-history.ts +2 -2
  180. package/clients/production-readiness.js +522 -0
  181. package/clients/production-readiness.ts +556 -0
  182. package/clients/project-index.js +255 -0
  183. package/clients/project-index.ts +383 -0
  184. package/clients/project-metadata.js +531 -0
  185. package/clients/project-metadata.ts +624 -0
  186. package/clients/ruff-client.js +56 -16
  187. package/clients/ruff-client.ts +72 -15
  188. package/clients/runner-tracker.js +152 -0
  189. package/clients/runner-tracker.ts +213 -0
  190. package/clients/rust-client.js +4 -11
  191. package/clients/rust-client.ts +5 -11
  192. package/clients/safe-spawn.js +96 -0
  193. package/clients/safe-spawn.ts +128 -0
  194. package/clients/scan-architectural-debt.js +3 -6
  195. package/clients/scan-architectural-debt.ts +3 -6
  196. package/clients/scan-utils.js +5 -20
  197. package/clients/scan-utils.ts +5 -29
  198. package/clients/secrets-scanner.js +3 -17
  199. package/clients/secrets-scanner.ts +4 -20
  200. package/clients/services/__tests__/effect-integration.test.js +86 -0
  201. package/clients/services/__tests__/effect-integration.test.ts +111 -0
  202. package/clients/services/effect-integration.js +194 -0
  203. package/clients/services/effect-integration.ts +268 -0
  204. package/clients/services/index.js +7 -0
  205. package/clients/services/index.ts +8 -0
  206. package/clients/services/runner-service.js +105 -0
  207. package/clients/services/runner-service.ts +179 -0
  208. package/clients/sg-runner.js +87 -13
  209. package/clients/sg-runner.ts +97 -13
  210. package/clients/state-matrix.js +160 -0
  211. package/clients/state-matrix.ts +202 -0
  212. package/clients/subprocess-client.js +10 -9
  213. package/clients/subprocess-client.ts +10 -8
  214. package/clients/test-runner-client.js +3 -7
  215. package/clients/test-runner-client.ts +3 -6
  216. package/clients/tool-availability.js +4 -10
  217. package/clients/tool-availability.ts +4 -9
  218. package/clients/tree-sitter-client.js +564 -0
  219. package/clients/tree-sitter-client.ts +797 -0
  220. package/clients/tree-sitter-query-loader.js +355 -0
  221. package/clients/tree-sitter-query-loader.ts +425 -0
  222. package/clients/type-coverage-client.js +3 -7
  223. package/clients/type-coverage-client.ts +3 -6
  224. package/clients/typescript-client.codefix.test.js +157 -0
  225. package/clients/typescript-client.codefix.test.ts +186 -0
  226. package/clients/typescript-client.js +43 -0
  227. package/clients/typescript-client.ts +98 -0
  228. package/commands/booboo.js +799 -219
  229. package/commands/booboo.ts +1004 -225
  230. package/commands/clients/ast-grep-client.js +250 -0
  231. package/commands/clients/ast-grep-parser.js +86 -0
  232. package/commands/clients/ast-grep-rule-manager.js +91 -0
  233. package/commands/clients/ast-grep-types.js +9 -0
  234. package/commands/clients/biome-client.js +380 -0
  235. package/commands/clients/complexity-client.js +667 -0
  236. package/commands/clients/file-kinds.js +177 -0
  237. package/commands/clients/file-utils.js +40 -0
  238. package/commands/clients/jscpd-client.js +169 -0
  239. package/commands/clients/knip-client.js +211 -0
  240. package/commands/clients/ruff-client.js +297 -0
  241. package/commands/clients/safe-spawn.js +88 -0
  242. package/commands/clients/scan-utils.js +83 -0
  243. package/commands/clients/sg-runner.js +190 -0
  244. package/commands/clients/types.js +11 -0
  245. package/commands/clients/typescript-client.js +505 -0
  246. package/commands/fix-from-booboo.js +398 -0
  247. package/commands/fix-from-booboo.ts +485 -0
  248. package/commands/fix-simplified.js +618 -0
  249. package/commands/fix-simplified.ts +768 -0
  250. package/commands/rate.js +10 -14
  251. package/commands/rate.ts +9 -16
  252. package/default-architect.yaml +59 -15
  253. package/index.ts +342 -429
  254. package/package.json +16 -3
  255. package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
  256. package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
  257. package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
  258. package/rules/python-slop-rules/.sgconfig.yml +4 -0
  259. package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
  260. package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
  261. package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
  262. package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
  263. package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
  264. package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
  265. package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
  266. package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
  267. package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
  268. package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
  269. package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
  270. package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
  271. package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
  272. package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
  273. package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
  274. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
  275. package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
  276. package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
  277. package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
  278. package/rules/ts-slop-rules/.sgconfig.yml +4 -0
  279. package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
  280. package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
  281. package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
  282. package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
  283. package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
  284. package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
  285. package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
  286. package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
  287. package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
  288. package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
  289. package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
  290. package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
  291. package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
  292. package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
  293. package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
  294. package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
  295. package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
  296. package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
  297. package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
  298. package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
  299. package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
  300. package/skills/ast-grep/SKILL.md +182 -0
  301. package/clients/dispatch/runners/secrets.js +0 -109
  302. package/commands/fix.js +0 -244
  303. package/commands/fix.ts +0 -373
  304. package/rules/ast-grep-rules/rules/no-lonely-if.yml +0 -13
@@ -0,0 +1,235 @@
1
+ /**
2
+ * LSP Client for pi-lens
3
+ *
4
+ * Handles JSON-RPC communication with language servers:
5
+ * - Initialize/shutdown lifecycle
6
+ * - Document synchronization (didOpen, didChange)
7
+ * - Diagnostics with debouncing
8
+ * - Request/response handling
9
+ */
10
+ import { pathToFileURL } from "node:url";
11
+ import { createMessageConnection, StreamMessageReader, StreamMessageWriter, } from "vscode-jsonrpc/node.js";
12
+ import { DiagnosticFound } from "../bus/events.js";
13
+ import { normalizeMapKey, uriToPath } from "./path-utils.js";
14
+ // --- Constants ---
15
+ const DIAGNOSTICS_DEBOUNCE_MS = 150;
16
+ const INITIALIZE_TIMEOUT_MS = 120000; // 2 minutes (was 45s) - allows time for npx to download packages
17
+ // --- Client Factory ---
18
+ export async function createLSPClient(options) {
19
+ const { serverId, process: lspProcess, root, initialization } = options;
20
+ // Create JSON-RPC connection
21
+ const connection = createMessageConnection(new StreamMessageReader(lspProcess.stdout), new StreamMessageWriter(lspProcess.stdin));
22
+ // Track diagnostics per file
23
+ const diagnostics = new Map();
24
+ const pendingDiagnostics = new Map();
25
+ // Handle incoming diagnostics with debouncing
26
+ connection.onNotification("textDocument/publishDiagnostics", (params) => {
27
+ const filePath = uriToPath(params.uri);
28
+ const newDiags = params.diagnostics || [];
29
+ // Debounce: clear existing timer and set new one
30
+ const existingTimer = pendingDiagnostics.get(filePath);
31
+ if (existingTimer)
32
+ clearTimeout(existingTimer);
33
+ const timer = setTimeout(() => {
34
+ diagnostics.set(filePath, newDiags);
35
+ pendingDiagnostics.delete(filePath);
36
+ // Publish to bus
37
+ // Defensive: filter out malformed diagnostics that may lack range
38
+ const validDiags = newDiags.filter((d) => d.range?.start?.line !== undefined);
39
+ DiagnosticFound.publish({
40
+ runnerId: serverId,
41
+ filePath,
42
+ diagnostics: validDiags.map((d) => ({
43
+ id: `${serverId}:${d.code ?? "unknown"}:${d.range.start.line}`,
44
+ message: d.message,
45
+ filePath,
46
+ line: d.range.start.line + 1,
47
+ column: d.range.start.character + 1,
48
+ severity: severityFromNumber(d.severity),
49
+ semantic: d.severity === 1
50
+ ? "blocking"
51
+ : d.severity === 2
52
+ ? "warning"
53
+ : "silent",
54
+ tool: serverId,
55
+ })),
56
+ durationMs: 0,
57
+ });
58
+ }, DIAGNOSTICS_DEBOUNCE_MS);
59
+ pendingDiagnostics.set(filePath, timer);
60
+ });
61
+ // Handle server requests
62
+ connection.onRequest("workspace/workspaceFolders", () => [
63
+ {
64
+ name: "workspace",
65
+ uri: pathToFileURL(root).href,
66
+ },
67
+ ]);
68
+ connection.onRequest("client/registerCapability", async () => { });
69
+ connection.onRequest("client/unregisterCapability", async () => { });
70
+ connection.onRequest("workspace/configuration", async () => [
71
+ initialization ?? {},
72
+ ]);
73
+ connection.onRequest("window/workDoneProgress/create", async () => { });
74
+ // Start listening
75
+ connection.listen();
76
+ // Send initialize request
77
+ await withTimeout(connection.sendRequest("initialize", {
78
+ processId: process.pid,
79
+ rootUri: pathToFileURL(root).href,
80
+ workspaceFolders: [
81
+ {
82
+ name: "workspace",
83
+ uri: pathToFileURL(root).href,
84
+ },
85
+ ],
86
+ capabilities: {
87
+ window: {
88
+ workDoneProgress: true,
89
+ },
90
+ workspace: {
91
+ workspaceFolders: {
92
+ supported: true,
93
+ changeNotifications: true,
94
+ },
95
+ configuration: true,
96
+ didChangeWatchedFiles: {
97
+ dynamicRegistration: true,
98
+ },
99
+ },
100
+ textDocument: {
101
+ synchronization: {
102
+ didOpen: true,
103
+ didChange: true,
104
+ },
105
+ publishDiagnostics: {
106
+ versionSupport: true,
107
+ },
108
+ },
109
+ },
110
+ initializationOptions: initialization,
111
+ }), INITIALIZE_TIMEOUT_MS);
112
+ // Send initialized notification
113
+ await connection.sendNotification("initialized", {});
114
+ // Send configuration if provided (helps pyright and other servers)
115
+ if (initialization) {
116
+ await connection.sendNotification("workspace/didChangeConfiguration", {
117
+ settings: initialization,
118
+ });
119
+ }
120
+ // Track open documents with version numbers
121
+ const documentVersions = new Map();
122
+ return {
123
+ serverId,
124
+ root,
125
+ connection,
126
+ notify: {
127
+ async open(filePath, content, languageId) {
128
+ const uri = pathToFileURL(filePath).href;
129
+ // Normalize path for Windows case-insensitive lookup
130
+ const normalizedPath = normalizeMapKey(filePath);
131
+ documentVersions.set(normalizedPath, 0);
132
+ diagnostics.delete(normalizedPath); // Clear stale diagnostics
133
+ // Send workspace notification first (like opencode does)
134
+ await connection.sendNotification("workspace/didChangeWatchedFiles", {
135
+ changes: [
136
+ {
137
+ uri,
138
+ type: 1, // Created
139
+ },
140
+ ],
141
+ });
142
+ await connection.sendNotification("textDocument/didOpen", {
143
+ textDocument: {
144
+ uri,
145
+ languageId,
146
+ version: 0,
147
+ text: content,
148
+ },
149
+ });
150
+ },
151
+ async change(filePath, content) {
152
+ const uri = pathToFileURL(filePath).href;
153
+ const version = (documentVersions.get(filePath) ?? 0) + 1;
154
+ documentVersions.set(filePath, version);
155
+ await connection.sendNotification("textDocument/didChange", {
156
+ textDocument: { uri, version },
157
+ contentChanges: [{ text: content }],
158
+ });
159
+ },
160
+ },
161
+ getDiagnostics(filePath) {
162
+ // Normalize path for Windows case-insensitive lookup
163
+ const normalizedPath = normalizeMapKey(filePath);
164
+ return diagnostics.get(normalizedPath) ?? [];
165
+ },
166
+ async waitForDiagnostics(filePath, timeoutMs = 10000) {
167
+ const normalizedPath = normalizeMapKey(filePath);
168
+ if (diagnostics.has(normalizedPath))
169
+ return;
170
+ // Use bus subscription like OpenCode - more reliable than polling
171
+ return new Promise((resolve) => {
172
+ let debounceTimer;
173
+ // Subscribe to diagnostic events from this server
174
+ const unsub = DiagnosticFound.subscribe((event) => {
175
+ if (event.properties.filePath === normalizedPath &&
176
+ event.properties.runnerId === serverId) {
177
+ // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
178
+ if (debounceTimer)
179
+ clearTimeout(debounceTimer);
180
+ debounceTimer = setTimeout(() => {
181
+ unsub();
182
+ clearTimeout(timeout);
183
+ resolve();
184
+ }, DIAGNOSTICS_DEBOUNCE_MS);
185
+ }
186
+ });
187
+ const timeout = setTimeout(() => {
188
+ if (debounceTimer)
189
+ clearTimeout(debounceTimer);
190
+ unsub();
191
+ resolve();
192
+ }, timeoutMs);
193
+ });
194
+ },
195
+ async shutdown() {
196
+ // Clear pending timers
197
+ for (const timer of pendingDiagnostics.values()) {
198
+ clearTimeout(timer);
199
+ }
200
+ pendingDiagnostics.clear();
201
+ // Graceful shutdown
202
+ try {
203
+ await connection.sendRequest("shutdown");
204
+ await connection.sendNotification("exit");
205
+ }
206
+ catch {
207
+ /* ignore */
208
+ }
209
+ connection.dispose();
210
+ lspProcess.process.kill();
211
+ },
212
+ };
213
+ }
214
+ // --- Utilities ---
215
+ // Using shared path utilities from path-utils.ts
216
+ function severityFromNumber(sev) {
217
+ switch (sev) {
218
+ case 1:
219
+ return "error";
220
+ case 2:
221
+ return "warning";
222
+ case 3:
223
+ return "info";
224
+ case 4:
225
+ return "hint";
226
+ default:
227
+ return "error";
228
+ }
229
+ }
230
+ async function withTimeout(promise, timeoutMs) {
231
+ return Promise.race([
232
+ promise,
233
+ new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${timeoutMs}ms`)), timeoutMs)),
234
+ ]);
235
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * LSP Client for pi-lens
3
+ *
4
+ * Handles JSON-RPC communication with language servers:
5
+ * - Initialize/shutdown lifecycle
6
+ * - Document synchronization (didOpen, didChange)
7
+ * - Diagnostics with debouncing
8
+ * - Request/response handling
9
+ */
10
+
11
+ import { pathToFileURL } from "node:url";
12
+ import type { MessageConnection } from "vscode-jsonrpc";
13
+ import {
14
+ createMessageConnection,
15
+ StreamMessageReader,
16
+ StreamMessageWriter,
17
+ } from "vscode-jsonrpc/node.js";
18
+ import { DiagnosticFound } from "../bus/events.js";
19
+ import type { LSPProcess } from "./launch.js";
20
+ import { normalizeMapKey, uriToPath } from "./path-utils.js";
21
+
22
+ // --- Types ---
23
+
24
+ export interface LSPDiagnostic {
25
+ severity: 1 | 2 | 3 | 4; // Error, Warning, Info, Hint
26
+ message: string;
27
+ range: {
28
+ start: { line: number; character: number };
29
+ end: { line: number; character: number };
30
+ };
31
+ code?: string | number;
32
+ source?: string;
33
+ }
34
+
35
+ export interface LSPClientInfo {
36
+ serverId: string;
37
+ root: string;
38
+ connection: MessageConnection;
39
+ notify: {
40
+ open(filePath: string, content: string, languageId: string): Promise<void>;
41
+ change(filePath: string, content: string): Promise<void>;
42
+ };
43
+ getDiagnostics(filePath: string): LSPDiagnostic[];
44
+ waitForDiagnostics(filePath: string, timeoutMs?: number): Promise<void>;
45
+ shutdown(): Promise<void>;
46
+ }
47
+
48
+ // --- Constants ---
49
+
50
+ const DIAGNOSTICS_DEBOUNCE_MS = 150;
51
+ const INITIALIZE_TIMEOUT_MS = 120_000; // 2 minutes (was 45s) - allows time for npx to download packages
52
+
53
+ // --- Client Factory ---
54
+
55
+ export async function createLSPClient(options: {
56
+ serverId: string;
57
+ process: LSPProcess;
58
+ root: string;
59
+ initialization?: Record<string, unknown>;
60
+ }): Promise<LSPClientInfo> {
61
+ const { serverId, process: lspProcess, root, initialization } = options;
62
+
63
+ // Create JSON-RPC connection
64
+ const connection = createMessageConnection(
65
+ new StreamMessageReader(lspProcess.stdout),
66
+ new StreamMessageWriter(lspProcess.stdin),
67
+ );
68
+
69
+ // Track diagnostics per file
70
+ const diagnostics = new Map<string, LSPDiagnostic[]>();
71
+ const pendingDiagnostics = new Map<string, ReturnType<typeof setTimeout>>();
72
+
73
+ // Handle incoming diagnostics with debouncing
74
+ connection.onNotification(
75
+ "textDocument/publishDiagnostics",
76
+ (params: { uri: string; diagnostics?: LSPDiagnostic[] }) => {
77
+ const filePath = uriToPath(params.uri);
78
+ const newDiags: LSPDiagnostic[] = params.diagnostics || [];
79
+
80
+ // Debounce: clear existing timer and set new one
81
+ const existingTimer = pendingDiagnostics.get(filePath);
82
+ if (existingTimer) clearTimeout(existingTimer);
83
+
84
+ const timer = setTimeout(() => {
85
+ diagnostics.set(filePath, newDiags);
86
+ pendingDiagnostics.delete(filePath);
87
+
88
+ // Publish to bus
89
+ // Defensive: filter out malformed diagnostics that may lack range
90
+ const validDiags = newDiags.filter(
91
+ (d) => d.range?.start?.line !== undefined,
92
+ );
93
+ DiagnosticFound.publish({
94
+ runnerId: serverId,
95
+ filePath,
96
+ diagnostics: validDiags.map((d) => ({
97
+ id: `${serverId}:${d.code ?? "unknown"}:${d.range.start.line}`,
98
+ message: d.message,
99
+ filePath,
100
+ line: d.range.start.line + 1,
101
+ column: d.range.start.character + 1,
102
+ severity: severityFromNumber(d.severity),
103
+ semantic:
104
+ d.severity === 1
105
+ ? "blocking"
106
+ : d.severity === 2
107
+ ? "warning"
108
+ : "silent",
109
+ tool: serverId,
110
+ })),
111
+ durationMs: 0,
112
+ });
113
+ }, DIAGNOSTICS_DEBOUNCE_MS);
114
+
115
+ pendingDiagnostics.set(filePath, timer);
116
+ },
117
+ );
118
+
119
+ // Handle server requests
120
+ connection.onRequest("workspace/workspaceFolders", () => [
121
+ {
122
+ name: "workspace",
123
+ uri: pathToFileURL(root).href,
124
+ },
125
+ ]);
126
+
127
+ connection.onRequest("client/registerCapability", async () => {});
128
+ connection.onRequest("client/unregisterCapability", async () => {});
129
+ connection.onRequest("workspace/configuration", async () => [
130
+ initialization ?? {},
131
+ ]);
132
+ connection.onRequest("window/workDoneProgress/create", async () => {});
133
+
134
+ // Start listening
135
+ connection.listen();
136
+
137
+ // Send initialize request
138
+ await withTimeout(
139
+ connection.sendRequest("initialize", {
140
+ processId: process.pid,
141
+ rootUri: pathToFileURL(root).href,
142
+ workspaceFolders: [
143
+ {
144
+ name: "workspace",
145
+ uri: pathToFileURL(root).href,
146
+ },
147
+ ],
148
+ capabilities: {
149
+ window: {
150
+ workDoneProgress: true,
151
+ },
152
+ workspace: {
153
+ workspaceFolders: {
154
+ supported: true,
155
+ changeNotifications: true,
156
+ },
157
+ configuration: true,
158
+ didChangeWatchedFiles: {
159
+ dynamicRegistration: true,
160
+ },
161
+ },
162
+ textDocument: {
163
+ synchronization: {
164
+ didOpen: true,
165
+ didChange: true,
166
+ },
167
+ publishDiagnostics: {
168
+ versionSupport: true,
169
+ },
170
+ },
171
+ },
172
+ initializationOptions: initialization,
173
+ }),
174
+ INITIALIZE_TIMEOUT_MS,
175
+ );
176
+
177
+ // Send initialized notification
178
+ await connection.sendNotification("initialized", {});
179
+
180
+ // Send configuration if provided (helps pyright and other servers)
181
+ if (initialization) {
182
+ await connection.sendNotification("workspace/didChangeConfiguration", {
183
+ settings: initialization,
184
+ });
185
+ }
186
+
187
+ // Track open documents with version numbers
188
+ const documentVersions = new Map<string, number>();
189
+
190
+ return {
191
+ serverId,
192
+ root,
193
+ connection,
194
+
195
+ notify: {
196
+ async open(filePath, content, languageId) {
197
+ const uri = pathToFileURL(filePath).href;
198
+ // Normalize path for Windows case-insensitive lookup
199
+ const normalizedPath = normalizeMapKey(filePath);
200
+ documentVersions.set(normalizedPath, 0);
201
+ diagnostics.delete(normalizedPath); // Clear stale diagnostics
202
+
203
+ // Send workspace notification first (like opencode does)
204
+ await connection.sendNotification("workspace/didChangeWatchedFiles", {
205
+ changes: [
206
+ {
207
+ uri,
208
+ type: 1, // Created
209
+ },
210
+ ],
211
+ });
212
+
213
+ await connection.sendNotification("textDocument/didOpen", {
214
+ textDocument: {
215
+ uri,
216
+ languageId,
217
+ version: 0,
218
+ text: content,
219
+ },
220
+ });
221
+ },
222
+
223
+ async change(filePath, content) {
224
+ const uri = pathToFileURL(filePath).href;
225
+ const version = (documentVersions.get(filePath) ?? 0) + 1;
226
+ documentVersions.set(filePath, version);
227
+
228
+ await connection.sendNotification("textDocument/didChange", {
229
+ textDocument: { uri, version },
230
+ contentChanges: [{ text: content }],
231
+ });
232
+ },
233
+ },
234
+
235
+ getDiagnostics(filePath) {
236
+ // Normalize path for Windows case-insensitive lookup
237
+ const normalizedPath = normalizeMapKey(filePath);
238
+ return diagnostics.get(normalizedPath) ?? [];
239
+ },
240
+
241
+ async waitForDiagnostics(filePath, timeoutMs = 10000) {
242
+ const normalizedPath = normalizeMapKey(filePath);
243
+ if (diagnostics.has(normalizedPath)) return;
244
+
245
+ // Use bus subscription like OpenCode - more reliable than polling
246
+ return new Promise((resolve) => {
247
+ let debounceTimer: ReturnType<typeof setTimeout> | undefined;
248
+
249
+ // Subscribe to diagnostic events from this server
250
+ const unsub = DiagnosticFound.subscribe((event) => {
251
+ if (
252
+ event.properties.filePath === normalizedPath &&
253
+ event.properties.runnerId === serverId
254
+ ) {
255
+ // Debounce to allow LSP to send follow-up diagnostics (e.g., semantic after syntax)
256
+ if (debounceTimer) clearTimeout(debounceTimer);
257
+ debounceTimer = setTimeout(() => {
258
+ unsub();
259
+ clearTimeout(timeout);
260
+ resolve();
261
+ }, DIAGNOSTICS_DEBOUNCE_MS);
262
+ }
263
+ });
264
+
265
+ const timeout = setTimeout(() => {
266
+ if (debounceTimer) clearTimeout(debounceTimer);
267
+ unsub();
268
+ resolve();
269
+ }, timeoutMs);
270
+ });
271
+ },
272
+
273
+ async shutdown() {
274
+ // Clear pending timers
275
+ for (const timer of pendingDiagnostics.values()) {
276
+ clearTimeout(timer);
277
+ }
278
+ pendingDiagnostics.clear();
279
+
280
+ // Graceful shutdown
281
+ try {
282
+ await connection.sendRequest("shutdown");
283
+ await connection.sendNotification("exit");
284
+ } catch {
285
+ /* ignore */
286
+ }
287
+
288
+ connection.dispose();
289
+ lspProcess.process.kill();
290
+ },
291
+ };
292
+ }
293
+
294
+ // --- Utilities ---
295
+
296
+ // Using shared path utilities from path-utils.ts
297
+
298
+ function severityFromNumber(
299
+ sev: number,
300
+ ): "error" | "warning" | "info" | "hint" {
301
+ switch (sev) {
302
+ case 1:
303
+ return "error";
304
+ case 2:
305
+ return "warning";
306
+ case 3:
307
+ return "info";
308
+ case 4:
309
+ return "hint";
310
+ default:
311
+ return "error";
312
+ }
313
+ }
314
+
315
+ async function withTimeout<T>(
316
+ promise: Promise<T>,
317
+ timeoutMs: number,
318
+ ): Promise<T> {
319
+ return Promise.race([
320
+ promise,
321
+ new Promise<T>((_, reject) =>
322
+ setTimeout(
323
+ () => reject(new Error(`Timeout after ${timeoutMs}ms`)),
324
+ timeoutMs,
325
+ ),
326
+ ),
327
+ ]);
328
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * LSP Configuration for pi-lens
3
+ *
4
+ * Allows users to define custom LSP servers via configuration.
5
+ *
6
+ * Config file: .pi-lens/lsp.json
7
+ *
8
+ * Example:
9
+ * {
10
+ * "servers": {
11
+ * "my-server": {
12
+ * "name": "My Custom LSP",
13
+ * "extensions": [".myext"],
14
+ * "command": "my-lsp-server",
15
+ * "args": ["--stdio"],
16
+ * "rootMarkers": ["package.json"]
17
+ * }
18
+ * }
19
+ * }
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";
25
+ import { launchLSP } from "./launch.js";
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ // --- Config Loading ---
28
+ const CONFIG_PATHS = [
29
+ ".pi-lens/lsp.json",
30
+ ".pi-lens.json",
31
+ "pi-lsp.json",
32
+ ];
33
+ /**
34
+ * Load LSP configuration from file
35
+ */
36
+ export async function loadLSPConfig(cwd) {
37
+ for (const configPath of CONFIG_PATHS) {
38
+ const fullPath = path.join(cwd, configPath);
39
+ try {
40
+ const content = await fs.readFile(fullPath, "utf-8");
41
+ const config = JSON.parse(content);
42
+ console.error(`[lsp-config] Loaded config from ${configPath}`);
43
+ return config;
44
+ }
45
+ catch {
46
+ // File doesn't exist or is invalid, try next
47
+ }
48
+ }
49
+ return {};
50
+ }
51
+ // --- Custom Server Factory ---
52
+ /**
53
+ * Create LSPServerInfo from user configuration
54
+ */
55
+ export function createCustomServer(config, id) {
56
+ return {
57
+ id,
58
+ name: config.name,
59
+ extensions: config.extensions,
60
+ root: config.rootMarkers
61
+ ? createRootDetector(config.rootMarkers)
62
+ : async () => process.cwd(),
63
+ async spawn(root) {
64
+ const proc = launchLSP(config.command, config.args ?? ["--stdio"], {
65
+ cwd: root,
66
+ env: config.env ? { ...process.env, ...config.env } : process.env,
67
+ });
68
+ return { process: proc };
69
+ },
70
+ };
71
+ }
72
+ // --- Registry Management ---
73
+ let customServers = [];
74
+ let disabledServerIds = new Set();
75
+ /**
76
+ * Initialize LSP configuration (call at session start)
77
+ */
78
+ export async function initLSPConfig(cwd) {
79
+ const config = await loadLSPConfig(cwd);
80
+ // Clear previous custom servers
81
+ customServers = [];
82
+ disabledServerIds = new Set(config.disabledServers ?? []);
83
+ // Register custom servers from config
84
+ if (config.servers) {
85
+ for (const [id, serverConfig] of Object.entries(config.servers)) {
86
+ try {
87
+ const server = createCustomServer(serverConfig, id);
88
+ customServers.push(server);
89
+ console.error(`[lsp-config] Registered custom server: ${id} (${serverConfig.name})`);
90
+ }
91
+ catch (err) {
92
+ console.error(`[lsp-config] Failed to register server ${id}:`, err);
93
+ }
94
+ }
95
+ }
96
+ }
97
+ /**
98
+ * Get all available servers (built-in + custom, minus disabled)
99
+ */
100
+ export function getAllServers() {
101
+ const all = [...LSP_SERVERS, ...customServers];
102
+ return all.filter(s => !disabledServerIds.has(s.id));
103
+ }
104
+ /**
105
+ * Check if a server is disabled
106
+ */
107
+ export function isServerDisabled(serverId) {
108
+ return disabledServerIds.has(serverId);
109
+ }
110
+ export function getServersForFileWithConfig(filePath) {
111
+ const ext = path.extname(filePath).toLowerCase();
112
+ return getAllServers().filter((server) => server.extensions.includes(ext));
113
+ }
114
+ // Re-export with config support
115
+ export { getAllServers as getServersForFile };