Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 23 additions & 13 deletions astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,26 +32,36 @@ export default defineConfig({
PageTitle: "./src/components/starlight/PageTitle.astro",
},
sidebar: [
{
label: "About",
autogenerate: { directory: "about" },
},
{
label: "Core Concepts",
items: [
// Each item here is one entry in the navigation menu.
{ label: "Why Volar?", link: "/core-concepts/why-volar" },
{ label: "Embedded Languages", link: "/core-concepts/embedded-languages" },
{ label: "Volar Labs", link: "/core-concepts/volar-labs" },
],
// TODO: Use `autogenerate` once it allows you to order the sidebar
autogenerate: { directory: "core-concepts" },
},
{
label: "Guides",
items: [
{ label: "Your First Volar Language Server", link: "/guides/first-server" },
{ label: "File Structure", link: "/guides/file-structure" },
],
autogenerate: { directory: "guides" },
},
{
label: "Service Methods",
autogenerate: { directory: "service-methods" },
collapsed: true,
},
{
label: "Packages",
autogenerate: { directory: "packages" },
collapsed: true,
},
{
label: "Services",
autogenerate: { directory: "services" },
collapsed: true,
},
{
label: "Reference",
autogenerate: { directory: "reference" },
label: "Tools",
autogenerate: { directory: "tools" },
},
],
}),
Expand Down
282 changes: 282 additions & 0 deletions src/content/docs/core-concepts/language-definition.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,282 @@
---
title: Language Definition
description: Define and implement Volar language plugins and VirtualCode, including all hooks, mappings, and best practices.
---

> This page is a work in progress. Interested in contributing some documentation to it, or want to improve it? [Edit this page on GitHub](https://github.com/volarjs/docs/blob/main/src/content/docs/core-concepts/language-definition.mdx)

As can be expected from a framework for language servers, Volar allows you to define the languages you want to support in your project.

## Shape of a language definition

A language definition is a JavaScript object that contains a `createVirtualCode` and a `updateVirtualCode` function.

```ts title="src/my-language.ts"
export const language = {
createVirtualCode(fileId, languageId, snapshot) {
// Create a virtual code object
},
updateVirtualCode(_fileId, languageCode, snapshot) {
// Update the virtual code object
},
};
```

As the names suggests, those methods create and update a `VirtualCode`. A `VirtualCode` object is created for each file that your language server will handle. These can then be accessed in the hooks of the [services](/core-concepts/service-definition) that provides the features of your language server, as such they're also a place you can store additional information about the file that could be useful to know for the features of your services.

Albeit not required, a common pattern is to define a JavaScript class that implements the `VirtualCode` interface, as this makes it easier to later add more properties and methods to the virtual code object and unlock the ability to use `instanceof` to check if a virtual code object is of a certain type.

```ts title="src/my-language.ts"
import type { LanguagePlugin, VirtualCode } from '@volar/language-core';

export const language = {
createVirtualCode(fileId, languageId, snapshot) {
if (languageId !== 'my-language')
return;

return new MyLanguageVirtualCode(snapshot);
},
updateVirtualCode(_fileId, languageCode, snapshot) {
languageCode.update(snapshot);
return languageCode;
},
} satisfies LanguagePlugin<MyLanguageVirtualCode>;

export class MyLanguageVirtualCode implements VirtualCode {
id = 'root';
languageId = 'my-language';
mappings = []

constructor(
public snapshot: ts.IScriptSnapshot
) {
this.onSnapshotUpdated();
}

public update(newSnapshot: ts.IScriptSnapshot) {
this.snapshot = newSnapshot;
this.onSnapshotUpdated();
}

onSnapshotUpdated() {
// Update the virtual code object
}
}
```

This is a simple example of a language definition, where `MyVirtualLanguageCode` only does the strict minimum possible. In a real language definition, you would most likely have a lot more properties and methods available on the `MyLanguageVirtualCode` class.

### Embedded languages

If your language supports [embedded languages](/core-concepts/embedded-languages/), your instance `VirtualCode` should include a `embeddedCodes` property that contains an array of `VirtualCode` instances for the embedded languages.

```ts title="src/my-language.ts" ins={20, 34-52} collapse={1-14, 22-31}
import type { LanguagePlugin, VirtualCode } from '@volar/language-core';

export const language = {
createVirtualCode(fileId, languageId, snapshot) {
if (languageId !== 'my-language')
return;

return new MyLanguageVirtualCode(snapshot);
},
updateVirtualCode(_fileId, languageCode, snapshot) {
languageCode.update(snapshot);
return languageCode;
},
} satisfies LanguagePlugin<MyLanguageVirtualCode>;

export class MyLanguageVirtualCode implements VirtualCode {
id = 'root';
languageId = 'my-language';
mappings = []
embeddedCodes: VirtualCode[] = []

constructor(
public snapshot: ts.IScriptSnapshot
) {
this.onSnapshotUpdated();
}

public update(newSnapshot: ts.IScriptSnapshot) {
this.snapshot = newSnapshot;
this.onSnapshotUpdated();
}

onSnapshotUpdated() {
const snapshotContent = this.snapshot.getText(0, this.snapshot.getLength());

// Find embedded languages
const embeddedLanguages = findEmbeddedLanguages(snapshotContent);

// Create virtual code objects for embedded languages
this.embeddedCodes = embeddedLanguages.map(embeddedLanguage => {
return {
id: embeddedLanguage.id,
languageId: embeddedLanguage.languageId,
mappings: [],
snapshot: {
getText: (start, end) => embeddedLanguage.content.substring(start, end),
getLength: () => embeddedLanguage.content.length,
getChangeRange: () => undefined,
}
}
});
}
}
```

## Full LanguagePlugin hooks

While only two methods are required, Volar's language plugin commonly implements three hooks:

- `getLanguageId(fileUri)?: string | undefined`
- Return a language id (e.g. `"vue"`, `"svelte"`, `"html1"`) for `fileUri`, or `undefined` to skip.
- Use this to opt files in/out by extension, shebang, or contents.
- `createVirtualCode(fileUri, languageId, snapshot): VirtualCode | undefined`
- Create the root `VirtualCode` for a file. Build initial mappings and any embedded `VirtualCode`s.
- `updateVirtualCode(fileUri, languageCode, snapshot): VirtualCode`
- Incrementally update the existing `VirtualCode` to reflect a new snapshot, then return it.

Notes:
- `snapshot` is immutable for a given version. Build all parsing and indexing on the snapshot text.
- Keep updates incremental; avoid full re-parses if you can track deltas.

## The VirtualCode contract

Your `VirtualCode` should implement the following essential properties:

- `id: string`
- Unique within the file. The root is commonly `"root"`.
- `languageId: string`
- The language id of the root or embedded segment (`"my-language"`, `"html"`, `"css"`, etc.).
- `snapshot: ts.IScriptSnapshot`
- Provides stable reads via `getText(start, end)` and `getLength()`.
- `embeddedCodes?: VirtualCode[]`
- Child segments for embedded languages (e.g., HTML template + CSS block).
- `mappings?: Array<{ sourceRange: [number, number]; mappedRange: [number, number]; data?: unknown }>`
- Optional raw mappings that your plugin feeds into higher-level source-mapping utilities.

Tip: You can store additional parsed state on the instance (AST, symbol tables, indexes) keyed by the current snapshot to speed up service methods.

## Mappings and source maps

Mappings connect source text offsets to generated text offsets for embedded/virtual files and vice versa. Accurate mappings are the foundation for “jump to definition,” “rename,” “highlight,” etc.

- Use `@volar/source-map` to build and query mappings reliably.
- Prefer coarse-enough segments to avoid mapping explosions.
- Attach capability flags in mapping `data` to signal which features should traverse a mapping.

See: [@volar/source-map](/packages/source-map), [@volar/code-gen](/packages/code-gen), [@volar/transforms](/packages/transforms).

## Implementing getLanguageId

Use `getLanguageId` to rout files to your plugin:

```ts
getLanguageId(uri) {
if (uri.path.endsWith('.html1')) return 'html1';
}
```

Avoid expensive I/O; for content sniffing, consider a cheap prefix/marker check from a lightweight cache.

## Implementing createVirtualCode

Parse the snapshot, build initial state and mappings, and create any embedded `VirtualCode`s:

```ts
createVirtualCode(uri, languageId, snapshot) {
if (languageId !== 'my-language') return;
const code = new MyLanguageVirtualCode(snapshot);
// Build mappings for template / style regions, if any
code.embeddedCodes = [
/* e.g., HTML virtual code, CSS virtual code */
];
return code;
}
```

## Implementing updateVirtualCode

Keep it incremental when possible:

```ts
updateVirtualCode(uri, languageCode, snapshot) {
if (languageCode.snapshot === snapshot) return languageCode; // no-op
languageCode.update(snapshot); // recompute mappings/embeds that changed
return languageCode;
}
```

## Best practices

- Performance
- Cache parse results and indexes per snapshot version.
- Recompute only affected regions on update.
- Keep mapping segments focused; avoid 1:1 character mappings unless necessary.
- Correctness
- Always operate on the provided snapshot; never read from the file system directly.
- Keep `embeddedCodes` synchronized with source regions on each update.
- Normalize URIs (use consistent schemes like `file://`).
- Debuggability
- Add a `name` to your service (in the service layer) and log mapping spans during development.
- Provide toggles to dump virtual files for inspection.

## Example: language with HTML and CSS embeds

```ts
export class HtmlLikeCode implements VirtualCode {
id = 'root';
languageId = 'htmllike';
embeddedCodes = [];
constructor(public snapshot: ts.IScriptSnapshot) {
this.onSnapshotUpdated();
}
update(s: ts.IScriptSnapshot) {
this.snapshot = s;
this.onSnapshotUpdated();
}
private onSnapshotUpdated() {
const text = this.snapshot.getText(0, this.snapshot.getLength());
const { templateRange, styleRange } = parseRegions(text);
this.embeddedCodes = [];
if (templateRange) {
this.embeddedCodes.push(new HtmlEmbeddedCode(this.snapshot, templateRange));
}
if (styleRange) {
this.embeddedCodes.push(new CssEmbeddedCode(this.snapshot, styleRange));
}
}
}
```

Pair this with first-party services:
- HTML: `volar-service-html`
- CSS: `volar-service-css`

## Interplay with Services

Language plugins define structure (files, embeds, mappings). Services implement features (completion, hover, formatting, etc.). Services rely on your plugin’s mappings to translate positions correctly.

- Learn service method semantics in [Service Methods](/service-methods/).
- Use purpose-built services (HTML/CSS/TS/JSON/YAML) where possible, and write custom services for language-specific logic.

## Troubleshooting

- Features act on the wrong text
- Check mapping direction and off-by-one offsets; ensure ranges are half-open `[start, end)`.
- Embedded regions not updating
- Ensure `updateVirtualCode` recomputes `embeddedCodes` when the snapshot changes.
- Poor performance with large files
- Cache parse results per snapshot; avoid full re-parse on every change.
- Reduce mapping granularity; prefer region-level segments.

## Related reading

- VS Code API (extension environment, client-side expectations): https://code.visualstudio.com/api
- LSP server/client libraries for VS Code: https://github.com/microsoft/vscode-languageserver-node
- Source mapping and transforms:
- [@volar/source-map](/packages/source-map)
- [@volar/code-gen](/packages/code-gen)
- [@volar/transforms](/packages/transforms)
Loading