Skip to content

Commit 87c655d

Browse files
authored
Merge pull request #10320 from microsoft/docs/markdown-code-block-editors
Update Markdown extension documentation to include code block editor providers
2 parents 3ef3035 + 8f2fc48 commit 87c655d

2 files changed

Lines changed: 250 additions & 3 deletions

File tree

api/extension-guides/markdown-extension.md

Lines changed: 249 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,12 @@ ContentId: 1664249a-ba7a-4a53-b3f0-9d757cff7d27
44
DateApproved: 9/16/2026
55

66
# Summarize the whole topic in less than 300 characters for SEO purpose
7-
MetaDescription: Learn how to extend Visual Studio Code's built-in Markdown preview.
7+
MetaDescription: Extend Visual Studio Code Markdown features with preview styles, scripts, markdown-it plugins, and interactive code block editors.
88
---
99

1010
# Markdown Extension
1111

12-
Markdown extensions allow you to extend and enhance Visual Studio Code's built-in Markdown preview. This includes changing the look of the preview or adding support for new Markdown syntax.
12+
Markdown extensions allow you to extend and enhance Visual Studio Code's built-in Markdown support. You can change the look of the Markdown preview, add support for new Markdown syntax, and contribute interactive editors for fenced code blocks.
1313

1414
## Changing the look of the Markdown preview with CSS
1515

@@ -85,3 +85,250 @@ For advanced functionality, extensions may contribute scripts that are executed
8585
Contributed scripts are loaded asynchronously and reloaded on every content change.
8686

8787
The [Markdown Preview Mermaid Support](https://marketplace.visualstudio.com/items?itemName=bierner.markdown-mermaid) extension demonstrates using scripts to add [Mermaid](https://mermaid.js.org) diagrams and flowchart support to the markdown preview. You can review the Mermaid extension's source code on [GitHub](https://github.com/mjbvz/vscode-markdown-mermaid).
88+
89+
## Add code block editors (Experimental)
90+
91+
Extensions can replace fenced code blocks with interactive, iframe-based editors in the Markdown editor. For example, an extension can provide a form editor for JSON, a diagram editor, or a task progress view. The code block remains the canonical document content and stays synchronized with the contributed editor.
92+
93+
> [!NOTE]
94+
> Markdown code block editors are experimental and apply to the Markdown editor, not the standard Markdown preview. The contribution point and extension export API might change.
95+
96+
Code block editors only load in a [trusted workspace](/docs/editing/workspaces/workspace-trust.md). In Restricted Mode, the Markdown editor does not load contributed code block editors or their resources.
97+
98+
### Register a static code block editor
99+
100+
Use the `markdown.codeBlockEditorProviders` contribution point to select fenced code blocks and provide an HTML entry point:
101+
102+
```json
103+
{
104+
"contributes": {
105+
"markdown.codeBlockEditorProviders": [
106+
{
107+
"id": "taskProgress",
108+
"selector": {
109+
"language": "task-progress"
110+
},
111+
"source": {
112+
"kind": "static",
113+
"entrypoint": "./editor/index.html"
114+
},
115+
"runtimeKey": "task-progress-v1",
116+
"contentType": "text",
117+
"initialHeight": 80
118+
}
119+
]
120+
}
121+
}
122+
```
123+
124+
This contribution replaces fenced code blocks whose info string is exactly `task-progress`:
125+
126+
````markdown
127+
```task-progress
128+
- [x] Create the extension
129+
- [ ] Publish the extension
130+
```
131+
````
132+
133+
The provider supports these properties:
134+
135+
| Property | Required | Description |
136+
| --- | --- | --- |
137+
| `id` | Yes | Identifies the provider within the extension. The value must not be empty. |
138+
| `selector` | Yes | Selects an exact info string with `language`, or all info strings that start with a value by using `languagePrefix`. Specify one selector type. Values must not be empty. |
139+
| `source` | Yes | Uses an extension-relative HTML `entrypoint` for a `static` provider, or an extension export API for an `exportApi` provider. |
140+
| `runtimeKey` | No | Identifies compatible iframe runtimes that can be reused. The value must contain 1 to 256 characters. |
141+
| `contentType` | No | Represents code block content as `text` or `json`. The default is `text`. |
142+
| `initialHeight` | No | Reserves a positive height in pixels until the editor reports its measured height. |
143+
| `sandbox` | No | Sets the maximum optional iframe permissions the provider can request. Supported properties are `forms`, `downloads`, `pointerLock`, and `clipboardWrite`. Each permission defaults to `false`. |
144+
145+
The `entrypoint` path is relative to the extension root. Relative scripts, stylesheets, and other assets in the HTML resolve from the entry point's directory.
146+
147+
### Connect the iframe editor
148+
149+
Install and bundle the experimental `@vscode/web-editors` package into the iframe application. The package synchronizes content, read-only state, and sizing between the code block and the iframe.
150+
151+
The HTML entry point can load a bundled JavaScript module:
152+
153+
```html
154+
<!doctype html>
155+
<html lang="en">
156+
<head>
157+
<meta charset="UTF-8">
158+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
159+
<title>Task progress editor</title>
160+
</head>
161+
<body>
162+
<textarea id="editor"></textarea>
163+
<script type="module" src="./dist/main.js"></script>
164+
</body>
165+
</html>
166+
```
167+
168+
Connect the iframe to its parent window, apply user edits to the root content value, and respond to updates from the host:
169+
170+
```ts
171+
import { WebEditorClient } from '@vscode/web-editors';
172+
173+
const editor = document.querySelector<HTMLTextAreaElement>('#editor');
174+
if (!editor) {
175+
throw new Error('Task progress editor element not found');
176+
}
177+
178+
const client = await WebEditorClient.connect({
179+
connection: 'windowParent',
180+
contentType: 'text'
181+
});
182+
183+
const updateContent = (content: unknown) => {
184+
if (typeof content === 'string' && editor.value !== content) {
185+
editor.value = content;
186+
}
187+
};
188+
189+
updateContent(client.getContent());
190+
editor.readOnly = client.getReadOnly();
191+
192+
client.onDidChangeContent(event => updateContent(event.content));
193+
client.onDidChangeReadOnly(event => {
194+
editor.readOnly = event.readOnly;
195+
});
196+
197+
editor.addEventListener('input', () => {
198+
client.applyEdits([{
199+
kind: 'replace',
200+
path: [],
201+
newValue: editor.value
202+
}]);
203+
});
204+
205+
const resizeObserver = new ResizeObserver(() => {
206+
client.reportSize(document.documentElement.scrollHeight);
207+
});
208+
resizeObserver.observe(document.documentElement);
209+
210+
window.addEventListener('beforeunload', () => {
211+
resizeObserver.disconnect();
212+
client.dispose();
213+
}, { once: true });
214+
```
215+
216+
### Resolve editors from the extension host
217+
218+
Use an `exportApi` source when the extension host needs to choose the HTML dynamically or communicate with an iframe runtime. Register the source with API version 2:
219+
220+
```json
221+
{
222+
"contributes": {
223+
"markdown.codeBlockEditorProviders": [
224+
{
225+
"id": "taskProgress",
226+
"selector": {
227+
"languagePrefix": "task-progress"
228+
},
229+
"source": {
230+
"kind": "exportApi",
231+
"apiVersion": 2
232+
},
233+
"runtimeKey": "task-progress-v1",
234+
"contentType": "text",
235+
"initialHeight": 80
236+
}
237+
]
238+
}
239+
}
240+
```
241+
242+
Return the `markdownCodeBlockEditors.apiV2` API from the extension's `activate` function. The provider ID returned by `getProvider` must match the contribution's `id`.
243+
244+
```ts
245+
import * as vscode from 'vscode';
246+
247+
interface ResolveRequest {
248+
readonly providerId: string;
249+
readonly language: string;
250+
readonly documentUri: vscode.Uri;
251+
}
252+
253+
interface HostTransport {
254+
readonly runtimeKey: string;
255+
readonly onDidReceiveMessage: vscode.Event<unknown>;
256+
readonly onDidDispose: vscode.Event<void>;
257+
sendMessage(message: unknown): void;
258+
}
259+
260+
const providerId = 'taskProgress';
261+
262+
export function activate(context: vscode.ExtensionContext) {
263+
const provider = {
264+
resolve(_request: ResolveRequest, _token: vscode.CancellationToken) {
265+
return {
266+
content: {
267+
uri: vscode.Uri.joinPath(context.extensionUri, 'editor', 'index.html')
268+
},
269+
runtimeKey: 'task-progress-v1',
270+
contentType: 'text' as const,
271+
initialHeight: 80
272+
};
273+
},
274+
275+
createHostTransport(
276+
transport: HostTransport,
277+
token: vscode.CancellationToken
278+
) {
279+
if (token.isCancellationRequested) {
280+
return;
281+
}
282+
283+
return transport.onDidReceiveMessage(message => {
284+
if (isReadyMessage(message)) {
285+
transport.sendMessage({ type: 'hostReady' });
286+
}
287+
});
288+
}
289+
};
290+
291+
return {
292+
markdownCodeBlockEditors: {
293+
apiV2: {
294+
getProvider(id: string) {
295+
return id === providerId ? provider : undefined;
296+
}
297+
}
298+
}
299+
};
300+
}
301+
302+
function isReadyMessage(message: unknown): message is { type: 'ready' } {
303+
return typeof message === 'object'
304+
&& message !== null
305+
&& 'type' in message
306+
&& message.type === 'ready';
307+
}
308+
```
309+
310+
The `resolve` method receives the provider ID, full fenced code block info string, and Markdown document URI. It can return HTML directly with `content.html` and an optional `content.baseUri`, or return an HTML file with `content.uri`. Returned resources must be within the extension or workspace.
311+
312+
The resolved editor can also override `runtimeKey`, `contentType`, `initialHeight`, and `sandbox`. A returned `runtimeKey` must also contain 1 to 256 characters. A returned sandbox permission is granted only when the contribution also permits it.
313+
314+
API version 2 providers can implement `createHostTransport` for bidirectional notifications between the extension host and an iframe runtime. In the iframe, use the optional `WebEditorClient.hostTransport`:
315+
316+
```ts
317+
const transport = client.hostTransport;
318+
if (transport) {
319+
transport.onMessage(message => {
320+
console.log('Message from the extension host', message);
321+
});
322+
transport.sendMessage({ type: 'ready' });
323+
}
324+
```
325+
326+
The host buffers messages while `createHostTransport` initializes. Return a `vscode.Disposable` to clean up listeners and resources when the iframe runtime is disposed. Use `transport.onDidDispose` when the extension must react immediately to runtime disposal.
327+
328+
### Reuse iframe runtimes
329+
330+
The Markdown editor virtualizes and pools physical iframes. Editors with the same `runtimeKey` can reuse an iframe runtime as code blocks enter and leave the viewport. Use the same key only when the editors have compatible HTML, scripts, and runtime behavior.
331+
332+
For a static provider, the default runtime key combines the provider ID and extension version. For an exported provider, the resolved `runtimeKey` takes precedence over the contribution value. If neither is present, the Markdown editor derives a key from the provider, extension version, and language.
333+
334+
Runtime reuse preserves the iframe application and host transport, but the web editor protocol updates the code block content and read-only state for each logical editor. Do not store code block-specific state outside the synchronized content unless you reset that state when the content changes.

api/extension-guides/overview.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Here are the guides on the VS Code website, including their usage of the [VS Cod
4040
| [Task Provider](https://code.visualstudio.com/api/extension-guides/task-provider) | [tasks.registerTaskProvider](https://code.visualstudio.com/api/references/vscode-api#tasks.registerTaskProvider)<br>[Task](https://code.visualstudio.com/api/references/vscode-api#Task)<br>[ShellExecution](https://code.visualstudio.com/api/references/vscode-api#ShellExecution)<br>[contributes.taskDefinitions](https://code.visualstudio.com/api/references/contribution-points#contributes.taskDefinitions) |
4141
| [Source Control](https://code.visualstudio.com/api/extension-guides/scm-provider) | [workspace.workspaceFolders](https://code.visualstudio.com/api/references/vscode-api#workspace.workspaceFolders)<br>[SourceControl](https://code.visualstudio.com/api/references/vscode-api#SourceControl)<br>[SourceControlResourceGroup](https://code.visualstudio.com/api/references/vscode-api#SourceControlResourceGroup)<br>[scm.createSourceControl](https://code.visualstudio.com/api/references/vscode-api#scm.createSourceControl)<br>[TextDocumentContentProvider](https://code.visualstudio.com/api/references/vscode-api#TextDocumentContentProvider)<br>[contributes.menus](https://code.visualstudio.com/api/references/contribution-points#contributes.menus) |
4242
| [Debugger Extension](https://code.visualstudio.com/api/extension-guides/debugger-extension) | [contributes.breakpoints](https://code.visualstudio.com/api/references/contribution-points#contributes.breakpoints)<br>[contributes.debuggers](https://code.visualstudio.com/api/references/contribution-points#contributes.debuggers)<br>[debug](https://code.visualstudio.com/api/references/vscode-api#debug) |
43-
| [Markdown Extension](https://code.visualstudio.com/api/extension-guides/markdown-extension) | markdown.previewStyles<br>markdown.markdownItPlugins<br>markdown.previewScripts |
43+
| [Markdown Extension](https://code.visualstudio.com/api/extension-guides/markdown-extension) | markdown.previewStyles<br>markdown.markdownItPlugins<br>markdown.previewScripts<br>markdown.codeBlockEditorProviders |
4444
| [Test Extension](https://code.visualstudio.com/api/extension-guides/testing) | [TestController](https://code.visualstudio.com/api/references/vscode-api#TestController)<br>[TestItem](https://code.visualstudio.com/api/references/vscode-api#TestItem) |
4545
| [Custom Data Extension](https://code.visualstudio.com/api/extension-guides/custom-data-extension) | contributes.html.customData<br>contributes.css.customData |
4646
<br>

0 commit comments

Comments
 (0)