Files
volar-docs/docs/source-map-and-code-gen.md
2025-11-09 22:22:52 -06:00

8.9 KiB
Raw Blame History

Deep Dive: @volar/source-map & @volar/code-gen

Docs IndexRepo READMEPlugin AuthoringPerformance Guide

When you author language plugins, you must translate source text into generated code without losing the ability to map diagnostics, hovers, and refactors back to the original file. Volar provides two foundational packages for this job:

  • @volar/source-map: low-level mapping utilities used everywhere Volar needs to relate two coordinate spaces (source ↔ generated).
  • @volar/code-gen: higher-level builder that emits text and source maps in lockstep, making it painless to describe complex virtual files.

This guide explains every exported API, flag, and workflow, with exhaustive examples and best practices.

@volar/source-map Basics

import { SourceMap, Mapping, CodeInformation } from '@volar/source-map';

const map = new SourceMap<CodeInformation>();
map.add({
  data: { verification: true, navigation: true },
  sourceRange: [srcStart, srcEnd],
  mappedRange: [genStart, genEnd],
});

const mapped = map.getMappedRange([srcStart, srcEnd]);
const reverse = map.getSourceRange([genStart, genEnd]);

Mapping Fields

  • sourceRange: [startOffset, endOffset] in the original document.
  • mappedRange: [startOffset, endOffset] in the generated document.
  • data: a CodeInformation object controlling feature visibility:
    • verification: allow diagnostics + code actions.
    • completion: allow completions; { isAdditional: true } to mark extra suggestions (imports, etc.).
    • semantic: enable hover/inlay semantics.
    • navigation: rename/reference/definition support.
    • structure: outlines/folding.
    • format: formatting support.

Use the SourceMap methods:

  • getMappedRange(range, filter?) yields generated segments for a source range.
  • getSourceRange(range, filter?) inverse lookup.
  • forEach() iterate all mappings.

Best Practices

  1. Minimal coverage map only the user-authored slices; synthetic scaffolding should either be unmapped or flagged with verification: false.
  2. Feature-specific filters when computing rename ranges, pass a filter to getMappedRange that checks data.navigation.
  3. Chunk by content type create separate mappings for template vs script vs style segments so downstream plugins can enable/disable features per chunk.

@volar/code-gen Workflow

@volar/code-gen builds on the source-map APIs and gives you a friendly builder for emitting text + mappings simultaneously.

import { CodeGen } from '@volar/code-gen';

const codeGen = new CodeGen();
codeGen.addText('const components = ');
codeGen.addCode(
  'getComponents()',
  { start: templateOffset, end: templateOffset + 13 },
  {
    verification: true,
    navigation: true,
  },
);
codeGen.addText(';\n');

const text = codeGen.getText();
const mappings = codeGen.getMappings();
const embeddedFiles = codeGen.getEmbeddedFiles();

Core Methods

  • addText(text: string) append raw generated text (no mapping).
  • addCode(generatedText, sourceRange, codeInfo) append text and register a mapping back to the source.
  • addMapping(mapping) manually push a Mapping.
  • addMapping2 (advanced) register multi-segment mappings.
  • addExtraMapping for completion-only or formatting-only segments.
  • getText() returns the generated string.
  • getMappings() returns an array of Mapping<CodeInformation>.
  • getLength() current length of generated text.
  • getEmbeddedFiles() nested CodeGen instances for embedded content (templates within scripts, etc.).
  • getTextDocument(uri, languageId) convenience for building a TextDocument from the generated content.

Embedded Files

Use codeGen.addEmbeddedFile(file) when your language plugin produces nested virtual documents (e.g., a <template> section that becomes HTML + TS). Each embedded file is itself a CodeGen instance with its own text/mappings.

const templateFile = codeGen.addEmbeddedFile('template.html');
templateFile.addText('<div>');
templateFile.addCode('{{ value }}', sourceRange, codeInfo);
templateFile.addText('</div>');

Flags & Options

CodeGen constructor accepts options:

const codeGen = new CodeGen({
  useBlockNewLine: true, // automatically insert newline when switching blocks
  mappingMode: 'offset' | 'lengthDiff', // default 'offset'
  mapRange: true, // include mapped ranges even for addText (rare)
});
  • useBlockNewLine: helpful when generating multi-line scripts; ensures mappings stay aligned.
  • mappingMode:
    • 'offset' store absolute offsets (default, recommended).
    • 'lengthDiff' store mapping as delta relative to previous entries (smaller payloads, but harder to debug). Only use if you profile serialization issues.
  • mapRange: set to true if you want addText to also produce zero-length mappings (useful when adding sentinel tokens that still need navigation info).

Example: Vue Template to TS

const codeGen = new CodeGen();
codeGen.addText('const __ctx = ctx;\n');

// Map interpolation
const expr = templateContent.slice(start, end);
codeGen.addText('__ctx.');
codeGen.addCode(expr, { start, end }, {
  navigation: true,
  semantic: true,
  completion: true,
});
codeGen.addText(';\n');

Generated text:

const __ctx = ctx;
__ctx.user.name;

Mappings:

Source Range Generated Range Flags
[start, end] [len('__ctx.') + start2, ...] navigation, semantic, completion

Merging CodeGen Output

CodeGen exposes codeGen.getMappings() which you can feed directly into your language plugins virtual file. When combining multiple code generators (template + script), simply concatenate the text and map offsets accordingly.

Performance Tips

  1. Reuse builders instantiate CodeGen once per virtual file; avoid nested builders unless you truly have embedded content.
  2. Avoid string concatenation always use addText / addCode instead of manual +=; CodeGen handles buffer growth efficiently.
  3. Batch mappings if you generate repeated patterns (e.g., loop expansions), precompute mapping templates to minimize overhead.

Putting It Together

A full language plugin typically:

  1. Parses the source (AST, tokens).
  2. Uses CodeGen to emit TypeScript/JavaScript/HTML/CSS virtual documents with precise mappings.
  3. Exposes the generated text + mappings via the plugins createVirtualCode.
  4. (Optional) Provides TypeScript integration, so TS features (diagnostics, completions) run on the generated code.
export const fooLanguagePlugin: LanguagePlugin = {
  getLanguageId(scriptId) {
    if (scriptId.endsWith('.foo')) return 'foo';
  },
  createVirtualCode(scriptId, languageId, snapshot) {
    const content = snapshot.getText(0, snapshot.getLength());
    const codeGen = new CodeGen();
    // ... populate codeGen ...
    return {
      id: scriptId + '.ts',
      languageId: 'typescript',
      snapshot: ts.ScriptSnapshot.fromString(codeGen.getText()),
      mappings: codeGen.getMappings(),
    };
  },
};

Debugging Source Maps

  • codeGen.toSourceMap() returns a SourceMap instance for debugging. Call map.getMappings() to inspect raw entries.
  • Use tools like source-map-visualizer (community) to visualize segments.
  • Log mismatches by running map.getMappedRange and verifying the output matches expectations.

Best Practices Summary

  1. Align offsets Use codeGen.getLength() to track the current generated offset before adding new segments; this ensures mapping ranges start/end at exact positions.
  2. Preserve indentation When generating multi-line strings, maintain consistent indentation so formatting/renames remain intuitive.
  3. Flag-only sections If you need completions/rename but no emitted text (e.g., placeholders), use addMapping with zero-length ranges and data describing the desired capabilities.
  4. Embedded file boundaries annotate the start/end of embedded files with sentinel comments so you can debug generated output more easily.
  5. Unit tests snapshot the generated text + mappings; assert that getMappedRange and getSourceRange round-trip.
  6. Schema for data store additional metadata (AST nodes, symbol tables) alongside snapshots so service plugins can augment diagnostics without reparsing.
  7. Export metadata include a meta object (custom) on your virtual file describing compile options, feature flags, etc., so language-service plugins can tailor behavior.

By mastering @volar/code-gen and @volar/source-map, you can confidently generate virtual documents that preserve every semantic relationship. Accurate mappings are the foundation for precise diagnostics, correct renames, and intuitive editor experiences—invest time in them and the rest of your plugin stack will inherit the payoff.