pi-lsp-lite 0.3.2 → 0.5.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/README.md +4 -4
- package/index.ts +58 -31
- package/package.json +12 -4
- package/src/client.ts +54 -13
- package/src/config.ts +28 -12
- package/src/format.ts +43 -12
- package/src/install-registry.ts +16 -6
- package/src/languages.ts +11 -0
- package/src/server-manager.ts +4 -3
- package/src/status.ts +95 -0
- package/src/util.ts +21 -17
- package/.github/workflows/ci.yml +0 -30
- package/.github/workflows/integration.yml +0 -79
- package/.github/workflows/release.yml +0 -34
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ The agent sees these too — they're appended to the tool result, so it can self
|
|
|
53
53
|
| `pylsp` | Python | `pip install python-lsp-server` |
|
|
54
54
|
| `clangd` | C/C++ | Xcode CLI tools / `apt install clangd` |
|
|
55
55
|
|
|
56
|
-
Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio. Or add it to
|
|
56
|
+
Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio. Or add it to global config (`~/.pi-lsp-lite.json`):
|
|
57
57
|
|
|
58
58
|
```json
|
|
59
59
|
{
|
|
@@ -70,7 +70,7 @@ Missing a server? `/lsp-add` lets you configure any LSP server that speaks stdio
|
|
|
70
70
|
|
|
71
71
|
## Configuration
|
|
72
72
|
|
|
73
|
-
Works without config.
|
|
73
|
+
Works without config. Use project config (`.pi-lsp-lite.json` or `.pi/lsp-lite.json`) for safe local tuning, and global config (`~/.pi-lsp-lite.json`) for trusted executable/server-shape changes like custom servers, `command`, `args`, `extensions`, and `rootPatterns`:
|
|
74
74
|
|
|
75
75
|
| Field | Description | Default |
|
|
76
76
|
|-------|-------------|---------|
|
|
@@ -80,7 +80,7 @@ Works without config. For customisation, create `.pi-lsp-lite.json` (project) or
|
|
|
80
80
|
| `diagnosticTimeout` | Global default timeout (ms) | `5000` |
|
|
81
81
|
| `documentIdleTimeout` | Close idle documents after (ms) | `120000` |
|
|
82
82
|
|
|
83
|
-
Project config merges over global.
|
|
83
|
+
Project config merges over global for safe tuning fields. Repositories can disable servers and tune timeouts/retries, but they cannot change the executable, argv, extensions, or root patterns for any existing server; put those trusted changes in global config.
|
|
84
84
|
|
|
85
85
|
## How it works
|
|
86
86
|
|
|
@@ -101,7 +101,7 @@ Servers are lazy (spawn on first edit), idle-shutdown after 240s, and clean up o
|
|
|
101
101
|
git clone https://github.com/mcphailtom/pi-lsp-lite
|
|
102
102
|
cd pi-lsp-lite && npm install
|
|
103
103
|
npm run check # typecheck
|
|
104
|
-
npm test # unit tests (
|
|
104
|
+
npm test # unit tests (no servers needed)
|
|
105
105
|
npm run test:integration # real server tests (needs servers on PATH)
|
|
106
106
|
```
|
|
107
107
|
|
package/index.ts
CHANGED
|
@@ -4,9 +4,11 @@ import { languageForFile, checkExtensionOverlaps, builtinLanguages, type Languag
|
|
|
4
4
|
import { formatDiagnostics } from "./src/format.js";
|
|
5
5
|
import { DiagnosticSeverity } from "vscode-languageserver-protocol";
|
|
6
6
|
import { loadConfig, writeGlobalConfig, readGlobalConfig } from "./src/config.js";
|
|
7
|
-
import { fileUri, which } from "./src/util.js";
|
|
8
|
-
import { installRegistry } from "./src/install-registry.js";
|
|
9
|
-
import {
|
|
7
|
+
import { fileUri, which, isInsideCwd } from "./src/util.js";
|
|
8
|
+
import { installRegistry, installCommandFor } from "./src/install-registry.js";
|
|
9
|
+
import { buildServerStates, formatServerStates } from "./src/status.js";
|
|
10
|
+
import { resolve } from "node:path";
|
|
11
|
+
import { realpath } from "node:fs/promises";
|
|
10
12
|
import { fileURLToPath } from "node:url";
|
|
11
13
|
|
|
12
14
|
export default function (pi: ExtensionAPI) {
|
|
@@ -28,6 +30,17 @@ export default function (pi: ExtensionAPI) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
|
|
33
|
+
async function currentServerStates() {
|
|
34
|
+
return buildServerStates({
|
|
35
|
+
builtins: builtinLanguages,
|
|
36
|
+
active: servers,
|
|
37
|
+
globalConfig: await readGlobalConfig(),
|
|
38
|
+
running: manager.status(),
|
|
39
|
+
installRegistry,
|
|
40
|
+
resolveCommand: which,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
31
44
|
pi.on("session_start", async (_event, ctx) => {
|
|
32
45
|
await initConfig(ctx.cwd);
|
|
33
46
|
});
|
|
@@ -38,16 +51,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
38
51
|
const rawPath = event.input?.path;
|
|
39
52
|
const filePath = typeof rawPath === "string" ? rawPath : undefined;
|
|
40
53
|
if (!filePath) return;
|
|
54
|
+
if (event.isError) return;
|
|
41
55
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
56
|
+
let absolutePath: string;
|
|
57
|
+
try {
|
|
58
|
+
absolutePath = await realpath(resolve(ctx.cwd, filePath));
|
|
59
|
+
} catch {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (!isInsideCwd(absolutePath, ctx.cwd)) return;
|
|
45
63
|
const langConfig = languageForFile(absolutePath, servers);
|
|
46
64
|
if (!langConfig) return;
|
|
47
65
|
|
|
48
66
|
try {
|
|
49
67
|
const result = await manager.handleEdit(absolutePath, langConfig, ctx.cwd);
|
|
50
|
-
const formatted = formatDiagnostics(filePath, result);
|
|
68
|
+
const formatted = formatDiagnostics(filePath, result, ctx.cwd);
|
|
51
69
|
if (!formatted) return;
|
|
52
70
|
|
|
53
71
|
ctx.ui.notify(formatted.trim(), "warning");
|
|
@@ -65,19 +83,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
83
|
});
|
|
66
84
|
|
|
67
85
|
pi.registerCommand("lsp-status", {
|
|
68
|
-
description: "Show
|
|
86
|
+
description: "Show configured LSP servers, install state, and running processes",
|
|
69
87
|
handler: async (_args, ctx) => {
|
|
70
|
-
|
|
71
|
-
if (running.length === 0) {
|
|
72
|
-
ctx.ui.notify("pi-lsp-lite: no servers running", "info");
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const lines = running.map((s) => {
|
|
76
|
-
const idle = Math.round((Date.now() - s.lastActivity) / 1000);
|
|
77
|
-
const up = Math.round(s.uptime / 1000);
|
|
78
|
-
return `${s.id} (pid ${s.pid}) root=${s.root} — ${s.openDocuments} open files, up ${up}s, idle ${idle}s`;
|
|
79
|
-
});
|
|
80
|
-
ctx.ui.notify(lines.join("\n"), "info");
|
|
88
|
+
ctx.ui.notify(formatServerStates(await currentServerStates()), "info");
|
|
81
89
|
},
|
|
82
90
|
});
|
|
83
91
|
|
|
@@ -163,18 +171,19 @@ export default function (pi: ExtensionAPI) {
|
|
|
163
171
|
const rootPatterns = rootRaw ? rootRaw.split(",").map((r) => r.trim()).filter(Boolean) : [];
|
|
164
172
|
|
|
165
173
|
const resolved = await which(command);
|
|
166
|
-
if (!resolved) {
|
|
167
|
-
ctx.ui.notify(`pi-lsp-lite: "${command}" not found on PATH — server added but won't start until installed`, "warning");
|
|
168
|
-
}
|
|
169
|
-
|
|
170
174
|
await writeGlobalConfig({ servers: { [id]: { command, args, extensions, rootPatterns } } });
|
|
171
175
|
await initConfig(ctx.cwd);
|
|
172
|
-
|
|
176
|
+
|
|
177
|
+
if (!resolved) {
|
|
178
|
+
ctx.ui.notify(`pi-lsp-lite: configured server "${id}", but "${command}" is missing from PATH — install it manually before use`, "warning");
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
ctx.ui.notify(`pi-lsp-lite: configured server "${id}" (${resolved})`, "info");
|
|
173
182
|
},
|
|
174
183
|
});
|
|
175
184
|
|
|
176
185
|
pi.registerCommand("lsp-remove", {
|
|
177
|
-
description: "
|
|
186
|
+
description: "Disable a language server",
|
|
178
187
|
handler: async (_args, ctx) => {
|
|
179
188
|
if (!ctx.hasUI) {
|
|
180
189
|
ctx.ui.notify("pi-lsp-lite: /lsp-remove requires interactive mode", "error");
|
|
@@ -187,10 +196,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
187
196
|
}
|
|
188
197
|
|
|
189
198
|
const ids = servers.map((s) => s.id);
|
|
190
|
-
const selected = await ctx.ui.select("
|
|
199
|
+
const selected = await ctx.ui.select("Disable which server?", ids);
|
|
191
200
|
if (!selected) return;
|
|
192
201
|
|
|
193
|
-
const confirmed = await ctx.ui.confirm("Confirm
|
|
202
|
+
const confirmed = await ctx.ui.confirm("Confirm disable", `Disable server "${selected}"?`);
|
|
194
203
|
if (!confirmed) return;
|
|
195
204
|
|
|
196
205
|
await writeGlobalConfig({ servers: { [selected]: { disabled: true } } });
|
|
@@ -240,7 +249,23 @@ export default function (pi: ExtensionAPI) {
|
|
|
240
249
|
}
|
|
241
250
|
|
|
242
251
|
await initConfig(ctx.cwd);
|
|
243
|
-
|
|
252
|
+
|
|
253
|
+
if (isCurrentlyEnabled) {
|
|
254
|
+
ctx.ui.notify(`pi-lsp-lite: disabled server "${id}"`, "info");
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const state = (await currentServerStates()).find((s) => s.id === id);
|
|
259
|
+
if (state?.installed === false) {
|
|
260
|
+
const installHint = state.installable ? "run /lsp-install" : "install it manually";
|
|
261
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}", but "${state.command}" is missing from PATH — ${installHint} before use`, "warning");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (state?.installed === null) {
|
|
265
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}", but its command is incomplete — check global config`, "warning");
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
ctx.ui.notify(`pi-lsp-lite: enabled server "${id}"`, "info");
|
|
244
269
|
},
|
|
245
270
|
});
|
|
246
271
|
|
|
@@ -257,13 +282,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
257
282
|
const lang = builtinLanguages.find((l) => l.id === id);
|
|
258
283
|
const binary = lang?.command ?? id;
|
|
259
284
|
const found = await which(binary);
|
|
260
|
-
return found ? null : { id, command: binary, installCmd: entry
|
|
285
|
+
return found ? null : { id, command: binary, installCmd: installCommandFor(entry), description: entry.description };
|
|
261
286
|
}),
|
|
262
287
|
);
|
|
263
288
|
const missing = checks.filter((c): c is NonNullable<typeof c> => c !== null);
|
|
264
289
|
|
|
265
290
|
if (missing.length === 0) {
|
|
266
|
-
ctx.ui.notify("pi-lsp-lite: all
|
|
291
|
+
ctx.ui.notify("pi-lsp-lite: all built-in installable servers are available; custom servers must be installed manually", "info");
|
|
267
292
|
return;
|
|
268
293
|
}
|
|
269
294
|
|
|
@@ -277,7 +302,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
277
302
|
const confirmed = await ctx.ui.confirm("Confirm install", `Run: ${selected.installCmd}`);
|
|
278
303
|
if (!confirmed) return;
|
|
279
304
|
|
|
280
|
-
const result =
|
|
305
|
+
const result = process.platform === "win32"
|
|
306
|
+
? await pi.exec(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", selected.installCmd])
|
|
307
|
+
: await pi.exec("sh", ["-c", selected.installCmd]);
|
|
281
308
|
if (result.code !== 0) {
|
|
282
309
|
ctx.ui.notify(`pi-lsp-lite: install failed (exit ${result.code})\n${result.stderr}`, "error");
|
|
283
310
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-lsp-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "LSP diagnostics for pi — errors and warnings on every edit, same turn. Go, Rust, TypeScript, Python, C/C++.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -29,7 +29,9 @@
|
|
|
29
29
|
]
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"
|
|
32
|
+
"cross-spawn": "^7.0.6",
|
|
33
|
+
"vscode-languageserver-protocol": "~3.18.1",
|
|
34
|
+
"which": "^2.0.2"
|
|
33
35
|
},
|
|
34
36
|
"peerDependencies": {
|
|
35
37
|
"@earendil-works/pi-coding-agent": "*",
|
|
@@ -37,13 +39,19 @@
|
|
|
37
39
|
},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@earendil-works/pi-coding-agent": "^0.74.0",
|
|
40
|
-
"@types/
|
|
42
|
+
"@types/cross-spawn": "^6.0.6",
|
|
43
|
+
"@types/node": "^22.0.0",
|
|
44
|
+
"@types/which": "^2.0.2",
|
|
45
|
+
"cross-env": "^7.0.3",
|
|
41
46
|
"tsx": "^4.21.0",
|
|
42
47
|
"typescript": "^5.4.0"
|
|
43
48
|
},
|
|
44
49
|
"scripts": {
|
|
45
50
|
"check": "tsc --noEmit",
|
|
46
51
|
"test": "tsx --test test/*.test.ts",
|
|
47
|
-
"test:integration": "INTEGRATION=1 tsx --test test/*.test.ts test/integration/*.test.ts"
|
|
52
|
+
"test:integration": "cross-env INTEGRATION=1 tsx --test test/*.test.ts test/integration/*.test.ts"
|
|
53
|
+
},
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22.19.0"
|
|
48
56
|
}
|
|
49
57
|
}
|
package/src/client.ts
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
DiagnosticSeverity,
|
|
14
14
|
type InitializeParams,
|
|
15
15
|
type Diagnostic,
|
|
16
|
-
} from "vscode-languageserver-protocol/node
|
|
16
|
+
} from "vscode-languageserver-protocol/node";
|
|
17
17
|
import type { ChildProcess } from "node:child_process";
|
|
18
18
|
import { fileUri } from "./util.js";
|
|
19
19
|
|
|
@@ -21,6 +21,7 @@ export interface OtherFileDiagnostics {
|
|
|
21
21
|
uri: string;
|
|
22
22
|
errorCount: number;
|
|
23
23
|
warningCount: number;
|
|
24
|
+
firstDiagnostic?: { severity: number; line: number; col: number; message: string; source?: string };
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
export interface DiagnosticResult {
|
|
@@ -53,6 +54,32 @@ function countDiagnostics(diags: Diagnostic[]): { errors: number; warnings: numb
|
|
|
53
54
|
return { errors, warnings };
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
function diagnosticMessage(d: Diagnostic): string {
|
|
58
|
+
return typeof d.message === "string" ? d.message : d.message.value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function diagnosticFingerprint(d: Diagnostic): string {
|
|
62
|
+
return `${d.severity}:${d.range.start.line}:${d.range.start.character}:${diagnosticMessage(d)}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function fingerprintSet(diags: Diagnostic[]): Set<string> {
|
|
66
|
+
const set = new Set<string>();
|
|
67
|
+
for (const d of diags) {
|
|
68
|
+
if (d.severity === DiagnosticSeverity.Error || d.severity === DiagnosticSeverity.Warning) {
|
|
69
|
+
set.add(diagnosticFingerprint(d));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return set;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function setsEqual(a: Set<string>, b: Set<string>): boolean {
|
|
76
|
+
if (a.size !== b.size) return false;
|
|
77
|
+
for (const v of a) {
|
|
78
|
+
if (!b.has(v)) return false;
|
|
79
|
+
}
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
export function createLspClient(child: ChildProcess): LspClient {
|
|
57
84
|
if (!child.stdout || !child.stdin) {
|
|
58
85
|
throw new Error("LSP child process must be spawned with stdio: pipe");
|
|
@@ -152,10 +179,10 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
152
179
|
async waitForDiagnostics(uri: string, timeoutMs: number): Promise<DiagnosticResult> {
|
|
153
180
|
const targetGen = uriGeneration.get(uri) ?? 0;
|
|
154
181
|
|
|
155
|
-
const preSnapshot = new Map<string,
|
|
182
|
+
const preSnapshot = new Map<string, Set<string>>();
|
|
156
183
|
for (const [trackedUri, entry] of diagnosticsMap) {
|
|
157
184
|
if (trackedUri !== uri) {
|
|
158
|
-
preSnapshot.set(trackedUri,
|
|
185
|
+
preSnapshot.set(trackedUri, fingerprintSet(entry.diagnostics));
|
|
159
186
|
}
|
|
160
187
|
}
|
|
161
188
|
|
|
@@ -163,13 +190,27 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
163
190
|
const result: OtherFileDiagnostics[] = [];
|
|
164
191
|
for (const [trackedUri, entry] of diagnosticsMap) {
|
|
165
192
|
if (trackedUri === uri) continue;
|
|
193
|
+
const postFp = fingerprintSet(entry.diagnostics);
|
|
194
|
+
const preFp = preSnapshot.get(trackedUri) ?? new Set();
|
|
195
|
+
if (setsEqual(postFp, preFp)) continue;
|
|
166
196
|
const post = countDiagnostics(entry.diagnostics);
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
197
|
+
const first =
|
|
198
|
+
entry.diagnostics.find((d) => d.severity === DiagnosticSeverity.Error) ??
|
|
199
|
+
entry.diagnostics.find((d) => d.severity === DiagnosticSeverity.Warning);
|
|
200
|
+
result.push({
|
|
201
|
+
uri: trackedUri,
|
|
202
|
+
errorCount: post.errors,
|
|
203
|
+
warningCount: post.warnings,
|
|
204
|
+
...(first && {
|
|
205
|
+
firstDiagnostic: {
|
|
206
|
+
severity: first.severity ?? DiagnosticSeverity.Error,
|
|
207
|
+
line: first.range.start.line,
|
|
208
|
+
col: first.range.start.character,
|
|
209
|
+
message: diagnosticMessage(first),
|
|
210
|
+
...(first.source && { source: first.source }),
|
|
211
|
+
},
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
173
214
|
}
|
|
174
215
|
return result;
|
|
175
216
|
};
|
|
@@ -214,9 +255,9 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
214
255
|
// edited file is valid but dependents break
|
|
215
256
|
crossFileCallback = (changedUri: string) => {
|
|
216
257
|
if (settled || changedUri === uri) return;
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
if (
|
|
258
|
+
const preFp = preSnapshot.get(changedUri) ?? new Set<string>();
|
|
259
|
+
const postFp = fingerprintSet(diagnosticsMap.get(changedUri)?.diagnostics ?? []);
|
|
260
|
+
if (!setsEqual(preFp, postFp)) {
|
|
220
261
|
clearTimeout(timeout);
|
|
221
262
|
resetQuiescence();
|
|
222
263
|
}
|
|
@@ -248,7 +289,7 @@ export function createLspClient(child: ChildProcess): LspClient {
|
|
|
248
289
|
timer = setTimeout(() => reject(new Error("shutdown timed out")), SHUTDOWN_TIMEOUT_MS);
|
|
249
290
|
}),
|
|
250
291
|
]);
|
|
251
|
-
connection.sendNotification(ExitNotification.type);
|
|
292
|
+
await connection.sendNotification(ExitNotification.type);
|
|
252
293
|
} catch {
|
|
253
294
|
// timed out or server already exited
|
|
254
295
|
} finally {
|
package/src/config.ts
CHANGED
|
@@ -66,7 +66,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
|
|
|
66
66
|
console.error(`[pi-lsp-lite] config "${id}": extensions must be a non-empty string array, skipping`);
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
|
-
override.extensions =
|
|
69
|
+
override.extensions = raw.extensions.map((e) => e.toLowerCase());
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (raw.command !== undefined) {
|
|
@@ -74,7 +74,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
|
|
|
74
74
|
console.error(`[pi-lsp-lite] config "${id}": command must be a non-empty string, skipping`);
|
|
75
75
|
return null;
|
|
76
76
|
}
|
|
77
|
-
override.command = raw.command
|
|
77
|
+
override.command = raw.command;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
if (raw.args !== undefined) {
|
|
@@ -82,7 +82,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
|
|
|
82
82
|
console.error(`[pi-lsp-lite] config "${id}": args must be a string array, skipping`);
|
|
83
83
|
return null;
|
|
84
84
|
}
|
|
85
|
-
override.args = raw.args
|
|
85
|
+
override.args = raw.args;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
88
|
if (raw.rootPatterns !== undefined) {
|
|
@@ -90,7 +90,7 @@ function validateOverride(id: string, raw: unknown): ServerConfigOverride | null
|
|
|
90
90
|
console.error(`[pi-lsp-lite] config "${id}": rootPatterns must be a string array, skipping`);
|
|
91
91
|
return null;
|
|
92
92
|
}
|
|
93
|
-
override.rootPatterns = raw.rootPatterns
|
|
93
|
+
override.rootPatterns = raw.rootPatterns;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
if (raw.diagnosticTimeout !== undefined) {
|
|
@@ -150,12 +150,18 @@ async function findProjectConfig(cwd: string): Promise<UserConfig | null> {
|
|
|
150
150
|
|
|
151
151
|
type ConfigSource = "global" | "project";
|
|
152
152
|
|
|
153
|
+
interface MergeResult {
|
|
154
|
+
servers: LanguageServerConfig[];
|
|
155
|
+
perServerTimeouts: Map<string, number>;
|
|
156
|
+
}
|
|
157
|
+
|
|
153
158
|
function mergeConfigs(
|
|
154
159
|
base: LanguageServerConfig[],
|
|
155
160
|
overrides: Record<string, ServerConfigOverride>,
|
|
156
161
|
source: ConfigSource,
|
|
157
|
-
):
|
|
162
|
+
): MergeResult {
|
|
158
163
|
const result = new Map<string, LanguageServerConfig>();
|
|
164
|
+
const perServerTimeouts = new Map<string, number>();
|
|
159
165
|
|
|
160
166
|
for (const server of base) {
|
|
161
167
|
result.set(server.id, { ...server });
|
|
@@ -170,9 +176,21 @@ function mergeConfigs(
|
|
|
170
176
|
continue;
|
|
171
177
|
}
|
|
172
178
|
|
|
179
|
+
if (override.diagnosticTimeout !== undefined) {
|
|
180
|
+
perServerTimeouts.set(id, override.diagnosticTimeout);
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
const existing = result.get(id);
|
|
174
184
|
if (existing) {
|
|
175
185
|
const { disabled: _, diagnosticTimeout: __, ...lspFields } = override;
|
|
186
|
+
if (source === "project") {
|
|
187
|
+
for (const field of ["command", "args", "extensions", "rootPatterns"] as const) {
|
|
188
|
+
if (lspFields[field] !== undefined) {
|
|
189
|
+
console.error(`[pi-lsp-lite] project config cannot override "${field}" for server "${id}" — ignoring (use global config instead)`);
|
|
190
|
+
delete lspFields[field];
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
176
194
|
const defined = Object.fromEntries(
|
|
177
195
|
Object.entries(lspFields).filter(([, v]) => v !== undefined),
|
|
178
196
|
);
|
|
@@ -197,7 +215,7 @@ function mergeConfigs(
|
|
|
197
215
|
}
|
|
198
216
|
}
|
|
199
217
|
|
|
200
|
-
return Array.from(result.values());
|
|
218
|
+
return { servers: Array.from(result.values()), perServerTimeouts };
|
|
201
219
|
}
|
|
202
220
|
|
|
203
221
|
export function globalConfigFilePath(globalConfigPath?: string): string {
|
|
@@ -275,12 +293,10 @@ export async function loadConfig(cwd: string, globalConfigPath?: string): Promis
|
|
|
275
293
|
for (const [layer, source] of layers) {
|
|
276
294
|
if (!layer) continue;
|
|
277
295
|
if (layer.servers && isPlainObject(layer.servers)) {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
perServerTimeout.set(id, override.diagnosticTimeout);
|
|
283
|
-
}
|
|
296
|
+
const merged = mergeConfigs(servers, layer.servers as Record<string, ServerConfigOverride>, source);
|
|
297
|
+
servers = merged.servers;
|
|
298
|
+
for (const [id, timeout] of merged.perServerTimeouts) {
|
|
299
|
+
perServerTimeout.set(id, timeout);
|
|
284
300
|
}
|
|
285
301
|
}
|
|
286
302
|
if (layer.diagnosticTimeout !== undefined) {
|
package/src/format.ts
CHANGED
|
@@ -1,20 +1,30 @@
|
|
|
1
1
|
import { DiagnosticSeverity } from "vscode-languageserver-protocol";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { relative } from "node:path";
|
|
2
4
|
import type { DiagnosticResult } from "./client.js";
|
|
3
5
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
const MAX_DIAGNOSTICS_PER_FILE = 50;
|
|
7
|
+
|
|
8
|
+
export function formatDiagnostics(filePath: string, result: DiagnosticResult, cwd?: string): string {
|
|
9
|
+
const allRelevant = result.diagnostics.filter(
|
|
6
10
|
(d) => d.severity === DiagnosticSeverity.Error || d.severity === DiagnosticSeverity.Warning,
|
|
7
11
|
);
|
|
8
12
|
|
|
9
|
-
if (
|
|
10
|
-
|
|
13
|
+
if (allRelevant.length === 0 && result.status === "ok" && result.otherFiles.length === 0) return "";
|
|
14
|
+
|
|
15
|
+
if (result.status === "unavailable") {
|
|
16
|
+
return `\n⚠ LSP diagnostics unavailable for ${filePath} (server missing or failed to start)`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const truncated = allRelevant.length > MAX_DIAGNOSTICS_PER_FILE;
|
|
20
|
+
const relevant = truncated ? allRelevant.slice(0, MAX_DIAGNOSTICS_PER_FILE) : allRelevant;
|
|
11
21
|
|
|
12
22
|
const retryNote = result.status === "timeout" && result.retryAttempts > 0
|
|
13
23
|
? ` after ${result.retryAttempts} ${result.retryAttempts === 1 ? "retry" : "retries"}`
|
|
14
24
|
: "";
|
|
15
25
|
|
|
16
26
|
if (relevant.length === 0 && result.status === "ok" && result.otherFiles.length > 0) {
|
|
17
|
-
return `\n⚠ LSP diagnostics for ${filePath}: no issues${otherFilesFooter(result)}`;
|
|
27
|
+
return `\n⚠ LSP diagnostics for ${filePath}: no issues${otherFilesFooter(result, cwd)}`;
|
|
18
28
|
}
|
|
19
29
|
|
|
20
30
|
const lines = relevant.map((d) => {
|
|
@@ -25,8 +35,11 @@ export function formatDiagnostics(filePath: string, result: DiagnosticResult): s
|
|
|
25
35
|
return ` ${severity} ${line}:${col} ${source}${d.message}`;
|
|
26
36
|
});
|
|
27
37
|
|
|
28
|
-
|
|
29
|
-
const
|
|
38
|
+
let errorCount = 0;
|
|
39
|
+
for (const d of allRelevant) {
|
|
40
|
+
if (d.severity === DiagnosticSeverity.Error) errorCount++;
|
|
41
|
+
}
|
|
42
|
+
const warnCount = allRelevant.length - errorCount;
|
|
30
43
|
|
|
31
44
|
const summary = [
|
|
32
45
|
errorCount > 0 ? `${errorCount} error${errorCount > 1 ? "s" : ""}` : "",
|
|
@@ -36,12 +49,30 @@ export function formatDiagnostics(filePath: string, result: DiagnosticResult): s
|
|
|
36
49
|
.filter(Boolean)
|
|
37
50
|
.join(", ");
|
|
38
51
|
|
|
39
|
-
|
|
52
|
+
const truncatedNote = truncated ? `\n ... and ${allRelevant.length - MAX_DIAGNOSTICS_PER_FILE} more` : "";
|
|
53
|
+
|
|
54
|
+
return `\n⚠ LSP diagnostics for ${filePath} (${summary}):\n${lines.join("\n")}${truncatedNote}${otherFilesFooter(result, cwd)}`;
|
|
40
55
|
}
|
|
41
56
|
|
|
42
|
-
function otherFilesFooter(result: DiagnosticResult): string {
|
|
57
|
+
function otherFilesFooter(result: DiagnosticResult, cwd?: string): string {
|
|
43
58
|
if (result.otherFiles.length === 0) return "";
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
const lines = result.otherFiles.map((f) => {
|
|
60
|
+
let path: string;
|
|
61
|
+
try {
|
|
62
|
+
const abs = fileURLToPath(f.uri);
|
|
63
|
+
path = cwd ? relative(cwd, abs) : abs;
|
|
64
|
+
} catch {
|
|
65
|
+
path = f.uri;
|
|
66
|
+
}
|
|
67
|
+
const counts = [
|
|
68
|
+
f.errorCount > 0 ? `${f.errorCount} error${f.errorCount > 1 ? "s" : ""}` : "",
|
|
69
|
+
f.warningCount > 0 ? `${f.warningCount} warning${f.warningCount > 1 ? "s" : ""}` : "",
|
|
70
|
+
].filter(Boolean).join(", ");
|
|
71
|
+
if (!f.firstDiagnostic) return ` ${path} (${counts})`;
|
|
72
|
+
const d = f.firstDiagnostic;
|
|
73
|
+
const sev = d.severity === DiagnosticSeverity.Error ? "error" : "warning";
|
|
74
|
+
const src = d.source ? `[${d.source}] ` : "";
|
|
75
|
+
return ` ${path} (${counts}): ${sev} ${d.line + 1}:${d.col + 1} ${src}${d.message}`;
|
|
76
|
+
});
|
|
77
|
+
return `\n${lines.join("\n")}`;
|
|
47
78
|
}
|
package/src/install-registry.ts
CHANGED
|
@@ -1,27 +1,37 @@
|
|
|
1
1
|
export interface InstallEntry {
|
|
2
|
-
command
|
|
2
|
+
// Per-platform install command; `win32` overrides `default` on Windows.
|
|
3
|
+
command: { default: string; win32?: string };
|
|
3
4
|
description: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
7
|
export const installRegistry = new Map<string, InstallEntry>([
|
|
7
8
|
["go", {
|
|
8
|
-
command: "go install golang.org/x/tools/gopls@latest",
|
|
9
|
+
command: { default: "go install golang.org/x/tools/gopls@latest" },
|
|
9
10
|
description: "Go language server",
|
|
10
11
|
}],
|
|
11
12
|
["rust", {
|
|
12
|
-
command: "rustup component add rust-analyzer",
|
|
13
|
+
command: { default: "rustup component add rust-analyzer" },
|
|
13
14
|
description: "Rust language server",
|
|
14
15
|
}],
|
|
15
16
|
["typescript", {
|
|
16
|
-
command: "npm install -g typescript-language-server typescript",
|
|
17
|
+
command: { default: "npm install -g typescript-language-server typescript" },
|
|
17
18
|
description: "TypeScript/JavaScript language server",
|
|
18
19
|
}],
|
|
19
20
|
["python", {
|
|
20
|
-
command: "pip install python-lsp-server",
|
|
21
|
+
command: { default: "pip install python-lsp-server" },
|
|
21
22
|
description: "Python language server",
|
|
22
23
|
}],
|
|
23
24
|
["cpp", {
|
|
24
|
-
command:
|
|
25
|
+
command: {
|
|
26
|
+
default: "sudo apt-get install -y clangd || brew install llvm",
|
|
27
|
+
win32: "winget install -e --id LLVM.clangd",
|
|
28
|
+
},
|
|
25
29
|
description: "C/C++ language server",
|
|
26
30
|
}],
|
|
27
31
|
]);
|
|
32
|
+
|
|
33
|
+
// Resolve the install command for the current platform.
|
|
34
|
+
export function installCommandFor(entry: InstallEntry): string {
|
|
35
|
+
if (process.platform === "win32" && entry.command.win32) return entry.command.win32;
|
|
36
|
+
return entry.command.default;
|
|
37
|
+
}
|
package/src/languages.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface LanguageServerConfig {
|
|
|
6
6
|
rootPatterns: string[];
|
|
7
7
|
diagnosticTimeout?: number;
|
|
8
8
|
maxRetries?: number;
|
|
9
|
+
languageIds?: Record<string, string>;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export const builtinLanguages: LanguageServerConfig[] = [
|
|
@@ -32,6 +33,7 @@ export const builtinLanguages: LanguageServerConfig[] = [
|
|
|
32
33
|
args: ["--stdio"],
|
|
33
34
|
rootPatterns: ["tsconfig.json", "package.json"],
|
|
34
35
|
diagnosticTimeout: 30_000,
|
|
36
|
+
languageIds: { ".tsx": "typescriptreact", ".js": "javascript", ".jsx": "javascriptreact" },
|
|
35
37
|
},
|
|
36
38
|
{
|
|
37
39
|
id: "python",
|
|
@@ -48,6 +50,7 @@ export const builtinLanguages: LanguageServerConfig[] = [
|
|
|
48
50
|
args: [],
|
|
49
51
|
rootPatterns: ["compile_commands.json", "CMakeLists.txt", ".clangd"],
|
|
50
52
|
diagnosticTimeout: 15_000,
|
|
53
|
+
languageIds: { ".c": "c", ".h": "c", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp", ".hxx": "cpp" },
|
|
51
54
|
},
|
|
52
55
|
];
|
|
53
56
|
|
|
@@ -56,6 +59,14 @@ export function languageForFile(path: string, configs: LanguageServerConfig[]):
|
|
|
56
59
|
return configs.find((lang) => lang.extensions.some((ext) => lower.endsWith(ext)));
|
|
57
60
|
}
|
|
58
61
|
|
|
62
|
+
export function languageIdForFile(filePath: string, config: LanguageServerConfig): string {
|
|
63
|
+
if (config.languageIds) {
|
|
64
|
+
const ext = filePath.toLowerCase().match(/\.[^.]+$/)?.[0];
|
|
65
|
+
if (ext && config.languageIds[ext]) return config.languageIds[ext];
|
|
66
|
+
}
|
|
67
|
+
return config.id;
|
|
68
|
+
}
|
|
69
|
+
|
|
59
70
|
export function checkExtensionOverlaps(configs: LanguageServerConfig[]): string[] {
|
|
60
71
|
const warnings: string[] = [];
|
|
61
72
|
const seen = new Map<string, string>();
|
package/src/server-manager.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
import spawn from "cross-spawn";
|
|
2
3
|
import { which, fileUri, findWorkspaceRoot } from "./util.js";
|
|
3
4
|
import { createLspClient, type LspClient, type DiagnosticResult } from "./client.js";
|
|
4
|
-
import type
|
|
5
|
+
import { type LanguageServerConfig, languageIdForFile } from "./languages.js";
|
|
5
6
|
import type { Diagnostic } from "vscode-languageserver-protocol";
|
|
6
7
|
import { DEFAULT_DIAGNOSTIC_TIMEOUT, DEFAULT_DOCUMENT_IDLE_TIMEOUT, DEFAULT_MAX_RETRIES } from "./config.js";
|
|
7
8
|
import { readFile } from "node:fs/promises";
|
|
@@ -221,7 +222,7 @@ export function createServerManager(options: ServerManagerOptions = {}): ServerM
|
|
|
221
222
|
if (server.openDocuments.has(uri)) {
|
|
222
223
|
server.client.didChange(uri, content);
|
|
223
224
|
} else {
|
|
224
|
-
server.client.didOpen(uri, server.config
|
|
225
|
+
server.client.didOpen(uri, languageIdForFile(filePath, server.config), content);
|
|
225
226
|
}
|
|
226
227
|
server.openDocuments.set(uri, Date.now());
|
|
227
228
|
|
package/src/status.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { UserConfig } from "./config.js";
|
|
2
|
+
import type { InstallEntry } from "./install-registry.js";
|
|
3
|
+
import type { LanguageServerConfig } from "./languages.js";
|
|
4
|
+
import type { ServerStatus } from "./server-manager.js";
|
|
5
|
+
|
|
6
|
+
export interface ServerState {
|
|
7
|
+
id: string;
|
|
8
|
+
command: string | null;
|
|
9
|
+
enabled: boolean;
|
|
10
|
+
installed: boolean | null;
|
|
11
|
+
installable: boolean;
|
|
12
|
+
running: ServerStatus[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BuildServerStatesOptions {
|
|
16
|
+
builtins: LanguageServerConfig[];
|
|
17
|
+
active: LanguageServerConfig[];
|
|
18
|
+
globalConfig: UserConfig | null;
|
|
19
|
+
running: ServerStatus[];
|
|
20
|
+
installRegistry: Map<string, InstallEntry>;
|
|
21
|
+
resolveCommand(command: string): Promise<string | null>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const RESERVED_SERVER_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
25
|
+
|
|
26
|
+
function globalServerIds(globalConfig: UserConfig | null): string[] {
|
|
27
|
+
const servers = globalConfig?.servers;
|
|
28
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers)) return [];
|
|
29
|
+
return Object.keys(servers).filter((id) => !RESERVED_SERVER_KEYS.has(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function commandFor(id: string, active: Map<string, LanguageServerConfig>, builtins: Map<string, LanguageServerConfig>, globalConfig: UserConfig | null): string | null {
|
|
33
|
+
const activeConfig = active.get(id);
|
|
34
|
+
if (activeConfig) return activeConfig.command;
|
|
35
|
+
|
|
36
|
+
const globalCommand = globalConfig?.servers?.[id]?.command;
|
|
37
|
+
if (typeof globalCommand === "string" && globalCommand.length > 0) return globalCommand;
|
|
38
|
+
|
|
39
|
+
const builtin = builtins.get(id);
|
|
40
|
+
return builtin?.command ?? null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function buildServerStates(options: BuildServerStatesOptions): Promise<ServerState[]> {
|
|
44
|
+
const active = new Map(options.active.map((server) => [server.id, server]));
|
|
45
|
+
const builtins = new Map(options.builtins.map((server) => [server.id, server]));
|
|
46
|
+
const runningById = new Map<string, ServerStatus[]>();
|
|
47
|
+
|
|
48
|
+
for (const status of options.running) {
|
|
49
|
+
const entries = runningById.get(status.id) ?? [];
|
|
50
|
+
entries.push(status);
|
|
51
|
+
runningById.set(status.id, entries);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ids = new Set<string>([
|
|
55
|
+
...builtins.keys(),
|
|
56
|
+
...active.keys(),
|
|
57
|
+
...globalServerIds(options.globalConfig),
|
|
58
|
+
]);
|
|
59
|
+
|
|
60
|
+
const states = await Promise.all([...ids].sort().map(async (id): Promise<ServerState> => {
|
|
61
|
+
const command = commandFor(id, active, builtins, options.globalConfig);
|
|
62
|
+
const installed = command ? (await options.resolveCommand(command)) !== null : null;
|
|
63
|
+
return {
|
|
64
|
+
id,
|
|
65
|
+
command,
|
|
66
|
+
enabled: active.has(id),
|
|
67
|
+
installed,
|
|
68
|
+
installable: options.installRegistry.has(id),
|
|
69
|
+
running: runningById.get(id) ?? [],
|
|
70
|
+
};
|
|
71
|
+
}));
|
|
72
|
+
|
|
73
|
+
return states;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function formatServerStates(states: ServerState[]): string {
|
|
77
|
+
if (states.length === 0) return "pi-lsp-lite: no servers configured";
|
|
78
|
+
|
|
79
|
+
return states.map((state) => {
|
|
80
|
+
const enabled = state.enabled ? "enabled" : "disabled";
|
|
81
|
+
const installed = state.installed === null ? "unknown" : state.installed ? "installed" : "missing";
|
|
82
|
+
const installHint = state.installed === false
|
|
83
|
+
? state.installable ? "installable via /lsp-install" : "manual install required"
|
|
84
|
+
: "";
|
|
85
|
+
const running = state.running.length > 0
|
|
86
|
+
? state.running.map((s) => {
|
|
87
|
+
const idle = Math.round((Date.now() - s.lastActivity) / 1000);
|
|
88
|
+
const up = Math.round(s.uptime / 1000);
|
|
89
|
+
return `running pid=${s.pid} root=${s.root} open=${s.openDocuments} up=${up}s idle=${idle}s`;
|
|
90
|
+
}).join("; ")
|
|
91
|
+
: "not running";
|
|
92
|
+
const command = state.command ? `cmd=${state.command}` : "cmd=unknown";
|
|
93
|
+
return [state.id, enabled, installed, running, command, installHint].filter(Boolean).join(" — ");
|
|
94
|
+
}).join("\n");
|
|
95
|
+
}
|
package/src/util.ts
CHANGED
|
@@ -1,29 +1,28 @@
|
|
|
1
|
-
import { access
|
|
2
|
-
import { join, dirname } from "node:path";
|
|
1
|
+
import { access } from "node:fs/promises";
|
|
2
|
+
import { join, dirname, relative, isAbsolute } from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
+
import which_ from "which";
|
|
4
5
|
|
|
5
6
|
export function fileUri(absolutePath: string): string {
|
|
6
7
|
return pathToFileURL(absolutePath).href;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
// Find a binary on PATH. Delegates to the `which` package — the same resolver
|
|
11
|
+
// cross-spawn uses to locate the command it launches — so preflight resolution
|
|
12
|
+
// and the eventual spawn agree on every platform.
|
|
13
|
+
function isNotFoundError(err: unknown): boolean {
|
|
14
|
+
return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export async function which(command: string): Promise<string | null> {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return null;
|
|
18
|
+
try {
|
|
19
|
+
return await which_(command);
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (!isNotFoundError(err)) {
|
|
22
|
+
console.error(`[pi-lsp-lite] failed to resolve command "${command}":`, err);
|
|
16
23
|
}
|
|
24
|
+
return null;
|
|
17
25
|
}
|
|
18
|
-
const pathDirs = (process.env.PATH ?? "").split(":");
|
|
19
|
-
for (const dir of pathDirs) {
|
|
20
|
-
const candidate = join(dir, command);
|
|
21
|
-
try {
|
|
22
|
-
await access(candidate, constants.X_OK);
|
|
23
|
-
return candidate;
|
|
24
|
-
} catch {}
|
|
25
|
-
}
|
|
26
|
-
return null;
|
|
27
26
|
}
|
|
28
27
|
|
|
29
28
|
export async function findWorkspaceRoot(filePath: string, rootPatterns: string[], cwd: string): Promise<string> {
|
|
@@ -42,3 +41,8 @@ export async function findWorkspaceRoot(filePath: string, rootPatterns: string[]
|
|
|
42
41
|
}
|
|
43
42
|
return cwd;
|
|
44
43
|
}
|
|
44
|
+
|
|
45
|
+
export function isInsideCwd(absolutePath: string, cwd: string): boolean {
|
|
46
|
+
const rel = relative(cwd, absolutePath);
|
|
47
|
+
return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
48
|
+
}
|
package/.github/workflows/ci.yml
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
name: CI
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [main]
|
|
8
|
-
|
|
9
|
-
permissions:
|
|
10
|
-
contents: read
|
|
11
|
-
|
|
12
|
-
jobs:
|
|
13
|
-
check:
|
|
14
|
-
name: typecheck + unit tests
|
|
15
|
-
runs-on: ubuntu-latest
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v6
|
|
18
|
-
|
|
19
|
-
- uses: actions/setup-node@v6
|
|
20
|
-
with:
|
|
21
|
-
node-version: 20
|
|
22
|
-
cache: npm
|
|
23
|
-
|
|
24
|
-
- run: npm ci
|
|
25
|
-
|
|
26
|
-
- name: typecheck
|
|
27
|
-
run: npm run check
|
|
28
|
-
|
|
29
|
-
- name: unit tests
|
|
30
|
-
run: npm test
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
name: Integration Tests
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
pull_request:
|
|
5
|
-
branches: [main]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
|
|
10
|
-
jobs:
|
|
11
|
-
gopls:
|
|
12
|
-
name: gopls
|
|
13
|
-
runs-on: ubuntu-latest
|
|
14
|
-
steps:
|
|
15
|
-
- uses: actions/checkout@v6
|
|
16
|
-
- uses: actions/setup-node@v6
|
|
17
|
-
with:
|
|
18
|
-
node-version: 20
|
|
19
|
-
cache: npm
|
|
20
|
-
- uses: actions/setup-go@v6
|
|
21
|
-
with:
|
|
22
|
-
go-version: stable
|
|
23
|
-
- run: go install golang.org/x/tools/gopls@latest
|
|
24
|
-
- run: npm ci
|
|
25
|
-
- run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/gopls.test.ts
|
|
26
|
-
|
|
27
|
-
rust-analyzer:
|
|
28
|
-
name: rust-analyzer
|
|
29
|
-
runs-on: ubuntu-latest
|
|
30
|
-
steps:
|
|
31
|
-
- uses: actions/checkout@v6
|
|
32
|
-
- uses: actions/setup-node@v6
|
|
33
|
-
with:
|
|
34
|
-
node-version: 20
|
|
35
|
-
cache: npm
|
|
36
|
-
- run: |
|
|
37
|
-
rustup update stable
|
|
38
|
-
rustup component add rust-analyzer
|
|
39
|
-
- run: npm ci
|
|
40
|
-
- run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/rust-analyzer.test.ts
|
|
41
|
-
|
|
42
|
-
typescript:
|
|
43
|
-
name: typescript-language-server
|
|
44
|
-
runs-on: ubuntu-latest
|
|
45
|
-
steps:
|
|
46
|
-
- uses: actions/checkout@v6
|
|
47
|
-
- uses: actions/setup-node@v6
|
|
48
|
-
with:
|
|
49
|
-
node-version: 20
|
|
50
|
-
cache: npm
|
|
51
|
-
- run: npm install -g typescript-language-server typescript
|
|
52
|
-
- run: npm ci
|
|
53
|
-
- run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/typescript.test.ts
|
|
54
|
-
|
|
55
|
-
pylsp:
|
|
56
|
-
name: pylsp
|
|
57
|
-
runs-on: ubuntu-latest
|
|
58
|
-
steps:
|
|
59
|
-
- uses: actions/checkout@v6
|
|
60
|
-
- uses: actions/setup-node@v6
|
|
61
|
-
with:
|
|
62
|
-
node-version: 20
|
|
63
|
-
cache: npm
|
|
64
|
-
- run: pip install 'python-lsp-server[all]'
|
|
65
|
-
- run: npm ci
|
|
66
|
-
- run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/pylsp.test.ts
|
|
67
|
-
|
|
68
|
-
clangd:
|
|
69
|
-
name: clangd
|
|
70
|
-
runs-on: ubuntu-latest
|
|
71
|
-
steps:
|
|
72
|
-
- uses: actions/checkout@v6
|
|
73
|
-
- uses: actions/setup-node@v6
|
|
74
|
-
with:
|
|
75
|
-
node-version: 20
|
|
76
|
-
cache: npm
|
|
77
|
-
- run: sudo apt-get update -qq && sudo apt-get install -y -qq clangd
|
|
78
|
-
- run: npm ci
|
|
79
|
-
- run: INTEGRATION=1 npx tsx --test test/*.test.ts test/integration/clangd.test.ts
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
name: Publish to npm
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
release:
|
|
5
|
-
types: [published]
|
|
6
|
-
|
|
7
|
-
permissions:
|
|
8
|
-
contents: read
|
|
9
|
-
id-token: write
|
|
10
|
-
|
|
11
|
-
jobs:
|
|
12
|
-
publish:
|
|
13
|
-
name: publish to npm
|
|
14
|
-
runs-on: ubuntu-latest
|
|
15
|
-
environment: public
|
|
16
|
-
steps:
|
|
17
|
-
- uses: actions/checkout@v6
|
|
18
|
-
|
|
19
|
-
- uses: actions/setup-node@v6
|
|
20
|
-
with:
|
|
21
|
-
node-version: 24
|
|
22
|
-
registry-url: https://registry.npmjs.org
|
|
23
|
-
cache: npm
|
|
24
|
-
|
|
25
|
-
- run: npm ci
|
|
26
|
-
|
|
27
|
-
- name: typecheck
|
|
28
|
-
run: npm run check
|
|
29
|
-
|
|
30
|
-
- name: unit tests
|
|
31
|
-
run: npm test
|
|
32
|
-
|
|
33
|
-
- name: publish
|
|
34
|
-
run: npm publish --access public
|