mirror of
https://github.com/LukeHagar/volar-docs.git
synced 2025-12-06 12:57:47 +00:00
203 lines
8.9 KiB
Markdown
203 lines
8.9 KiB
Markdown
# Deep Dive: `@volar/source-map` & `@volar/code-gen`
|
||
|
||
> [Docs Index](README.md) • [Repo README](../README.md) • [Plugin Authoring](plugin-authoring.md) • [Performance Guide](performance-and-debugging.md)
|
||
|
||
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
|
||
|
||
```ts
|
||
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.
|
||
|
||
```ts
|
||
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.
|
||
|
||
```ts
|
||
const templateFile = codeGen.addEmbeddedFile('template.html');
|
||
templateFile.addText('<div>');
|
||
templateFile.addCode('{{ value }}', sourceRange, codeInfo);
|
||
templateFile.addText('</div>');
|
||
```
|
||
|
||
### Flags & Options
|
||
|
||
`CodeGen` constructor accepts options:
|
||
|
||
```ts
|
||
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
|
||
|
||
```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:
|
||
|
||
```ts
|
||
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
|
||
|
||
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 plugin’s `createVirtualCode`.
|
||
4. (Optional) Provides TypeScript integration, so TS features (diagnostics, completions) run on the generated code.
|
||
|
||
```ts
|
||
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.
|