mirror of
https://github.com/LukeHagar/volar-docs.git
synced 2025-12-06 04:22:01 +00:00
282 lines
12 KiB
Markdown
282 lines
12 KiB
Markdown
# Authoring Volar Plugins & Language Services
|
||
|
||
> [Docs Index](README.md) • [Repo README](../README.md) • [Code Gen Guide](source-map-and-code-gen.md) • [Testing & CI](testing-and-ci.md)
|
||
|
||
Volar’s architecture is deliberately modular: language plugins transform source files into virtual code, while language-service plugins add editor-facing features such as completions, hovers, diagnostics, and code actions. This guide documents every hook, option, flag, and best practice so you can author custom functionality with confidence.
|
||
|
||
## Mental Model
|
||
|
||
```
|
||
Source file ──▶ Language plugin (virtual code + source maps) ──▶ Language service plugin (capabilities) ──▶ Host (LSP, Monaco, CLI)
|
||
```
|
||
|
||
- **Language plugins** live in `@volar/language-core`. They describe how a file decomposes into embedded scripts (e.g., templates, styles) and how generated code maps back to the original source.
|
||
- **Language service plugins** live in `@volar/language-service`. They consume the virtual code graph and implement hover/completion/diagnostics, etc.
|
||
- **TypeScript plugins** (part of `@volar/typescript`) bridge Volar’s virtual code into the TS language service so TypeScript-based tooling understands your files.
|
||
|
||
## Language Plugin API (`@volar/language-core`)
|
||
|
||
```ts
|
||
import type { LanguagePlugin, VirtualCode, CodegenContext, IScriptSnapshot } from '@volar/language-core';
|
||
|
||
const myLanguagePlugin: LanguagePlugin = {
|
||
getLanguageId(scriptId) {
|
||
if (scriptId.endsWith('.foo')) return 'foo';
|
||
},
|
||
createVirtualCode(scriptId, languageId, snapshot, ctx) {
|
||
const code = transformFooToTs(snapshot.getText(0, snapshot.getLength()));
|
||
return {
|
||
id: scriptId + '.ts',
|
||
languageId: 'typescript',
|
||
snapshot: createSnapshot(code),
|
||
mappings: buildMappings(snapshot, code),
|
||
embeddedCodes: [],
|
||
};
|
||
},
|
||
updateVirtualCode(scriptId, previous, newSnapshot, ctx) {
|
||
// Optional incremental update. Return undefined to fallback to createVirtualCode.
|
||
},
|
||
disposeVirtualCode(scriptId, virtualCode) {
|
||
virtualCode.snapshot.dispose?.();
|
||
},
|
||
isAssociatedFileOnly(scriptId) {
|
||
// Return true if the file exists purely to feed generated code (rare).
|
||
},
|
||
};
|
||
```
|
||
|
||
### Key Fields
|
||
|
||
- `id`: unique identifier for the virtual code (commonly `${sourceId}.${ext}`).
|
||
- `languageId`: logical language (e.g., `typescript`, `html`, `json`). Hosts use this to decide which plugins run.
|
||
- `snapshot`: an `IScriptSnapshot` describing the generated text. Use `ts.ScriptSnapshot.fromString` or a custom snapshot implementation.
|
||
- `mappings`: array of `CodeMapping` entries describing how generated ranges map back to source ranges.
|
||
- `embeddedCodes`: nested virtual codes (e.g., template -> script -> CSS). Walk them recursively so downstream plugins see the full graph.
|
||
- `linkedCodeMappings`: optional cross-file relationships (e.g., macro expansions). Populate when features like rename need multi-file context.
|
||
|
||
### Mapping Flags (`CodeInformation`)
|
||
|
||
Each mapping entry can enable/disable specific language features:
|
||
|
||
- `verification`: allow diagnostics/code actions. Use `false` for generated scaffolding you don’t want to expose.
|
||
- `completion`: mark sections as completion targets. Use `onlyImport` for import-only completions.
|
||
- `semantic`: enable hover/inlay highlight generation.
|
||
- `navigation`: rename/reference/definition support.
|
||
- `structure`: drive folding/outline.
|
||
- `format`: allow formatting engines to reflow the generated region.
|
||
|
||
**Best practice:** map only the user-authored slices of generated code. If a block of your generated TS is purely synthetic, set `verification: false` and omit `navigation` so diagnostics and rename never escape into that region.
|
||
|
||
## TypeScript Bridge (`@volar/typescript`)
|
||
|
||
If you need TypeScript to understand your virtual files:
|
||
|
||
```ts
|
||
import { createLanguageServiceHost } from '@volar/typescript';
|
||
|
||
const { languageServiceHost, getExtraServiceScript } = createLanguageServiceHost(
|
||
ts,
|
||
sys,
|
||
language,
|
||
(fileName) => fileName as URI,
|
||
projectHost,
|
||
);
|
||
```
|
||
|
||
- **Extra service scripts**: use `languagePlugin.typescript?.getServiceScript` to expose additional virtual files (e.g., template-extracted scripts) directly to the TS service.
|
||
- **Incremental programs**: `createTypeScriptProject` (from `@volar/language-server/lib/project/typescriptProject`) wraps the TS host with file watching and multi-config support.
|
||
|
||
### TypeScript Plugin Options
|
||
|
||
Within a language plugin you can define `typescript` options:
|
||
|
||
```ts
|
||
const plugin: LanguagePlugin = {
|
||
typescript: {
|
||
extraFileExtensions: [{ extension: 'foo', isMixedContent: false, scriptKind: ts.ScriptKind.Deferred }],
|
||
getServiceScript(root) { /* return TypeScript service script */ },
|
||
getExtraServiceScripts(fileName, root) { /* return additional scripts (d.ts, .ts) */ },
|
||
resolveHiddenExtensions: true, // allow hidden files to be used by TS
|
||
},
|
||
};
|
||
```
|
||
|
||
- Use `extraFileExtensions` so TypeScript doesn’t treat `.foo` as plain text.
|
||
- `resolveHiddenExtensions`: set to `true` if your virtual files live under hidden paths (e.g., `.volar` dirs) and still need TS features.
|
||
|
||
## Language Service Plugin API (`@volar/language-service`)
|
||
|
||
```ts
|
||
import type { LanguageServicePlugin } from '@volar/language-service';
|
||
|
||
const myServicePlugin: LanguageServicePlugin = {
|
||
name: 'foo-diagnostics',
|
||
capabilities: {
|
||
diagnosticProvider: true,
|
||
completionProvider: { triggerCharacters: [':'] },
|
||
codeActionProvider: true,
|
||
},
|
||
create(context) {
|
||
return {
|
||
provideDiagnostics(document) {
|
||
return [{ message: 'Invalid foo', range: ... }];
|
||
},
|
||
provideCompletionItems(document, position) {
|
||
return { items: [{ label: 'fooOption' }] };
|
||
},
|
||
provideCodeActions(document, range, context) {
|
||
return [{ title: 'Fix foo', edit: ... }];
|
||
},
|
||
};
|
||
},
|
||
};
|
||
```
|
||
|
||
### Available Hooks
|
||
|
||
- `provideDiagnostics(document)` – return an array of `Diagnostic`.
|
||
- `provideCompletionItems(document, position, context)` – return `CompletionList`.
|
||
- `resolveCompletionItem(item)` – add lazy detail (e.g., docs).
|
||
- `provideHover(document, position)` – return `Hover`.
|
||
- `provideDocumentSymbols`, `provideFoldingRanges`, `provideSelectionRanges`.
|
||
- `provideDefinition`, `provideTypeDefinition`, `provideImplementation`.
|
||
- `provideCodeActions`, `resolveCodeAction`.
|
||
- `provideCodeLens`, `resolveCodeLens`.
|
||
- `provideDocumentFormattingEdits` / `provideDocumentOnTypeFormattingEdits`.
|
||
- `provideLinkedEditingRanges`, `provideDocumentHighlights`.
|
||
- `provideDocumentLinks`, `resolveDocumentLink`.
|
||
- `provideInlayHints`, `resolveInlayHint`.
|
||
- `provideMoniker`, `provideInlineValues`.
|
||
- `provideSemanticTokens`, `provideWorkspaceSymbols`.
|
||
- `provideDocumentDropEdits`, `provideAutoInsert`, `provideCommand`.
|
||
|
||
**Capabilities Flag Reference:**
|
||
|
||
Set the matching flag in `capabilities` to signal what hooks you implement:
|
||
|
||
- `diagnosticProvider: true`
|
||
- `completionProvider: { triggerCharacters, resolveProvider }`
|
||
- `hoverProvider: true`
|
||
- `codeActionProvider: true`
|
||
- `documentFormattingProvider`, `documentOnTypeFormattingProvider`
|
||
- `documentSymbolProvider`, `workspaceSymbolProvider`
|
||
- `documentLinkProvider`
|
||
- `codeLensProvider`
|
||
- `inlayHintProvider`
|
||
- `semanticTokensProvider`
|
||
- `autoInsertionProvider`
|
||
- `documentDropProvider`
|
||
- `monikerProvider`
|
||
- `inlineValueProvider`
|
||
|
||
**Best practice:** Always set the minimal capability set, so hosts skip calling hooks you don’t implement.
|
||
|
||
## Registering Plugins
|
||
|
||
### Language-Core Plugins
|
||
|
||
When creating a language core:
|
||
|
||
```ts
|
||
import { createLanguage, createUriMap } from '@volar/language-core';
|
||
|
||
const language = createLanguage(
|
||
[myLanguagePlugin],
|
||
createUriMap(false),
|
||
(uri, includeFs, shouldRegister) => { /* sync hook */ },
|
||
);
|
||
```
|
||
|
||
### Language-Service Plugins
|
||
|
||
When creating a language service:
|
||
|
||
```ts
|
||
import { createLanguageService } from '@volar/language-service';
|
||
|
||
const service = createLanguageService(language, [myServicePlugin], env, project);
|
||
```
|
||
|
||
### Language Server Integration
|
||
|
||
In `@volar/language-server`, register plugins via the project factory:
|
||
|
||
```ts
|
||
createTypeScriptProject(ts, tsLocalized, async (context) => ({
|
||
languagePlugins: [myLanguagePlugin],
|
||
setup({ language, project }) {
|
||
project.languageServicePlugins.push(myServicePlugin);
|
||
},
|
||
}));
|
||
```
|
||
|
||
For non-TS projects, pass the plugins directly to `createSimpleProject`.
|
||
|
||
## End-to-End Example: Custom `.foo` Language
|
||
|
||
1. **Language plugin**: parse `.foo` files, translate them into `.ts` virtual code with source maps.
|
||
2. **Language service plugin**: inspect the `.foo` AST (via metadata stored in the snapshot) to provide diagnostics and completions.
|
||
3. **TypeScript integration**: register `.foo` via `extraFileExtensions` so TS sees the generated `.ts` output.
|
||
4. **Server**: include the plugins in your Volar language server project and expose configuration toggles via `server.configurations`.
|
||
|
||
```ts
|
||
// fooLanguagePlugin.ts
|
||
export const fooLanguagePlugin: LanguagePlugin = {
|
||
getLanguageId(scriptId) {
|
||
if (scriptId.endsWith('.foo')) return 'foo';
|
||
},
|
||
createVirtualCode(scriptId, languageId, snapshot) {
|
||
const { code, mappings } = compileFoo(snapshot.getText(0, snapshot.getLength()));
|
||
return {
|
||
id: scriptId + '.ts',
|
||
languageId: 'typescript',
|
||
snapshot: ts.ScriptSnapshot.fromString(code),
|
||
mappings,
|
||
};
|
||
},
|
||
typescript: {
|
||
extraFileExtensions: [{ extension: 'foo', isMixedContent: false, scriptKind: ts.ScriptKind.Deferred }],
|
||
},
|
||
};
|
||
```
|
||
|
||
```ts
|
||
// fooServicePlugin.ts
|
||
export const fooServicePlugin: LanguageServicePlugin = {
|
||
name: 'foo',
|
||
capabilities: {
|
||
diagnosticProvider: true,
|
||
completionProvider: { triggerCharacters: [':'] },
|
||
hoverProvider: true,
|
||
},
|
||
create({ language }) {
|
||
return {
|
||
provideDiagnostics(document) { /* ... */ },
|
||
provideCompletionItems(document, position) { /* ... */ },
|
||
provideHover(document, position) { /* ... */ },
|
||
};
|
||
},
|
||
};
|
||
```
|
||
|
||
```ts
|
||
// server setup
|
||
const project = createSimpleProject([fooLanguagePlugin]);
|
||
const languageServicePlugins = [fooServicePlugin];
|
||
```
|
||
|
||
## Best Practices Checklist
|
||
|
||
1. **Type-safe snapshots** – store metadata (ASTs, parse errors) on the snapshot instance to avoid reparsing in service plugins.
|
||
2. **Map-only relevant regions** – keep mappings minimal to reduce LSP payload sizes and avoid duplicate diagnostics on synthetic sections.
|
||
3. **Handle incremental updates** – implement `updateVirtualCode` for large files so you don’t recreate snapshots on every keystroke.
|
||
4. **Watch configuration** – expose settings via `server.configurations` and apply them inside your plugins (e.g., lint severity).
|
||
5. **Test every hook** – use the `vscode-languageserver` test harness to simulate completions, hovers, diagnostics, etc., and assert on the results.
|
||
6. **Profile plugin performance** – wrap expensive hooks (diagnostics, completions) with timing logs in development to catch regressions early.
|
||
7. **Respect cancellation** – if your plugin performs async work (e.g., fetch data), cancel promptly when the provided token fires.
|
||
8. **Document capabilities** – consumers should know what your plugin does (e.g., “Provides hover + quick fixes for .foo files but not formatting”).
|
||
9. **Wire telemetry** – emit `connection.telemetry.logEvent` for significant plugin actions (cache misses, external request failures) to aid debugging.
|
||
10. **Stay in sync with Volar releases** – watch for changes in `@volar/language-*` packages; run integration tests against new versions to catch API shifts.
|
||
|
||
By following these steps and filling in all hooks, flags, and options, your Volar plugins will integrate cleanly with any host—from VS Code to custom CLI tooling—while delivering full-fidelity language intelligence.
|