@macroforge/typescript-plugin 0.1.33 → 0.1.34
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 +74 -11
- package/dist/index.d.ts +107 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +727 -46
- package/dist/source-map.d.ts +75 -0
- package/dist/source-map.d.ts.map +1 -1
- package/dist/source-map.js +9 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,92 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview TypeScript Language Service Plugin for Macroforge
|
|
4
|
+
*
|
|
5
|
+
* This plugin integrates Macroforge's compile-time macro expansion with TypeScript's
|
|
6
|
+
* Language Service to provide seamless IDE support for macro-decorated classes.
|
|
7
|
+
*
|
|
8
|
+
* ## Architecture Overview
|
|
9
|
+
*
|
|
10
|
+
* The plugin operates by intercepting TypeScript's Language Service methods and
|
|
11
|
+
* transforming source code on-the-fly:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Macro Expansion**: When TypeScript requests a file's content via `getScriptSnapshot`,
|
|
14
|
+
* this plugin intercepts the call and returns the macro-expanded version instead.
|
|
15
|
+
*
|
|
16
|
+
* 2. **Position Mapping**: Since expanded code has different positions than the original,
|
|
17
|
+
* the plugin maintains a {@link PositionMapper} for each file to translate positions
|
|
18
|
+
* between original and expanded coordinates.
|
|
19
|
+
*
|
|
20
|
+
* 3. **Virtual .d.ts Files**: For each macro-containing file, the plugin generates a
|
|
21
|
+
* companion `.macroforge.d.ts` file containing type declarations for generated methods.
|
|
22
|
+
*
|
|
23
|
+
* ## Supported File Types
|
|
24
|
+
*
|
|
25
|
+
* - `.ts` - TypeScript files
|
|
26
|
+
* - `.tsx` - TypeScript JSX files
|
|
27
|
+
* - `.svelte` - Svelte components (with `<script lang="ts">`)
|
|
28
|
+
*
|
|
29
|
+
* ## Hook Categories
|
|
30
|
+
*
|
|
31
|
+
* The plugin hooks into three categories of Language Service methods:
|
|
32
|
+
*
|
|
33
|
+
* - **Host-level hooks**: Control what TypeScript "sees" (`getScriptSnapshot`, `fileExists`, etc.)
|
|
34
|
+
* - **Diagnostic hooks**: Map error positions back to original source (`getSemanticDiagnostics`)
|
|
35
|
+
* - **Navigation hooks**: Handle go-to-definition, references, completions, etc.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // tsconfig.json
|
|
40
|
+
* {
|
|
41
|
+
* "compilerOptions": {
|
|
42
|
+
* "plugins": [{ "name": "@macroforge/typescript-plugin" }]
|
|
43
|
+
* }
|
|
44
|
+
* }
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* @see {@link init} - The main plugin factory function
|
|
48
|
+
* @see {@link PositionMapper} - Position mapping between original and expanded code
|
|
49
|
+
* @module @macroforge/typescript-plugin
|
|
50
|
+
*/
|
|
2
51
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
52
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
53
|
};
|
|
5
54
|
const macroforge_1 = require("macroforge");
|
|
6
55
|
const path_1 = __importDefault(require("path"));
|
|
7
56
|
const fs_1 = __importDefault(require("fs"));
|
|
8
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Cached macro manifest for hover information.
|
|
59
|
+
*
|
|
60
|
+
* This cache stores macro and decorator metadata loaded from the native Macroforge
|
|
61
|
+
* plugin. The cache is populated on first access and persists for the lifetime of
|
|
62
|
+
* the language server process.
|
|
63
|
+
*
|
|
64
|
+
* @internal
|
|
65
|
+
*/
|
|
9
66
|
let macroManifestCache = null;
|
|
67
|
+
/**
|
|
68
|
+
* Retrieves the cached macro manifest, loading it if necessary.
|
|
69
|
+
*
|
|
70
|
+
* The manifest contains metadata about all available macros and decorators,
|
|
71
|
+
* including their names, descriptions, and documentation. This information
|
|
72
|
+
* is used to provide hover tooltips in the IDE.
|
|
73
|
+
*
|
|
74
|
+
* @returns The macro manifest with Maps for quick lookup by name, or `null` if
|
|
75
|
+
* the manifest could not be loaded (e.g., native plugin not available)
|
|
76
|
+
*
|
|
77
|
+
* @remarks
|
|
78
|
+
* The manifest is cached after first load. Macro names and decorator exports
|
|
79
|
+
* are stored in lowercase for case-insensitive lookups.
|
|
80
|
+
*
|
|
81
|
+
* @example
|
|
82
|
+
* ```typescript
|
|
83
|
+
* const manifest = getMacroManifest();
|
|
84
|
+
* if (manifest) {
|
|
85
|
+
* const debugMacro = manifest.macros.get('debug');
|
|
86
|
+
* const serdeDecorator = manifest.decorators.get('serde');
|
|
87
|
+
* }
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
10
90
|
function getMacroManifest() {
|
|
11
91
|
if (macroManifestCache)
|
|
12
92
|
return macroManifestCache;
|
|
@@ -23,7 +103,40 @@ function getMacroManifest() {
|
|
|
23
103
|
}
|
|
24
104
|
}
|
|
25
105
|
/**
|
|
26
|
-
*
|
|
106
|
+
* Finds a macro name within `@derive(...)` decorators at a given cursor position.
|
|
107
|
+
*
|
|
108
|
+
* This function parses JSDoc comments looking for `@derive` directives and determines
|
|
109
|
+
* if the cursor position falls within a specific macro name in the argument list.
|
|
110
|
+
*
|
|
111
|
+
* @param text - The source text to search
|
|
112
|
+
* @param position - The cursor position as a 0-indexed character offset from the start of the file
|
|
113
|
+
* @returns An object containing the macro name and its character span, or `null` if the
|
|
114
|
+
* position is not within a macro name
|
|
115
|
+
*
|
|
116
|
+
* @remarks
|
|
117
|
+
* The function uses the regex `/@derive\s*\(\s*([^)]+)\s*\)/gi` to find all `@derive`
|
|
118
|
+
* decorators, then parses the comma-separated macro names within the parentheses.
|
|
119
|
+
*
|
|
120
|
+
* Position calculation accounts for:
|
|
121
|
+
* - Whitespace between `@derive` and the opening parenthesis
|
|
122
|
+
* - Whitespace around macro names in the argument list
|
|
123
|
+
* - Multiple macros separated by commas
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```typescript
|
|
127
|
+
* // Given text: "/** @derive(Debug, Clone) * /"
|
|
128
|
+
* // Position 14 (on "Debug") returns:
|
|
129
|
+
* findDeriveAtPosition(text, 14);
|
|
130
|
+
* // => { macroName: "Debug", start: 12, end: 17 }
|
|
131
|
+
*
|
|
132
|
+
* // Position 20 (on "Clone") returns:
|
|
133
|
+
* findDeriveAtPosition(text, 20);
|
|
134
|
+
* // => { macroName: "Clone", start: 19, end: 24 }
|
|
135
|
+
*
|
|
136
|
+
* // Position 5 (before @derive) returns:
|
|
137
|
+
* findDeriveAtPosition(text, 5);
|
|
138
|
+
* // => null
|
|
139
|
+
* ```
|
|
27
140
|
*/
|
|
28
141
|
function findDeriveAtPosition(text, position) {
|
|
29
142
|
const derivePattern = /@derive\s*\(\s*([^)]+)\s*\)/gi;
|
|
@@ -52,7 +165,39 @@ function findDeriveAtPosition(text, position) {
|
|
|
52
165
|
return null;
|
|
53
166
|
}
|
|
54
167
|
/**
|
|
55
|
-
*
|
|
168
|
+
* Finds a field decorator (like `@serde` or `@debug`) at a given cursor position.
|
|
169
|
+
*
|
|
170
|
+
* This function searches for decorator patterns (`@name`) in the source text and
|
|
171
|
+
* determines if the cursor falls within one. It's used to provide hover information
|
|
172
|
+
* for Macroforge field decorators.
|
|
173
|
+
*
|
|
174
|
+
* @param text - The source text to search
|
|
175
|
+
* @param position - The cursor position as a 0-indexed character offset
|
|
176
|
+
* @returns An object containing the decorator name (without `@`) and its span
|
|
177
|
+
* (including the `@` symbol), or `null` if not found
|
|
178
|
+
*
|
|
179
|
+
* @remarks
|
|
180
|
+
* This function explicitly skips `@derive` decorators that appear within JSDoc comments,
|
|
181
|
+
* as those are handled by {@link findDeriveAtPosition} instead. The detection works by
|
|
182
|
+
* checking if the match is between an unclosed JSDoc start and end markers.
|
|
183
|
+
*
|
|
184
|
+
* The span returned includes the `@` symbol, so for `@serde`:
|
|
185
|
+
* - `start` points to the `@` character
|
|
186
|
+
* - `end` points to the character after the last letter of the name
|
|
187
|
+
*
|
|
188
|
+
* @example
|
|
189
|
+
* ```typescript
|
|
190
|
+
* // Given text: "class User { @serde name: string; }"
|
|
191
|
+
* findDecoratorAtPosition(text, 14);
|
|
192
|
+
* // => { name: "serde", start: 13, end: 19 }
|
|
193
|
+
*
|
|
194
|
+
* // @derive in JSDoc is skipped (handled by findDeriveAtPosition)
|
|
195
|
+
* // Given text: "/** @derive(Debug) * /"
|
|
196
|
+
* findDecoratorAtPosition(text, 5);
|
|
197
|
+
* // => null
|
|
198
|
+
* ```
|
|
199
|
+
*
|
|
200
|
+
* @see {@link findDeriveAtPosition} - For `@derive` decorators in JSDoc comments
|
|
56
201
|
*/
|
|
57
202
|
function findDecoratorAtPosition(text, position) {
|
|
58
203
|
const decoratorPattern = /@([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
|
|
@@ -77,8 +222,47 @@ function findDecoratorAtPosition(text, position) {
|
|
|
77
222
|
return null;
|
|
78
223
|
}
|
|
79
224
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
225
|
+
* Generates hover information (QuickInfo) for macros and decorators at a cursor position.
|
|
226
|
+
*
|
|
227
|
+
* This function provides IDE hover tooltips for Macroforge-specific syntax:
|
|
228
|
+
* - Macro names within `@derive(...)` JSDoc decorators
|
|
229
|
+
* - Field decorators like `@serde`, `@debug`, etc.
|
|
230
|
+
*
|
|
231
|
+
* @param text - The source text to analyze
|
|
232
|
+
* @param position - The cursor position as a 0-indexed character offset
|
|
233
|
+
* @param tsModule - The TypeScript module reference (for creating QuickInfo structures)
|
|
234
|
+
* @returns A TypeScript QuickInfo object suitable for hover display, or `null` if the
|
|
235
|
+
* position is not on a recognized macro or decorator
|
|
236
|
+
*
|
|
237
|
+
* @remarks
|
|
238
|
+
* The function checks positions in the following order:
|
|
239
|
+
* 1. First, check if cursor is on a macro name within `@derive(...)` via {@link findDeriveAtPosition}
|
|
240
|
+
* 2. Then, check if cursor is on a field decorator via {@link findDecoratorAtPosition}
|
|
241
|
+
*
|
|
242
|
+
* The returned QuickInfo includes:
|
|
243
|
+
* - `kind`: Always `functionElement` (displayed as a function in the IDE)
|
|
244
|
+
* - `textSpan`: The highlighted range in the editor
|
|
245
|
+
* - `displayParts`: The formatted display text (e.g., "@derive(Debug)")
|
|
246
|
+
* - `documentation`: The macro/decorator description from the manifest
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```typescript
|
|
250
|
+
* // Hovering over "Debug" in "@derive(Debug, Clone)"
|
|
251
|
+
* const info = getMacroHoverInfo(text, 14, ts);
|
|
252
|
+
* // Returns QuickInfo with:
|
|
253
|
+
* // - displayParts: "@derive(Debug)"
|
|
254
|
+
* // - documentation: "Generates a fmt_debug() method for debugging output"
|
|
255
|
+
*
|
|
256
|
+
* // Hovering over "@serde" field decorator
|
|
257
|
+
* const info = getMacroHoverInfo(text, 5, ts);
|
|
258
|
+
* // Returns QuickInfo with:
|
|
259
|
+
* // - displayParts: "@serde"
|
|
260
|
+
* // - documentation: "Serialization/deserialization field options"
|
|
261
|
+
* ```
|
|
262
|
+
*
|
|
263
|
+
* @see {@link findDeriveAtPosition} - Locates macro names in @derive decorators
|
|
264
|
+
* @see {@link findDecoratorAtPosition} - Locates field decorators
|
|
265
|
+
* @see {@link getMacroManifest} - Provides macro/decorator metadata
|
|
82
266
|
*/
|
|
83
267
|
function getMacroHoverInfo(text, position, tsModule) {
|
|
84
268
|
const manifest = getMacroManifest();
|
|
@@ -149,7 +333,36 @@ function getMacroHoverInfo(text, position, tsModule) {
|
|
|
149
333
|
}
|
|
150
334
|
return null;
|
|
151
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* File extensions that the plugin will process for macro expansion.
|
|
338
|
+
* @internal
|
|
339
|
+
*/
|
|
152
340
|
const FILE_EXTENSIONS = [".ts", ".tsx", ".svelte"];
|
|
341
|
+
/**
|
|
342
|
+
* Determines whether a file should be processed for macro expansion.
|
|
343
|
+
*
|
|
344
|
+
* This is a gatekeeper function that filters out files that should not
|
|
345
|
+
* go through macro expansion, either because they're in excluded directories
|
|
346
|
+
* or have unsupported file types.
|
|
347
|
+
*
|
|
348
|
+
* @param fileName - The absolute path to the file
|
|
349
|
+
* @returns `true` if the file should be processed, `false` otherwise
|
|
350
|
+
*
|
|
351
|
+
* @remarks
|
|
352
|
+
* Files are excluded if they:
|
|
353
|
+
* - Are in `node_modules` (dependencies should not be processed)
|
|
354
|
+
* - Are in the `.macroforge` cache directory
|
|
355
|
+
* - End with `.macroforge.d.ts` (generated type declaration files)
|
|
356
|
+
* - Don't have a supported extension (`.ts`, `.tsx`, `.svelte`)
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```typescript
|
|
360
|
+
* shouldProcess('/project/src/User.ts'); // => true
|
|
361
|
+
* shouldProcess('/project/src/App.svelte'); // => true
|
|
362
|
+
* shouldProcess('/project/node_modules/...'); // => false
|
|
363
|
+
* shouldProcess('/project/User.macroforge.d.ts'); // => false
|
|
364
|
+
* ```
|
|
365
|
+
*/
|
|
153
366
|
function shouldProcess(fileName) {
|
|
154
367
|
const lower = fileName.toLowerCase();
|
|
155
368
|
if (lower.includes("node_modules"))
|
|
@@ -161,11 +374,68 @@ function shouldProcess(fileName) {
|
|
|
161
374
|
return false;
|
|
162
375
|
return FILE_EXTENSIONS.some((ext) => lower.endsWith(ext));
|
|
163
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Performs a quick check to determine if a file contains any macro-related directives.
|
|
379
|
+
*
|
|
380
|
+
* This is a fast pre-filter to avoid expensive macro expansion on files that
|
|
381
|
+
* don't contain any macros. It uses simple string/regex checks rather than
|
|
382
|
+
* full parsing for performance.
|
|
383
|
+
*
|
|
384
|
+
* @param text - The source text to check
|
|
385
|
+
* @returns `true` if the file likely contains macro directives, `false` otherwise
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* The function checks for the following patterns:
|
|
389
|
+
* - `@derive` anywhere in the text (catches both JSDoc and decorator usage)
|
|
390
|
+
* - `/** @derive(` pattern (JSDoc macro declaration)
|
|
391
|
+
* - `/** import macro` pattern (inline macro import syntax)
|
|
392
|
+
*
|
|
393
|
+
* This is intentionally permissive - it's better to have false positives
|
|
394
|
+
* (which just result in unnecessary expansion attempts) than false negatives
|
|
395
|
+
* (which would break macro functionality).
|
|
396
|
+
*
|
|
397
|
+
* @example
|
|
398
|
+
* ```typescript
|
|
399
|
+
* hasMacroDirectives('/** @derive(Debug) * /'); // => true
|
|
400
|
+
* hasMacroDirectives('@Debug class User {}'); // => true (contains @derive substring? no, but @Debug yes)
|
|
401
|
+
* hasMacroDirectives('class User {}'); // => false
|
|
402
|
+
* ```
|
|
403
|
+
*/
|
|
164
404
|
function hasMacroDirectives(text) {
|
|
165
405
|
return (text.includes("@derive") ||
|
|
166
406
|
/\/\*\*\s*@derive\s*\(/i.test(text) ||
|
|
167
407
|
/\/\*\*\s*import\s+macro\b/i.test(text));
|
|
168
408
|
}
|
|
409
|
+
/**
|
|
410
|
+
* Loads Macroforge configuration by searching for `macroforge.json` up the directory tree.
|
|
411
|
+
*
|
|
412
|
+
* Starting from the given directory, this function walks up the filesystem hierarchy
|
|
413
|
+
* looking for a `macroforge.json` configuration file. The first one found is parsed
|
|
414
|
+
* and its settings are returned.
|
|
415
|
+
*
|
|
416
|
+
* @param startDir - The directory to start searching from (typically the project root)
|
|
417
|
+
* @returns The parsed configuration, or default values if no config file is found
|
|
418
|
+
*
|
|
419
|
+
* @remarks
|
|
420
|
+
* The search stops when:
|
|
421
|
+
* - A `macroforge.json` file is found and successfully parsed
|
|
422
|
+
* - The filesystem root is reached
|
|
423
|
+
* - A parse error occurs (falls back to defaults)
|
|
424
|
+
*
|
|
425
|
+
* This allows monorepo setups where a root `macroforge.json` can configure
|
|
426
|
+
* all packages, while individual packages can override with their own config.
|
|
427
|
+
*
|
|
428
|
+
* @example
|
|
429
|
+
* ```typescript
|
|
430
|
+
* // With /project/macroforge.json containing: { "keepDecorators": true }
|
|
431
|
+
* loadMacroConfig('/project/src/components');
|
|
432
|
+
* // => { keepDecorators: true }
|
|
433
|
+
*
|
|
434
|
+
* // With no macroforge.json found:
|
|
435
|
+
* loadMacroConfig('/some/other/path');
|
|
436
|
+
* // => { keepDecorators: false }
|
|
437
|
+
* ```
|
|
438
|
+
*/
|
|
169
439
|
function loadMacroConfig(startDir) {
|
|
170
440
|
let current = startDir;
|
|
171
441
|
const fallback = { keepDecorators: false };
|
|
@@ -188,23 +458,145 @@ function loadMacroConfig(startDir) {
|
|
|
188
458
|
}
|
|
189
459
|
return fallback;
|
|
190
460
|
}
|
|
461
|
+
/**
|
|
462
|
+
* Main plugin factory function conforming to the TypeScript Language Service Plugin API.
|
|
463
|
+
*
|
|
464
|
+
* This function is called by TypeScript when the plugin is loaded. It receives the
|
|
465
|
+
* TypeScript module reference and returns an object with a `create` function that
|
|
466
|
+
* TypeScript will call to instantiate the plugin for each project.
|
|
467
|
+
*
|
|
468
|
+
* @param modules - Object containing the TypeScript module reference
|
|
469
|
+
* @param modules.typescript - The TypeScript module (`typescript/lib/tsserverlibrary`)
|
|
470
|
+
* @returns An object with a `create` method that TypeScript calls to instantiate the plugin
|
|
471
|
+
*
|
|
472
|
+
* @remarks
|
|
473
|
+
* The plugin follows the standard TypeScript Language Service Plugin pattern:
|
|
474
|
+
* 1. `init()` is called once when the plugin is loaded
|
|
475
|
+
* 2. `create()` is called for each TypeScript project that uses the plugin
|
|
476
|
+
* 3. The returned LanguageService has hooked methods that intercept TypeScript operations
|
|
477
|
+
*
|
|
478
|
+
* ## Plugin Architecture
|
|
479
|
+
*
|
|
480
|
+
* The plugin maintains several internal data structures:
|
|
481
|
+
* - **virtualDtsFiles**: Stores generated `.macroforge.d.ts` type declaration files
|
|
482
|
+
* - **snapshotCache**: Caches expanded file snapshots for stable identity across TS requests
|
|
483
|
+
* - **processingFiles**: Guards against reentrancy during macro expansion
|
|
484
|
+
* - **nativePlugin**: Rust-backed expansion engine (handles actual macro processing)
|
|
485
|
+
*
|
|
486
|
+
* ## Hooked Methods
|
|
487
|
+
*
|
|
488
|
+
* The plugin hooks into ~22 TypeScript Language Service methods to provide seamless
|
|
489
|
+
* IDE support. These fall into three categories:
|
|
490
|
+
*
|
|
491
|
+
* 1. **Host-level hooks** (what TS "sees"):
|
|
492
|
+
* - `getScriptSnapshot` - Returns expanded code instead of original
|
|
493
|
+
* - `getScriptVersion` - Provides versions for virtual .d.ts files
|
|
494
|
+
* - `getScriptFileNames` - Includes virtual .d.ts in project file list
|
|
495
|
+
* - `fileExists` - Resolves virtual .d.ts files
|
|
496
|
+
*
|
|
497
|
+
* 2. **Diagnostic hooks** (error reporting):
|
|
498
|
+
* - `getSemanticDiagnostics` - Maps error positions, adds macro errors
|
|
499
|
+
* - `getSyntacticDiagnostics` - Maps syntax error positions
|
|
500
|
+
*
|
|
501
|
+
* 3. **Navigation hooks** (IDE features):
|
|
502
|
+
* - `getQuickInfoAtPosition` - Hover information
|
|
503
|
+
* - `getCompletionsAtPosition` - IntelliSense completions
|
|
504
|
+
* - `getDefinitionAtPosition` - Go to definition
|
|
505
|
+
* - `findReferences` - Find all references
|
|
506
|
+
* - ... and many more
|
|
507
|
+
*
|
|
508
|
+
* @example
|
|
509
|
+
* ```typescript
|
|
510
|
+
* // This is how TypeScript loads the plugin (internal to TS)
|
|
511
|
+
* const plugin = require('@macroforge/typescript-plugin');
|
|
512
|
+
* const { create } = plugin(modules);
|
|
513
|
+
* const languageService = create(pluginCreateInfo);
|
|
514
|
+
* ```
|
|
515
|
+
*
|
|
516
|
+
* @see {@link shouldProcess} - File filtering logic
|
|
517
|
+
* @see {@link processFile} - Main macro expansion entry point
|
|
518
|
+
*/
|
|
191
519
|
function init(modules) {
|
|
520
|
+
/**
|
|
521
|
+
* Creates the plugin instance for a TypeScript project.
|
|
522
|
+
*
|
|
523
|
+
* This function is called by TypeScript for each project that has the plugin configured.
|
|
524
|
+
* It sets up all the necessary hooks and state, then returns the modified LanguageService.
|
|
525
|
+
*
|
|
526
|
+
* @param info - Plugin creation info provided by TypeScript, containing:
|
|
527
|
+
* - `project`: The TypeScript project instance
|
|
528
|
+
* - `languageService`: The base LanguageService to augment
|
|
529
|
+
* - `languageServiceHost`: The host providing file system access
|
|
530
|
+
* - `config`: Plugin configuration from tsconfig.json
|
|
531
|
+
* @returns The augmented LanguageService with macro support
|
|
532
|
+
*/
|
|
192
533
|
function create(info) {
|
|
193
534
|
const tsModule = modules.typescript;
|
|
194
|
-
|
|
535
|
+
/**
|
|
536
|
+
* Map storing generated virtual `.macroforge.d.ts` files.
|
|
537
|
+
*
|
|
538
|
+
* For each source file containing macros, we generate a companion `.d.ts` file
|
|
539
|
+
* with type declarations for the generated methods. These virtual files are
|
|
540
|
+
* served to TypeScript as if they existed on disk.
|
|
541
|
+
*
|
|
542
|
+
* @remarks
|
|
543
|
+
* Key: Virtual file path (e.g., `/project/src/User.ts.macroforge.d.ts`)
|
|
544
|
+
* Value: ScriptSnapshot containing the generated type declarations
|
|
545
|
+
*/
|
|
195
546
|
const virtualDtsFiles = new Map();
|
|
196
|
-
|
|
547
|
+
/**
|
|
548
|
+
* Cache for processed file snapshots to ensure identity stability.
|
|
549
|
+
*
|
|
550
|
+
* TypeScript's incremental compiler relies on snapshot identity to detect changes.
|
|
551
|
+
* By caching snapshots keyed by version, we ensure the same snapshot object is
|
|
552
|
+
* returned for unchanged files, preventing unnecessary recompilation.
|
|
553
|
+
*
|
|
554
|
+
* @remarks
|
|
555
|
+
* Key: Source file path
|
|
556
|
+
* Value: Object containing the file version and its expanded snapshot
|
|
557
|
+
*/
|
|
197
558
|
const snapshotCache = new Map();
|
|
198
|
-
|
|
559
|
+
/**
|
|
560
|
+
* Set of files currently being processed for macro expansion.
|
|
561
|
+
*
|
|
562
|
+
* This guards against reentrancy - if TypeScript requests a file while we're
|
|
563
|
+
* already processing it (e.g., due to import resolution during expansion),
|
|
564
|
+
* we return the original content to prevent infinite loops.
|
|
565
|
+
*/
|
|
199
566
|
const processingFiles = new Set();
|
|
200
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Native Rust-backed plugin instance for macro expansion.
|
|
569
|
+
*
|
|
570
|
+
* The NativePlugin handles the actual macro expansion logic, caching, and
|
|
571
|
+
* source mapping. It's implemented in Rust for performance and is accessed
|
|
572
|
+
* via N-API bindings.
|
|
573
|
+
*/
|
|
201
574
|
const nativePlugin = new macroforge_1.NativePlugin();
|
|
575
|
+
/**
|
|
576
|
+
* Gets the current working directory for the project.
|
|
577
|
+
*
|
|
578
|
+
* Tries multiple sources in order of preference:
|
|
579
|
+
* 1. Project's getCurrentDirectory method
|
|
580
|
+
* 2. Language service host's getCurrentDirectory method
|
|
581
|
+
* 3. Falls back to process.cwd()
|
|
582
|
+
*
|
|
583
|
+
* @returns The project's root directory path
|
|
584
|
+
*/
|
|
202
585
|
const getCurrentDirectory = () => info.project.getCurrentDirectory?.() ??
|
|
203
586
|
info.languageServiceHost.getCurrentDirectory?.() ??
|
|
204
587
|
process.cwd();
|
|
205
588
|
const macroConfig = loadMacroConfig(getCurrentDirectory());
|
|
206
589
|
const keepDecorators = macroConfig.keepDecorators;
|
|
207
|
-
|
|
590
|
+
/**
|
|
591
|
+
* Logs a message to multiple destinations for debugging.
|
|
592
|
+
*
|
|
593
|
+
* Messages are sent to:
|
|
594
|
+
* 1. The native Rust plugin (for unified logging)
|
|
595
|
+
* 2. TypeScript's project service logger (visible in tsserver logs)
|
|
596
|
+
* 3. stderr (for development debugging)
|
|
597
|
+
*
|
|
598
|
+
* @param msg - The message to log (will be prefixed with timestamp and [macroforge])
|
|
599
|
+
*/
|
|
208
600
|
const log = (msg) => {
|
|
209
601
|
const line = `[${new Date().toISOString()}] ${msg}`;
|
|
210
602
|
nativePlugin.log(line);
|
|
@@ -217,6 +609,19 @@ function init(modules) {
|
|
|
217
609
|
}
|
|
218
610
|
catch { }
|
|
219
611
|
};
|
|
612
|
+
/**
|
|
613
|
+
* Registers a virtual `.macroforge.d.ts` file with TypeScript's project service.
|
|
614
|
+
*
|
|
615
|
+
* This makes TypeScript aware of our generated type declaration files so they
|
|
616
|
+
* can be resolved during import resolution and type checking.
|
|
617
|
+
*
|
|
618
|
+
* @param fileName - The path to the virtual .d.ts file to register
|
|
619
|
+
*
|
|
620
|
+
* @remarks
|
|
621
|
+
* Uses internal TypeScript APIs (`getOrCreateScriptInfoNotOpenedByClient`)
|
|
622
|
+
* which may change between TypeScript versions. The function gracefully
|
|
623
|
+
* handles missing APIs.
|
|
624
|
+
*/
|
|
220
625
|
const ensureVirtualDtsRegistered = (fileName) => {
|
|
221
626
|
const projectService = info.project.projectService;
|
|
222
627
|
const register = projectService?.getOrCreateScriptInfoNotOpenedByClient;
|
|
@@ -233,6 +638,19 @@ function init(modules) {
|
|
|
233
638
|
log(`Failed to register virtual .d.ts ${fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
234
639
|
}
|
|
235
640
|
};
|
|
641
|
+
/**
|
|
642
|
+
* Removes a virtual `.macroforge.d.ts` file from TypeScript's project service.
|
|
643
|
+
*
|
|
644
|
+
* Called when a source file no longer generates types (e.g., macros removed)
|
|
645
|
+
* to clean up stale virtual files and prevent memory leaks.
|
|
646
|
+
*
|
|
647
|
+
* @param fileName - The path to the virtual .d.ts file to remove
|
|
648
|
+
*
|
|
649
|
+
* @remarks
|
|
650
|
+
* The cleanup is conservative - it only deletes the ScriptInfo if:
|
|
651
|
+
* 1. The file is not open in an editor
|
|
652
|
+
* 2. The file is not attached to any other projects
|
|
653
|
+
*/
|
|
236
654
|
const cleanupVirtualDts = (fileName) => {
|
|
237
655
|
const projectService = info.project.projectService;
|
|
238
656
|
const getScriptInfo = projectService?.getScriptInfo;
|
|
@@ -252,6 +670,12 @@ function init(modules) {
|
|
|
252
670
|
log(`Failed to clean up virtual .d.ts ${fileName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
253
671
|
}
|
|
254
672
|
};
|
|
673
|
+
/**
|
|
674
|
+
* Override projectService.setDocument to handle virtual files safely.
|
|
675
|
+
*
|
|
676
|
+
* This guards against TypeScript crashes when it tries to cache source files
|
|
677
|
+
* for virtual .d.ts files that don't have full ScriptInfo backing.
|
|
678
|
+
*/
|
|
255
679
|
const projectService = info.project.projectService;
|
|
256
680
|
if (projectService?.setDocument) {
|
|
257
681
|
projectService.setDocument = (key, filePath, sourceFile) => {
|
|
@@ -272,9 +696,41 @@ function init(modules) {
|
|
|
272
696
|
}
|
|
273
697
|
};
|
|
274
698
|
}
|
|
275
|
-
// Log plugin initialization
|
|
276
699
|
log("Plugin initialized");
|
|
277
|
-
|
|
700
|
+
/**
|
|
701
|
+
* Processes a file through macro expansion via the native Rust plugin.
|
|
702
|
+
*
|
|
703
|
+
* This is the main entry point for macro expansion. It delegates to the native
|
|
704
|
+
* Rust plugin for the actual transformation and handles virtual .d.ts file
|
|
705
|
+
* management for generated type declarations.
|
|
706
|
+
*
|
|
707
|
+
* @param fileName - The absolute path to the source file
|
|
708
|
+
* @param content - The source file content to expand
|
|
709
|
+
* @param version - The file version (used for cache invalidation)
|
|
710
|
+
* @returns An object containing:
|
|
711
|
+
* - `result`: The full ExpandResult from the native plugin (includes diagnostics, source mapping)
|
|
712
|
+
* - `code`: The expanded code (shorthand for result.code)
|
|
713
|
+
*
|
|
714
|
+
* @remarks
|
|
715
|
+
* The function handles several important concerns:
|
|
716
|
+
*
|
|
717
|
+
* 1. **Empty file fast path**: Returns immediately for empty content
|
|
718
|
+
* 2. **Virtual .d.ts management**: Creates/updates/removes companion type declaration files
|
|
719
|
+
* 3. **Error recovery**: On expansion failure, returns original content and cleans up virtual files
|
|
720
|
+
*
|
|
721
|
+
* Caching is handled by the native Rust plugin based on the version parameter.
|
|
722
|
+
* If the version hasn't changed since the last call, the cached result is returned.
|
|
723
|
+
*
|
|
724
|
+
* @example
|
|
725
|
+
* ```typescript
|
|
726
|
+
* const { result, code } = processFile('/project/src/User.ts', sourceText, '1');
|
|
727
|
+
*
|
|
728
|
+
* // result.code - The expanded TypeScript code
|
|
729
|
+
* // result.types - Generated .d.ts content (if any)
|
|
730
|
+
* // result.diagnostics - Macro expansion errors/warnings
|
|
731
|
+
* // result.sourceMapping - Position mapping data
|
|
732
|
+
* ```
|
|
733
|
+
*/
|
|
278
734
|
function processFile(fileName, content, version) {
|
|
279
735
|
// Fast Exit: Empty Content
|
|
280
736
|
if (!content || content.trim().length === 0) {
|
|
@@ -325,11 +781,23 @@ function init(modules) {
|
|
|
325
781
|
};
|
|
326
782
|
}
|
|
327
783
|
}
|
|
328
|
-
//
|
|
784
|
+
// =========================================================================
|
|
785
|
+
// HOST-LEVEL HOOKS
|
|
786
|
+
// These hooks control what TypeScript "sees" - the file content, versions,
|
|
787
|
+
// and existence checks. They're the foundation of the plugin's operation.
|
|
788
|
+
// =========================================================================
|
|
789
|
+
/**
|
|
790
|
+
* Hook: getScriptVersion
|
|
791
|
+
*
|
|
792
|
+
* Provides version strings for virtual `.macroforge.d.ts` files by deriving
|
|
793
|
+
* them from the source file's version. This ensures TypeScript invalidates
|
|
794
|
+
* the virtual file when its source changes.
|
|
795
|
+
*/
|
|
329
796
|
const originalGetScriptVersion = info.languageServiceHost.getScriptVersion.bind(info.languageServiceHost);
|
|
330
797
|
info.languageServiceHost.getScriptVersion = (fileName) => {
|
|
331
798
|
try {
|
|
332
799
|
if (virtualDtsFiles.has(fileName)) {
|
|
800
|
+
// Virtual .d.ts files inherit version from their source file
|
|
333
801
|
const sourceFileName = fileName.replace(".macroforge.d.ts", "");
|
|
334
802
|
return originalGetScriptVersion(sourceFileName);
|
|
335
803
|
}
|
|
@@ -340,8 +808,13 @@ function init(modules) {
|
|
|
340
808
|
return originalGetScriptVersion(fileName);
|
|
341
809
|
}
|
|
342
810
|
};
|
|
343
|
-
|
|
344
|
-
|
|
811
|
+
/**
|
|
812
|
+
* Hook: getScriptFileNames
|
|
813
|
+
*
|
|
814
|
+
* Includes virtual `.macroforge.d.ts` files in the project's file list.
|
|
815
|
+
* This allows TypeScript to "see" our generated type declaration files
|
|
816
|
+
* and include them in type checking and import resolution.
|
|
817
|
+
*/
|
|
345
818
|
const originalGetScriptFileNames = info.languageServiceHost
|
|
346
819
|
.getScriptFileNames
|
|
347
820
|
? info.languageServiceHost.getScriptFileNames.bind(info.languageServiceHost)
|
|
@@ -349,6 +822,7 @@ function init(modules) {
|
|
|
349
822
|
info.languageServiceHost.getScriptFileNames = () => {
|
|
350
823
|
try {
|
|
351
824
|
const originalFiles = originalGetScriptFileNames();
|
|
825
|
+
// Append all virtual .d.ts files to the project's file list
|
|
352
826
|
return [...originalFiles, ...Array.from(virtualDtsFiles.keys())];
|
|
353
827
|
}
|
|
354
828
|
catch (e) {
|
|
@@ -356,14 +830,20 @@ function init(modules) {
|
|
|
356
830
|
return originalGetScriptFileNames();
|
|
357
831
|
}
|
|
358
832
|
};
|
|
359
|
-
|
|
833
|
+
/**
|
|
834
|
+
* Hook: fileExists
|
|
835
|
+
*
|
|
836
|
+
* Makes virtual `.macroforge.d.ts` files appear to exist on disk.
|
|
837
|
+
* This allows TypeScript's module resolution to find our generated
|
|
838
|
+
* type declaration files.
|
|
839
|
+
*/
|
|
360
840
|
const originalFileExists = info.languageServiceHost.fileExists
|
|
361
841
|
? info.languageServiceHost.fileExists.bind(info.languageServiceHost)
|
|
362
842
|
: tsModule.sys.fileExists;
|
|
363
843
|
info.languageServiceHost.fileExists = (fileName) => {
|
|
364
844
|
try {
|
|
365
845
|
if (virtualDtsFiles.has(fileName)) {
|
|
366
|
-
return true;
|
|
846
|
+
return true; // Virtual file exists in our cache
|
|
367
847
|
}
|
|
368
848
|
return originalFileExists(fileName);
|
|
369
849
|
}
|
|
@@ -372,22 +852,40 @@ function init(modules) {
|
|
|
372
852
|
return originalFileExists(fileName);
|
|
373
853
|
}
|
|
374
854
|
};
|
|
375
|
-
|
|
855
|
+
/**
|
|
856
|
+
* Hook: getScriptSnapshot (CRITICAL)
|
|
857
|
+
*
|
|
858
|
+
* This is the most important hook - it intercepts file content requests
|
|
859
|
+
* and returns macro-expanded code instead of the original source.
|
|
860
|
+
*
|
|
861
|
+
* The hook handles several scenarios:
|
|
862
|
+
* 1. Virtual .d.ts files - Returns the generated type declarations
|
|
863
|
+
* 2. Reentrancy - Returns original content if file is already being processed
|
|
864
|
+
* 3. Excluded files - Returns original content for node_modules, etc.
|
|
865
|
+
* 4. Non-macro files - Returns original content if no @derive directives
|
|
866
|
+
* 5. Macro files - Returns expanded content with generated methods
|
|
867
|
+
*
|
|
868
|
+
* @remarks
|
|
869
|
+
* Caching strategy:
|
|
870
|
+
* - Uses `snapshotCache` for identity stability (TS incremental compiler needs this)
|
|
871
|
+
* - Uses `processingFiles` Set to prevent infinite loops during expansion
|
|
872
|
+
* - Version-based cache invalidation ensures fresh expansions on file changes
|
|
873
|
+
*/
|
|
376
874
|
const originalGetScriptSnapshot = info.languageServiceHost.getScriptSnapshot.bind(info.languageServiceHost);
|
|
377
875
|
info.languageServiceHost.getScriptSnapshot = (fileName) => {
|
|
378
876
|
try {
|
|
379
877
|
log(`getScriptSnapshot: ${fileName}`);
|
|
380
|
-
//
|
|
878
|
+
// Scenario 1: Virtual .d.ts file - return from our cache
|
|
381
879
|
if (virtualDtsFiles.has(fileName)) {
|
|
382
880
|
log(` -> virtual .d.ts cache hit`);
|
|
383
881
|
return virtualDtsFiles.get(fileName);
|
|
384
882
|
}
|
|
385
|
-
//
|
|
883
|
+
// Scenario 2: Reentrancy guard - prevent infinite loops
|
|
386
884
|
if (processingFiles.has(fileName)) {
|
|
387
885
|
log(` -> REENTRANCY DETECTED, returning original`);
|
|
388
886
|
return originalGetScriptSnapshot(fileName);
|
|
389
887
|
}
|
|
390
|
-
//
|
|
888
|
+
// Scenario 3: Excluded file (node_modules, .macroforge, wrong extension)
|
|
391
889
|
if (!shouldProcess(fileName)) {
|
|
392
890
|
log(` -> not processable (excluded file), returning original`);
|
|
393
891
|
return originalGetScriptSnapshot(fileName);
|
|
@@ -399,18 +897,18 @@ function init(modules) {
|
|
|
399
897
|
return tsModule.ScriptSnapshot.fromString("");
|
|
400
898
|
}
|
|
401
899
|
const text = snapshot.getText(0, snapshot.getLength());
|
|
402
|
-
//
|
|
900
|
+
// Scenario 4: No macro directives - return original
|
|
403
901
|
if (!hasMacroDirectives(text)) {
|
|
404
902
|
log(` -> no macro directives, returning original`);
|
|
405
903
|
return snapshot;
|
|
406
904
|
}
|
|
905
|
+
// Scenario 5: Has macros - expand and return
|
|
407
906
|
log(` -> has @derive, expanding...`);
|
|
408
|
-
// Mark as processing to prevent reentrancy
|
|
409
907
|
processingFiles.add(fileName);
|
|
410
908
|
try {
|
|
411
909
|
const version = info.languageServiceHost.getScriptVersion(fileName);
|
|
412
910
|
log(` -> version: ${version}`);
|
|
413
|
-
// Check
|
|
911
|
+
// Check snapshot cache for stable identity
|
|
414
912
|
const cached = snapshotCache.get(fileName);
|
|
415
913
|
if (cached && cached.version === version) {
|
|
416
914
|
log(` -> snapshot cache hit`);
|
|
@@ -421,7 +919,7 @@ function init(modules) {
|
|
|
421
919
|
if (code && code !== text) {
|
|
422
920
|
log(` -> creating expanded snapshot (${code.length} chars)`);
|
|
423
921
|
const expandedSnapshot = tsModule.ScriptSnapshot.fromString(code);
|
|
424
|
-
// Cache
|
|
922
|
+
// Cache for stable identity across TS requests
|
|
425
923
|
snapshotCache.set(fileName, {
|
|
426
924
|
version,
|
|
427
925
|
snapshot: expandedSnapshot,
|
|
@@ -429,7 +927,7 @@ function init(modules) {
|
|
|
429
927
|
log(` -> returning expanded snapshot`);
|
|
430
928
|
return expandedSnapshot;
|
|
431
929
|
}
|
|
432
|
-
//
|
|
930
|
+
// No change after expansion - cache original
|
|
433
931
|
snapshotCache.set(fileName, { version, snapshot });
|
|
434
932
|
return snapshot;
|
|
435
933
|
}
|
|
@@ -439,11 +937,25 @@ function init(modules) {
|
|
|
439
937
|
}
|
|
440
938
|
catch (e) {
|
|
441
939
|
log(`ERROR in getScriptSnapshot for ${fileName}: ${e instanceof Error ? e.stack || e.message : String(e)}`);
|
|
442
|
-
// Make sure we clean up on error
|
|
443
940
|
processingFiles.delete(fileName);
|
|
444
941
|
return originalGetScriptSnapshot(fileName);
|
|
445
942
|
}
|
|
446
943
|
};
|
|
944
|
+
// =========================================================================
|
|
945
|
+
// DIAGNOSTIC HELPER FUNCTIONS
|
|
946
|
+
// These utilities convert and map diagnostics between expanded and original
|
|
947
|
+
// code positions.
|
|
948
|
+
// =========================================================================
|
|
949
|
+
/**
|
|
950
|
+
* Converts a TypeScript diagnostic to a plain object for the native plugin.
|
|
951
|
+
*
|
|
952
|
+
* The native Rust plugin expects a simplified diagnostic format. This function
|
|
953
|
+
* extracts the essential fields and normalizes the message text (which can be
|
|
954
|
+
* either a string or a DiagnosticMessageChain).
|
|
955
|
+
*
|
|
956
|
+
* @param diag - The TypeScript diagnostic to convert
|
|
957
|
+
* @returns A plain object with diagnostic information
|
|
958
|
+
*/
|
|
447
959
|
function toPlainDiagnostic(diag) {
|
|
448
960
|
const message = typeof diag.messageText === "string"
|
|
449
961
|
? diag.messageText
|
|
@@ -461,13 +973,24 @@ function init(modules) {
|
|
|
461
973
|
category,
|
|
462
974
|
};
|
|
463
975
|
}
|
|
976
|
+
/**
|
|
977
|
+
* Applies mapped positions to diagnostics, updating their start/length.
|
|
978
|
+
*
|
|
979
|
+
* Takes the original diagnostics and a parallel array of mapped positions
|
|
980
|
+
* (from the native plugin) and creates new diagnostics with corrected positions
|
|
981
|
+
* pointing to the original source instead of the expanded code.
|
|
982
|
+
*
|
|
983
|
+
* @param original - The original diagnostics from TypeScript
|
|
984
|
+
* @param mapped - Array of mapped positions (parallel to original)
|
|
985
|
+
* @returns New diagnostic array with corrected positions
|
|
986
|
+
*/
|
|
464
987
|
function applyMappedDiagnostics(original, mapped) {
|
|
465
988
|
return original.map((diag, idx) => {
|
|
466
989
|
const mappedDiag = mapped[idx];
|
|
467
990
|
if (!mappedDiag ||
|
|
468
991
|
mappedDiag.start === undefined ||
|
|
469
992
|
mappedDiag.length === undefined) {
|
|
470
|
-
return diag;
|
|
993
|
+
return diag; // No mapping available, keep original
|
|
471
994
|
}
|
|
472
995
|
return {
|
|
473
996
|
...diag,
|
|
@@ -476,7 +999,23 @@ function init(modules) {
|
|
|
476
999
|
};
|
|
477
1000
|
});
|
|
478
1001
|
}
|
|
479
|
-
//
|
|
1002
|
+
// =========================================================================
|
|
1003
|
+
// DIAGNOSTIC HOOKS
|
|
1004
|
+
// These hooks map error positions from expanded code back to original source
|
|
1005
|
+
// and inject macro-specific diagnostics.
|
|
1006
|
+
// =========================================================================
|
|
1007
|
+
/**
|
|
1008
|
+
* Hook: getSemanticDiagnostics (COMPLEX)
|
|
1009
|
+
*
|
|
1010
|
+
* This is one of the most complex hooks. It handles:
|
|
1011
|
+
* 1. Mapping TypeScript error positions from expanded code back to original
|
|
1012
|
+
* 2. Converting errors in generated code to point at the responsible @derive macro
|
|
1013
|
+
* 3. Injecting Macroforge-specific diagnostics (expansion errors, warnings)
|
|
1014
|
+
*
|
|
1015
|
+
* The hook uses sophisticated position mapping to ensure errors appear at
|
|
1016
|
+
* meaningful locations in the user's source code, even when the actual error
|
|
1017
|
+
* occurred in macro-generated code.
|
|
1018
|
+
*/
|
|
480
1019
|
const originalGetSemanticDiagnostics = info.languageService.getSemanticDiagnostics.bind(info.languageService);
|
|
481
1020
|
info.languageService.getSemanticDiagnostics = (fileName) => {
|
|
482
1021
|
try {
|
|
@@ -701,7 +1240,13 @@ function init(modules) {
|
|
|
701
1240
|
return originalGetSemanticDiagnostics(fileName);
|
|
702
1241
|
}
|
|
703
1242
|
};
|
|
704
|
-
|
|
1243
|
+
/**
|
|
1244
|
+
* Hook: getSyntacticDiagnostics
|
|
1245
|
+
*
|
|
1246
|
+
* Maps syntax error positions from expanded code back to original source.
|
|
1247
|
+
* Simpler than semantic diagnostics as it doesn't need to handle generated
|
|
1248
|
+
* code errors (syntax errors are in user code, not generated code).
|
|
1249
|
+
*/
|
|
705
1250
|
const originalGetSyntacticDiagnostics = info.languageService.getSyntacticDiagnostics.bind(info.languageService);
|
|
706
1251
|
info.languageService.getSyntacticDiagnostics = (fileName) => {
|
|
707
1252
|
try {
|
|
@@ -724,7 +1269,28 @@ function init(modules) {
|
|
|
724
1269
|
return originalGetSyntacticDiagnostics(fileName);
|
|
725
1270
|
}
|
|
726
1271
|
};
|
|
727
|
-
//
|
|
1272
|
+
// =========================================================================
|
|
1273
|
+
// NAVIGATION & IDE FEATURE HOOKS
|
|
1274
|
+
// These hooks provide IDE features like hover, completions, go-to-definition,
|
|
1275
|
+
// find references, rename, etc. All follow a similar pattern:
|
|
1276
|
+
// 1. Map input position from original to expanded coordinates
|
|
1277
|
+
// 2. Call the original method on expanded code
|
|
1278
|
+
// 3. Map output positions back from expanded to original coordinates
|
|
1279
|
+
// =========================================================================
|
|
1280
|
+
/**
|
|
1281
|
+
* Hook: getQuickInfoAtPosition
|
|
1282
|
+
*
|
|
1283
|
+
* Provides hover information for symbols. This hook has special handling
|
|
1284
|
+
* for Macroforge-specific syntax:
|
|
1285
|
+
*
|
|
1286
|
+
* 1. First checks for macro hover info (@derive macros, field decorators)
|
|
1287
|
+
* 2. If not on a macro, maps position and delegates to TypeScript
|
|
1288
|
+
* 3. Maps result spans back to original positions
|
|
1289
|
+
*
|
|
1290
|
+
* @remarks
|
|
1291
|
+
* If the hover would be in generated code, returns undefined to hide it
|
|
1292
|
+
* (prevents confusing users with hover info for code they can't see).
|
|
1293
|
+
*/
|
|
728
1294
|
const originalGetQuickInfoAtPosition = info.languageService.getQuickInfoAtPosition.bind(info.languageService);
|
|
729
1295
|
info.languageService.getQuickInfoAtPosition = (fileName, position) => {
|
|
730
1296
|
try {
|
|
@@ -766,7 +1332,13 @@ function init(modules) {
|
|
|
766
1332
|
return originalGetQuickInfoAtPosition(fileName, position);
|
|
767
1333
|
}
|
|
768
1334
|
};
|
|
769
|
-
|
|
1335
|
+
/**
|
|
1336
|
+
* Hook: getCompletionsAtPosition
|
|
1337
|
+
*
|
|
1338
|
+
* Provides IntelliSense completions. Maps the cursor position to expanded
|
|
1339
|
+
* coordinates to get accurate completions that include generated methods,
|
|
1340
|
+
* then maps any replacement spans back to original coordinates.
|
|
1341
|
+
*/
|
|
770
1342
|
const originalGetCompletionsAtPosition = info.languageService.getCompletionsAtPosition.bind(info.languageService);
|
|
771
1343
|
info.languageService.getCompletionsAtPosition = (fileName, position, options, formattingSettings) => {
|
|
772
1344
|
try {
|
|
@@ -812,7 +1384,18 @@ function init(modules) {
|
|
|
812
1384
|
return originalGetCompletionsAtPosition(fileName, position, options, formattingSettings);
|
|
813
1385
|
}
|
|
814
1386
|
};
|
|
815
|
-
|
|
1387
|
+
/**
|
|
1388
|
+
* Hook: getDefinitionAtPosition
|
|
1389
|
+
*
|
|
1390
|
+
* Provides "Go to Definition" functionality. Maps cursor position to
|
|
1391
|
+
* expanded code, gets definitions, then maps definition spans back
|
|
1392
|
+
* to original positions.
|
|
1393
|
+
*
|
|
1394
|
+
* @remarks
|
|
1395
|
+
* For definitions in other files (not macro-expanded), positions are
|
|
1396
|
+
* passed through unchanged. Only same-file definitions need mapping.
|
|
1397
|
+
* Definitions pointing to generated code are filtered out.
|
|
1398
|
+
*/
|
|
816
1399
|
const originalGetDefinitionAtPosition = info.languageService.getDefinitionAtPosition.bind(info.languageService);
|
|
817
1400
|
info.languageService.getDefinitionAtPosition = (fileName, position) => {
|
|
818
1401
|
try {
|
|
@@ -853,7 +1436,13 @@ function init(modules) {
|
|
|
853
1436
|
return originalGetDefinitionAtPosition(fileName, position);
|
|
854
1437
|
}
|
|
855
1438
|
};
|
|
856
|
-
|
|
1439
|
+
/**
|
|
1440
|
+
* Hook: getDefinitionAndBoundSpan
|
|
1441
|
+
*
|
|
1442
|
+
* Enhanced version of getDefinitionAtPosition that also returns the
|
|
1443
|
+
* text span that was used to find the definition (useful for highlighting).
|
|
1444
|
+
* Maps both the bound span and definition spans.
|
|
1445
|
+
*/
|
|
857
1446
|
const originalGetDefinitionAndBoundSpan = info.languageService.getDefinitionAndBoundSpan.bind(info.languageService);
|
|
858
1447
|
info.languageService.getDefinitionAndBoundSpan = (fileName, position) => {
|
|
859
1448
|
try {
|
|
@@ -905,7 +1494,13 @@ function init(modules) {
|
|
|
905
1494
|
return originalGetDefinitionAndBoundSpan(fileName, position);
|
|
906
1495
|
}
|
|
907
1496
|
};
|
|
908
|
-
|
|
1497
|
+
/**
|
|
1498
|
+
* Hook: getTypeDefinitionAtPosition
|
|
1499
|
+
*
|
|
1500
|
+
* Provides "Go to Type Definition" functionality. Similar to
|
|
1501
|
+
* getDefinitionAtPosition but navigates to the type's definition
|
|
1502
|
+
* rather than the symbol's definition.
|
|
1503
|
+
*/
|
|
909
1504
|
const originalGetTypeDefinitionAtPosition = info.languageService.getTypeDefinitionAtPosition.bind(info.languageService);
|
|
910
1505
|
info.languageService.getTypeDefinitionAtPosition = (fileName, position) => {
|
|
911
1506
|
try {
|
|
@@ -945,7 +1540,13 @@ function init(modules) {
|
|
|
945
1540
|
return originalGetTypeDefinitionAtPosition(fileName, position);
|
|
946
1541
|
}
|
|
947
1542
|
};
|
|
948
|
-
|
|
1543
|
+
/**
|
|
1544
|
+
* Hook: getReferencesAtPosition
|
|
1545
|
+
*
|
|
1546
|
+
* Provides "Find All References" functionality. Maps the cursor position,
|
|
1547
|
+
* finds all references in the expanded code, then maps each reference
|
|
1548
|
+
* span back to original positions. References in generated code are filtered.
|
|
1549
|
+
*/
|
|
949
1550
|
const originalGetReferencesAtPosition = info.languageService.getReferencesAtPosition.bind(info.languageService);
|
|
950
1551
|
info.languageService.getReferencesAtPosition = (fileName, position) => {
|
|
951
1552
|
try {
|
|
@@ -985,7 +1586,12 @@ function init(modules) {
|
|
|
985
1586
|
return originalGetReferencesAtPosition(fileName, position);
|
|
986
1587
|
}
|
|
987
1588
|
};
|
|
988
|
-
|
|
1589
|
+
/**
|
|
1590
|
+
* Hook: findReferences
|
|
1591
|
+
*
|
|
1592
|
+
* Alternative "Find All References" that returns grouped references by symbol.
|
|
1593
|
+
* Similar to getReferencesAtPosition but with richer structure.
|
|
1594
|
+
*/
|
|
989
1595
|
const originalFindReferences = info.languageService.findReferences.bind(info.languageService);
|
|
990
1596
|
info.languageService.findReferences = (fileName, position) => {
|
|
991
1597
|
try {
|
|
@@ -1030,7 +1636,12 @@ function init(modules) {
|
|
|
1030
1636
|
return originalFindReferences(fileName, position);
|
|
1031
1637
|
}
|
|
1032
1638
|
};
|
|
1033
|
-
|
|
1639
|
+
/**
|
|
1640
|
+
* Hook: getSignatureHelpItems
|
|
1641
|
+
*
|
|
1642
|
+
* Provides function signature help (parameter hints shown while typing
|
|
1643
|
+
* function arguments). Maps cursor position and the applicable span.
|
|
1644
|
+
*/
|
|
1034
1645
|
const originalGetSignatureHelpItems = info.languageService.getSignatureHelpItems.bind(info.languageService);
|
|
1035
1646
|
info.languageService.getSignatureHelpItems = (fileName, position, options) => {
|
|
1036
1647
|
try {
|
|
@@ -1062,8 +1673,22 @@ function init(modules) {
|
|
|
1062
1673
|
return originalGetSignatureHelpItems(fileName, position, options);
|
|
1063
1674
|
}
|
|
1064
1675
|
};
|
|
1065
|
-
|
|
1676
|
+
/**
|
|
1677
|
+
* Hook: getRenameInfo
|
|
1678
|
+
*
|
|
1679
|
+
* Provides information about whether a symbol can be renamed and what
|
|
1680
|
+
* text span should be highlighted. Returns an error message if the
|
|
1681
|
+
* cursor is in generated code (can't rename generated symbols).
|
|
1682
|
+
*
|
|
1683
|
+
* @remarks
|
|
1684
|
+
* Uses a compatibility wrapper (callGetRenameInfo) to handle different
|
|
1685
|
+
* TypeScript version signatures for this method.
|
|
1686
|
+
*/
|
|
1066
1687
|
const originalGetRenameInfo = info.languageService.getRenameInfo.bind(info.languageService);
|
|
1688
|
+
/**
|
|
1689
|
+
* Compatibility wrapper for getRenameInfo that handles both old and new
|
|
1690
|
+
* TypeScript API signatures.
|
|
1691
|
+
*/
|
|
1067
1692
|
const callGetRenameInfo = (fileName, position, options) => {
|
|
1068
1693
|
// Prefer object overload if available; otherwise fall back to legacy args
|
|
1069
1694
|
if (originalGetRenameInfo.length <= 2) {
|
|
@@ -1101,8 +1726,22 @@ function init(modules) {
|
|
|
1101
1726
|
return originalGetRenameInfo(fileName, position, options);
|
|
1102
1727
|
}
|
|
1103
1728
|
};
|
|
1104
|
-
|
|
1729
|
+
/**
|
|
1730
|
+
* Hook: findRenameLocations
|
|
1731
|
+
*
|
|
1732
|
+
* Finds all locations that would be affected by a rename operation.
|
|
1733
|
+
* Maps each location's span back to original positions. Locations in
|
|
1734
|
+
* generated code are filtered out.
|
|
1735
|
+
*
|
|
1736
|
+
* @remarks
|
|
1737
|
+
* Uses a compatibility wrapper (callFindRenameLocations) to handle
|
|
1738
|
+
* different TypeScript version signatures.
|
|
1739
|
+
*/
|
|
1105
1740
|
const originalFindRenameLocations = info.languageService.findRenameLocations.bind(info.languageService);
|
|
1741
|
+
/**
|
|
1742
|
+
* Compatibility wrapper for findRenameLocations that handles both old
|
|
1743
|
+
* and new TypeScript API signatures.
|
|
1744
|
+
*/
|
|
1106
1745
|
const callFindRenameLocations = (fileName, position, opts) => {
|
|
1107
1746
|
// Prefer object overload if available; otherwise fall back to legacy args
|
|
1108
1747
|
if (originalFindRenameLocations.length <= 3) {
|
|
@@ -1148,7 +1787,13 @@ function init(modules) {
|
|
|
1148
1787
|
return callFindRenameLocations(fileName, position, options);
|
|
1149
1788
|
}
|
|
1150
1789
|
};
|
|
1151
|
-
|
|
1790
|
+
/**
|
|
1791
|
+
* Hook: getDocumentHighlights
|
|
1792
|
+
*
|
|
1793
|
+
* Highlights all occurrences of a symbol in the document (used when you
|
|
1794
|
+
* click on a variable and see all usages highlighted). Maps highlight
|
|
1795
|
+
* spans back to original positions.
|
|
1796
|
+
*/
|
|
1152
1797
|
const originalGetDocumentHighlights = info.languageService.getDocumentHighlights.bind(info.languageService);
|
|
1153
1798
|
info.languageService.getDocumentHighlights = (fileName, position, filesToSearch) => {
|
|
1154
1799
|
try {
|
|
@@ -1193,7 +1838,12 @@ function init(modules) {
|
|
|
1193
1838
|
return originalGetDocumentHighlights(fileName, position, filesToSearch);
|
|
1194
1839
|
}
|
|
1195
1840
|
};
|
|
1196
|
-
|
|
1841
|
+
/**
|
|
1842
|
+
* Hook: getImplementationAtPosition
|
|
1843
|
+
*
|
|
1844
|
+
* Provides "Go to Implementation" functionality. Similar to definition
|
|
1845
|
+
* but finds concrete implementations of abstract methods/interfaces.
|
|
1846
|
+
*/
|
|
1197
1847
|
const originalGetImplementationAtPosition = info.languageService.getImplementationAtPosition.bind(info.languageService);
|
|
1198
1848
|
info.languageService.getImplementationAtPosition = (fileName, position) => {
|
|
1199
1849
|
try {
|
|
@@ -1233,7 +1883,18 @@ function init(modules) {
|
|
|
1233
1883
|
return originalGetImplementationAtPosition(fileName, position);
|
|
1234
1884
|
}
|
|
1235
1885
|
};
|
|
1236
|
-
|
|
1886
|
+
/**
|
|
1887
|
+
* Hook: getCodeFixesAtPosition
|
|
1888
|
+
*
|
|
1889
|
+
* Provides quick fix suggestions for errors at a position. Maps the
|
|
1890
|
+
* input span to expanded coordinates to get fixes that work with
|
|
1891
|
+
* generated code context.
|
|
1892
|
+
*
|
|
1893
|
+
* @remarks
|
|
1894
|
+
* Note: The returned fixes may include edits to expanded code, which
|
|
1895
|
+
* could be problematic. Consider filtering or mapping fix edits in
|
|
1896
|
+
* future versions.
|
|
1897
|
+
*/
|
|
1237
1898
|
const originalGetCodeFixesAtPosition = info.languageService.getCodeFixesAtPosition.bind(info.languageService);
|
|
1238
1899
|
info.languageService.getCodeFixesAtPosition = (fileName, start, end, errorCodes, formatOptions, preferences) => {
|
|
1239
1900
|
try {
|
|
@@ -1253,7 +1914,12 @@ function init(modules) {
|
|
|
1253
1914
|
return originalGetCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
|
|
1254
1915
|
}
|
|
1255
1916
|
};
|
|
1256
|
-
|
|
1917
|
+
/**
|
|
1918
|
+
* Hook: getNavigationTree
|
|
1919
|
+
*
|
|
1920
|
+
* Provides the document outline/structure tree (shown in the Outline
|
|
1921
|
+
* panel). Recursively maps all spans in the tree back to original positions.
|
|
1922
|
+
*/
|
|
1257
1923
|
const originalGetNavigationTree = info.languageService.getNavigationTree.bind(info.languageService);
|
|
1258
1924
|
info.languageService.getNavigationTree = (fileName) => {
|
|
1259
1925
|
try {
|
|
@@ -1293,7 +1959,12 @@ function init(modules) {
|
|
|
1293
1959
|
return originalGetNavigationTree(fileName);
|
|
1294
1960
|
}
|
|
1295
1961
|
};
|
|
1296
|
-
|
|
1962
|
+
/**
|
|
1963
|
+
* Hook: getOutliningSpans
|
|
1964
|
+
*
|
|
1965
|
+
* Provides code folding regions. Maps both the text span (what gets
|
|
1966
|
+
* folded) and hint span (what's shown when collapsed) back to original.
|
|
1967
|
+
*/
|
|
1297
1968
|
const originalGetOutliningSpans = info.languageService.getOutliningSpans.bind(info.languageService);
|
|
1298
1969
|
info.languageService.getOutliningSpans = (fileName) => {
|
|
1299
1970
|
try {
|
|
@@ -1328,7 +1999,17 @@ function init(modules) {
|
|
|
1328
1999
|
return originalGetOutliningSpans(fileName);
|
|
1329
2000
|
}
|
|
1330
2001
|
};
|
|
1331
|
-
|
|
2002
|
+
/**
|
|
2003
|
+
* Hook: provideInlayHints
|
|
2004
|
+
*
|
|
2005
|
+
* Provides inlay hints (inline type annotations shown in the editor).
|
|
2006
|
+
* Maps the requested span to expanded coordinates, then maps each hint's
|
|
2007
|
+
* position back to original. Hints in generated code are filtered out.
|
|
2008
|
+
*
|
|
2009
|
+
* @remarks
|
|
2010
|
+
* This hook is conditional - provideInlayHints may not exist in older
|
|
2011
|
+
* TypeScript versions.
|
|
2012
|
+
*/
|
|
1332
2013
|
const originalProvideInlayHints = info.languageService.provideInlayHints?.bind(info.languageService);
|
|
1333
2014
|
if (originalProvideInlayHints) {
|
|
1334
2015
|
info.languageService.provideInlayHints = (fileName, span, preferences) => {
|