pi-lens 2.0.7 → 2.0.8
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/clients/ast-grep-client.test.ts +146 -116
- package/clients/ast-grep-client.ts +645 -551
- package/clients/biome-client.test.ts +154 -137
- package/clients/biome-client.ts +397 -337
- package/clients/complexity-client.test.ts +188 -200
- package/clients/complexity-client.ts +815 -667
- package/clients/dependency-checker.test.ts +55 -55
- package/clients/dependency-checker.ts +358 -333
- package/clients/go-client.test.ts +121 -111
- package/clients/go-client.ts +218 -216
- package/clients/jscpd-client.test.ts +132 -132
- package/clients/jscpd-client.ts +155 -118
- package/clients/knip-client.test.ts +123 -133
- package/clients/knip-client.ts +231 -218
- package/clients/metrics-client.test.ts +171 -167
- package/clients/metrics-client.ts +283 -252
- package/clients/ruff-client.test.ts +128 -117
- package/clients/ruff-client.ts +300 -269
- package/clients/rust-client.test.ts +104 -85
- package/clients/rust-client.ts +241 -234
- package/clients/subprocess-client.ts +1 -1
- package/clients/test-runner-client.test.ts +248 -215
- package/clients/test-runner-client.ts +728 -608
- package/clients/test-utils.ts +10 -3
- package/clients/todo-scanner.test.ts +288 -202
- package/clients/todo-scanner.ts +225 -187
- package/clients/type-coverage-client.test.ts +119 -119
- package/clients/type-coverage-client.ts +142 -115
- package/clients/types.ts +28 -28
- package/clients/typescript-client.test.ts +99 -93
- package/clients/typescript-client.ts +527 -502
- package/index.ts +662 -212
- package/package.json +1 -1
- package/tsconfig.json +12 -12
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* This is lighter weight than spawning tsserver and provides the same features.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
+
import * as fs from "node:fs";
|
|
9
|
+
import * as path from "node:path";
|
|
8
10
|
import * as ts from "typescript";
|
|
9
|
-
import * as path from "path";
|
|
10
|
-
import * as fs from "fs";
|
|
11
11
|
import type {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
CompletionItem,
|
|
13
|
+
Diagnostic,
|
|
14
|
+
DiagnosticSeverity,
|
|
15
|
+
FoldingRange,
|
|
16
|
+
HoverInfo,
|
|
17
|
+
Location,
|
|
18
|
+
SymbolInfo,
|
|
19
19
|
} from "./types.js";
|
|
20
20
|
|
|
21
21
|
// TypeScript file extensions
|
|
@@ -29,35 +29,40 @@ const TS_EXTENSIONS = new Set([".ts", ".tsx", ".js", ".jsx"]);
|
|
|
29
29
|
* Language Service looks up those names relative to cwd, not the TS install dir.
|
|
30
30
|
*/
|
|
31
31
|
function buildDefaultCompilerOptions(): ts.CompilerOptions {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
32
|
+
const fakeConfig = {
|
|
33
|
+
compilerOptions: {
|
|
34
|
+
target: "ES2020",
|
|
35
|
+
module: "ESNext",
|
|
36
|
+
moduleResolution: "bundler",
|
|
37
|
+
strict: true,
|
|
38
|
+
esModuleInterop: true,
|
|
39
|
+
skipLibCheck: true,
|
|
40
|
+
lib: ["es2020", "dom", "dom.iterable"],
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
44
|
+
fakeConfig,
|
|
45
|
+
ts.sys,
|
|
46
|
+
process.cwd(),
|
|
47
|
+
);
|
|
48
|
+
return { ...parsed.options, skipLibCheck: true };
|
|
45
49
|
}
|
|
46
50
|
|
|
47
|
-
const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions =
|
|
51
|
+
const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions =
|
|
52
|
+
buildDefaultCompilerOptions();
|
|
48
53
|
|
|
49
54
|
/**
|
|
50
55
|
* Walk up from startDir until we find a tsconfig.json, or hit the fs root.
|
|
51
56
|
*/
|
|
52
57
|
function findTsConfig(startDir: string): string | null {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
let dir = startDir;
|
|
59
|
+
while (true) {
|
|
60
|
+
const candidate = path.join(dir, "tsconfig.json");
|
|
61
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
62
|
+
const parent = path.dirname(dir);
|
|
63
|
+
if (parent === dir) return null; // reached root
|
|
64
|
+
dir = parent;
|
|
65
|
+
}
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
/**
|
|
@@ -65,478 +70,498 @@ function findTsConfig(startDir: string): string | null {
|
|
|
65
70
|
* Falls back to DEFAULT_COMPILER_OPTIONS on any error.
|
|
66
71
|
*/
|
|
67
72
|
function loadCompilerOptions(tsconfigPath: string): ts.CompilerOptions {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
try {
|
|
74
|
+
const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
75
|
+
if (configFile.error) return DEFAULT_COMPILER_OPTIONS;
|
|
76
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
77
|
+
configFile.config,
|
|
78
|
+
ts.sys,
|
|
79
|
+
path.dirname(tsconfigPath),
|
|
80
|
+
);
|
|
81
|
+
if (parsed.errors.length) return DEFAULT_COMPILER_OPTIONS;
|
|
82
|
+
// Always set skipLibCheck to avoid noise from node_modules
|
|
83
|
+
return { ...parsed.options, skipLibCheck: true };
|
|
84
|
+
} catch {
|
|
85
|
+
return DEFAULT_COMPILER_OPTIONS;
|
|
86
|
+
}
|
|
82
87
|
}
|
|
83
88
|
|
|
84
89
|
export class TypeScriptClient {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
90
|
+
private fileVersions = new Map<string, number>();
|
|
91
|
+
private fileContents = new Map<string, string>();
|
|
92
|
+
private languageService: ts.LanguageService | null = null;
|
|
93
|
+
private compilerOptions: ts.CompilerOptions = DEFAULT_COMPILER_OPTIONS;
|
|
94
|
+
private lastTsconfigDir: string | null = null;
|
|
95
|
+
|
|
96
|
+
constructor() {
|
|
97
|
+
this.initialize();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Normalize file path for consistent cross-platform use
|
|
102
|
+
*/
|
|
103
|
+
normalizePath(filePath: string): string {
|
|
104
|
+
return path.resolve(filePath).replace(/\\/g, "/");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a file is a TypeScript/JavaScript file
|
|
109
|
+
*/
|
|
110
|
+
isTypeScriptFile(filePath: string): boolean {
|
|
111
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
112
|
+
return TS_EXTENSIONS.has(ext);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private initialize(): void {
|
|
116
|
+
const host: ts.LanguageServiceHost = {
|
|
117
|
+
getScriptFileNames: () => Array.from(this.fileContents.keys()),
|
|
118
|
+
getScriptVersion: (fileName: string) => {
|
|
119
|
+
const normalized = fileName.replace(/\\/g, "/");
|
|
120
|
+
return String(this.fileVersions.get(normalized) ?? 0);
|
|
121
|
+
},
|
|
122
|
+
getScriptSnapshot: (fileName: string) => {
|
|
123
|
+
const normalized = fileName.replace(/\\/g, "/");
|
|
124
|
+
const content = this.fileContents.get(normalized);
|
|
125
|
+
if (content) return ts.ScriptSnapshot.fromString(content);
|
|
126
|
+
if (fs.existsSync(fileName)) {
|
|
127
|
+
return ts.ScriptSnapshot.fromString(
|
|
128
|
+
fs.readFileSync(fileName, "utf-8"),
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
},
|
|
133
|
+
getCurrentDirectory: () => process.cwd(),
|
|
134
|
+
getCompilationSettings: () => this.compilerOptions,
|
|
135
|
+
getDefaultLibFileName: (options) => ts.getDefaultLibFilePath(options),
|
|
136
|
+
fileExists: (fileName) => ts.sys.fileExists(fileName),
|
|
137
|
+
readFile: (fileName) => {
|
|
138
|
+
const normalized = fileName.replace(/\\/g, "/");
|
|
139
|
+
const cached = this.fileContents.get(normalized);
|
|
140
|
+
if (cached !== undefined) return cached;
|
|
141
|
+
return ts.sys.readFile(fileName);
|
|
142
|
+
},
|
|
143
|
+
directoryExists: (dirName) => ts.sys.directoryExists(dirName),
|
|
144
|
+
getDirectories: (dir) => ts.sys.getDirectories(dir),
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
this.languageService = ts.createLanguageService(
|
|
148
|
+
host,
|
|
149
|
+
ts.createDocumentRegistry(),
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Detect tsconfig for the given file and refresh compilerOptions if the
|
|
155
|
+
* project root changed (avoids redundant re-parses across edits to the same project).
|
|
156
|
+
*/
|
|
157
|
+
private refreshCompilerOptions(filePath: string): void {
|
|
158
|
+
const dir = path.dirname(path.resolve(filePath));
|
|
159
|
+
const tsconfigPath = findTsConfig(dir);
|
|
160
|
+
const key = tsconfigPath ?? dir;
|
|
161
|
+
if (key === this.lastTsconfigDir) return; // same project, no change
|
|
162
|
+
this.lastTsconfigDir = key;
|
|
163
|
+
this.compilerOptions = tsconfigPath
|
|
164
|
+
? loadCompilerOptions(tsconfigPath)
|
|
165
|
+
: DEFAULT_COMPILER_OPTIONS;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Add a file to the language service
|
|
170
|
+
*/
|
|
171
|
+
addFile(filePath: string, content: string): void {
|
|
172
|
+
const normalized = this.normalizePath(filePath);
|
|
173
|
+
this.fileContents.set(normalized, content);
|
|
174
|
+
this.fileVersions.set(
|
|
175
|
+
normalized,
|
|
176
|
+
(this.fileVersions.get(normalized) || 0) + 1,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Update a file's content — also refreshes compilerOptions if project changed
|
|
182
|
+
*/
|
|
183
|
+
updateFile(filePath: string, content: string): void {
|
|
184
|
+
this.refreshCompilerOptions(filePath);
|
|
185
|
+
const normalized = this.normalizePath(filePath);
|
|
186
|
+
this.fileVersions.set(
|
|
187
|
+
normalized,
|
|
188
|
+
(this.fileVersions.get(normalized) ?? 0) + 1,
|
|
189
|
+
);
|
|
190
|
+
this.fileContents.set(normalized, content);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Ensure a file is loaded from disk (refreshes cache)
|
|
195
|
+
*/
|
|
196
|
+
ensureFile(filePath: string): void {
|
|
197
|
+
const normalized = this.normalizePath(filePath);
|
|
198
|
+
if (fs.existsSync(filePath)) {
|
|
199
|
+
const diskContent = fs.readFileSync(filePath, "utf-8");
|
|
200
|
+
const cachedContent = this.fileContents.get(normalized);
|
|
201
|
+
if (cachedContent !== diskContent) {
|
|
202
|
+
this.updateFile(filePath, diskContent);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Get all tracked files
|
|
209
|
+
*/
|
|
210
|
+
getTrackedFiles(): string[] {
|
|
211
|
+
return Array.from(this.fileContents.keys());
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Convert line/character to position offset
|
|
216
|
+
*/
|
|
217
|
+
lineCharToPosition(content: string, line: number, character: number): number {
|
|
218
|
+
const lines = content.split("\n");
|
|
219
|
+
let position = 0;
|
|
220
|
+
for (let i = 0; i < Math.min(line, lines.length); i++) {
|
|
221
|
+
position += lines[i].length + 1;
|
|
222
|
+
}
|
|
223
|
+
return position + character;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Get diagnostics (errors and warnings) for a file
|
|
228
|
+
*/
|
|
229
|
+
getDiagnostics(filePath: string): Diagnostic[] {
|
|
230
|
+
this.refreshCompilerOptions(filePath);
|
|
231
|
+
const normalized = this.normalizePath(filePath);
|
|
232
|
+
this.ensureFile(filePath);
|
|
233
|
+
if (!this.languageService) return [];
|
|
234
|
+
|
|
235
|
+
const syntactic = this.languageService.getSyntacticDiagnostics(normalized);
|
|
236
|
+
const semantic = this.languageService.getSemanticDiagnostics(normalized);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
[...syntactic, ...semantic]
|
|
240
|
+
.filter((diag) => diag.file && diag.start !== undefined)
|
|
241
|
+
// Filter cross-file "redeclare" noise — happens when non-module scripts
|
|
242
|
+
// share global scope across multiple tracked files (TS2300, TS2451)
|
|
243
|
+
.filter((diag) => {
|
|
244
|
+
if (diag.code !== 2300 && diag.code !== 2451) return true;
|
|
245
|
+
// Only keep if the related information points back to the same file
|
|
246
|
+
const related = diag.relatedInformation ?? [];
|
|
247
|
+
return related.every(
|
|
248
|
+
(r) =>
|
|
249
|
+
!r.file || this.normalizePath(r.file.fileName) === normalized,
|
|
250
|
+
);
|
|
251
|
+
})
|
|
252
|
+
.map((diag) => {
|
|
253
|
+
const startPos = diag.file?.getLineAndCharacterOfPosition(
|
|
254
|
+
diag.start!,
|
|
255
|
+
)!;
|
|
256
|
+
const endPos = diag.file?.getLineAndCharacterOfPosition(
|
|
257
|
+
diag.start! + diag.length!,
|
|
258
|
+
)!;
|
|
259
|
+
return {
|
|
260
|
+
range: {
|
|
261
|
+
start: { line: startPos.line, character: startPos.character },
|
|
262
|
+
end: { line: endPos.line, character: endPos.character },
|
|
263
|
+
},
|
|
264
|
+
severity: (diag.category === ts.DiagnosticCategory.Error
|
|
265
|
+
? 1
|
|
266
|
+
: 2) as DiagnosticSeverity,
|
|
267
|
+
code: diag.code,
|
|
268
|
+
message: ts.flattenDiagnosticMessageText(diag.messageText, "\n"),
|
|
269
|
+
source: "typescript",
|
|
270
|
+
};
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Get hover information at a position
|
|
277
|
+
*/
|
|
278
|
+
getHover(
|
|
279
|
+
filePath: string,
|
|
280
|
+
line: number,
|
|
281
|
+
character: number,
|
|
282
|
+
): HoverInfo | null {
|
|
283
|
+
const normalized = this.normalizePath(filePath);
|
|
284
|
+
this.ensureFile(filePath);
|
|
285
|
+
if (!this.languageService) return null;
|
|
286
|
+
|
|
287
|
+
const content = this.fileContents.get(normalized);
|
|
288
|
+
if (!content) return null;
|
|
289
|
+
|
|
290
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
291
|
+
const info = this.languageService.getQuickInfoAtPosition(
|
|
292
|
+
normalized,
|
|
293
|
+
position,
|
|
294
|
+
);
|
|
295
|
+
if (!info) return null;
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
type: ts.displayPartsToString(info.displayParts),
|
|
299
|
+
documentation: info.documentation
|
|
300
|
+
? ts.displayPartsToString(info.documentation)
|
|
301
|
+
: undefined,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Go to definition
|
|
307
|
+
*/
|
|
308
|
+
getDefinition(filePath: string, line: number, character: number): Location[] {
|
|
309
|
+
const normalized = this.normalizePath(filePath);
|
|
310
|
+
this.ensureFile(filePath);
|
|
311
|
+
if (!this.languageService) return [];
|
|
312
|
+
|
|
313
|
+
const content = this.fileContents.get(normalized);
|
|
314
|
+
if (!content) return [];
|
|
315
|
+
|
|
316
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
317
|
+
const definitions = this.languageService.getDefinitionAtPosition(
|
|
318
|
+
normalized,
|
|
319
|
+
position,
|
|
320
|
+
);
|
|
321
|
+
if (!definitions) return [];
|
|
322
|
+
|
|
323
|
+
return definitions.map((def) => {
|
|
324
|
+
const _pos = def.fileName
|
|
325
|
+
? { line: 0, character: 0 }
|
|
326
|
+
: { line: 0, character: 0 };
|
|
327
|
+
// For file-based definitions, we need to get line/char from the span
|
|
328
|
+
if (def.textSpan) {
|
|
329
|
+
const defFile = def.fileName || normalized;
|
|
330
|
+
const defContent = this.fileContents.get(defFile) || "";
|
|
331
|
+
if (defContent) {
|
|
332
|
+
const lines = defContent.substring(0, def.textSpan.start).split("\n");
|
|
333
|
+
return {
|
|
334
|
+
file: defFile,
|
|
335
|
+
line: lines.length - 1,
|
|
336
|
+
character: lines[lines.length - 1].length,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return { file: def.fileName, line: 0, character: 0 };
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Get type definition
|
|
346
|
+
*/
|
|
347
|
+
getTypeDefinition(
|
|
348
|
+
filePath: string,
|
|
349
|
+
line: number,
|
|
350
|
+
character: number,
|
|
351
|
+
): Location[] {
|
|
352
|
+
const normalized = this.normalizePath(filePath);
|
|
353
|
+
this.ensureFile(filePath);
|
|
354
|
+
if (!this.languageService) return [];
|
|
355
|
+
|
|
356
|
+
const content = this.fileContents.get(normalized);
|
|
357
|
+
if (!content) return [];
|
|
358
|
+
|
|
359
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
360
|
+
const defs = this.languageService.getTypeDefinitionAtPosition(
|
|
361
|
+
normalized,
|
|
362
|
+
position,
|
|
363
|
+
);
|
|
364
|
+
if (!defs) return [];
|
|
365
|
+
|
|
366
|
+
return defs.map((def) => {
|
|
367
|
+
const defFile = def.fileName || normalized;
|
|
368
|
+
return { file: defFile, line: 0, character: 0 };
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Find references
|
|
374
|
+
*/
|
|
375
|
+
getReferences(filePath: string, line: number, character: number): Location[] {
|
|
376
|
+
const normalized = this.normalizePath(filePath);
|
|
377
|
+
this.ensureFile(filePath);
|
|
378
|
+
if (!this.languageService) return [];
|
|
379
|
+
|
|
380
|
+
const content = this.fileContents.get(normalized);
|
|
381
|
+
if (!content) return [];
|
|
382
|
+
|
|
383
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
384
|
+
const references = this.languageService.getReferencesAtPosition(
|
|
385
|
+
normalized,
|
|
386
|
+
position,
|
|
387
|
+
);
|
|
388
|
+
if (!references) return [];
|
|
389
|
+
|
|
390
|
+
return references.map((ref) => ({
|
|
391
|
+
file: ref.fileName,
|
|
392
|
+
line: 0,
|
|
393
|
+
character: 0,
|
|
394
|
+
}));
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Get document symbols
|
|
399
|
+
*/
|
|
400
|
+
getSymbols(filePath: string): SymbolInfo[] {
|
|
401
|
+
const normalized = this.normalizePath(filePath);
|
|
402
|
+
this.ensureFile(filePath);
|
|
403
|
+
if (!this.languageService) return [];
|
|
404
|
+
|
|
405
|
+
const tree = this.languageService.getNavigationTree(normalized);
|
|
406
|
+
if (!tree) return [];
|
|
407
|
+
|
|
408
|
+
const symbols: SymbolInfo[] = [];
|
|
409
|
+
|
|
410
|
+
const extract = (node: any, container?: string) => {
|
|
411
|
+
if (node.span) {
|
|
412
|
+
symbols.push({
|
|
413
|
+
name: node.text,
|
|
414
|
+
kind: this.symbolKind(node.kind),
|
|
415
|
+
line: 0,
|
|
416
|
+
containerName: container,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
if (node.childItems) {
|
|
420
|
+
for (const child of node.childItems) {
|
|
421
|
+
extract(child, node.text);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
extract(tree);
|
|
427
|
+
return symbols;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Get completions at a position
|
|
432
|
+
*/
|
|
433
|
+
getCompletions(
|
|
434
|
+
filePath: string,
|
|
435
|
+
line: number,
|
|
436
|
+
character: number,
|
|
437
|
+
): CompletionItem[] {
|
|
438
|
+
const normalized = this.normalizePath(filePath);
|
|
439
|
+
this.ensureFile(filePath);
|
|
440
|
+
if (!this.languageService) return [];
|
|
441
|
+
|
|
442
|
+
const content = this.fileContents.get(normalized);
|
|
443
|
+
if (!content) return [];
|
|
444
|
+
|
|
445
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
446
|
+
const completions = this.languageService.getCompletionsAtPosition(
|
|
447
|
+
normalized,
|
|
448
|
+
position,
|
|
449
|
+
{},
|
|
450
|
+
);
|
|
451
|
+
if (!completions) return [];
|
|
452
|
+
|
|
453
|
+
return completions.entries.slice(0, 50).map((entry) => ({
|
|
454
|
+
name: entry.name,
|
|
455
|
+
kind: this.completionKind(entry.kind),
|
|
456
|
+
sortText: entry.sortText,
|
|
457
|
+
}));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Go to implementation
|
|
462
|
+
*/
|
|
463
|
+
getImplementation(
|
|
464
|
+
filePath: string,
|
|
465
|
+
line: number,
|
|
466
|
+
character: number,
|
|
467
|
+
): Location[] {
|
|
468
|
+
const normalized = this.normalizePath(filePath);
|
|
469
|
+
this.ensureFile(filePath);
|
|
470
|
+
if (!this.languageService) return [];
|
|
471
|
+
|
|
472
|
+
const content = this.fileContents.get(normalized);
|
|
473
|
+
if (!content) return [];
|
|
474
|
+
|
|
475
|
+
const position = this.lineCharToPosition(content, line, character);
|
|
476
|
+
const implementations = this.languageService.getImplementationAtPosition(
|
|
477
|
+
normalized,
|
|
478
|
+
position,
|
|
479
|
+
);
|
|
480
|
+
if (!implementations) return [];
|
|
481
|
+
|
|
482
|
+
return implementations.map((impl) => ({
|
|
483
|
+
file: impl.fileName,
|
|
484
|
+
line: 0,
|
|
485
|
+
character: 0,
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Get folding ranges
|
|
491
|
+
*/
|
|
492
|
+
getFoldingRanges(filePath: string): FoldingRange[] {
|
|
493
|
+
const normalized = this.normalizePath(filePath);
|
|
494
|
+
this.ensureFile(filePath);
|
|
495
|
+
if (!this.languageService) return [];
|
|
496
|
+
|
|
497
|
+
const tree = this.languageService.getNavigationTree(normalized);
|
|
498
|
+
if (!tree) return [];
|
|
499
|
+
|
|
500
|
+
const ranges: FoldingRange[] = [];
|
|
501
|
+
|
|
502
|
+
const findFolds = (node: any) => {
|
|
503
|
+
if (!node?.span) return;
|
|
504
|
+
|
|
505
|
+
if (node.kind === "function" || node.kind === "class") {
|
|
506
|
+
ranges.push({
|
|
507
|
+
startLine: 0,
|
|
508
|
+
endLine: 0,
|
|
509
|
+
kind: node.kind,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (node.childItems) {
|
|
514
|
+
for (const child of node.childItems) {
|
|
515
|
+
findFolds(child);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
findFolds(tree);
|
|
521
|
+
return ranges;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Explain an error at a specific line
|
|
526
|
+
*/
|
|
527
|
+
explainError(
|
|
528
|
+
filePath: string,
|
|
529
|
+
line: number,
|
|
530
|
+
): { message: string; code?: number } | null {
|
|
531
|
+
const diagnostics = this.getDiagnostics(filePath);
|
|
532
|
+
const errorAtLine = diagnostics.find(
|
|
533
|
+
(d) => d.range.start.line === line && d.severity === 1,
|
|
534
|
+
);
|
|
535
|
+
if (!errorAtLine) return null;
|
|
536
|
+
return { message: errorAtLine.message, code: errorAtLine.code as number };
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
private symbolKind(kind: string): string {
|
|
540
|
+
const map: Record<string, string> = {
|
|
541
|
+
script: "file",
|
|
542
|
+
class: "class",
|
|
543
|
+
interface: "interface",
|
|
544
|
+
function: "function",
|
|
545
|
+
method: "method",
|
|
546
|
+
property: "property",
|
|
547
|
+
variable: "variable",
|
|
548
|
+
enum: "enum",
|
|
549
|
+
module: "module",
|
|
550
|
+
};
|
|
551
|
+
return map[kind] || "unknown";
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private completionKind(kind: string): string {
|
|
555
|
+
const map: Record<string, string> = {
|
|
556
|
+
property: "property",
|
|
557
|
+
method: "method",
|
|
558
|
+
class: "class",
|
|
559
|
+
interface: "interface",
|
|
560
|
+
enum: "enum",
|
|
561
|
+
variable: "variable",
|
|
562
|
+
function: "function",
|
|
563
|
+
keyword: "keyword",
|
|
564
|
+
};
|
|
565
|
+
return map[kind] || "text";
|
|
566
|
+
}
|
|
542
567
|
}
|