Files
volar-docs/docs/plugin-authoring.md
2025-11-09 22:22:52 -06:00

282 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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)
Volars 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 Volars 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 dont 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 doesnt 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 dont 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 dont 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.