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.
- package/CHANGELOG.md +198 -0
- package/README.md +709 -519
- package/clients/__tests__/file-time.test.js +216 -0
- package/clients/__tests__/file-time.test.ts +276 -0
- package/clients/__tests__/format-service.test.js +245 -0
- package/clients/__tests__/format-service.test.ts +339 -0
- package/clients/__tests__/formatters.test.js +271 -0
- package/clients/__tests__/formatters.test.ts +401 -0
- package/clients/amain-types.js +164 -0
- package/clients/amain-types.ts +165 -0
- package/clients/architect-client.js +56 -12
- package/clients/architect-client.ts +81 -16
- package/clients/ast-grep-client.js +2 -2
- package/clients/ast-grep-client.ts +14 -39
- package/clients/ast-grep-parser.ts +1 -1
- package/clients/ast-grep-rule-manager.js +8 -0
- package/clients/ast-grep-rule-manager.ts +10 -1
- package/clients/ast-grep-types.js +9 -0
- package/clients/ast-grep-types.ts +106 -0
- package/clients/auto-loop.js +10 -0
- package/clients/auto-loop.ts +14 -1
- package/clients/biome-client.js +81 -19
- package/clients/biome-client.ts +103 -22
- package/clients/bus/bus.js +191 -0
- package/clients/bus/bus.ts +251 -0
- package/clients/bus/events.js +214 -0
- package/clients/bus/events.ts +279 -0
- package/clients/bus/index.js +8 -0
- package/clients/bus/index.ts +9 -0
- package/clients/bus/integration.js +158 -0
- package/clients/bus/integration.ts +214 -0
- package/clients/complexity-client.js +13 -7
- package/clients/complexity-client.ts +13 -7
- package/clients/config-validator.js +465 -0
- package/clients/config-validator.ts +558 -0
- package/clients/dependency-checker.js +4 -10
- package/clients/dependency-checker.ts +4 -10
- package/clients/dispatch/__tests__/autofix-integration.test.js +245 -0
- package/clients/dispatch/__tests__/autofix-integration.test.ts +300 -0
- package/clients/dispatch/__tests__/runner-registration.test.js +236 -0
- package/clients/dispatch/__tests__/runner-registration.test.ts +282 -0
- package/clients/dispatch/bus-dispatcher.js +177 -0
- package/clients/dispatch/bus-dispatcher.ts +251 -0
- package/clients/dispatch/dispatcher.edge.test.js +82 -0
- package/clients/dispatch/dispatcher.edge.test.ts +100 -0
- package/clients/dispatch/dispatcher.format.test.js +46 -0
- package/clients/dispatch/dispatcher.format.test.ts +58 -0
- package/clients/dispatch/dispatcher.inline.test.js +74 -0
- package/clients/dispatch/dispatcher.inline.test.ts +93 -0
- package/clients/dispatch/dispatcher.js +19 -53
- package/clients/dispatch/dispatcher.ts +20 -67
- package/clients/dispatch/plan.js +9 -4
- package/clients/dispatch/plan.ts +9 -4
- package/clients/dispatch/runners/architect.js +21 -7
- package/clients/dispatch/runners/architect.test.js +138 -0
- package/clients/dispatch/runners/architect.test.ts +162 -0
- package/clients/dispatch/runners/architect.ts +22 -7
- package/clients/dispatch/runners/ast-grep-napi.js +462 -0
- package/clients/dispatch/runners/ast-grep-napi.test.js +111 -0
- package/clients/dispatch/runners/ast-grep-napi.test.ts +133 -0
- package/clients/dispatch/runners/ast-grep-napi.ts +506 -0
- package/clients/dispatch/runners/ast-grep.js +62 -19
- package/clients/dispatch/runners/ast-grep.ts +70 -18
- package/clients/dispatch/runners/biome.js +29 -53
- package/clients/dispatch/runners/biome.ts +29 -63
- package/clients/dispatch/runners/config-validation.js +67 -0
- package/clients/dispatch/runners/config-validation.ts +82 -0
- package/clients/dispatch/runners/go-vet.js +4 -28
- package/clients/dispatch/runners/go-vet.ts +4 -32
- package/clients/dispatch/runners/index.js +30 -10
- package/clients/dispatch/runners/index.ts +30 -10
- package/clients/dispatch/runners/oxlint.js +141 -0
- package/clients/dispatch/runners/oxlint.test.js +230 -0
- package/clients/dispatch/runners/oxlint.test.ts +303 -0
- package/clients/dispatch/runners/oxlint.ts +175 -0
- package/clients/dispatch/runners/pyright.js +40 -70
- package/clients/dispatch/runners/pyright.test.js +16 -2
- package/clients/dispatch/runners/pyright.test.ts +14 -2
- package/clients/dispatch/runners/pyright.ts +48 -91
- package/clients/dispatch/runners/python-slop.js +97 -0
- package/clients/dispatch/runners/python-slop.test.js +203 -0
- package/clients/dispatch/runners/python-slop.test.ts +298 -0
- package/clients/dispatch/runners/python-slop.ts +124 -0
- package/clients/dispatch/runners/ruff.js +18 -71
- package/clients/dispatch/runners/ruff.ts +19 -79
- package/clients/dispatch/runners/rust-clippy.js +28 -32
- package/clients/dispatch/runners/rust-clippy.ts +29 -31
- package/clients/dispatch/runners/scan_codebase.test.js +89 -0
- package/clients/dispatch/runners/scan_codebase.test.ts +105 -0
- package/clients/dispatch/runners/shellcheck.js +147 -0
- package/clients/dispatch/runners/shellcheck.test.js +98 -0
- package/clients/dispatch/runners/shellcheck.test.ts +129 -0
- package/clients/dispatch/runners/shellcheck.ts +188 -0
- package/clients/dispatch/runners/similarity.js +230 -0
- package/clients/dispatch/runners/similarity.ts +339 -0
- package/clients/dispatch/runners/spellcheck.js +106 -0
- package/clients/dispatch/runners/spellcheck.test.js +158 -0
- package/clients/dispatch/runners/spellcheck.test.ts +214 -0
- package/clients/dispatch/runners/spellcheck.ts +136 -0
- package/clients/dispatch/runners/tree-sitter.js +107 -0
- package/clients/dispatch/runners/tree-sitter.ts +135 -0
- package/clients/dispatch/runners/ts-lsp.js +104 -33
- package/clients/dispatch/runners/ts-lsp.ts +120 -38
- package/clients/dispatch/runners/ts-slop.js +113 -0
- package/clients/dispatch/runners/ts-slop.test.js +180 -0
- package/clients/dispatch/runners/ts-slop.test.ts +230 -0
- package/clients/dispatch/runners/ts-slop.ts +142 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.js +134 -0
- package/clients/dispatch/runners/utils/diagnostic-parsers.ts +186 -0
- package/clients/dispatch/runners/utils/runner-helpers.js +115 -0
- package/clients/dispatch/runners/utils/runner-helpers.ts +167 -0
- package/clients/dispatch/runners/utils.js +2 -4
- package/clients/dispatch/runners/utils.ts +2 -4
- package/clients/dispatch/types.ts +1 -1
- package/clients/dispatch/utils/format-utils.js +49 -0
- package/clients/dispatch/utils/format-utils.ts +60 -0
- package/clients/dogfood.test.js +201 -0
- package/clients/dogfood.test.ts +269 -0
- package/clients/file-time.js +152 -0
- package/clients/file-time.ts +208 -0
- package/clients/file-utils.js +40 -0
- package/clients/file-utils.ts +44 -0
- package/clients/fix-scanners.js +10 -20
- package/clients/fix-scanners.ts +10 -22
- package/clients/format-service.js +172 -0
- package/clients/format-service.ts +254 -0
- package/clients/formatters.js +435 -0
- package/clients/formatters.ts +508 -0
- package/clients/go-client.js +5 -14
- package/clients/go-client.ts +5 -13
- package/clients/installer/index.js +356 -0
- package/clients/installer/index.ts +426 -0
- package/clients/jscpd-client.js +11 -9
- package/clients/jscpd-client.ts +12 -8
- package/clients/knip-client.js +3 -7
- package/clients/knip-client.ts +3 -6
- package/clients/lsp/__tests__/client.test.js +325 -0
- package/clients/lsp/__tests__/client.test.ts +434 -0
- package/clients/lsp/__tests__/config.test.js +166 -0
- package/clients/lsp/__tests__/config.test.ts +209 -0
- package/clients/lsp/__tests__/error-recovery.test.js +213 -0
- package/clients/lsp/__tests__/error-recovery.test.ts +279 -0
- package/clients/lsp/__tests__/integration.test.js +127 -0
- package/clients/lsp/__tests__/integration.test.ts +160 -0
- package/clients/lsp/__tests__/launch.test.js +260 -0
- package/clients/lsp/__tests__/launch.test.ts +329 -0
- package/clients/lsp/__tests__/server.test.js +259 -0
- package/clients/lsp/__tests__/server.test.ts +332 -0
- package/clients/lsp/__tests__/service.test.js +417 -0
- package/clients/lsp/__tests__/service.test.ts +499 -0
- package/clients/lsp/client.js +235 -0
- package/clients/lsp/client.ts +328 -0
- package/clients/lsp/config.js +115 -0
- package/clients/lsp/config.ts +149 -0
- package/clients/lsp/index.js +222 -0
- package/clients/lsp/index.ts +280 -0
- package/clients/lsp/installer/index.js +391 -0
- package/clients/lsp/interactive-install.js +210 -0
- package/clients/lsp/interactive-install.ts +251 -0
- package/clients/lsp/language.js +170 -0
- package/clients/lsp/language.ts +216 -0
- package/clients/lsp/launch.js +174 -0
- package/clients/lsp/launch.ts +240 -0
- package/clients/lsp/lsp/launch.js +116 -0
- package/clients/lsp/lsp/server.js +532 -0
- package/clients/lsp/lsp-index.js +10 -0
- package/clients/lsp/lsp-index.ts +11 -0
- package/clients/lsp/path-utils.js +48 -0
- package/clients/lsp/path-utils.ts +52 -0
- package/clients/lsp/server.js +615 -0
- package/clients/lsp/server.ts +800 -0
- package/clients/lsp/test-py-spawn/requirements.txt +1 -0
- package/clients/lsp/test-py-spawn/test.py +3 -0
- package/clients/lsp/test-py-svc/requirements.txt +1 -0
- package/clients/lsp/test-py-svc/test.py +3 -0
- package/clients/lsp/test-python-project/requirements.txt +1 -0
- package/clients/lsp/test-python-project/test.py +5 -0
- package/clients/metrics-history.js +2 -2
- package/clients/metrics-history.ts +2 -2
- package/clients/production-readiness.js +522 -0
- package/clients/production-readiness.ts +556 -0
- package/clients/project-index.js +255 -0
- package/clients/project-index.ts +383 -0
- package/clients/project-metadata.js +531 -0
- package/clients/project-metadata.ts +624 -0
- package/clients/ruff-client.js +56 -16
- package/clients/ruff-client.ts +72 -15
- package/clients/runner-tracker.js +152 -0
- package/clients/runner-tracker.ts +213 -0
- package/clients/rust-client.js +4 -11
- package/clients/rust-client.ts +5 -11
- package/clients/safe-spawn.js +96 -0
- package/clients/safe-spawn.ts +128 -0
- package/clients/scan-architectural-debt.js +3 -6
- package/clients/scan-architectural-debt.ts +3 -6
- package/clients/scan-utils.js +5 -20
- package/clients/scan-utils.ts +5 -29
- package/clients/secrets-scanner.js +3 -17
- package/clients/secrets-scanner.ts +4 -20
- package/clients/services/__tests__/effect-integration.test.js +86 -0
- package/clients/services/__tests__/effect-integration.test.ts +111 -0
- package/clients/services/effect-integration.js +194 -0
- package/clients/services/effect-integration.ts +268 -0
- package/clients/services/index.js +7 -0
- package/clients/services/index.ts +8 -0
- package/clients/services/runner-service.js +105 -0
- package/clients/services/runner-service.ts +179 -0
- package/clients/sg-runner.js +87 -13
- package/clients/sg-runner.ts +97 -13
- package/clients/state-matrix.js +160 -0
- package/clients/state-matrix.ts +202 -0
- package/clients/subprocess-client.js +10 -9
- package/clients/subprocess-client.ts +10 -8
- package/clients/test-runner-client.js +3 -7
- package/clients/test-runner-client.ts +3 -6
- package/clients/tool-availability.js +4 -10
- package/clients/tool-availability.ts +4 -9
- package/clients/tree-sitter-client.js +564 -0
- package/clients/tree-sitter-client.ts +797 -0
- package/clients/tree-sitter-query-loader.js +355 -0
- package/clients/tree-sitter-query-loader.ts +425 -0
- package/clients/type-coverage-client.js +3 -7
- package/clients/type-coverage-client.ts +3 -6
- package/clients/typescript-client.codefix.test.js +157 -0
- package/clients/typescript-client.codefix.test.ts +186 -0
- package/clients/typescript-client.js +43 -0
- package/clients/typescript-client.ts +98 -0
- package/commands/booboo.js +799 -219
- package/commands/booboo.ts +1004 -225
- package/commands/clients/ast-grep-client.js +250 -0
- package/commands/clients/ast-grep-parser.js +86 -0
- package/commands/clients/ast-grep-rule-manager.js +91 -0
- package/commands/clients/ast-grep-types.js +9 -0
- package/commands/clients/biome-client.js +380 -0
- package/commands/clients/complexity-client.js +667 -0
- package/commands/clients/file-kinds.js +177 -0
- package/commands/clients/file-utils.js +40 -0
- package/commands/clients/jscpd-client.js +169 -0
- package/commands/clients/knip-client.js +211 -0
- package/commands/clients/ruff-client.js +297 -0
- package/commands/clients/safe-spawn.js +88 -0
- package/commands/clients/scan-utils.js +83 -0
- package/commands/clients/sg-runner.js +190 -0
- package/commands/clients/types.js +11 -0
- package/commands/clients/typescript-client.js +505 -0
- package/commands/fix-from-booboo.js +398 -0
- package/commands/fix-from-booboo.ts +485 -0
- package/commands/fix-simplified.js +618 -0
- package/commands/fix-simplified.ts +768 -0
- package/commands/rate.js +10 -14
- package/commands/rate.ts +9 -16
- package/default-architect.yaml +59 -15
- package/index.ts +342 -429
- package/package.json +16 -3
- package/rules/ast-grep-rules/rules/empty-catch.yml +38 -13
- package/rules/ast-grep-rules/rules/no-array-constructor.yml +1 -0
- package/rules/ast-grep-rules/rules/no-debugger.yml +2 -0
- package/rules/python-slop-rules/.sgconfig.yml +4 -0
- package/rules/python-slop-rules/rules/slop-rules.yml +647 -0
- package/rules/tree-sitter-queries/python/bare-except.yml +54 -0
- package/rules/tree-sitter-queries/python/eval-exec.yml +50 -0
- package/rules/tree-sitter-queries/python/is-vs-equals.yml +60 -0
- package/rules/tree-sitter-queries/python/mutable-default-arg.yml +57 -0
- package/rules/tree-sitter-queries/python/unreachable-except.yml +60 -0
- package/rules/tree-sitter-queries/python/wildcard-import.yml +46 -0
- package/rules/tree-sitter-queries/tsx/dangerously-set-inner-html.yml +63 -0
- package/rules/tree-sitter-queries/typescript/await-in-loop.yml +56 -0
- package/rules/tree-sitter-queries/typescript/console-statement.yml +47 -0
- package/rules/tree-sitter-queries/typescript/debugger.yml +47 -0
- package/rules/tree-sitter-queries/typescript/deep-nesting.yml +117 -0
- package/rules/tree-sitter-queries/typescript/deep-promise-chain.yml +73 -0
- package/rules/tree-sitter-queries/typescript/empty-catch.yml +64 -0
- package/rules/tree-sitter-queries/typescript/eval.yml +48 -0
- package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +78 -0
- package/rules/tree-sitter-queries/typescript/long-parameter-list.yml +62 -0
- package/rules/tree-sitter-queries/typescript/mixed-async-styles.yml +49 -0
- package/rules/tree-sitter-queries/typescript/nested-ternary.yml +45 -0
- package/rules/ts-slop-rules/.sgconfig.yml +4 -0
- package/rules/ts-slop-rules/rules/in-correct-optional-input-type.yml +10 -0
- package/rules/ts-slop-rules/rules/jwt-no-verify.yml +13 -0
- package/rules/ts-slop-rules/rules/no-architecture-violation.yml +10 -0
- package/rules/ts-slop-rules/rules/no-case-declarations.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dangerously-set-inner-html.yml +10 -0
- package/rules/ts-slop-rules/rules/no-debugger.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-args.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-class-members.yml +10 -0
- package/rules/ts-slop-rules/rules/no-dupe-keys.yml +10 -0
- package/rules/ts-slop-rules/rules/no-eval.yml +13 -0
- package/rules/ts-slop-rules/rules/no-hardcoded-secrets.yml +12 -0
- package/rules/ts-slop-rules/rules/no-implied-eval.yml +12 -0
- package/rules/ts-slop-rules/rules/no-inner-html.yml +13 -0
- package/rules/ts-slop-rules/rules/no-javascript-url.yml +10 -0
- package/rules/ts-slop-rules/rules/no-mutable-default.yml +10 -0
- package/rules/ts-slop-rules/rules/no-nested-links.yml +12 -0
- package/rules/ts-slop-rules/rules/no-new-symbol.yml +10 -0
- package/rules/ts-slop-rules/rules/no-new-wrappers.yml +13 -0
- package/rules/ts-slop-rules/rules/no-open-redirect.yml +16 -0
- package/rules/ts-slop-rules/rules/slop-rules.yml +455 -0
- package/rules/ts-slop-rules/rules/weak-rsa-key.yml +12 -0
- package/skills/ast-grep/SKILL.md +182 -0
- package/clients/dispatch/runners/secrets.js +0 -109
- package/commands/fix.js +0 -244
- package/commands/fix.ts +0 -373
- 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 };
|