@macroforge/typescript-plugin 0.1.32 → 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/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
- // Macro manifest cache for hover info
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
- * Find macro name at position in @derive(...) comment
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
- * Find decorator at position like @serde or @debug
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
- * Get macro hover info at position
81
- * Returns QuickInfo if hovering on a macro or decorator, null otherwise
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
- // Map to store generated virtual .d.ts files
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
- // Cache snapshots to ensure identity stability for TypeScript's incremental compiler
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
- // Guard against reentrancy
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
- // Instantiate native plugin (handles caching and logging in Rust)
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
- // Log helper - delegates to Rust
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
- // Process file through macro expansion (caching handled in Rust)
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
- // Hook getScriptVersion to provide versions for virtual .d.ts files
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
- // Hook getScriptFileNames to include our virtual .d.ts files
344
- // This allows TS to "see" these new files as part of the project
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
- // Hook fileExists to resolve our virtual .d.ts files
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
- // Hook getScriptSnapshot to provide the "expanded" type definition view
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
- // If it's one of our virtual .d.ts files, return its snapshot
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
- // Guard against reentrancy - if we're already processing this file, return original
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
- // Don't process non-TypeScript files or .expanded.ts files
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
- // Only process files with macro directives
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 if we have a cached snapshot for this version
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 the snapshot for stable identity
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
- // Cache the original snapshot
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
- // Hook getSemanticDiagnostics to provide macro errors and map positions
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
- // Hook getSyntacticDiagnostics to map positions
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
- // Hook getQuickInfoAtPosition to map input position and output spans
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
- // Hook getCompletionsAtPosition to map input position
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
- // Hook getDefinitionAtPosition to map input and output positions
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
- // Hook getDefinitionAndBoundSpan for more complete definition handling
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
- // Hook getTypeDefinitionAtPosition
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
- // Hook getReferencesAtPosition
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
- // Hook findReferences
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
- // Hook getSignatureHelpItems
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
- // Hook getRenameInfo
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
- // Hook findRenameLocations (newer overload prefers options object)
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
- // Hook getDocumentHighlights
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
- // Hook getImplementationAtPosition
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
- // Hook getCodeFixesAtPosition
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
- // Hook getNavigationTree
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
- // Hook getOutliningSpans
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
- // Hook provideInlayHints to map positions
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) => {