8.9 KiB
Deep Dive: @volar/source-map & @volar/code-gen
Docs Index • Repo README • Plugin Authoring • Performance 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: aCodeInformationobject 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
- Minimal coverage – map only the user-authored slices; synthetic scaffolding should either be unmapped or flagged with
verification: false. - Feature-specific filters – when computing rename ranges, pass a filter to
getMappedRangethat checksdata.navigation. - 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 aMapping.addMapping2(advanced) – register multi-segment mappings.addExtraMapping– for completion-only or formatting-only segments.getText()– returns the generated string.getMappings()– returns an array ofMapping<CodeInformation>.getLength()– current length of generated text.getEmbeddedFiles()– nestedCodeGeninstances for embedded content (templates within scripts, etc.).getTextDocument(uri, languageId)– convenience for building aTextDocumentfrom 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 totrueif you wantaddTextto 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 plugin’s virtual file. When combining multiple code generators (template + script), simply concatenate the text and map offsets accordingly.
Performance Tips
- Reuse builders – instantiate
CodeGenonce per virtual file; avoid nested builders unless you truly have embedded content. - Avoid string concatenation – always use
addText/addCodeinstead of manual+=;CodeGenhandles buffer growth efficiently. - 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:
- Parses the source (AST, tokens).
- Uses
CodeGento emit TypeScript/JavaScript/HTML/CSS virtual documents with precise mappings. - Exposes the generated text + mappings via the plugin’s
createVirtualCode. - (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 aSourceMapinstance for debugging. Callmap.getMappings()to inspect raw entries.- Use tools like
source-map-visualizer(community) to visualize segments. - Log mismatches by running
map.getMappedRangeand verifying the output matches expectations.
Best Practices Summary
- Align offsets – Use
codeGen.getLength()to track the current generated offset before adding new segments; this ensures mapping ranges start/end at exact positions. - Preserve indentation – When generating multi-line strings, maintain consistent indentation so formatting/renames remain intuitive.
- Flag-only sections – If you need completions/rename but no emitted text (e.g., placeholders), use
addMappingwith zero-length ranges anddatadescribing the desired capabilities. - Embedded file boundaries – annotate the start/end of embedded files with sentinel comments so you can debug generated output more easily.
- Unit tests – snapshot the generated text + mappings; assert that
getMappedRangeandgetSourceRangeround-trip. - Schema for data – store additional metadata (AST nodes, symbol tables) alongside snapshots so service plugins can augment diagnostics without reparsing.
- Export metadata – include a
metaobject (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.