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,898 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for LSP hook - configuration and utility functions
|
|
3
|
+
*
|
|
4
|
+
* Run with: npm test
|
|
5
|
+
*
|
|
6
|
+
* These tests cover:
|
|
7
|
+
* - Project root detection for various languages
|
|
8
|
+
* - Language ID mappings
|
|
9
|
+
* - URI construction
|
|
10
|
+
* - Server configuration correctness
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { mkdtemp, rm, writeFile, mkdir } from "fs/promises";
|
|
14
|
+
import { tmpdir } from "os";
|
|
15
|
+
import { join } from "path";
|
|
16
|
+
import { pathToFileURL } from "url";
|
|
17
|
+
import { LSP_SERVERS, LANGUAGE_IDS } from "../lsp-core.js";
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Test utilities
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
interface TestResult {
|
|
24
|
+
name: string;
|
|
25
|
+
passed: boolean;
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const tests: Array<{ name: string; fn: () => Promise<void> }> = [];
|
|
30
|
+
|
|
31
|
+
function test(name: string, fn: () => Promise<void>) {
|
|
32
|
+
tests.push({ name, fn });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function assert(condition: boolean, message: string) {
|
|
36
|
+
if (!condition) throw new Error(message);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function assertEquals<T>(actual: T, expected: T, message: string) {
|
|
40
|
+
assert(
|
|
41
|
+
actual === expected,
|
|
42
|
+
`${message}\nExpected: ${JSON.stringify(expected)}\nActual: ${JSON.stringify(actual)}`
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function assertIncludes(arr: string[], item: string, message: string) {
|
|
47
|
+
assert(arr.includes(item), `${message}\nArray: [${arr.join(", ")}]\nMissing: ${item}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Create a temp directory with optional file structure */
|
|
51
|
+
async function withTempDir(
|
|
52
|
+
structure: Record<string, string | null>, // null = directory, string = file content
|
|
53
|
+
fn: (dir: string) => Promise<void>
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const dir = await mkdtemp(join(tmpdir(), "lsp-test-"));
|
|
56
|
+
try {
|
|
57
|
+
for (const [path, content] of Object.entries(structure)) {
|
|
58
|
+
const fullPath = join(dir, path);
|
|
59
|
+
if (content === null) {
|
|
60
|
+
await mkdir(fullPath, { recursive: true });
|
|
61
|
+
} else {
|
|
62
|
+
await mkdir(join(dir, path.split("/").slice(0, -1).join("/")), { recursive: true }).catch(() => {});
|
|
63
|
+
await writeFile(fullPath, content);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
await fn(dir);
|
|
67
|
+
} finally {
|
|
68
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Language ID tests
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
test("LANGUAGE_IDS: TypeScript extensions", async () => {
|
|
77
|
+
assertEquals(LANGUAGE_IDS[".ts"], "typescript", ".ts should map to typescript");
|
|
78
|
+
assertEquals(LANGUAGE_IDS[".tsx"], "typescriptreact", ".tsx should map to typescriptreact");
|
|
79
|
+
assertEquals(LANGUAGE_IDS[".mts"], "typescript", ".mts should map to typescript");
|
|
80
|
+
assertEquals(LANGUAGE_IDS[".cts"], "typescript", ".cts should map to typescript");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("LANGUAGE_IDS: JavaScript extensions", async () => {
|
|
84
|
+
assertEquals(LANGUAGE_IDS[".js"], "javascript", ".js should map to javascript");
|
|
85
|
+
assertEquals(LANGUAGE_IDS[".jsx"], "javascriptreact", ".jsx should map to javascriptreact");
|
|
86
|
+
assertEquals(LANGUAGE_IDS[".mjs"], "javascript", ".mjs should map to javascript");
|
|
87
|
+
assertEquals(LANGUAGE_IDS[".cjs"], "javascript", ".cjs should map to javascript");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("LANGUAGE_IDS: Dart extension", async () => {
|
|
91
|
+
assertEquals(LANGUAGE_IDS[".dart"], "dart", ".dart should map to dart");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("LANGUAGE_IDS: Go extension", async () => {
|
|
95
|
+
assertEquals(LANGUAGE_IDS[".go"], "go", ".go should map to go");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("LANGUAGE_IDS: Rust extension", async () => {
|
|
99
|
+
assertEquals(LANGUAGE_IDS[".rs"], "rust", ".rs should map to rust");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("LANGUAGE_IDS: Kotlin extensions", async () => {
|
|
103
|
+
assertEquals(LANGUAGE_IDS[".kt"], "kotlin", ".kt should map to kotlin");
|
|
104
|
+
assertEquals(LANGUAGE_IDS[".kts"], "kotlin", ".kts should map to kotlin");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("LANGUAGE_IDS: Swift extension", async () => {
|
|
108
|
+
assertEquals(LANGUAGE_IDS[".swift"], "swift", ".swift should map to swift");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("LANGUAGE_IDS: Python extensions", async () => {
|
|
112
|
+
assertEquals(LANGUAGE_IDS[".py"], "python", ".py should map to python");
|
|
113
|
+
assertEquals(LANGUAGE_IDS[".pyi"], "python", ".pyi should map to python");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("LANGUAGE_IDS: Vue/Svelte/Astro extensions", async () => {
|
|
117
|
+
assertEquals(LANGUAGE_IDS[".vue"], "vue", ".vue should map to vue");
|
|
118
|
+
assertEquals(LANGUAGE_IDS[".svelte"], "svelte", ".svelte should map to svelte");
|
|
119
|
+
assertEquals(LANGUAGE_IDS[".astro"], "astro", ".astro should map to astro");
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ============================================================================
|
|
123
|
+
// Server configuration tests
|
|
124
|
+
// ============================================================================
|
|
125
|
+
|
|
126
|
+
test("LSP_SERVERS: has TypeScript server", async () => {
|
|
127
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript");
|
|
128
|
+
assert(server !== undefined, "Should have typescript server");
|
|
129
|
+
assertIncludes(server!.extensions, ".ts", "Should handle .ts");
|
|
130
|
+
assertIncludes(server!.extensions, ".tsx", "Should handle .tsx");
|
|
131
|
+
assertIncludes(server!.extensions, ".js", "Should handle .js");
|
|
132
|
+
assertIncludes(server!.extensions, ".jsx", "Should handle .jsx");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("LSP_SERVERS: has Dart server", async () => {
|
|
136
|
+
const server = LSP_SERVERS.find(s => s.id === "dart");
|
|
137
|
+
assert(server !== undefined, "Should have dart server");
|
|
138
|
+
assertIncludes(server!.extensions, ".dart", "Should handle .dart");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("LSP_SERVERS: has Rust Analyzer server", async () => {
|
|
142
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer");
|
|
143
|
+
assert(server !== undefined, "Should have rust-analyzer server");
|
|
144
|
+
assertIncludes(server!.extensions, ".rs", "Should handle .rs");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("LSP_SERVERS: has Gopls server", async () => {
|
|
148
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls");
|
|
149
|
+
assert(server !== undefined, "Should have gopls server");
|
|
150
|
+
assertIncludes(server!.extensions, ".go", "Should handle .go");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("LSP_SERVERS: has Kotlin server", async () => {
|
|
154
|
+
const server = LSP_SERVERS.find(s => s.id === "kotlin");
|
|
155
|
+
assert(server !== undefined, "Should have kotlin server");
|
|
156
|
+
assertIncludes(server!.extensions, ".kt", "Should handle .kt");
|
|
157
|
+
assertIncludes(server!.extensions, ".kts", "Should handle .kts");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("LSP_SERVERS: has Swift server", async () => {
|
|
161
|
+
const server = LSP_SERVERS.find(s => s.id === "swift");
|
|
162
|
+
assert(server !== undefined, "Should have swift server");
|
|
163
|
+
assertIncludes(server!.extensions, ".swift", "Should handle .swift");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("LSP_SERVERS: has Pyright server", async () => {
|
|
167
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright");
|
|
168
|
+
assert(server !== undefined, "Should have pyright server");
|
|
169
|
+
assertIncludes(server!.extensions, ".py", "Should handle .py");
|
|
170
|
+
assertIncludes(server!.extensions, ".pyi", "Should handle .pyi");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ============================================================================
|
|
174
|
+
// TypeScript root detection tests
|
|
175
|
+
// ============================================================================
|
|
176
|
+
|
|
177
|
+
test("typescript: finds root with package.json", async () => {
|
|
178
|
+
await withTempDir({
|
|
179
|
+
"package.json": "{}",
|
|
180
|
+
"src/index.ts": "export const x = 1;",
|
|
181
|
+
}, async (dir) => {
|
|
182
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
183
|
+
const root = server.findRoot(join(dir, "src/index.ts"), dir);
|
|
184
|
+
assertEquals(root, dir, "Should find root at package.json location");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test("typescript: finds root with tsconfig.json", async () => {
|
|
189
|
+
await withTempDir({
|
|
190
|
+
"tsconfig.json": "{}",
|
|
191
|
+
"src/index.ts": "export const x = 1;",
|
|
192
|
+
}, async (dir) => {
|
|
193
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
194
|
+
const root = server.findRoot(join(dir, "src/index.ts"), dir);
|
|
195
|
+
assertEquals(root, dir, "Should find root at tsconfig.json location");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("typescript: finds root with jsconfig.json", async () => {
|
|
200
|
+
await withTempDir({
|
|
201
|
+
"jsconfig.json": "{}",
|
|
202
|
+
"src/app.js": "const x = 1;",
|
|
203
|
+
}, async (dir) => {
|
|
204
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
205
|
+
const root = server.findRoot(join(dir, "src/app.js"), dir);
|
|
206
|
+
assertEquals(root, dir, "Should find root at jsconfig.json location");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("typescript: returns undefined for deno projects", async () => {
|
|
211
|
+
await withTempDir({
|
|
212
|
+
"deno.json": "{}",
|
|
213
|
+
"main.ts": "console.log('deno');",
|
|
214
|
+
}, async (dir) => {
|
|
215
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
216
|
+
const root = server.findRoot(join(dir, "main.ts"), dir);
|
|
217
|
+
assertEquals(root, undefined, "Should return undefined for deno projects");
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("typescript: nested package finds nearest root", async () => {
|
|
222
|
+
await withTempDir({
|
|
223
|
+
"package.json": "{}",
|
|
224
|
+
"packages/web/package.json": "{}",
|
|
225
|
+
"packages/web/src/index.ts": "export const x = 1;",
|
|
226
|
+
}, async (dir) => {
|
|
227
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
228
|
+
const root = server.findRoot(join(dir, "packages/web/src/index.ts"), dir);
|
|
229
|
+
assertEquals(root, join(dir, "packages/web"), "Should find nearest package.json");
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// ============================================================================
|
|
234
|
+
// Dart root detection tests
|
|
235
|
+
// ============================================================================
|
|
236
|
+
|
|
237
|
+
test("dart: finds root with pubspec.yaml", async () => {
|
|
238
|
+
await withTempDir({
|
|
239
|
+
"pubspec.yaml": "name: my_app",
|
|
240
|
+
"lib/main.dart": "void main() {}",
|
|
241
|
+
}, async (dir) => {
|
|
242
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
243
|
+
const root = server.findRoot(join(dir, "lib/main.dart"), dir);
|
|
244
|
+
assertEquals(root, dir, "Should find root at pubspec.yaml location");
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("dart: finds root with analysis_options.yaml", async () => {
|
|
249
|
+
await withTempDir({
|
|
250
|
+
"analysis_options.yaml": "linter: rules:",
|
|
251
|
+
"lib/main.dart": "void main() {}",
|
|
252
|
+
}, async (dir) => {
|
|
253
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
254
|
+
const root = server.findRoot(join(dir, "lib/main.dart"), dir);
|
|
255
|
+
assertEquals(root, dir, "Should find root at analysis_options.yaml location");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("dart: nested package finds nearest root", async () => {
|
|
260
|
+
await withTempDir({
|
|
261
|
+
"pubspec.yaml": "name: monorepo",
|
|
262
|
+
"packages/core/pubspec.yaml": "name: core",
|
|
263
|
+
"packages/core/lib/core.dart": "void init() {}",
|
|
264
|
+
}, async (dir) => {
|
|
265
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
266
|
+
const root = server.findRoot(join(dir, "packages/core/lib/core.dart"), dir);
|
|
267
|
+
assertEquals(root, join(dir, "packages/core"), "Should find nearest pubspec.yaml");
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
// ============================================================================
|
|
272
|
+
// Rust root detection tests
|
|
273
|
+
// ============================================================================
|
|
274
|
+
|
|
275
|
+
test("rust: finds root with Cargo.toml", async () => {
|
|
276
|
+
await withTempDir({
|
|
277
|
+
"Cargo.toml": "[package]\nname = \"my_crate\"",
|
|
278
|
+
"src/lib.rs": "pub fn hello() {}",
|
|
279
|
+
}, async (dir) => {
|
|
280
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
281
|
+
const root = server.findRoot(join(dir, "src/lib.rs"), dir);
|
|
282
|
+
assertEquals(root, dir, "Should find root at Cargo.toml location");
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("rust: nested workspace member finds nearest Cargo.toml", async () => {
|
|
287
|
+
await withTempDir({
|
|
288
|
+
"Cargo.toml": "[workspace]\nmembers = [\"crates/*\"]",
|
|
289
|
+
"crates/core/Cargo.toml": "[package]\nname = \"core\"",
|
|
290
|
+
"crates/core/src/lib.rs": "pub fn init() {}",
|
|
291
|
+
}, async (dir) => {
|
|
292
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
293
|
+
const root = server.findRoot(join(dir, "crates/core/src/lib.rs"), dir);
|
|
294
|
+
assertEquals(root, join(dir, "crates/core"), "Should find nearest Cargo.toml");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Go root detection tests (including gopls bug fix verification)
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
test("gopls: finds root with go.mod", async () => {
|
|
303
|
+
await withTempDir({
|
|
304
|
+
"go.mod": "module example.com/myapp",
|
|
305
|
+
"main.go": "package main",
|
|
306
|
+
}, async (dir) => {
|
|
307
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
308
|
+
const root = server.findRoot(join(dir, "main.go"), dir);
|
|
309
|
+
assertEquals(root, dir, "Should find root at go.mod location");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("gopls: finds root with go.work (workspace)", async () => {
|
|
314
|
+
await withTempDir({
|
|
315
|
+
"go.work": "go 1.21\nuse ./app",
|
|
316
|
+
"app/go.mod": "module example.com/app",
|
|
317
|
+
"app/main.go": "package main",
|
|
318
|
+
}, async (dir) => {
|
|
319
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
320
|
+
const root = server.findRoot(join(dir, "app/main.go"), dir);
|
|
321
|
+
assertEquals(root, dir, "Should find root at go.work location (workspace root)");
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("gopls: prefers go.work over go.mod", async () => {
|
|
326
|
+
await withTempDir({
|
|
327
|
+
"go.work": "go 1.21\nuse ./app",
|
|
328
|
+
"go.mod": "module example.com/root",
|
|
329
|
+
"app/go.mod": "module example.com/app",
|
|
330
|
+
"app/main.go": "package main",
|
|
331
|
+
}, async (dir) => {
|
|
332
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
333
|
+
const root = server.findRoot(join(dir, "app/main.go"), dir);
|
|
334
|
+
// go.work is found first, so it should return the go.work location
|
|
335
|
+
assertEquals(root, dir, "Should prefer go.work over go.mod");
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
test("gopls: returns undefined when no go.mod or go.work (bug fix verification)", async () => {
|
|
340
|
+
await withTempDir({
|
|
341
|
+
"main.go": "package main",
|
|
342
|
+
}, async (dir) => {
|
|
343
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
344
|
+
const root = server.findRoot(join(dir, "main.go"), dir);
|
|
345
|
+
// This test verifies the bug fix: previously this would return undefined
|
|
346
|
+
// because `undefined !== cwd` was true, skipping the go.mod check
|
|
347
|
+
assertEquals(root, undefined, "Should return undefined when no go.mod or go.work");
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("gopls: finds go.mod when go.work not present (bug fix verification)", async () => {
|
|
352
|
+
await withTempDir({
|
|
353
|
+
"go.mod": "module example.com/myapp",
|
|
354
|
+
"cmd/server/main.go": "package main",
|
|
355
|
+
}, async (dir) => {
|
|
356
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
357
|
+
const root = server.findRoot(join(dir, "cmd/server/main.go"), dir);
|
|
358
|
+
// This is the key test for the bug fix
|
|
359
|
+
// Previously: findRoot(go.work) returns undefined, then `undefined !== cwd` is true,
|
|
360
|
+
// so it would return undefined without checking go.mod
|
|
361
|
+
// After fix: if go.work not found, falls through to check go.mod
|
|
362
|
+
assertEquals(root, dir, "Should find go.mod when go.work is not present");
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ============================================================================
|
|
367
|
+
// Kotlin root detection tests
|
|
368
|
+
// ============================================================================
|
|
369
|
+
|
|
370
|
+
test("kotlin: finds root with settings.gradle.kts", async () => {
|
|
371
|
+
await withTempDir({
|
|
372
|
+
"settings.gradle.kts": "rootProject.name = \"myapp\"",
|
|
373
|
+
"app/src/main/kotlin/Main.kt": "fun main() {}",
|
|
374
|
+
}, async (dir) => {
|
|
375
|
+
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
376
|
+
const root = server.findRoot(join(dir, "app/src/main/kotlin/Main.kt"), dir);
|
|
377
|
+
assertEquals(root, dir, "Should find root at settings.gradle.kts location");
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("kotlin: prefers settings.gradle(.kts) over nested build.gradle", async () => {
|
|
382
|
+
await withTempDir({
|
|
383
|
+
"settings.gradle": "rootProject.name = 'root'",
|
|
384
|
+
"app/build.gradle": "plugins {}",
|
|
385
|
+
"app/src/main/kotlin/Main.kt": "fun main() {}",
|
|
386
|
+
}, async (dir) => {
|
|
387
|
+
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
388
|
+
const root = server.findRoot(join(dir, "app/src/main/kotlin/Main.kt"), dir);
|
|
389
|
+
assertEquals(root, dir, "Should prefer settings.gradle at workspace root");
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
test("kotlin: finds root with pom.xml", async () => {
|
|
394
|
+
await withTempDir({
|
|
395
|
+
"pom.xml": "<project></project>",
|
|
396
|
+
"src/main/kotlin/Main.kt": "fun main() {}",
|
|
397
|
+
}, async (dir) => {
|
|
398
|
+
const server = LSP_SERVERS.find(s => s.id === "kotlin")!;
|
|
399
|
+
const root = server.findRoot(join(dir, "src/main/kotlin/Main.kt"), dir);
|
|
400
|
+
assertEquals(root, dir, "Should find root at pom.xml location");
|
|
401
|
+
});
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
// ============================================================================
|
|
405
|
+
// Swift root detection tests
|
|
406
|
+
// ============================================================================
|
|
407
|
+
|
|
408
|
+
test("swift: finds root with Package.swift", async () => {
|
|
409
|
+
await withTempDir({
|
|
410
|
+
"Package.swift": "// swift-tools-version: 5.9",
|
|
411
|
+
"Sources/App/main.swift": "print(\"hi\")",
|
|
412
|
+
}, async (dir) => {
|
|
413
|
+
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
414
|
+
const root = server.findRoot(join(dir, "Sources/App/main.swift"), dir);
|
|
415
|
+
assertEquals(root, dir, "Should find root at Package.swift location");
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("swift: finds root with Xcode project", async () => {
|
|
420
|
+
await withTempDir({
|
|
421
|
+
"MyApp.xcodeproj/project.pbxproj": "// pbxproj",
|
|
422
|
+
"MyApp/main.swift": "print(\"hi\")",
|
|
423
|
+
}, async (dir) => {
|
|
424
|
+
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
425
|
+
const root = server.findRoot(join(dir, "MyApp/main.swift"), dir);
|
|
426
|
+
assertEquals(root, dir, "Should find root at Xcode project location");
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("swift: finds root with Xcode workspace", async () => {
|
|
431
|
+
await withTempDir({
|
|
432
|
+
"MyApp.xcworkspace/contents.xcworkspacedata": "<Workspace/>",
|
|
433
|
+
"MyApp/main.swift": "print(\"hi\")",
|
|
434
|
+
}, async (dir) => {
|
|
435
|
+
const server = LSP_SERVERS.find(s => s.id === "swift")!;
|
|
436
|
+
const root = server.findRoot(join(dir, "MyApp/main.swift"), dir);
|
|
437
|
+
assertEquals(root, dir, "Should find root at Xcode workspace location");
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
// ============================================================================
|
|
442
|
+
// Python root detection tests
|
|
443
|
+
// ============================================================================
|
|
444
|
+
|
|
445
|
+
test("pyright: finds root with pyproject.toml", async () => {
|
|
446
|
+
await withTempDir({
|
|
447
|
+
"pyproject.toml": "[project]\nname = \"myapp\"",
|
|
448
|
+
"src/main.py": "print('hello')",
|
|
449
|
+
}, async (dir) => {
|
|
450
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
451
|
+
const root = server.findRoot(join(dir, "src/main.py"), dir);
|
|
452
|
+
assertEquals(root, dir, "Should find root at pyproject.toml location");
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
test("pyright: finds root with setup.py", async () => {
|
|
457
|
+
await withTempDir({
|
|
458
|
+
"setup.py": "from setuptools import setup",
|
|
459
|
+
"myapp/main.py": "print('hello')",
|
|
460
|
+
}, async (dir) => {
|
|
461
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
462
|
+
const root = server.findRoot(join(dir, "myapp/main.py"), dir);
|
|
463
|
+
assertEquals(root, dir, "Should find root at setup.py location");
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
test("pyright: finds root with requirements.txt", async () => {
|
|
468
|
+
await withTempDir({
|
|
469
|
+
"requirements.txt": "flask>=2.0",
|
|
470
|
+
"app.py": "from flask import Flask",
|
|
471
|
+
}, async (dir) => {
|
|
472
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
473
|
+
const root = server.findRoot(join(dir, "app.py"), dir);
|
|
474
|
+
assertEquals(root, dir, "Should find root at requirements.txt location");
|
|
475
|
+
});
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ============================================================================
|
|
479
|
+
// URI construction tests (pathToFileURL)
|
|
480
|
+
// ============================================================================
|
|
481
|
+
|
|
482
|
+
test("pathToFileURL: handles simple paths", async () => {
|
|
483
|
+
const uri = pathToFileURL("/home/user/project/file.ts").href;
|
|
484
|
+
assertEquals(uri, "file:///home/user/project/file.ts", "Should create proper file URI");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("pathToFileURL: encodes special characters", async () => {
|
|
488
|
+
const uri = pathToFileURL("/home/user/my project/file.ts").href;
|
|
489
|
+
assert(uri.includes("my%20project"), "Should URL-encode spaces");
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
test("pathToFileURL: handles unicode", async () => {
|
|
493
|
+
const uri = pathToFileURL("/home/user/项目/file.ts").href;
|
|
494
|
+
// pathToFileURL properly encodes unicode
|
|
495
|
+
assert(uri.startsWith("file:///"), "Should start with file:///");
|
|
496
|
+
assert(uri.includes("file.ts"), "Should contain filename");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// ============================================================================
|
|
500
|
+
// Vue/Svelte root detection tests
|
|
501
|
+
// ============================================================================
|
|
502
|
+
|
|
503
|
+
test("vue: finds root with package.json", async () => {
|
|
504
|
+
await withTempDir({
|
|
505
|
+
"package.json": "{}",
|
|
506
|
+
"src/App.vue": "<template></template>",
|
|
507
|
+
}, async (dir) => {
|
|
508
|
+
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
509
|
+
const root = server.findRoot(join(dir, "src/App.vue"), dir);
|
|
510
|
+
assertEquals(root, dir, "Should find root at package.json location");
|
|
511
|
+
});
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
test("vue: finds root with vite.config.ts", async () => {
|
|
515
|
+
await withTempDir({
|
|
516
|
+
"vite.config.ts": "export default {}",
|
|
517
|
+
"src/App.vue": "<template></template>",
|
|
518
|
+
}, async (dir) => {
|
|
519
|
+
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
520
|
+
const root = server.findRoot(join(dir, "src/App.vue"), dir);
|
|
521
|
+
assertEquals(root, dir, "Should find root at vite.config.ts location");
|
|
522
|
+
});
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("svelte: finds root with svelte.config.js", async () => {
|
|
526
|
+
await withTempDir({
|
|
527
|
+
"svelte.config.js": "export default {}",
|
|
528
|
+
"src/App.svelte": "<script></script>",
|
|
529
|
+
}, async (dir) => {
|
|
530
|
+
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
531
|
+
const root = server.findRoot(join(dir, "src/App.svelte"), dir);
|
|
532
|
+
assertEquals(root, dir, "Should find root at svelte.config.js location");
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// Additional Rust tests (parity with TypeScript)
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
test("rust: finds root in src subdirectory", async () => {
|
|
541
|
+
await withTempDir({
|
|
542
|
+
"Cargo.toml": "[package]\nname = \"myapp\"",
|
|
543
|
+
"src/main.rs": "fn main() {}",
|
|
544
|
+
"src/lib.rs": "pub mod utils;",
|
|
545
|
+
"src/utils/mod.rs": "pub fn helper() {}",
|
|
546
|
+
}, async (dir) => {
|
|
547
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
548
|
+
const root = server.findRoot(join(dir, "src/utils/mod.rs"), dir);
|
|
549
|
+
assertEquals(root, dir, "Should find root from deeply nested src file");
|
|
550
|
+
});
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
test("rust: workspace with multiple crates", async () => {
|
|
554
|
+
await withTempDir({
|
|
555
|
+
"Cargo.toml": "[workspace]\nmembers = [\"crates/*\"]",
|
|
556
|
+
"crates/api/Cargo.toml": "[package]\nname = \"api\"",
|
|
557
|
+
"crates/api/src/lib.rs": "pub fn serve() {}",
|
|
558
|
+
"crates/core/Cargo.toml": "[package]\nname = \"core\"",
|
|
559
|
+
"crates/core/src/lib.rs": "pub fn init() {}",
|
|
560
|
+
}, async (dir) => {
|
|
561
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
562
|
+
// Each crate should find its own Cargo.toml
|
|
563
|
+
const apiRoot = server.findRoot(join(dir, "crates/api/src/lib.rs"), dir);
|
|
564
|
+
const coreRoot = server.findRoot(join(dir, "crates/core/src/lib.rs"), dir);
|
|
565
|
+
assertEquals(apiRoot, join(dir, "crates/api"), "API crate should find its Cargo.toml");
|
|
566
|
+
assertEquals(coreRoot, join(dir, "crates/core"), "Core crate should find its Cargo.toml");
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("rust: returns undefined when no Cargo.toml", async () => {
|
|
571
|
+
await withTempDir({
|
|
572
|
+
"main.rs": "fn main() {}",
|
|
573
|
+
}, async (dir) => {
|
|
574
|
+
const server = LSP_SERVERS.find(s => s.id === "rust-analyzer")!;
|
|
575
|
+
const root = server.findRoot(join(dir, "main.rs"), dir);
|
|
576
|
+
assertEquals(root, undefined, "Should return undefined when no Cargo.toml");
|
|
577
|
+
});
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// ============================================================================
|
|
581
|
+
// Additional Dart tests (parity with TypeScript)
|
|
582
|
+
// ============================================================================
|
|
583
|
+
|
|
584
|
+
test("dart: Flutter project with pubspec.yaml", async () => {
|
|
585
|
+
await withTempDir({
|
|
586
|
+
"pubspec.yaml": "name: my_flutter_app\ndependencies:\n flutter:\n sdk: flutter",
|
|
587
|
+
"lib/main.dart": "import 'package:flutter/material.dart';",
|
|
588
|
+
"lib/screens/home.dart": "class HomeScreen {}",
|
|
589
|
+
}, async (dir) => {
|
|
590
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
591
|
+
const root = server.findRoot(join(dir, "lib/screens/home.dart"), dir);
|
|
592
|
+
assertEquals(root, dir, "Should find root for Flutter project");
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
test("dart: returns undefined when no marker files", async () => {
|
|
597
|
+
await withTempDir({
|
|
598
|
+
"main.dart": "void main() {}",
|
|
599
|
+
}, async (dir) => {
|
|
600
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
601
|
+
const root = server.findRoot(join(dir, "main.dart"), dir);
|
|
602
|
+
assertEquals(root, undefined, "Should return undefined when no pubspec.yaml or analysis_options.yaml");
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
|
|
606
|
+
test("dart: monorepo with multiple packages", async () => {
|
|
607
|
+
await withTempDir({
|
|
608
|
+
"pubspec.yaml": "name: monorepo",
|
|
609
|
+
"packages/auth/pubspec.yaml": "name: auth",
|
|
610
|
+
"packages/auth/lib/auth.dart": "class Auth {}",
|
|
611
|
+
"packages/ui/pubspec.yaml": "name: ui",
|
|
612
|
+
"packages/ui/lib/widgets.dart": "class Button {}",
|
|
613
|
+
}, async (dir) => {
|
|
614
|
+
const server = LSP_SERVERS.find(s => s.id === "dart")!;
|
|
615
|
+
const authRoot = server.findRoot(join(dir, "packages/auth/lib/auth.dart"), dir);
|
|
616
|
+
const uiRoot = server.findRoot(join(dir, "packages/ui/lib/widgets.dart"), dir);
|
|
617
|
+
assertEquals(authRoot, join(dir, "packages/auth"), "Auth package should find its pubspec");
|
|
618
|
+
assertEquals(uiRoot, join(dir, "packages/ui"), "UI package should find its pubspec");
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
// ============================================================================
|
|
623
|
+
// Additional Python tests (parity with TypeScript)
|
|
624
|
+
// ============================================================================
|
|
625
|
+
|
|
626
|
+
test("pyright: finds root with pyrightconfig.json", async () => {
|
|
627
|
+
await withTempDir({
|
|
628
|
+
"pyrightconfig.json": "{}",
|
|
629
|
+
"src/app.py": "print('hello')",
|
|
630
|
+
}, async (dir) => {
|
|
631
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
632
|
+
const root = server.findRoot(join(dir, "src/app.py"), dir);
|
|
633
|
+
assertEquals(root, dir, "Should find root at pyrightconfig.json location");
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
test("pyright: returns undefined when no marker files", async () => {
|
|
638
|
+
await withTempDir({
|
|
639
|
+
"script.py": "print('hello')",
|
|
640
|
+
}, async (dir) => {
|
|
641
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
642
|
+
const root = server.findRoot(join(dir, "script.py"), dir);
|
|
643
|
+
assertEquals(root, undefined, "Should return undefined when no Python project markers");
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("pyright: monorepo with multiple packages", async () => {
|
|
648
|
+
await withTempDir({
|
|
649
|
+
"pyproject.toml": "[project]\nname = \"monorepo\"",
|
|
650
|
+
"packages/api/pyproject.toml": "[project]\nname = \"api\"",
|
|
651
|
+
"packages/api/src/main.py": "from flask import Flask",
|
|
652
|
+
"packages/worker/pyproject.toml": "[project]\nname = \"worker\"",
|
|
653
|
+
"packages/worker/src/tasks.py": "def process(): pass",
|
|
654
|
+
}, async (dir) => {
|
|
655
|
+
const server = LSP_SERVERS.find(s => s.id === "pyright")!;
|
|
656
|
+
const apiRoot = server.findRoot(join(dir, "packages/api/src/main.py"), dir);
|
|
657
|
+
const workerRoot = server.findRoot(join(dir, "packages/worker/src/tasks.py"), dir);
|
|
658
|
+
assertEquals(apiRoot, join(dir, "packages/api"), "API package should find its pyproject.toml");
|
|
659
|
+
assertEquals(workerRoot, join(dir, "packages/worker"), "Worker package should find its pyproject.toml");
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// ============================================================================
|
|
664
|
+
// Additional Go tests
|
|
665
|
+
// ============================================================================
|
|
666
|
+
|
|
667
|
+
test("gopls: monorepo with multiple modules", async () => {
|
|
668
|
+
await withTempDir({
|
|
669
|
+
"go.work": "go 1.21\nuse (\n ./api\n ./worker\n)",
|
|
670
|
+
"api/go.mod": "module example.com/api",
|
|
671
|
+
"api/main.go": "package main",
|
|
672
|
+
"worker/go.mod": "module example.com/worker",
|
|
673
|
+
"worker/main.go": "package main",
|
|
674
|
+
}, async (dir) => {
|
|
675
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
676
|
+
// With go.work present, all files should use workspace root
|
|
677
|
+
const apiRoot = server.findRoot(join(dir, "api/main.go"), dir);
|
|
678
|
+
const workerRoot = server.findRoot(join(dir, "worker/main.go"), dir);
|
|
679
|
+
assertEquals(apiRoot, dir, "API module should use go.work root");
|
|
680
|
+
assertEquals(workerRoot, dir, "Worker module should use go.work root");
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
test("gopls: nested cmd directory", async () => {
|
|
685
|
+
await withTempDir({
|
|
686
|
+
"go.mod": "module example.com/myapp",
|
|
687
|
+
"cmd/server/main.go": "package main",
|
|
688
|
+
"cmd/cli/main.go": "package main",
|
|
689
|
+
"internal/db/db.go": "package db",
|
|
690
|
+
}, async (dir) => {
|
|
691
|
+
const server = LSP_SERVERS.find(s => s.id === "gopls")!;
|
|
692
|
+
const serverRoot = server.findRoot(join(dir, "cmd/server/main.go"), dir);
|
|
693
|
+
const cliRoot = server.findRoot(join(dir, "cmd/cli/main.go"), dir);
|
|
694
|
+
const dbRoot = server.findRoot(join(dir, "internal/db/db.go"), dir);
|
|
695
|
+
assertEquals(serverRoot, dir, "cmd/server should find go.mod at root");
|
|
696
|
+
assertEquals(cliRoot, dir, "cmd/cli should find go.mod at root");
|
|
697
|
+
assertEquals(dbRoot, dir, "internal/db should find go.mod at root");
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// ============================================================================
|
|
702
|
+
// Additional TypeScript tests
|
|
703
|
+
// ============================================================================
|
|
704
|
+
|
|
705
|
+
test("typescript: pnpm workspace", async () => {
|
|
706
|
+
await withTempDir({
|
|
707
|
+
"package.json": "{}",
|
|
708
|
+
"pnpm-workspace.yaml": "packages:\n - packages/*",
|
|
709
|
+
"packages/web/package.json": "{}",
|
|
710
|
+
"packages/web/src/App.tsx": "export const App = () => null;",
|
|
711
|
+
"packages/api/package.json": "{}",
|
|
712
|
+
"packages/api/src/index.ts": "export const handler = () => {};",
|
|
713
|
+
}, async (dir) => {
|
|
714
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
715
|
+
const webRoot = server.findRoot(join(dir, "packages/web/src/App.tsx"), dir);
|
|
716
|
+
const apiRoot = server.findRoot(join(dir, "packages/api/src/index.ts"), dir);
|
|
717
|
+
assertEquals(webRoot, join(dir, "packages/web"), "Web package should find its package.json");
|
|
718
|
+
assertEquals(apiRoot, join(dir, "packages/api"), "API package should find its package.json");
|
|
719
|
+
});
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
test("typescript: returns undefined when no config files", async () => {
|
|
723
|
+
await withTempDir({
|
|
724
|
+
"script.ts": "const x = 1;",
|
|
725
|
+
}, async (dir) => {
|
|
726
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
727
|
+
const root = server.findRoot(join(dir, "script.ts"), dir);
|
|
728
|
+
assertEquals(root, undefined, "Should return undefined when no package.json or tsconfig.json");
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
test("typescript: prefers nearest tsconfig over package.json", async () => {
|
|
733
|
+
await withTempDir({
|
|
734
|
+
"package.json": "{}",
|
|
735
|
+
"apps/web/tsconfig.json": "{}",
|
|
736
|
+
"apps/web/src/index.ts": "export const x = 1;",
|
|
737
|
+
}, async (dir) => {
|
|
738
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
739
|
+
const root = server.findRoot(join(dir, "apps/web/src/index.ts"), dir);
|
|
740
|
+
// Should find tsconfig.json first (it's nearer than root package.json)
|
|
741
|
+
assertEquals(root, join(dir, "apps/web"), "Should find nearest config file");
|
|
742
|
+
});
|
|
743
|
+
});
|
|
744
|
+
|
|
745
|
+
// ============================================================================
|
|
746
|
+
// Additional Vue/Svelte tests
|
|
747
|
+
// ============================================================================
|
|
748
|
+
|
|
749
|
+
test("vue: Nuxt project", async () => {
|
|
750
|
+
await withTempDir({
|
|
751
|
+
"package.json": "{}",
|
|
752
|
+
"nuxt.config.ts": "export default {}",
|
|
753
|
+
"pages/index.vue": "<template></template>",
|
|
754
|
+
"components/Button.vue": "<template></template>",
|
|
755
|
+
}, async (dir) => {
|
|
756
|
+
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
757
|
+
const pagesRoot = server.findRoot(join(dir, "pages/index.vue"), dir);
|
|
758
|
+
const componentsRoot = server.findRoot(join(dir, "components/Button.vue"), dir);
|
|
759
|
+
assertEquals(pagesRoot, dir, "Pages should find root");
|
|
760
|
+
assertEquals(componentsRoot, dir, "Components should find root");
|
|
761
|
+
});
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
test("vue: returns undefined when no config", async () => {
|
|
765
|
+
await withTempDir({
|
|
766
|
+
"App.vue": "<template></template>",
|
|
767
|
+
}, async (dir) => {
|
|
768
|
+
const server = LSP_SERVERS.find(s => s.id === "vue")!;
|
|
769
|
+
const root = server.findRoot(join(dir, "App.vue"), dir);
|
|
770
|
+
assertEquals(root, undefined, "Should return undefined when no package.json or vite.config");
|
|
771
|
+
});
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
test("svelte: SvelteKit project", async () => {
|
|
775
|
+
await withTempDir({
|
|
776
|
+
"package.json": "{}",
|
|
777
|
+
"svelte.config.js": "export default {}",
|
|
778
|
+
"src/routes/+page.svelte": "<script></script>",
|
|
779
|
+
"src/lib/components/Button.svelte": "<script></script>",
|
|
780
|
+
}, async (dir) => {
|
|
781
|
+
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
782
|
+
const routeRoot = server.findRoot(join(dir, "src/routes/+page.svelte"), dir);
|
|
783
|
+
const libRoot = server.findRoot(join(dir, "src/lib/components/Button.svelte"), dir);
|
|
784
|
+
assertEquals(routeRoot, dir, "Route should find root");
|
|
785
|
+
assertEquals(libRoot, dir, "Lib component should find root");
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
test("svelte: returns undefined when no config", async () => {
|
|
790
|
+
await withTempDir({
|
|
791
|
+
"App.svelte": "<script></script>",
|
|
792
|
+
}, async (dir) => {
|
|
793
|
+
const server = LSP_SERVERS.find(s => s.id === "svelte")!;
|
|
794
|
+
const root = server.findRoot(join(dir, "App.svelte"), dir);
|
|
795
|
+
assertEquals(root, undefined, "Should return undefined when no package.json or svelte.config.js");
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// ============================================================================
|
|
800
|
+
// Stop boundary tests (findNearestFile respects cwd boundary)
|
|
801
|
+
// ============================================================================
|
|
802
|
+
|
|
803
|
+
test("stop boundary: does not search above cwd", async () => {
|
|
804
|
+
await withTempDir({
|
|
805
|
+
"package.json": "{}", // This is at root
|
|
806
|
+
"projects/myapp/src/index.ts": "export const x = 1;",
|
|
807
|
+
// Note: no package.json in projects/myapp
|
|
808
|
+
}, async (dir) => {
|
|
809
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
810
|
+
// When cwd is set to projects/myapp, it should NOT find the root package.json
|
|
811
|
+
const projectDir = join(dir, "projects/myapp");
|
|
812
|
+
const root = server.findRoot(join(projectDir, "src/index.ts"), projectDir);
|
|
813
|
+
assertEquals(root, undefined, "Should not find package.json above cwd boundary");
|
|
814
|
+
});
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
test("stop boundary: finds marker at cwd level", async () => {
|
|
818
|
+
await withTempDir({
|
|
819
|
+
"projects/myapp/package.json": "{}",
|
|
820
|
+
"projects/myapp/src/index.ts": "export const x = 1;",
|
|
821
|
+
}, async (dir) => {
|
|
822
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
823
|
+
const projectDir = join(dir, "projects/myapp");
|
|
824
|
+
const root = server.findRoot(join(projectDir, "src/index.ts"), projectDir);
|
|
825
|
+
assertEquals(root, projectDir, "Should find package.json at cwd level");
|
|
826
|
+
});
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// ============================================================================
|
|
830
|
+
// Edge cases
|
|
831
|
+
// ============================================================================
|
|
832
|
+
|
|
833
|
+
test("edge: deeply nested file finds correct root", async () => {
|
|
834
|
+
await withTempDir({
|
|
835
|
+
"package.json": "{}",
|
|
836
|
+
"src/components/ui/buttons/primary/Button.tsx": "export const Button = () => null;",
|
|
837
|
+
}, async (dir) => {
|
|
838
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
839
|
+
const root = server.findRoot(join(dir, "src/components/ui/buttons/primary/Button.tsx"), dir);
|
|
840
|
+
assertEquals(root, dir, "Should find root even for deeply nested files");
|
|
841
|
+
});
|
|
842
|
+
});
|
|
843
|
+
|
|
844
|
+
test("edge: file at root level finds root", async () => {
|
|
845
|
+
await withTempDir({
|
|
846
|
+
"package.json": "{}",
|
|
847
|
+
"index.ts": "console.log('root');",
|
|
848
|
+
}, async (dir) => {
|
|
849
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
850
|
+
const root = server.findRoot(join(dir, "index.ts"), dir);
|
|
851
|
+
assertEquals(root, dir, "Should find root for file at root level");
|
|
852
|
+
});
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
test("edge: no marker files returns undefined", async () => {
|
|
856
|
+
await withTempDir({
|
|
857
|
+
"random.ts": "const x = 1;",
|
|
858
|
+
}, async (dir) => {
|
|
859
|
+
const server = LSP_SERVERS.find(s => s.id === "typescript")!;
|
|
860
|
+
const root = server.findRoot(join(dir, "random.ts"), dir);
|
|
861
|
+
assertEquals(root, undefined, "Should return undefined when no marker files");
|
|
862
|
+
});
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// ============================================================================
|
|
866
|
+
// Run tests
|
|
867
|
+
// ============================================================================
|
|
868
|
+
|
|
869
|
+
async function runTests(): Promise<void> {
|
|
870
|
+
console.log("Running LSP tests...\n");
|
|
871
|
+
|
|
872
|
+
const results: TestResult[] = [];
|
|
873
|
+
let passed = 0;
|
|
874
|
+
let failed = 0;
|
|
875
|
+
|
|
876
|
+
for (const { name, fn } of tests) {
|
|
877
|
+
try {
|
|
878
|
+
await fn();
|
|
879
|
+
results.push({ name, passed: true });
|
|
880
|
+
console.log(` ${name}... ✓`);
|
|
881
|
+
passed++;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
884
|
+
results.push({ name, passed: false, error: errorMsg });
|
|
885
|
+
console.log(` ${name}... ✗`);
|
|
886
|
+
console.log(` Error: ${errorMsg}\n`);
|
|
887
|
+
failed++;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
console.log(`\n${passed} passed, ${failed} failed`);
|
|
892
|
+
|
|
893
|
+
if (failed > 0) {
|
|
894
|
+
process.exit(1);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
runTests();
|