Blocks Editor (.blocks)
A web extension for VS Code that opens MakeCode Arcade .blocks files in a
real Arcade block canvas — and co-edits over Live Share.
Runs in vscode.dev and github.dev, as well as desktop VS Code.
How collaboration works
This is the part worth understanding before you extend the editor, because it
constrains the design.
Live Share has no API for syncing webviews or custom editor state. It
synchronizes text documents. A custom editor that keeps its own document model
(CustomEditorProvider) is invisible to Live Share — each participant would see
their own private, diverging canvas.
So this extension is a CustomTextEditorProvider. The .blocks file stays a
plain text document at all times:
you drag a block
↓
webview serializes the workspace to XML
↓
extension host applies a WorkspaceEdit to the text document ← Live Share syncs HERE
↓
every participant's onDidChangeTextDocument fires
↓
their webview reloads the canvas from the new XML
Collaboration therefore comes from the document, not from any Live Share API —
the extension contains no Live Share code at all. It works with Live Share, with
two windows on the same file, with undo/redo, and with a text editor open on the
same file in a split pane.
Three things make that smooth rather than janky:
- Minimal edits. Every save is diffed against the current document and
applied as the smallest single-range replacement (
src/textDiff.ts). Replacing
the whole file on each change would make two people editing different blocks
clobber each other under Live Share's operational transform; narrowing the edit
lets those merges succeed.
- Block ids are preserved on serialization, for the same reason — stable ids
keep diffs small.
- Strict XML parsing. Blockly's own
textToDom falls back to lenient HTML
parsing, so half-typed XML does not fail — it quietly yields an almost-empty
workspace. Loading that would clear the canvas, and the next drag would write
the empty workspace back over the file. Since a collaborator editing the file
as text produces exactly those transient states, src/webview/parse.ts parses
strictly and the canvas holds its last good state (with an explanatory banner)
until the XML is valid again.
- Debounced writes and drag-awareness. Block changes are coalesced (default
200 ms, see
blocksEditor.writeDebounceMs), UI-only events (scroll, zoom,
selection) never produce a document revision, and a remote update arriving
mid-drag is queued until the drag ends instead of ripping the block out of
your hand.
Known limits
- Two people dragging the same block at the same time still conflict; last
write wins. There is no block-level operational transform.
- There are no remote cursors or presence indicators on the canvas. Live Share's
presence API reports text-document positions, which do not map onto a block
canvas without extra work.
- Live Share must be installed by each participant in desktop VS Code to host
a session. Browser participants can join a session in vscode.dev.
Two editors
blocksEditor.engine chooses what draws a .blocks file:
makecode (default) embeds the genuine MakeCode Arcade editor — every
block, the image and tilemap editors, the simulator. It needs
arcade.makecode.com to be reachable.
blockly uses the editor built into this extension, described below. It
needs no network at all.
Either way the .blocks file stays the source of truth, so Live Share
collaboration works the same with both.
Embedding under a cross-origin embedder policy
vscode.dev serves its pages with Cross-Origin-Embedder-Policy: require-corp.
Under that policy a cross-origin iframe must assert COEP itself, and MakeCode
sends only Cross-Origin-Resource-Policy, so a plain iframe is refused —
though it works in desktop VS Code, which sets no such policy.
The editor is therefore embedded in an <iframe credentialless>, the
attribute Chrome provides so a require-corp document can embed cross-origin
content lacking COEP: the frame loads without credentials, in ephemeral storage.
Nothing is lost, because the project reaches the editor over MakeCode's
controller protocol rather than through its cookies or its own storage.
blocksEditor.embedElement can select iframe, object or embed instead.
object and embed are refused by current Chrome under the same policy; a plain
iframe is useful on desktop, and on iOS, where the browser is WebKit (so
credentialless is ignored) and COEP is enforced differently.
MakeCode Arcade support
The editor ships Arcade's actual block library: 493 blocks and 69 dropdowns
across 15 categories, generated from MakeCode Arcade's own compiled API
metadata, with Arcade's real colours, icons, groups, tooltips and reference
links. sprite of kind, on game update, move with buttons, destroy with effect, tilemaps, music — they look and read the way they do in Arcade.
How it is built (scripts/generateArcadeBlocks.mjs):
- pxt has already parsed each block's layout into
attributes._def, so the
generator translates that structure instead of re-parsing TypeScript.
- The toolbox is limited to the packages a new Arcade project actually depends
on, resolved from
blocksprj's dependency closure. Optional extensions
(corgio, darts, radio, esp32…) stay defined — a project using them still
loads — but are not offered, because Arcade does not offer them either until
you add the extension.
- Categories carry MakeCode's own icon codepoints, drawn from a bundled
Font Awesome 4 (see
media/fonts/LICENSE.md for why not pxt's own font file).
- Built-in categories use MakeCode's palette verbatim from the target bundle:
loops
#20BF6B, logic #45AAF2, math #A55EEA, variables #EC3B59,
text #F5D547, arrays #FF8F08, functions #1446A0.
Refresh the library against the current Arcade release with npm run arcade:refresh; the generated output is committed so ordinary builds need no
network access.
What is faithful, and what is not
Verified against a file saved by MakeCode Arcade itself (sample/arcade-real.blocks),
which round-trips byte-for-byte in structure — sprite kinds, pixel-art image
literals, <data> payloads, expandable-block <mutation>s and all:
- Sprite kinds are workspace variables (
<variable type="KIND_SpriteKind">) and
the dropdown reads its options from them, so the kinds a game defines for
itself (SpriteKind.Coin) appear and survive saving.
- Blocks are drawn with the
zelos renderer, closest to MakeCode's own.
The sprite image painter is implemented. A sprite image renders as the
artwork itself on the block, and clicking it opens a pixel editor with Arcade's
16-colour palette (read from the target bundle, so it tracks upstream):
- Click or drag to paint; right-click or hold shift to erase.
- The transparent slot is drawn as a checkerboard, never as black.
- One stroke is one undo step.
- Edits are written back in MakeCode's exact
img\...`` layout, so a file this
editor saves opens in Arcade unchanged — and because the value travels through
the text document, painting syncs over Live Share like any other edit.
An image that is never edited is written back byte for byte rather than
reformatted. Palette colour parameters (colornumber, colorwheel) get a
matching swatch picker.
Not reproduced:
- The tilemap and tileset editors, the melody/sound-effect editors, and the
grid pickers. Their values are shown as plain editable text or numbers, which
preserves them exactly but is not the bespoke UI MakeCode draws.
- The "+" expand toggle. Blocks with optional arguments are drawn fully
expanded. Omitting those inputs would drop values a file already stores for
them, so they are always shown.
- No simulator. This edits games; it does not run them.
Nothing gets dropped
Blockly silently discards fields and mutations a block does not declare, which
on save would delete part of someone's game. Two passes prevent that, and they
do not depend on this editor's guesses being right:
- Any
<field> the definition does not declare is added to the block as a text
field before loading, so it survives.
- Any
<mutation> a block does not understand is stored verbatim and written
back unchanged.
- Block types Arcade does not define at all still render, as placeholder blocks
shaped from how the file uses them (
src/webview/stubBlocks.ts).
npm test enforces this: it loads every sample into real Blockly in real
Chromium, asserts the serialized structure matches the source, and asserts that
Arcade files use no placeholders at all.
Development
npm install
npm run compile # vendors Blockly into media/, builds both bundles
npm test # round-trip test against real Blockly in Chromium
npm run typecheck
To debug, press F5 with the Run Web Extension launch configuration —
it starts an extension host in a web worker, the same host vscode.dev uses, so
Node-only API misuse fails here exactly as it would in the browser.
To try it in a browser-hosted VS Code build:
npx vscode-test-web --extensionDevelopmentPath=. ./sample
Layout
| Path |
Purpose |
src/extension.ts |
Activation; registers the editor and the two commands. |
src/blocksEditorProvider.ts |
The CustomTextEditorProvider: webview HTML/CSP, document ↔ webview sync, write-back. |
src/textDiff.ts |
Minimal single-range replacement, for conflict-friendly edits. |
src/protocol.ts |
Message types shared by both halves. |
src/webview/main.ts |
Blockly injection, change handling, debounce, drag queueing. |
src/webview/serialize.ts |
Workspace → .blocks XML. |
src/webview/stubBlocks.ts |
Placeholder definitions for unknown block types. |
src/webview/arcade/register.ts |
Registers the Arcade library; preserves unknown fields and mutations. |
src/webview/arcade/dropdowns.ts |
Enum and sprite-kind dropdowns, extensible per document. |
src/webview/arcade/toolbox.ts |
Arcade toolbox, merged with MakeCode's built-in categories. |
src/webview/arcade/coreBlocks.ts |
on start and pxt's loop/math/variable blocks. |
src/webview/arcade/imageField.ts |
The sprite image painter (field_arcade_image). |
src/webview/arcade/imageLiteral.ts |
Reads and writes MakeCode's img literal. |
src/webview/arcade/imageRender.ts |
Draws sprites for the preview and painter. |
src/webview/arcade/colourField.ts |
Palette colour swatch picker. |
src/webview/arcade/theme.ts |
MakeCode's palette as a Blockly theme. |
scripts/fetchArcade.mjs |
Downloads the Arcade target bundle. |
scripts/generateArcadeBlocks.mjs |
Turns its API metadata into block definitions. |
scripts/vendor.mjs |
Copies Blockly out of node_modules into media/vendor/. |
Blockly is vendored, not loaded from a CDN — vscode.dev's webview content
security policy blocks remote scripts, and vendoring keeps the extension working
offline.
Why the code avoids Node
vscode.dev runs extensions in a web worker. There is no fs, no path, no
child_process, and no require of Node builtins. The manifest declares
"browser" (not "main"), esbuild targets platform: 'browser', and the
extension reads and writes exclusively through vscode.workspace APIs.
Commands
| Command |
Description |
Blocks: Open Active File as XML Text |
Switch the active .blocks file to the plain text editor. |
Blocks: Open Active File in Blocks Editor |
Switch back to the block canvas. |
Settings
| Setting |
Default |
Description |
blocksEditor.writeDebounceMs |
200 |
Delay after the last block change before writing to the file. Lower feels more live over Live Share, at the cost of more document revisions. |
blocksEditor.renderer |
zelos |
Blockly renderer. zelos most closely resembles MakeCode. |
License
MIT.
- Blockly is vendored under its own Apache-2.0 license, included at
media/vendor/blockly/LICENSE.
- Font Awesome 4.7 supplies the category icons, under SIL OFL 1.1 — see
media/fonts/LICENSE.md.
- The generated block definitions are derived from MakeCode Arcade's
published target metadata (MIT, Copyright (c) Microsoft Corporation). This
project is not affiliated with or endorsed by Microsoft.