@macroforge/typescript-plugin 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ import type ts from "typescript/lib/tsserverlibrary";
2
+ declare function init(modules: {
3
+ typescript: typeof ts;
4
+ }): {
5
+ create: (info: ts.server.PluginCreateInfo) => ts.LanguageService;
6
+ };
7
+ export = init;
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,gCAAgC,CAAC;AAqDrD,iBAAS,IAAI,CAAC,OAAO,EAAE;IAAE,UAAU,EAAE,OAAO,EAAE,CAAA;CAAE;mBACxB,EAAE,CAAC,MAAM,CAAC,gBAAgB;EAsnDjD;AAED,SAAS,IAAI,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,1224 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ const macroforge_1 = require("macroforge");
6
+ const path_1 = __importDefault(require("path"));
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const FILE_EXTENSIONS = [".ts", ".tsx", ".svelte"];
9
+ function shouldProcess(fileName) {
10
+ const lower = fileName.toLowerCase();
11
+ if (lower.includes("node_modules"))
12
+ return false;
13
+ if (fileName.includes(`${path_1.default.sep}.macroforge${path_1.default.sep}`))
14
+ return false;
15
+ // Skip generated .d.ts files
16
+ if (fileName.endsWith(".macroforge.d.ts"))
17
+ return false;
18
+ return FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
19
+ }
20
+ function hasMacroDirectives(text) {
21
+ return (text.includes("@derive") ||
22
+ /\/\*\*\s*@derive\s*\(/i.test(text) ||
23
+ /\/\*\*\s*import\s+macro\b/i.test(text));
24
+ }
25
+ function loadMacroConfig(startDir) {
26
+ let current = startDir;
27
+ const fallback = { keepDecorators: false };
28
+ while (true) {
29
+ const candidate = path_1.default.join(current, "macroforge.json");
30
+ if (fs_1.default.existsSync(candidate)) {
31
+ try {
32
+ const raw = fs_1.default.readFileSync(candidate, "utf8");
33
+ const parsed = JSON.parse(raw);
34
+ return { keepDecorators: Boolean(parsed.keepDecorators) };
35
+ }
36
+ catch {
37
+ return fallback;
38
+ }
39
+ }
40
+ const parent = path_1.default.dirname(current);
41
+ if (parent === current)
42
+ break;
43
+ current = parent;
44
+ }
45
+ return fallback;
46
+ }
47
+ function init(modules) {
48
+ function create(info) {
49
+ const tsModule = modules.typescript;
50
+ // Map to store generated virtual .d.ts files
51
+ const virtualDtsFiles = new Map();
52
+ // Cache snapshots to ensure identity stability for TypeScript's incremental compiler
53
+ const snapshotCache = new Map();
54
+ // Guard against reentrancy
55
+ const processingFiles = new Set();
56
+ // Instantiate native plugin (handles caching and logging in Rust)
57
+ const nativePlugin = new macroforge_1.NativePlugin();
58
+ const getCurrentDirectory = () => info.project.getCurrentDirectory?.() ??
59
+ info.languageServiceHost.getCurrentDirectory?.() ??
60
+ process.cwd();
61
+ const macroConfig = loadMacroConfig(getCurrentDirectory());
62
+ const keepDecorators = macroConfig.keepDecorators;
63
+ // Log helper - delegates to Rust
64
+ const log = (msg) => {
65
+ const line = `[${new Date().toISOString()}] ${msg}`;
66
+ nativePlugin.log(line);
67
+ try {
68
+ info.project.projectService.logger.info(`[macroforge] ${msg}`);
69
+ }
70
+ catch { }
71
+ try {
72
+ console.error(`[macroforge] ${msg}`);
73
+ }
74
+ catch { }
75
+ };
76
+ const ensureVirtualDtsRegistered = (fileName) => {
77
+ const projectService = info.project.projectService;
78
+ const register = projectService?.getOrCreateScriptInfoNotOpenedByClient;
79
+ if (!register)
80
+ return;
81
+ try {
82
+ const scriptInfo = register(fileName, getCurrentDirectory(), info.languageServiceHost,
83
+ /*deferredDeleteOk*/ false);
84
+ if (scriptInfo?.attachToProject) {
85
+ scriptInfo.attachToProject(info.project);
86
+ }
87
+ }
88
+ catch (error) {
89
+ log(`Failed to register virtual .d.ts ${fileName}: ${error instanceof Error ? error.message : String(error)}`);
90
+ }
91
+ };
92
+ const cleanupVirtualDts = (fileName) => {
93
+ const projectService = info.project.projectService;
94
+ const getScriptInfo = projectService?.getScriptInfo;
95
+ if (!getScriptInfo)
96
+ return;
97
+ try {
98
+ const scriptInfo = getScriptInfo.call(projectService, fileName);
99
+ if (!scriptInfo)
100
+ return;
101
+ scriptInfo.detachFromProject?.(info.project);
102
+ if (!scriptInfo.isScriptOpen?.() &&
103
+ scriptInfo.containingProjects?.length === 0) {
104
+ projectService.deleteScriptInfo?.(scriptInfo);
105
+ }
106
+ }
107
+ catch (error) {
108
+ log(`Failed to clean up virtual .d.ts ${fileName}: ${error instanceof Error ? error.message : String(error)}`);
109
+ }
110
+ };
111
+ const projectService = info.project.projectService;
112
+ if (projectService?.setDocument) {
113
+ projectService.setDocument = (key, filePath, sourceFile) => {
114
+ try {
115
+ const scriptInfo = projectService.getScriptInfoForPath?.(filePath) ??
116
+ projectService.getOrCreateScriptInfoNotOpenedByClient?.(filePath, getCurrentDirectory(), info.languageServiceHost,
117
+ /*deferredDeleteOk*/ false);
118
+ if (!scriptInfo) {
119
+ log(`Skipping cache write for missing ScriptInfo at ${filePath}`);
120
+ return;
121
+ }
122
+ scriptInfo.attachToProject?.(info.project);
123
+ // Mirror the behavior of the original setDocument but avoid throwing when ScriptInfo is absent.
124
+ scriptInfo.cacheSourceFile = { key, sourceFile };
125
+ }
126
+ catch (error) {
127
+ log(`Error in guarded setDocument for ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
128
+ }
129
+ };
130
+ }
131
+ // Log plugin initialization
132
+ log("Plugin initialized");
133
+ // Process file through macro expansion (caching handled in Rust)
134
+ function processFile(fileName, content, version) {
135
+ // Fast Exit: Empty Content
136
+ if (!content || content.trim().length === 0) {
137
+ return {
138
+ result: {
139
+ code: content,
140
+ types: undefined,
141
+ metadata: undefined,
142
+ diagnostics: [],
143
+ sourceMapping: undefined,
144
+ },
145
+ code: content,
146
+ };
147
+ }
148
+ try {
149
+ log(`Processing ${fileName}`);
150
+ const result = nativePlugin.processFile(fileName, content, {
151
+ keepDecorators,
152
+ version,
153
+ });
154
+ // Update virtual .d.ts files
155
+ const virtualDtsFileName = fileName + ".macroforge.d.ts";
156
+ if (result.types) {
157
+ virtualDtsFiles.set(virtualDtsFileName, tsModule.ScriptSnapshot.fromString(result.types));
158
+ ensureVirtualDtsRegistered(virtualDtsFileName);
159
+ log(`Generated virtual .d.ts for ${fileName}`);
160
+ }
161
+ else {
162
+ virtualDtsFiles.delete(virtualDtsFileName);
163
+ cleanupVirtualDts(virtualDtsFileName);
164
+ }
165
+ return { result, code: result.code };
166
+ }
167
+ catch (e) {
168
+ const errorMessage = e instanceof Error ? e.stack || e.message : String(e);
169
+ log(`Plugin expansion failed for ${fileName}: ${errorMessage}`);
170
+ virtualDtsFiles.delete(fileName + ".macroforge.d.ts");
171
+ cleanupVirtualDts(fileName + ".macroforge.d.ts");
172
+ return {
173
+ result: {
174
+ code: content,
175
+ types: undefined,
176
+ metadata: undefined,
177
+ diagnostics: [],
178
+ sourceMapping: undefined,
179
+ },
180
+ code: content,
181
+ };
182
+ }
183
+ }
184
+ // Hook getScriptVersion to provide versions for virtual .d.ts files
185
+ const originalGetScriptVersion = info.languageServiceHost.getScriptVersion.bind(info.languageServiceHost);
186
+ info.languageServiceHost.getScriptVersion = (fileName) => {
187
+ try {
188
+ if (virtualDtsFiles.has(fileName)) {
189
+ const sourceFileName = fileName.replace(".macroforge.d.ts", "");
190
+ return originalGetScriptVersion(sourceFileName);
191
+ }
192
+ return originalGetScriptVersion(fileName);
193
+ }
194
+ catch (e) {
195
+ log(`Error in getScriptVersion: ${e instanceof Error ? e.message : String(e)}`);
196
+ return originalGetScriptVersion(fileName);
197
+ }
198
+ };
199
+ // Hook getScriptFileNames to include our virtual .d.ts files
200
+ // This allows TS to "see" these new files as part of the project
201
+ const originalGetScriptFileNames = info.languageServiceHost
202
+ .getScriptFileNames
203
+ ? info.languageServiceHost.getScriptFileNames.bind(info.languageServiceHost)
204
+ : () => [];
205
+ info.languageServiceHost.getScriptFileNames = () => {
206
+ try {
207
+ const originalFiles = originalGetScriptFileNames();
208
+ return [...originalFiles, ...Array.from(virtualDtsFiles.keys())];
209
+ }
210
+ catch (e) {
211
+ log(`Error in getScriptFileNames: ${e instanceof Error ? e.message : String(e)}`);
212
+ return originalGetScriptFileNames();
213
+ }
214
+ };
215
+ // Hook fileExists to resolve our virtual .d.ts files
216
+ const originalFileExists = info.languageServiceHost.fileExists
217
+ ? info.languageServiceHost.fileExists.bind(info.languageServiceHost)
218
+ : tsModule.sys.fileExists;
219
+ info.languageServiceHost.fileExists = (fileName) => {
220
+ try {
221
+ if (virtualDtsFiles.has(fileName)) {
222
+ return true;
223
+ }
224
+ return originalFileExists(fileName);
225
+ }
226
+ catch (e) {
227
+ log(`Error in fileExists: ${e instanceof Error ? e.message : String(e)}`);
228
+ return originalFileExists(fileName);
229
+ }
230
+ };
231
+ // Hook getScriptSnapshot to provide the "expanded" type definition view
232
+ const originalGetScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost);
233
+ info.languageServiceHost.getScriptSnapshot = (fileName) => {
234
+ try {
235
+ log(`getScriptSnapshot: ${fileName}`);
236
+ // If it's one of our virtual .d.ts files, return its snapshot
237
+ if (virtualDtsFiles.has(fileName)) {
238
+ log(` -> virtual .d.ts cache hit`);
239
+ return virtualDtsFiles.get(fileName);
240
+ }
241
+ // Guard against reentrancy - if we're already processing this file, return original
242
+ if (processingFiles.has(fileName)) {
243
+ log(` -> REENTRANCY DETECTED, returning original`);
244
+ return originalGetScriptSnapshot(fileName);
245
+ }
246
+ // Don't process non-TypeScript files or .expanded.ts files
247
+ if (!shouldProcess(fileName)) {
248
+ log(` -> not processable (excluded file), returning original`);
249
+ return originalGetScriptSnapshot(fileName);
250
+ }
251
+ const snapshot = originalGetScriptSnapshot(fileName);
252
+ if (!snapshot) {
253
+ // Avoid tsserver crashes when a file was reported but no snapshot exists
254
+ log(` -> no snapshot available for ${fileName}, returning empty snapshot`);
255
+ return tsModule.ScriptSnapshot.fromString("");
256
+ }
257
+ const text = snapshot.getText(0, snapshot.getLength());
258
+ // Only process files with macro directives
259
+ if (!hasMacroDirectives(text)) {
260
+ log(` -> no macro directives, returning original`);
261
+ return snapshot;
262
+ }
263
+ log(` -> has @derive, expanding...`);
264
+ // Mark as processing to prevent reentrancy
265
+ processingFiles.add(fileName);
266
+ try {
267
+ const version = info.languageServiceHost.getScriptVersion(fileName);
268
+ log(` -> version: ${version}`);
269
+ // Check if we have a cached snapshot for this version
270
+ const cached = snapshotCache.get(fileName);
271
+ if (cached && cached.version === version) {
272
+ log(` -> snapshot cache hit`);
273
+ return cached.snapshot;
274
+ }
275
+ const { code } = processFile(fileName, text, version);
276
+ log(` -> processFile returned`);
277
+ if (code && code !== text) {
278
+ log(` -> creating expanded snapshot (${code.length} chars)`);
279
+ const expandedSnapshot = tsModule.ScriptSnapshot.fromString(code);
280
+ // Cache the snapshot for stable identity
281
+ snapshotCache.set(fileName, {
282
+ version,
283
+ snapshot: expandedSnapshot,
284
+ });
285
+ log(` -> returning expanded snapshot`);
286
+ return expandedSnapshot;
287
+ }
288
+ // Cache the original snapshot
289
+ snapshotCache.set(fileName, { version, snapshot });
290
+ return snapshot;
291
+ }
292
+ finally {
293
+ processingFiles.delete(fileName);
294
+ }
295
+ }
296
+ catch (e) {
297
+ log(`ERROR in getScriptSnapshot for ${fileName}: ${e instanceof Error ? e.stack || e.message : String(e)}`);
298
+ // Make sure we clean up on error
299
+ processingFiles.delete(fileName);
300
+ return originalGetScriptSnapshot(fileName);
301
+ }
302
+ };
303
+ function toPlainDiagnostic(diag) {
304
+ const message = typeof diag.messageText === "string"
305
+ ? diag.messageText
306
+ : diag.messageText.messageText;
307
+ const category = diag.category === tsModule.DiagnosticCategory.Error
308
+ ? "error"
309
+ : diag.category === tsModule.DiagnosticCategory.Warning
310
+ ? "warning"
311
+ : "message";
312
+ return {
313
+ start: diag.start,
314
+ length: diag.length,
315
+ message,
316
+ code: diag.code,
317
+ category,
318
+ };
319
+ }
320
+ function applyMappedDiagnostics(original, mapped) {
321
+ return original.map((diag, idx) => {
322
+ const mappedDiag = mapped[idx];
323
+ if (!mappedDiag ||
324
+ mappedDiag.start === undefined ||
325
+ mappedDiag.length === undefined) {
326
+ return diag;
327
+ }
328
+ return {
329
+ ...diag,
330
+ start: mappedDiag.start,
331
+ length: mappedDiag.length,
332
+ };
333
+ });
334
+ }
335
+ // Hook getSemanticDiagnostics to provide macro errors and map positions
336
+ const originalGetSemanticDiagnostics = info.languageService.getSemanticDiagnostics.bind(info.languageService);
337
+ info.languageService.getSemanticDiagnostics = (fileName) => {
338
+ try {
339
+ log(`getSemanticDiagnostics: ${fileName}`);
340
+ // If it's one of our virtual .d.ts files, don't get diagnostics for it
341
+ if (virtualDtsFiles.has(fileName)) {
342
+ log(` -> skipping virtual .d.ts`);
343
+ return [];
344
+ }
345
+ if (!shouldProcess(fileName)) {
346
+ log(` -> not processable, using original`);
347
+ return originalGetSemanticDiagnostics(fileName);
348
+ }
349
+ log(` -> getting original diagnostics...`);
350
+ const expandedDiagnostics = originalGetSemanticDiagnostics(fileName);
351
+ log(` -> got ${expandedDiagnostics.length} diagnostics`);
352
+ // Map diagnostics using mapper
353
+ const effectiveMapper = nativePlugin.getMapper(fileName);
354
+ let mappedDiagnostics;
355
+ // Collect diagnostics in generated code to report them at decorator positions
356
+ const generatedCodeDiagnostics = [];
357
+ if (effectiveMapper && !effectiveMapper.isEmpty()) {
358
+ log(` -> mapping diagnostics with mapper`);
359
+ mappedDiagnostics = expandedDiagnostics
360
+ .map((diag) => {
361
+ if (diag.start === undefined || diag.length === undefined) {
362
+ return diag;
363
+ }
364
+ const mapped = effectiveMapper.mapSpanToOriginal(diag.start, diag.length);
365
+ if (!mapped) {
366
+ // Diagnostic is in generated code - check if we should convert it
367
+ if (effectiveMapper.isInGenerated(diag.start)) {
368
+ // This is an error in macro-generated code
369
+ // Collect it to report at decorator position
370
+ const macroName = effectiveMapper.generatedBy(diag.start);
371
+ log(` -> collecting diagnostic in generated code (macro: ${macroName}): "${diag.messageText}"`);
372
+ generatedCodeDiagnostics.push(diag);
373
+ return null;
374
+ }
375
+ return diag;
376
+ }
377
+ return {
378
+ ...diag,
379
+ start: mapped.start,
380
+ length: mapped.length,
381
+ };
382
+ })
383
+ .filter((diag) => diag !== null);
384
+ }
385
+ else {
386
+ // Native plugin is guaranteed to exist after early return check
387
+ log(` -> mapping diagnostics in native plugin`);
388
+ mappedDiagnostics = applyMappedDiagnostics(expandedDiagnostics, nativePlugin.mapDiagnostics(fileName, expandedDiagnostics.map(toPlainDiagnostic)));
389
+ }
390
+ // Get macro diagnostics from Rust (hits cache if already expanded)
391
+ const snapshot = originalGetScriptSnapshot(fileName);
392
+ if (!snapshot) {
393
+ return mappedDiagnostics;
394
+ }
395
+ const text = snapshot.getText(0, snapshot.getLength());
396
+ const version = info.languageServiceHost.getScriptVersion(fileName);
397
+ const { result } = processFile(fileName, text, version);
398
+ // Convert diagnostics from generated code to macro diagnostics
399
+ // pointing to the specific macro name within the decorator
400
+ const generatedDiagsAsMacro = [];
401
+ if (generatedCodeDiagnostics.length > 0 && result.sourceMapping) {
402
+ // Find all @derive decorators with their macro arguments
403
+ const deriveRegex = /@derive\s*\(([^)]*)\)/g;
404
+ const deriveDecorators = [];
405
+ let match;
406
+ while ((match = deriveRegex.exec(text)) !== null) {
407
+ const fullStart = match.index;
408
+ const fullLength = match[0].length;
409
+ const argsStart = match.index + match[0].indexOf("(") + 1;
410
+ const argsText = match[1];
411
+ // Parse individual macro names from the arguments
412
+ const macros = [];
413
+ const macroNameRegex = /([A-Za-z_][A-Za-z0-9_]*)/g;
414
+ let macroMatch;
415
+ while ((macroMatch = macroNameRegex.exec(argsText)) !== null) {
416
+ macros.push({
417
+ name: macroMatch[1],
418
+ start: argsStart + macroMatch.index,
419
+ length: macroMatch[1].length,
420
+ });
421
+ }
422
+ deriveDecorators.push({ fullStart, fullLength, macros });
423
+ }
424
+ // Helper to find the specific macro position for a given expanded position
425
+ const findMacroPosition = (expandedPos, macroName) => {
426
+ // Find the generated region containing this position
427
+ const region = result.sourceMapping.generatedRegions.find((r) => expandedPos >= r.start && expandedPos < r.end);
428
+ if (!region) {
429
+ // Fallback to first decorator
430
+ const firstDec = deriveDecorators[0];
431
+ if (firstDec) {
432
+ const macro = firstDec.macros.find((m) => m.name === macroName);
433
+ if (macro)
434
+ return { start: macro.start, length: macro.length };
435
+ return {
436
+ start: firstDec.fullStart,
437
+ length: firstDec.fullLength,
438
+ };
439
+ }
440
+ return { start: 0, length: 7 };
441
+ }
442
+ // Find the segment that ends right before this generated region
443
+ const segments = result.sourceMapping.segments;
444
+ let insertionPointInOriginal = 0;
445
+ for (const seg of segments) {
446
+ if (seg.expandedEnd <= region.start) {
447
+ insertionPointInOriginal = seg.originalEnd;
448
+ }
449
+ }
450
+ // Find the nearest @derive decorator before this insertion point
451
+ let nearestDecorator = deriveDecorators[0];
452
+ for (const dec of deriveDecorators) {
453
+ if (dec.fullStart < insertionPointInOriginal) {
454
+ nearestDecorator = dec;
455
+ }
456
+ else {
457
+ break;
458
+ }
459
+ }
460
+ if (nearestDecorator) {
461
+ // Find the specific macro within this decorator
462
+ const macro = nearestDecorator.macros.find((m) => m.name === macroName);
463
+ if (macro) {
464
+ return { start: macro.start, length: macro.length };
465
+ }
466
+ // If macro name is "macro" (generic fallback) and there's exactly one macro,
467
+ // use that macro's position
468
+ if ((macroName === "macro" || macroName === "") &&
469
+ nearestDecorator.macros.length === 1) {
470
+ const onlyMacro = nearestDecorator.macros[0];
471
+ return { start: onlyMacro.start, length: onlyMacro.length };
472
+ }
473
+ // Fallback to full decorator if macro not found
474
+ return {
475
+ start: nearestDecorator.fullStart,
476
+ length: nearestDecorator.fullLength,
477
+ };
478
+ }
479
+ return { start: 0, length: 7 };
480
+ };
481
+ for (const diag of generatedCodeDiagnostics) {
482
+ const diagStart = diag.start ?? 0;
483
+ // Try to get the macro name from the mapper or from the generated region
484
+ let macroName = effectiveMapper?.generatedBy(diagStart) ?? null;
485
+ // If mapper didn't return a name, try to get it from the generated region
486
+ if (!macroName) {
487
+ const region = result.sourceMapping.generatedRegions.find((r) => diagStart >= r.start && diagStart < r.end);
488
+ macroName = region?.sourceMacro ?? "macro";
489
+ }
490
+ // Extract just the macro name if it contains a path (e.g., "derive::Debug" -> "Debug")
491
+ const simpleMacroName = macroName.includes("::")
492
+ ? (macroName.split("::").pop() ?? macroName)
493
+ : macroName;
494
+ log(` -> diagnostic at ${diagStart}, macroName="${macroName}", simpleMacroName="${simpleMacroName}"`);
495
+ log(` -> generatedRegions: ${JSON.stringify(result.sourceMapping.generatedRegions)}`);
496
+ log(` -> deriveDecorators: ${JSON.stringify(deriveDecorators.map((d) => ({ fullStart: d.fullStart, macros: d.macros })))}`);
497
+ const position = findMacroPosition(diagStart, simpleMacroName);
498
+ log(` -> resolved position: ${JSON.stringify(position)}`);
499
+ generatedDiagsAsMacro.push({
500
+ file: info.languageService.getProgram()?.getSourceFile(fileName),
501
+ start: position.start,
502
+ length: position.length,
503
+ messageText: `[${simpleMacroName}] ${typeof diag.messageText === "string" ? diag.messageText : diag.messageText.messageText}`,
504
+ category: diag.category,
505
+ code: 9998, // Different code for generated code errors
506
+ source: "macroforge-generated",
507
+ });
508
+ }
509
+ log(` -> converted ${generatedDiagsAsMacro.length} generated code diagnostics`);
510
+ }
511
+ else if (generatedCodeDiagnostics.length > 0) {
512
+ // Fallback when no source mapping available
513
+ const deriveMatch = text.match(/@derive\s*\(/);
514
+ const decoratorStart = deriveMatch?.index ?? 0;
515
+ const decoratorLength = deriveMatch?.[0].length ?? 7;
516
+ for (const diag of generatedCodeDiagnostics) {
517
+ const macroName = effectiveMapper?.generatedBy(diag.start ?? 0) ?? "macro";
518
+ generatedDiagsAsMacro.push({
519
+ file: info.languageService.getProgram()?.getSourceFile(fileName),
520
+ start: decoratorStart,
521
+ length: decoratorLength,
522
+ messageText: `[${macroName}] ${typeof diag.messageText === "string" ? diag.messageText : diag.messageText.messageText}`,
523
+ category: diag.category,
524
+ code: 9998, // Different code for generated code errors
525
+ source: "macroforge-generated",
526
+ });
527
+ }
528
+ log(` -> converted ${generatedDiagsAsMacro.length} generated code diagnostics (fallback)`);
529
+ }
530
+ if (!result.diagnostics || result.diagnostics.length === 0) {
531
+ return [...mappedDiagnostics, ...generatedDiagsAsMacro];
532
+ }
533
+ const macroDiagnostics = result.diagnostics.map((d) => {
534
+ const category = d.level === "error"
535
+ ? tsModule.DiagnosticCategory.Error
536
+ : d.level === "warning"
537
+ ? tsModule.DiagnosticCategory.Warning
538
+ : tsModule.DiagnosticCategory.Message;
539
+ return {
540
+ file: info.languageService.getProgram()?.getSourceFile(fileName),
541
+ start: d.start || 0,
542
+ length: (d.end || 0) - (d.start || 0),
543
+ messageText: d.message,
544
+ category,
545
+ code: 9999, // Custom error code
546
+ source: "macroforge",
547
+ };
548
+ });
549
+ return [
550
+ ...mappedDiagnostics,
551
+ ...macroDiagnostics,
552
+ ...generatedDiagsAsMacro,
553
+ ];
554
+ }
555
+ catch (e) {
556
+ log(`Error in getSemanticDiagnostics for ${fileName}: ${e instanceof Error ? e.stack || e.message : String(e)}`);
557
+ return originalGetSemanticDiagnostics(fileName);
558
+ }
559
+ };
560
+ // Hook getSyntacticDiagnostics to map positions
561
+ const originalGetSyntacticDiagnostics = info.languageService.getSyntacticDiagnostics.bind(info.languageService);
562
+ info.languageService.getSyntacticDiagnostics = (fileName) => {
563
+ try {
564
+ log(`getSyntacticDiagnostics: ${fileName}`);
565
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
566
+ log(` -> using original`);
567
+ return originalGetSyntacticDiagnostics(fileName);
568
+ }
569
+ // Ensure mapper ready
570
+ nativePlugin.getMapper(fileName);
571
+ const expandedDiagnostics = originalGetSyntacticDiagnostics(fileName);
572
+ log(` -> got ${expandedDiagnostics.length} diagnostics, mapping...`);
573
+ // Native plugin is guaranteed to exist after early return check
574
+ const result = applyMappedDiagnostics(expandedDiagnostics, nativePlugin.mapDiagnostics(fileName, expandedDiagnostics.map(toPlainDiagnostic)));
575
+ log(` -> returning ${result.length} mapped diagnostics`);
576
+ return result;
577
+ }
578
+ catch (e) {
579
+ log(`ERROR in getSyntacticDiagnostics: ${e instanceof Error ? e.stack || e.message : String(e)}`);
580
+ return originalGetSyntacticDiagnostics(fileName);
581
+ }
582
+ };
583
+ // Hook getQuickInfoAtPosition to map input position and output spans
584
+ const originalGetQuickInfoAtPosition = info.languageService.getQuickInfoAtPosition.bind(info.languageService);
585
+ info.languageService.getQuickInfoAtPosition = (fileName, position) => {
586
+ try {
587
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
588
+ return originalGetQuickInfoAtPosition(fileName, position);
589
+ }
590
+ const mapper = nativePlugin.getMapper(fileName);
591
+ if (!mapper) {
592
+ return originalGetQuickInfoAtPosition(fileName, position);
593
+ }
594
+ // Map original position to expanded
595
+ const expandedPos = mapper.originalToExpanded(position);
596
+ const result = originalGetQuickInfoAtPosition(fileName, expandedPos);
597
+ if (!result)
598
+ return result;
599
+ // Map result spans back to original
600
+ const mappedTextSpan = mapper.mapSpanToOriginal(result.textSpan.start, result.textSpan.length);
601
+ if (!mappedTextSpan)
602
+ return undefined; // In generated code - hide hover
603
+ return {
604
+ ...result,
605
+ textSpan: {
606
+ start: mappedTextSpan.start,
607
+ length: mappedTextSpan.length,
608
+ },
609
+ };
610
+ }
611
+ catch (e) {
612
+ log(`Error in getQuickInfoAtPosition: ${e instanceof Error ? e.message : String(e)}`);
613
+ return originalGetQuickInfoAtPosition(fileName, position);
614
+ }
615
+ };
616
+ // Hook getCompletionsAtPosition to map input position
617
+ const originalGetCompletionsAtPosition = info.languageService.getCompletionsAtPosition.bind(info.languageService);
618
+ info.languageService.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
619
+ try {
620
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
621
+ return originalGetCompletionsAtPosition(fileName, position, options, formattingSettings);
622
+ }
623
+ const mapper = nativePlugin.getMapper(fileName);
624
+ if (!mapper) {
625
+ return originalGetCompletionsAtPosition(fileName, position, options, formattingSettings);
626
+ }
627
+ const expandedPos = mapper.originalToExpanded(position);
628
+ const result = originalGetCompletionsAtPosition(fileName, expandedPos, options, formattingSettings);
629
+ if (!result)
630
+ return result;
631
+ // Map optionalReplacementSpan if present
632
+ let mappedOptionalSpan = undefined;
633
+ if (result.optionalReplacementSpan) {
634
+ const mapped = mapper.mapSpanToOriginal(result.optionalReplacementSpan.start, result.optionalReplacementSpan.length);
635
+ if (mapped) {
636
+ mappedOptionalSpan = { start: mapped.start, length: mapped.length };
637
+ }
638
+ }
639
+ // Map entries replacementSpan
640
+ const mappedEntries = result.entries.map((entry) => {
641
+ if (!entry.replacementSpan)
642
+ return entry;
643
+ const mapped = mapper.mapSpanToOriginal(entry.replacementSpan.start, entry.replacementSpan.length);
644
+ if (!mapped)
645
+ return { ...entry, replacementSpan: undefined }; // Remove invalid span
646
+ return {
647
+ ...entry,
648
+ replacementSpan: { start: mapped.start, length: mapped.length },
649
+ };
650
+ });
651
+ return {
652
+ ...result,
653
+ optionalReplacementSpan: mappedOptionalSpan,
654
+ entries: mappedEntries,
655
+ };
656
+ }
657
+ catch (e) {
658
+ log(`Error in getCompletionsAtPosition: ${e instanceof Error ? e.message : String(e)}`);
659
+ return originalGetCompletionsAtPosition(fileName, position, options, formattingSettings);
660
+ }
661
+ };
662
+ // Hook getDefinitionAtPosition to map input and output positions
663
+ const originalGetDefinitionAtPosition = info.languageService.getDefinitionAtPosition.bind(info.languageService);
664
+ info.languageService.getDefinitionAtPosition = (fileName, position) => {
665
+ try {
666
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
667
+ return originalGetDefinitionAtPosition(fileName, position);
668
+ }
669
+ const mapper = nativePlugin.getMapper(fileName);
670
+ if (!mapper) {
671
+ return originalGetDefinitionAtPosition(fileName, position);
672
+ }
673
+ const expandedPos = mapper.originalToExpanded(position);
674
+ const definitions = originalGetDefinitionAtPosition(fileName, expandedPos);
675
+ if (!definitions)
676
+ return definitions;
677
+ // Map each definition's span back to original (only for same file)
678
+ return definitions.reduce((acc, def) => {
679
+ if (def.fileName !== fileName) {
680
+ acc.push(def);
681
+ return acc;
682
+ }
683
+ const defMapper = nativePlugin.getMapper(def.fileName);
684
+ if (!defMapper) {
685
+ acc.push(def);
686
+ return acc;
687
+ }
688
+ const mapped = defMapper.mapSpanToOriginal(def.textSpan.start, def.textSpan.length);
689
+ if (mapped) {
690
+ acc.push({
691
+ ...def,
692
+ textSpan: { start: mapped.start, length: mapped.length },
693
+ });
694
+ }
695
+ return acc;
696
+ }, []);
697
+ }
698
+ catch (e) {
699
+ log(`Error in getDefinitionAtPosition: ${e instanceof Error ? e.message : String(e)}`);
700
+ return originalGetDefinitionAtPosition(fileName, position);
701
+ }
702
+ };
703
+ // Hook getDefinitionAndBoundSpan for more complete definition handling
704
+ const originalGetDefinitionAndBoundSpan = info.languageService.getDefinitionAndBoundSpan.bind(info.languageService);
705
+ info.languageService.getDefinitionAndBoundSpan = (fileName, position) => {
706
+ try {
707
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
708
+ return originalGetDefinitionAndBoundSpan(fileName, position);
709
+ }
710
+ const mapper = nativePlugin.getMapper(fileName);
711
+ if (!mapper) {
712
+ return originalGetDefinitionAndBoundSpan(fileName, position);
713
+ }
714
+ const expandedPos = mapper.originalToExpanded(position);
715
+ const result = originalGetDefinitionAndBoundSpan(fileName, expandedPos);
716
+ if (!result)
717
+ return result;
718
+ // Map textSpan back to original
719
+ const mappedTextSpan = mapper.mapSpanToOriginal(result.textSpan.start, result.textSpan.length);
720
+ if (!mappedTextSpan)
721
+ return undefined; // In generated code
722
+ // Map each definition's span
723
+ const mappedDefinitions = result.definitions?.reduce((acc, def) => {
724
+ if (def.fileName !== fileName) {
725
+ acc.push(def);
726
+ return acc;
727
+ }
728
+ const defMapper = nativePlugin.getMapper(def.fileName);
729
+ if (!defMapper) {
730
+ acc.push(def);
731
+ return acc;
732
+ }
733
+ const mapped = defMapper.mapSpanToOriginal(def.textSpan.start, def.textSpan.length);
734
+ if (mapped) {
735
+ acc.push({
736
+ ...def,
737
+ textSpan: { start: mapped.start, length: mapped.length },
738
+ });
739
+ }
740
+ return acc;
741
+ }, []);
742
+ return {
743
+ textSpan: {
744
+ start: mappedTextSpan.start,
745
+ length: mappedTextSpan.length,
746
+ },
747
+ definitions: mappedDefinitions,
748
+ };
749
+ }
750
+ catch (e) {
751
+ log(`Error in getDefinitionAndBoundSpan: ${e instanceof Error ? e.message : String(e)}`);
752
+ return originalGetDefinitionAndBoundSpan(fileName, position);
753
+ }
754
+ };
755
+ // Hook getTypeDefinitionAtPosition
756
+ const originalGetTypeDefinitionAtPosition = info.languageService.getTypeDefinitionAtPosition.bind(info.languageService);
757
+ info.languageService.getTypeDefinitionAtPosition = (fileName, position) => {
758
+ try {
759
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
760
+ return originalGetTypeDefinitionAtPosition(fileName, position);
761
+ }
762
+ const mapper = nativePlugin.getMapper(fileName);
763
+ if (!mapper) {
764
+ return originalGetTypeDefinitionAtPosition(fileName, position);
765
+ }
766
+ const expandedPos = mapper.originalToExpanded(position);
767
+ const definitions = originalGetTypeDefinitionAtPosition(fileName, expandedPos);
768
+ if (!definitions)
769
+ return definitions;
770
+ return definitions.reduce((acc, def) => {
771
+ if (def.fileName !== fileName) {
772
+ acc.push(def);
773
+ return acc;
774
+ }
775
+ const defMapper = nativePlugin.getMapper(def.fileName);
776
+ if (!defMapper) {
777
+ acc.push(def);
778
+ return acc;
779
+ }
780
+ const mapped = defMapper.mapSpanToOriginal(def.textSpan.start, def.textSpan.length);
781
+ if (mapped) {
782
+ acc.push({
783
+ ...def,
784
+ textSpan: { start: mapped.start, length: mapped.length },
785
+ });
786
+ }
787
+ return acc;
788
+ }, []);
789
+ }
790
+ catch (e) {
791
+ log(`Error in getTypeDefinitionAtPosition: ${e instanceof Error ? e.message : String(e)}`);
792
+ return originalGetTypeDefinitionAtPosition(fileName, position);
793
+ }
794
+ };
795
+ // Hook getReferencesAtPosition
796
+ const originalGetReferencesAtPosition = info.languageService.getReferencesAtPosition.bind(info.languageService);
797
+ info.languageService.getReferencesAtPosition = (fileName, position) => {
798
+ try {
799
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
800
+ return originalGetReferencesAtPosition(fileName, position);
801
+ }
802
+ const mapper = nativePlugin.getMapper(fileName);
803
+ if (!mapper) {
804
+ return originalGetReferencesAtPosition(fileName, position);
805
+ }
806
+ const expandedPos = mapper.originalToExpanded(position);
807
+ const refs = originalGetReferencesAtPosition(fileName, expandedPos);
808
+ if (!refs)
809
+ return refs;
810
+ return refs.reduce((acc, ref) => {
811
+ if (!shouldProcess(ref.fileName)) {
812
+ acc.push(ref);
813
+ return acc;
814
+ }
815
+ const refMapper = nativePlugin.getMapper(ref.fileName);
816
+ if (!refMapper) {
817
+ acc.push(ref);
818
+ return acc;
819
+ }
820
+ const mapped = refMapper.mapSpanToOriginal(ref.textSpan.start, ref.textSpan.length);
821
+ if (mapped) {
822
+ acc.push({
823
+ ...ref,
824
+ textSpan: { start: mapped.start, length: mapped.length },
825
+ });
826
+ }
827
+ return acc;
828
+ }, []);
829
+ }
830
+ catch (e) {
831
+ log(`Error in getReferencesAtPosition: ${e instanceof Error ? e.message : String(e)}`);
832
+ return originalGetReferencesAtPosition(fileName, position);
833
+ }
834
+ };
835
+ // Hook findReferences
836
+ const originalFindReferences = info.languageService.findReferences.bind(info.languageService);
837
+ info.languageService.findReferences = (fileName, position) => {
838
+ try {
839
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
840
+ return originalFindReferences(fileName, position);
841
+ }
842
+ const mapper = nativePlugin.getMapper(fileName);
843
+ if (!mapper) {
844
+ return originalFindReferences(fileName, position);
845
+ }
846
+ const expandedPos = mapper.originalToExpanded(position);
847
+ const refSymbols = originalFindReferences(fileName, expandedPos);
848
+ if (!refSymbols)
849
+ return refSymbols;
850
+ return refSymbols
851
+ .map((refSymbol) => ({
852
+ ...refSymbol,
853
+ references: refSymbol.references.reduce((acc, ref) => {
854
+ if (!shouldProcess(ref.fileName)) {
855
+ acc.push(ref);
856
+ return acc;
857
+ }
858
+ const refMapper = nativePlugin.getMapper(ref.fileName);
859
+ if (!refMapper) {
860
+ acc.push(ref);
861
+ return acc;
862
+ }
863
+ const mapped = refMapper.mapSpanToOriginal(ref.textSpan.start, ref.textSpan.length);
864
+ if (mapped) {
865
+ acc.push({
866
+ ...ref,
867
+ textSpan: { start: mapped.start, length: mapped.length },
868
+ });
869
+ }
870
+ return acc;
871
+ }, []),
872
+ }))
873
+ .filter((s) => s.references.length > 0);
874
+ }
875
+ catch (e) {
876
+ log(`Error in findReferences: ${e instanceof Error ? e.message : String(e)}`);
877
+ return originalFindReferences(fileName, position);
878
+ }
879
+ };
880
+ // Hook getSignatureHelpItems
881
+ const originalGetSignatureHelpItems = info.languageService.getSignatureHelpItems.bind(info.languageService);
882
+ info.languageService.getSignatureHelpItems = (fileName, position, options) => {
883
+ try {
884
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
885
+ return originalGetSignatureHelpItems(fileName, position, options);
886
+ }
887
+ const mapper = nativePlugin.getMapper(fileName);
888
+ if (!mapper) {
889
+ return originalGetSignatureHelpItems(fileName, position, options);
890
+ }
891
+ const expandedPos = mapper.originalToExpanded(position);
892
+ const result = originalGetSignatureHelpItems(fileName, expandedPos, options);
893
+ if (!result)
894
+ return result;
895
+ // Map applicableSpan back to original
896
+ const mappedSpan = mapper.mapSpanToOriginal(result.applicableSpan.start, result.applicableSpan.length);
897
+ if (!mappedSpan)
898
+ return undefined;
899
+ return {
900
+ ...result,
901
+ applicableSpan: {
902
+ start: mappedSpan.start,
903
+ length: mappedSpan.length,
904
+ },
905
+ };
906
+ }
907
+ catch (e) {
908
+ log(`Error in getSignatureHelpItems: ${e instanceof Error ? e.message : String(e)}`);
909
+ return originalGetSignatureHelpItems(fileName, position, options);
910
+ }
911
+ };
912
+ // Hook getRenameInfo
913
+ const originalGetRenameInfo = info.languageService.getRenameInfo.bind(info.languageService);
914
+ const callGetRenameInfo = (fileName, position, options) => {
915
+ // Prefer object overload if available; otherwise fall back to legacy args
916
+ if (originalGetRenameInfo.length <= 2) {
917
+ return originalGetRenameInfo(fileName, position, options);
918
+ }
919
+ return originalGetRenameInfo(fileName, position, options?.allowRenameOfImportPath);
920
+ };
921
+ info.languageService.getRenameInfo = (fileName, position, options) => {
922
+ try {
923
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
924
+ return callGetRenameInfo(fileName, position, options);
925
+ }
926
+ const mapper = nativePlugin.getMapper(fileName);
927
+ if (!mapper) {
928
+ return callGetRenameInfo(fileName, position, options);
929
+ }
930
+ const expandedPos = mapper.originalToExpanded(position);
931
+ const result = callGetRenameInfo(fileName, expandedPos, options);
932
+ if (!result.canRename || !result.triggerSpan)
933
+ return result;
934
+ const mappedSpan = mapper.mapSpanToOriginal(result.triggerSpan.start, result.triggerSpan.length);
935
+ if (!mappedSpan) {
936
+ return {
937
+ canRename: false,
938
+ localizedErrorMessage: "Cannot rename in generated code",
939
+ };
940
+ }
941
+ return {
942
+ ...result,
943
+ triggerSpan: { start: mappedSpan.start, length: mappedSpan.length },
944
+ };
945
+ }
946
+ catch (e) {
947
+ log(`Error in getRenameInfo: ${e instanceof Error ? e.message : String(e)}`);
948
+ return originalGetRenameInfo(fileName, position, options);
949
+ }
950
+ };
951
+ // Hook findRenameLocations (newer overload prefers options object)
952
+ const originalFindRenameLocations = info.languageService.findRenameLocations.bind(info.languageService);
953
+ const callFindRenameLocations = (fileName, position, opts) => {
954
+ // Prefer object overload if available; otherwise fall back to legacy args
955
+ if (originalFindRenameLocations.length <= 3) {
956
+ return originalFindRenameLocations(fileName, position, opts);
957
+ }
958
+ return originalFindRenameLocations(fileName, position, !!opts?.findInStrings, !!opts?.findInComments, !!opts?.providePrefixAndSuffixTextForRename);
959
+ };
960
+ info.languageService.findRenameLocations = (fileName, position, options) => {
961
+ try {
962
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
963
+ return callFindRenameLocations(fileName, position, options);
964
+ }
965
+ const mapper = nativePlugin.getMapper(fileName);
966
+ if (!mapper) {
967
+ return callFindRenameLocations(fileName, position, options);
968
+ }
969
+ const expandedPos = mapper.originalToExpanded(position);
970
+ const locations = callFindRenameLocations(fileName, expandedPos, options);
971
+ if (!locations)
972
+ return locations;
973
+ return locations.reduce((acc, loc) => {
974
+ if (!shouldProcess(loc.fileName)) {
975
+ acc.push(loc);
976
+ return acc;
977
+ }
978
+ const locMapper = nativePlugin.getMapper(loc.fileName);
979
+ if (!locMapper) {
980
+ acc.push(loc);
981
+ return acc;
982
+ }
983
+ const mapped = locMapper.mapSpanToOriginal(loc.textSpan.start, loc.textSpan.length);
984
+ if (mapped) {
985
+ acc.push({
986
+ ...loc,
987
+ textSpan: { start: mapped.start, length: mapped.length },
988
+ });
989
+ }
990
+ return acc;
991
+ }, []);
992
+ }
993
+ catch (e) {
994
+ log(`Error in findRenameLocations: ${e instanceof Error ? e.message : String(e)}`);
995
+ return callFindRenameLocations(fileName, position, options);
996
+ }
997
+ };
998
+ // Hook getDocumentHighlights
999
+ const originalGetDocumentHighlights = info.languageService.getDocumentHighlights.bind(info.languageService);
1000
+ info.languageService.getDocumentHighlights = (fileName, position, filesToSearch) => {
1001
+ try {
1002
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1003
+ return originalGetDocumentHighlights(fileName, position, filesToSearch);
1004
+ }
1005
+ const mapper = nativePlugin.getMapper(fileName);
1006
+ if (!mapper) {
1007
+ return originalGetDocumentHighlights(fileName, position, filesToSearch);
1008
+ }
1009
+ const expandedPos = mapper.originalToExpanded(position);
1010
+ const highlights = originalGetDocumentHighlights(fileName, expandedPos, filesToSearch);
1011
+ if (!highlights)
1012
+ return highlights;
1013
+ return highlights
1014
+ .map((docHighlight) => ({
1015
+ ...docHighlight,
1016
+ highlightSpans: docHighlight.highlightSpans.reduce((acc, span) => {
1017
+ if (!shouldProcess(docHighlight.fileName)) {
1018
+ acc.push(span);
1019
+ return acc;
1020
+ }
1021
+ const spanMapper = nativePlugin.getMapper(docHighlight.fileName);
1022
+ if (!spanMapper) {
1023
+ acc.push(span);
1024
+ return acc;
1025
+ }
1026
+ const mapped = spanMapper.mapSpanToOriginal(span.textSpan.start, span.textSpan.length);
1027
+ if (mapped) {
1028
+ acc.push({
1029
+ ...span,
1030
+ textSpan: { start: mapped.start, length: mapped.length },
1031
+ });
1032
+ }
1033
+ return acc;
1034
+ }, []),
1035
+ }))
1036
+ .filter((h) => h.highlightSpans.length > 0);
1037
+ }
1038
+ catch (e) {
1039
+ log(`Error in getDocumentHighlights: ${e instanceof Error ? e.message : String(e)}`);
1040
+ return originalGetDocumentHighlights(fileName, position, filesToSearch);
1041
+ }
1042
+ };
1043
+ // Hook getImplementationAtPosition
1044
+ const originalGetImplementationAtPosition = info.languageService.getImplementationAtPosition.bind(info.languageService);
1045
+ info.languageService.getImplementationAtPosition = (fileName, position) => {
1046
+ try {
1047
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1048
+ return originalGetImplementationAtPosition(fileName, position);
1049
+ }
1050
+ const mapper = nativePlugin.getMapper(fileName);
1051
+ if (!mapper) {
1052
+ return originalGetImplementationAtPosition(fileName, position);
1053
+ }
1054
+ const expandedPos = mapper.originalToExpanded(position);
1055
+ const implementations = originalGetImplementationAtPosition(fileName, expandedPos);
1056
+ if (!implementations)
1057
+ return implementations;
1058
+ return implementations.reduce((acc, impl) => {
1059
+ if (!shouldProcess(impl.fileName)) {
1060
+ acc.push(impl);
1061
+ return acc;
1062
+ }
1063
+ const implMapper = nativePlugin.getMapper(impl.fileName);
1064
+ if (!implMapper) {
1065
+ acc.push(impl);
1066
+ return acc;
1067
+ }
1068
+ const mapped = implMapper.mapSpanToOriginal(impl.textSpan.start, impl.textSpan.length);
1069
+ if (mapped) {
1070
+ acc.push({
1071
+ ...impl,
1072
+ textSpan: { start: mapped.start, length: mapped.length },
1073
+ });
1074
+ }
1075
+ return acc;
1076
+ }, []);
1077
+ }
1078
+ catch (e) {
1079
+ log(`Error in getImplementationAtPosition: ${e instanceof Error ? e.message : String(e)}`);
1080
+ return originalGetImplementationAtPosition(fileName, position);
1081
+ }
1082
+ };
1083
+ // Hook getCodeFixesAtPosition
1084
+ const originalGetCodeFixesAtPosition = info.languageService.getCodeFixesAtPosition.bind(info.languageService);
1085
+ info.languageService.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => {
1086
+ try {
1087
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1088
+ return originalGetCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
1089
+ }
1090
+ const mapper = nativePlugin.getMapper(fileName);
1091
+ if (!mapper) {
1092
+ return originalGetCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
1093
+ }
1094
+ const expandedStart = mapper.originalToExpanded(start);
1095
+ const expandedEnd = mapper.originalToExpanded(end);
1096
+ return originalGetCodeFixesAtPosition(fileName, expandedStart, expandedEnd, errorCodes, formatOptions, preferences);
1097
+ }
1098
+ catch (e) {
1099
+ log(`Error in getCodeFixesAtPosition: ${e instanceof Error ? e.message : String(e)}`);
1100
+ return originalGetCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
1101
+ }
1102
+ };
1103
+ // Hook getNavigationTree
1104
+ const originalGetNavigationTree = info.languageService.getNavigationTree.bind(info.languageService);
1105
+ info.languageService.getNavigationTree = (fileName) => {
1106
+ try {
1107
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1108
+ return originalGetNavigationTree(fileName);
1109
+ }
1110
+ const mapper = nativePlugin.getMapper(fileName);
1111
+ if (!mapper) {
1112
+ return originalGetNavigationTree(fileName);
1113
+ }
1114
+ const navMapper = mapper;
1115
+ const tree = originalGetNavigationTree(fileName);
1116
+ // Recursively map spans in navigation tree
1117
+ function mapNavigationItem(item) {
1118
+ const mappedSpans = item.spans.map((span) => {
1119
+ const mapped = navMapper.mapSpanToOriginal(span.start, span.length);
1120
+ return mapped
1121
+ ? { start: mapped.start, length: mapped.length }
1122
+ : span;
1123
+ });
1124
+ const mappedNameSpan = item.nameSpan
1125
+ ? (navMapper.mapSpanToOriginal(item.nameSpan.start, item.nameSpan.length) ?? item.nameSpan)
1126
+ : undefined;
1127
+ return {
1128
+ ...item,
1129
+ spans: mappedSpans,
1130
+ nameSpan: mappedNameSpan
1131
+ ? { start: mappedNameSpan.start, length: mappedNameSpan.length }
1132
+ : undefined,
1133
+ childItems: item.childItems?.map(mapNavigationItem),
1134
+ };
1135
+ }
1136
+ return mapNavigationItem(tree);
1137
+ }
1138
+ catch (e) {
1139
+ log(`Error in getNavigationTree: ${e instanceof Error ? e.message : String(e)}`);
1140
+ return originalGetNavigationTree(fileName);
1141
+ }
1142
+ };
1143
+ // Hook getOutliningSpans
1144
+ const originalGetOutliningSpans = info.languageService.getOutliningSpans.bind(info.languageService);
1145
+ info.languageService.getOutliningSpans = (fileName) => {
1146
+ try {
1147
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1148
+ return originalGetOutliningSpans(fileName);
1149
+ }
1150
+ const mapper = nativePlugin.getMapper(fileName);
1151
+ if (!mapper) {
1152
+ return originalGetOutliningSpans(fileName);
1153
+ }
1154
+ const spans = originalGetOutliningSpans(fileName);
1155
+ return spans.map((span) => {
1156
+ const mappedTextSpan = mapper.mapSpanToOriginal(span.textSpan.start, span.textSpan.length);
1157
+ const mappedHintSpan = mapper.mapSpanToOriginal(span.hintSpan.start, span.hintSpan.length);
1158
+ if (!mappedTextSpan || !mappedHintSpan)
1159
+ return span;
1160
+ return {
1161
+ ...span,
1162
+ textSpan: {
1163
+ start: mappedTextSpan.start,
1164
+ length: mappedTextSpan.length,
1165
+ },
1166
+ hintSpan: {
1167
+ start: mappedHintSpan.start,
1168
+ length: mappedHintSpan.length,
1169
+ },
1170
+ };
1171
+ });
1172
+ }
1173
+ catch (e) {
1174
+ log(`Error in getOutliningSpans: ${e instanceof Error ? e.message : String(e)}`);
1175
+ return originalGetOutliningSpans(fileName);
1176
+ }
1177
+ };
1178
+ // Hook provideInlayHints to map positions
1179
+ const originalProvideInlayHints = info.languageService.provideInlayHints?.bind(info.languageService);
1180
+ if (originalProvideInlayHints) {
1181
+ info.languageService.provideInlayHints = (fileName, span, preferences) => {
1182
+ try {
1183
+ if (virtualDtsFiles.has(fileName) || !shouldProcess(fileName)) {
1184
+ return originalProvideInlayHints(fileName, span, preferences);
1185
+ }
1186
+ const mapper = nativePlugin.getMapper(fileName);
1187
+ if (!mapper) {
1188
+ return originalProvideInlayHints(fileName, span, preferences);
1189
+ }
1190
+ // If no mapping info, avoid remapping to reduce risk
1191
+ if (mapper.isEmpty()) {
1192
+ return originalProvideInlayHints(fileName, span, preferences);
1193
+ }
1194
+ // Map the input span to expanded coordinates
1195
+ const expandedSpan = mapper.mapSpanToExpanded(span.start, span.length);
1196
+ const result = originalProvideInlayHints(fileName, expandedSpan, preferences);
1197
+ if (!result)
1198
+ return result;
1199
+ // Map each hint's position back to original coordinates
1200
+ return result.flatMap((hint) => {
1201
+ const originalPos = mapper.expandedToOriginal(hint.position);
1202
+ if (originalPos === null) {
1203
+ // Hint is in generated code, skip it
1204
+ return [];
1205
+ }
1206
+ return [
1207
+ {
1208
+ ...hint,
1209
+ position: originalPos,
1210
+ },
1211
+ ];
1212
+ });
1213
+ }
1214
+ catch (e) {
1215
+ log(`Error in provideInlayHints: ${e instanceof Error ? e.message : String(e)}`);
1216
+ return originalProvideInlayHints(fileName, span, preferences);
1217
+ }
1218
+ };
1219
+ }
1220
+ return info.languageService;
1221
+ }
1222
+ return { create };
1223
+ }
1224
+ module.exports = init;
@@ -0,0 +1,16 @@
1
+ import type { SourceMappingResult } from "macroforge";
2
+ export type SourceMapping = SourceMappingResult;
3
+ export interface PositionMapper {
4
+ originalToExpanded(pos: number): number;
5
+ expandedToOriginal(pos: number): number | null;
6
+ mapSpanToOriginal(start: number, length: number): {
7
+ start: number;
8
+ length: number;
9
+ } | null;
10
+ mapSpanToExpanded(start: number, length: number): {
11
+ start: number;
12
+ length: number;
13
+ };
14
+ isEmpty(): boolean;
15
+ }
16
+ //# sourceMappingURL=source-map.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"source-map.d.ts","sourceRoot":"","sources":["../src/source-map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAEtD,MAAM,MAAM,aAAa,GAAG,mBAAmB,CAAC;AAEhD,MAAM,WAAW,cAAc;IAC7B,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;IACxC,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC/C,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC5C,iBAAiB,CACf,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,GACb;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,OAAO,IAAI,OAAO,CAAC;CACpB"}
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@macroforge/typescript-plugin",
3
+ "version": "0.1.0",
4
+ "description": "TypeScript language service plugin that augments classes decorated with @derive to include macro-generated methods.",
5
+ "type": "commonjs",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/rymskip/macroforge.git"
11
+ },
12
+ "keywords": [
13
+ "typescript",
14
+ "tsserver",
15
+ "plugin",
16
+ "macros",
17
+ "derive",
18
+ "language-service"
19
+ ],
20
+ "author": "macroforge contributors",
21
+ "license": "MIT",
22
+ "bugs": {
23
+ "url": "https://github.com/rymskip/macroforge/issues"
24
+ },
25
+ "homepage": "https://github.com/rymskip/macroforge#readme",
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsc -p tsconfig.json",
31
+ "clean": "rm -rf dist",
32
+ "test": "bun run build && node --test tests/**/*.test.js",
33
+ "prepublishOnly": "bun run clean && bun run build"
34
+ },
35
+ "dependencies": {
36
+ "macroforge": "^0.1.0"
37
+ },
38
+ "peerDependencies": {
39
+ "typescript": ">=5.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/node": "^18.19.130",
43
+ "typescript": "^5.9.3"
44
+ }
45
+ }