lsp-pi 1.0.1
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 +178 -0
- package/lsp-core.ts +1125 -0
- package/lsp-tool.ts +339 -0
- package/lsp.ts +575 -0
- package/package.json +46 -0
- package/tests/index.test.ts +235 -0
- package/tests/lsp-integration.test.ts +602 -0
- package/tests/lsp.test.ts +898 -0
- package/tsconfig.json +13 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Integration tests for LSP - spawns real language servers and detects errors
|
|
3
|
+
*
|
|
4
|
+
* Run with: npm run test:integration
|
|
5
|
+
*
|
|
6
|
+
* Skips tests if language server is not installed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// Suppress stream errors from vscode-jsonrpc when LSP process exits
|
|
10
|
+
process.on('uncaughtException', (err) => {
|
|
11
|
+
if (err.message?.includes('write after end')) return;
|
|
12
|
+
console.error('Uncaught:', err);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
|
17
|
+
import { existsSync, statSync } from "fs";
|
|
18
|
+
import { tmpdir } from "os";
|
|
19
|
+
import { join, delimiter } from "path";
|
|
20
|
+
import { LSPManager } from "../lsp-core.js";
|
|
21
|
+
|
|
22
|
+
// ============================================================================
|
|
23
|
+
// Test utilities
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
const tests: Array<{ name: string; fn: () => Promise<void> }> = [];
|
|
27
|
+
let skipped = 0;
|
|
28
|
+
|
|
29
|
+
function test(name: string, fn: () => Promise<void>) {
|
|
30
|
+
tests.push({ name, fn });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function assert(condition: boolean, message: string) {
|
|
34
|
+
if (!condition) throw new Error(message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class SkipTest extends Error {
|
|
38
|
+
constructor(reason: string) {
|
|
39
|
+
super(reason);
|
|
40
|
+
this.name = "SkipTest";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function skip(reason: string): never {
|
|
45
|
+
throw new SkipTest(reason);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Search paths matching lsp-core.ts
|
|
49
|
+
const SEARCH_PATHS = [
|
|
50
|
+
...(process.env.PATH?.split(delimiter) || []),
|
|
51
|
+
"/usr/local/bin",
|
|
52
|
+
"/opt/homebrew/bin",
|
|
53
|
+
`${process.env.HOME || ""}/.pub-cache/bin`,
|
|
54
|
+
`${process.env.HOME || ""}/fvm/default/bin`,
|
|
55
|
+
`${process.env.HOME || ""}/go/bin`,
|
|
56
|
+
`${process.env.HOME || ""}/.cargo/bin`,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
function commandExists(cmd: string): boolean {
|
|
60
|
+
for (const dir of SEARCH_PATHS) {
|
|
61
|
+
const full = join(dir, cmd);
|
|
62
|
+
try {
|
|
63
|
+
if (existsSync(full) && statSync(full).isFile()) return true;
|
|
64
|
+
} catch {}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ============================================================================
|
|
70
|
+
// TypeScript
|
|
71
|
+
// ============================================================================
|
|
72
|
+
|
|
73
|
+
test("typescript: detects type errors", async () => {
|
|
74
|
+
if (!commandExists("typescript-language-server")) {
|
|
75
|
+
skip("typescript-language-server not installed");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-ts-"));
|
|
79
|
+
const manager = new LSPManager(dir);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await writeFile(join(dir, "package.json"), "{}");
|
|
83
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
84
|
+
compilerOptions: { strict: true, noEmit: true }
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
// Code with type error
|
|
88
|
+
const file = join(dir, "index.ts");
|
|
89
|
+
await writeFile(file, `const x: string = 123;`);
|
|
90
|
+
|
|
91
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 10000);
|
|
92
|
+
|
|
93
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
94
|
+
assert(
|
|
95
|
+
diagnostics.some(d => d.message.toLowerCase().includes("type") || d.severity === 1),
|
|
96
|
+
`Expected type error, got: ${diagnostics.map(d => d.message).join(", ")}`
|
|
97
|
+
);
|
|
98
|
+
} finally {
|
|
99
|
+
await manager.shutdown();
|
|
100
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("typescript: valid code has no errors", async () => {
|
|
105
|
+
if (!commandExists("typescript-language-server")) {
|
|
106
|
+
skip("typescript-language-server not installed");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-ts-"));
|
|
110
|
+
const manager = new LSPManager(dir);
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
await writeFile(join(dir, "package.json"), "{}");
|
|
114
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
115
|
+
compilerOptions: { strict: true, noEmit: true }
|
|
116
|
+
}));
|
|
117
|
+
|
|
118
|
+
const file = join(dir, "index.ts");
|
|
119
|
+
await writeFile(file, `const x: string = "hello";`);
|
|
120
|
+
|
|
121
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 10000);
|
|
122
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
123
|
+
|
|
124
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
125
|
+
} finally {
|
|
126
|
+
await manager.shutdown();
|
|
127
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// ============================================================================
|
|
132
|
+
// Dart
|
|
133
|
+
// ============================================================================
|
|
134
|
+
|
|
135
|
+
test("dart: detects type errors", async () => {
|
|
136
|
+
if (!commandExists("dart")) {
|
|
137
|
+
skip("dart not installed");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-dart-"));
|
|
141
|
+
const manager = new LSPManager(dir);
|
|
142
|
+
|
|
143
|
+
try {
|
|
144
|
+
await writeFile(join(dir, "pubspec.yaml"), "name: test_app\nenvironment:\n sdk: ^3.0.0");
|
|
145
|
+
|
|
146
|
+
await mkdir(join(dir, "lib"));
|
|
147
|
+
const file = join(dir, "lib/main.dart");
|
|
148
|
+
// Type error: assigning int to String
|
|
149
|
+
await writeFile(file, `
|
|
150
|
+
void main() {
|
|
151
|
+
String x = 123;
|
|
152
|
+
print(x);
|
|
153
|
+
}
|
|
154
|
+
`);
|
|
155
|
+
|
|
156
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 15000);
|
|
157
|
+
|
|
158
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
159
|
+
} finally {
|
|
160
|
+
await manager.shutdown();
|
|
161
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("dart: valid code has no errors", async () => {
|
|
166
|
+
if (!commandExists("dart")) {
|
|
167
|
+
skip("dart not installed");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-dart-"));
|
|
171
|
+
const manager = new LSPManager(dir);
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
await writeFile(join(dir, "pubspec.yaml"), "name: test_app\nenvironment:\n sdk: ^3.0.0");
|
|
175
|
+
|
|
176
|
+
await mkdir(join(dir, "lib"));
|
|
177
|
+
const file = join(dir, "lib/main.dart");
|
|
178
|
+
await writeFile(file, `
|
|
179
|
+
void main() {
|
|
180
|
+
String x = "hello";
|
|
181
|
+
print(x);
|
|
182
|
+
}
|
|
183
|
+
`);
|
|
184
|
+
|
|
185
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 15000);
|
|
186
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
187
|
+
|
|
188
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
189
|
+
} finally {
|
|
190
|
+
await manager.shutdown();
|
|
191
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
// ============================================================================
|
|
196
|
+
// Rust
|
|
197
|
+
// ============================================================================
|
|
198
|
+
|
|
199
|
+
test("rust: detects type errors", async () => {
|
|
200
|
+
if (!commandExists("rust-analyzer")) {
|
|
201
|
+
skip("rust-analyzer not installed");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-rust-"));
|
|
205
|
+
const manager = new LSPManager(dir);
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
await writeFile(join(dir, "Cargo.toml"), `[package]\nname = "test"\nversion = "0.1.0"\nedition = "2021"`);
|
|
209
|
+
|
|
210
|
+
await mkdir(join(dir, "src"));
|
|
211
|
+
const file = join(dir, "src/main.rs");
|
|
212
|
+
await writeFile(file, `fn main() {\n let x: i32 = "hello";\n}`);
|
|
213
|
+
|
|
214
|
+
// rust-analyzer needs a LOT of time to initialize (compiles the project)
|
|
215
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 60000);
|
|
216
|
+
|
|
217
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
218
|
+
} finally {
|
|
219
|
+
await manager.shutdown();
|
|
220
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("rust: valid code has no errors", async () => {
|
|
225
|
+
if (!commandExists("rust-analyzer")) {
|
|
226
|
+
skip("rust-analyzer not installed");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-rust-"));
|
|
230
|
+
const manager = new LSPManager(dir);
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
await writeFile(join(dir, "Cargo.toml"), `[package]\nname = "test"\nversion = "0.1.0"\nedition = "2021"`);
|
|
234
|
+
|
|
235
|
+
await mkdir(join(dir, "src"));
|
|
236
|
+
const file = join(dir, "src/main.rs");
|
|
237
|
+
await writeFile(file, `fn main() {\n let x = "hello";\n println!("{}", x);\n}`);
|
|
238
|
+
|
|
239
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 60000);
|
|
240
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
241
|
+
|
|
242
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
243
|
+
} finally {
|
|
244
|
+
await manager.shutdown();
|
|
245
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
// ============================================================================
|
|
250
|
+
// Go
|
|
251
|
+
// ============================================================================
|
|
252
|
+
|
|
253
|
+
test("go: detects type errors", async () => {
|
|
254
|
+
if (!commandExists("gopls")) {
|
|
255
|
+
skip("gopls not installed");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-go-"));
|
|
259
|
+
const manager = new LSPManager(dir);
|
|
260
|
+
|
|
261
|
+
try {
|
|
262
|
+
await writeFile(join(dir, "go.mod"), "module test\n\ngo 1.21");
|
|
263
|
+
|
|
264
|
+
const file = join(dir, "main.go");
|
|
265
|
+
// Type error: cannot use int as string
|
|
266
|
+
await writeFile(file, `package main
|
|
267
|
+
|
|
268
|
+
func main() {
|
|
269
|
+
var x string = 123
|
|
270
|
+
println(x)
|
|
271
|
+
}
|
|
272
|
+
`);
|
|
273
|
+
|
|
274
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 15000);
|
|
275
|
+
|
|
276
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
277
|
+
} finally {
|
|
278
|
+
await manager.shutdown();
|
|
279
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
280
|
+
}
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("go: valid code has no errors", async () => {
|
|
284
|
+
if (!commandExists("gopls")) {
|
|
285
|
+
skip("gopls not installed");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-go-"));
|
|
289
|
+
const manager = new LSPManager(dir);
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
await writeFile(join(dir, "go.mod"), "module test\n\ngo 1.21");
|
|
293
|
+
|
|
294
|
+
const file = join(dir, "main.go");
|
|
295
|
+
await writeFile(file, `package main
|
|
296
|
+
|
|
297
|
+
func main() {
|
|
298
|
+
var x string = "hello"
|
|
299
|
+
println(x)
|
|
300
|
+
}
|
|
301
|
+
`);
|
|
302
|
+
|
|
303
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 15000);
|
|
304
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
305
|
+
|
|
306
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
307
|
+
} finally {
|
|
308
|
+
await manager.shutdown();
|
|
309
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ============================================================================
|
|
314
|
+
// Kotlin
|
|
315
|
+
// ============================================================================
|
|
316
|
+
|
|
317
|
+
test("kotlin: detects syntax errors", async () => {
|
|
318
|
+
if (!commandExists("kotlin-language-server")) {
|
|
319
|
+
skip("kotlin-language-server not installed");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-kt-"));
|
|
323
|
+
const manager = new LSPManager(dir);
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
// Minimal Gradle markers so the LSP picks a root
|
|
327
|
+
await writeFile(join(dir, "settings.gradle.kts"), "rootProject.name = \"test\"\n");
|
|
328
|
+
await writeFile(join(dir, "build.gradle.kts"), "// empty\n");
|
|
329
|
+
|
|
330
|
+
await mkdir(join(dir, "src/main/kotlin"), { recursive: true });
|
|
331
|
+
const file = join(dir, "src/main/kotlin/Main.kt");
|
|
332
|
+
|
|
333
|
+
// Syntax error
|
|
334
|
+
await writeFile(file, "fun main() { val x = }\n");
|
|
335
|
+
|
|
336
|
+
const { diagnostics, receivedResponse } = await manager.touchFileAndWait(file, 30000);
|
|
337
|
+
|
|
338
|
+
assert(receivedResponse, "Expected Kotlin LSP to respond");
|
|
339
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
340
|
+
} finally {
|
|
341
|
+
await manager.shutdown();
|
|
342
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("kotlin: valid code has no errors", async () => {
|
|
347
|
+
if (!commandExists("kotlin-language-server")) {
|
|
348
|
+
skip("kotlin-language-server not installed");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-kt-"));
|
|
352
|
+
const manager = new LSPManager(dir);
|
|
353
|
+
|
|
354
|
+
try {
|
|
355
|
+
await writeFile(join(dir, "settings.gradle.kts"), "rootProject.name = \"test\"\n");
|
|
356
|
+
await writeFile(join(dir, "build.gradle.kts"), "// empty\n");
|
|
357
|
+
|
|
358
|
+
await mkdir(join(dir, "src/main/kotlin"), { recursive: true });
|
|
359
|
+
const file = join(dir, "src/main/kotlin/Main.kt");
|
|
360
|
+
|
|
361
|
+
await writeFile(file, "fun main() { val x = 1; println(x) }\n");
|
|
362
|
+
|
|
363
|
+
const { diagnostics, receivedResponse } = await manager.touchFileAndWait(file, 30000);
|
|
364
|
+
|
|
365
|
+
assert(receivedResponse, "Expected Kotlin LSP to respond");
|
|
366
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
367
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
368
|
+
} finally {
|
|
369
|
+
await manager.shutdown();
|
|
370
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ============================================================================
|
|
375
|
+
// Python
|
|
376
|
+
// ============================================================================
|
|
377
|
+
|
|
378
|
+
test("python: detects type errors", async () => {
|
|
379
|
+
if (!commandExists("pyright-langserver")) {
|
|
380
|
+
skip("pyright-langserver not installed");
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-py-"));
|
|
384
|
+
const manager = new LSPManager(dir);
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
await writeFile(join(dir, "pyproject.toml"), `[project]\nname = "test"`);
|
|
388
|
+
|
|
389
|
+
const file = join(dir, "main.py");
|
|
390
|
+
// Type error with type annotation
|
|
391
|
+
await writeFile(file, `
|
|
392
|
+
def greet(name: str) -> str:
|
|
393
|
+
return "Hello, " + name
|
|
394
|
+
|
|
395
|
+
x: str = 123 # Type error
|
|
396
|
+
result = greet(456) # Type error
|
|
397
|
+
`);
|
|
398
|
+
|
|
399
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 10000);
|
|
400
|
+
|
|
401
|
+
assert(diagnostics.length > 0, `Expected errors, got ${diagnostics.length}`);
|
|
402
|
+
} finally {
|
|
403
|
+
await manager.shutdown();
|
|
404
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("python: valid code has no errors", async () => {
|
|
409
|
+
if (!commandExists("pyright-langserver")) {
|
|
410
|
+
skip("pyright-langserver not installed");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-py-"));
|
|
414
|
+
const manager = new LSPManager(dir);
|
|
415
|
+
|
|
416
|
+
try {
|
|
417
|
+
await writeFile(join(dir, "pyproject.toml"), `[project]\nname = "test"`);
|
|
418
|
+
|
|
419
|
+
const file = join(dir, "main.py");
|
|
420
|
+
await writeFile(file, `
|
|
421
|
+
def greet(name: str) -> str:
|
|
422
|
+
return "Hello, " + name
|
|
423
|
+
|
|
424
|
+
x: str = "world"
|
|
425
|
+
result = greet(x)
|
|
426
|
+
`);
|
|
427
|
+
|
|
428
|
+
const { diagnostics } = await manager.touchFileAndWait(file, 10000);
|
|
429
|
+
const errors = diagnostics.filter(d => d.severity === 1);
|
|
430
|
+
|
|
431
|
+
assert(errors.length === 0, `Expected no errors, got: ${errors.map(d => d.message).join(", ")}`);
|
|
432
|
+
} finally {
|
|
433
|
+
await manager.shutdown();
|
|
434
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
435
|
+
}
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ============================================================================
|
|
439
|
+
// Rename (TypeScript)
|
|
440
|
+
// ============================================================================
|
|
441
|
+
|
|
442
|
+
test("typescript: rename symbol", async () => {
|
|
443
|
+
if (!commandExists("typescript-language-server")) {
|
|
444
|
+
skip("typescript-language-server not installed");
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-ts-rename-"));
|
|
448
|
+
const manager = new LSPManager(dir);
|
|
449
|
+
|
|
450
|
+
try {
|
|
451
|
+
await writeFile(join(dir, "package.json"), "{}");
|
|
452
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
453
|
+
compilerOptions: { strict: true, noEmit: true }
|
|
454
|
+
}));
|
|
455
|
+
|
|
456
|
+
const file = join(dir, "index.ts");
|
|
457
|
+
await writeFile(file, `function greet(name: string) {
|
|
458
|
+
return "Hello, " + name;
|
|
459
|
+
}
|
|
460
|
+
const result = greet("world");
|
|
461
|
+
`);
|
|
462
|
+
|
|
463
|
+
// Touch file first to ensure it's loaded
|
|
464
|
+
await manager.touchFileAndWait(file, 10000);
|
|
465
|
+
|
|
466
|
+
// Rename 'greet' at line 1, col 10
|
|
467
|
+
const edit = await manager.rename(file, 1, 10, "sayHello");
|
|
468
|
+
|
|
469
|
+
if (!edit) throw new Error("Expected rename to return WorkspaceEdit");
|
|
470
|
+
assert(
|
|
471
|
+
edit.changes !== undefined || edit.documentChanges !== undefined,
|
|
472
|
+
"Expected changes or documentChanges in WorkspaceEdit"
|
|
473
|
+
);
|
|
474
|
+
|
|
475
|
+
// Should have edits for both the function definition and the call
|
|
476
|
+
const allEdits: any[] = [];
|
|
477
|
+
if (edit.changes) {
|
|
478
|
+
for (const edits of Object.values(edit.changes)) {
|
|
479
|
+
allEdits.push(...(edits as any[]));
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (edit.documentChanges) {
|
|
483
|
+
for (const change of edit.documentChanges as any[]) {
|
|
484
|
+
if (change.edits) allEdits.push(...change.edits);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
assert(allEdits.length >= 2, `Expected at least 2 edits (definition + usage), got ${allEdits.length}`);
|
|
489
|
+
} finally {
|
|
490
|
+
await manager.shutdown();
|
|
491
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// ============================================================================
|
|
496
|
+
// Code Actions (TypeScript)
|
|
497
|
+
// ============================================================================
|
|
498
|
+
|
|
499
|
+
test("typescript: get code actions for error", async () => {
|
|
500
|
+
if (!commandExists("typescript-language-server")) {
|
|
501
|
+
skip("typescript-language-server not installed");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-ts-actions-"));
|
|
505
|
+
const manager = new LSPManager(dir);
|
|
506
|
+
|
|
507
|
+
try {
|
|
508
|
+
await writeFile(join(dir, "package.json"), "{}");
|
|
509
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
510
|
+
compilerOptions: { strict: true, noEmit: true }
|
|
511
|
+
}));
|
|
512
|
+
|
|
513
|
+
const file = join(dir, "index.ts");
|
|
514
|
+
// Missing import - should offer "Add import" code action
|
|
515
|
+
await writeFile(file, `const x: Promise<string> = Promise.resolve("hello");
|
|
516
|
+
console.log(x);
|
|
517
|
+
`);
|
|
518
|
+
|
|
519
|
+
// Touch to get diagnostics first
|
|
520
|
+
await manager.touchFileAndWait(file, 10000);
|
|
521
|
+
|
|
522
|
+
// Get code actions at line 1
|
|
523
|
+
const actions = await manager.getCodeActions(file, 1, 1, 1, 50);
|
|
524
|
+
|
|
525
|
+
// May or may not have actions depending on the code, but shouldn't throw
|
|
526
|
+
assert(Array.isArray(actions), "Expected array of code actions");
|
|
527
|
+
} finally {
|
|
528
|
+
await manager.shutdown();
|
|
529
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
test("typescript: code actions for missing function", async () => {
|
|
534
|
+
if (!commandExists("typescript-language-server")) {
|
|
535
|
+
skip("typescript-language-server not installed");
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-ts-actions2-"));
|
|
539
|
+
const manager = new LSPManager(dir);
|
|
540
|
+
|
|
541
|
+
try {
|
|
542
|
+
await writeFile(join(dir, "package.json"), "{}");
|
|
543
|
+
await writeFile(join(dir, "tsconfig.json"), JSON.stringify({
|
|
544
|
+
compilerOptions: { strict: true, noEmit: true }
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
const file = join(dir, "index.ts");
|
|
548
|
+
// Call undefined function - should offer quick fix
|
|
549
|
+
await writeFile(file, `const result = undefinedFunction();
|
|
550
|
+
`);
|
|
551
|
+
|
|
552
|
+
await manager.touchFileAndWait(file, 10000);
|
|
553
|
+
|
|
554
|
+
// Get code actions where the error is
|
|
555
|
+
const actions = await manager.getCodeActions(file, 1, 16, 1, 33);
|
|
556
|
+
|
|
557
|
+
// TypeScript should offer to create the function
|
|
558
|
+
assert(Array.isArray(actions), "Expected array of code actions");
|
|
559
|
+
// Note: we don't assert on action count since it depends on TS version
|
|
560
|
+
} finally {
|
|
561
|
+
await manager.shutdown();
|
|
562
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
// ============================================================================
|
|
567
|
+
// Run tests
|
|
568
|
+
// ============================================================================
|
|
569
|
+
|
|
570
|
+
async function runTests(): Promise<void> {
|
|
571
|
+
console.log("Running LSP integration tests...\n");
|
|
572
|
+
console.log("Note: Tests are skipped if language server is not installed.\n");
|
|
573
|
+
|
|
574
|
+
let passed = 0;
|
|
575
|
+
let failed = 0;
|
|
576
|
+
|
|
577
|
+
for (const { name, fn } of tests) {
|
|
578
|
+
try {
|
|
579
|
+
await fn();
|
|
580
|
+
console.log(` ${name}... ✓`);
|
|
581
|
+
passed++;
|
|
582
|
+
} catch (error) {
|
|
583
|
+
if (error instanceof SkipTest) {
|
|
584
|
+
console.log(` ${name}... ⊘ (${error.message})`);
|
|
585
|
+
skipped++;
|
|
586
|
+
} else {
|
|
587
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
588
|
+
console.log(` ${name}... ✗`);
|
|
589
|
+
console.log(` Error: ${msg}\n`);
|
|
590
|
+
failed++;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
console.log(`\n${passed} passed, ${failed} failed, ${skipped} skipped`);
|
|
596
|
+
|
|
597
|
+
if (failed > 0) {
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
runTests();
|