feat(diffs-highlight): Add a Figma plugin that highlights code with Pierre variables - #1053
Draft
mdo wants to merge 2 commits into
Draft
feat(diffs-highlight): Add a Figma plugin that highlights code with Pierre variables#1053mdo wants to merge 2 commits into
mdo wants to merge 2 commits into
Conversation
…ierre variables Designers can paste code into a Figma text layer and have it syntax highlighted from the Pierre variables imported by `@pierre/theme`. Because the colors come from variables rather than hex values, switching the collection's mode re-colors the sample, so one layer covers light, dark, soft, and the CVD variants. The plugin is split across Figma's two realms. `src/sandbox` owns the `figma` API and does no tokenizing: it reports the selection, lists the local collections, and binds the character ranges it is handed. `src/ui` owns Shiki. Shiki runs on the JavaScript regex engine, which matters more here than elsewhere in the repo: the plugin sandbox cannot load WebAssembly. Binding needs a role name, but Shiki returns a resolved color. All eight variants share one scope table and differ only in color, so the UI always tokenizes with `pierre-dark` as a probe, looks the role up by color, and binds by name; Figma resolves the color for whichever mode is active. `pierre-dark` is the probe because its syntax colors are all distinct, where the tritanopia variants collapse five into one. The lookup table is built from the committed `figma/semantic/dark.json`, which is why `@pierre/theme` grows a `./figma/*` export. Every language Shiki bundles is offered. Grammars are registered on demand, so the count does not affect startup, and their aliases resolve too. A one-time warm-up pass is needed because a fresh highlighter's first tokenization comes back coarser than every call after it. `test/highlight.test.ts` runs all 235 grammars through the JavaScript engine, since one that needed Oniguruma would fail in Figma with no other warning. All of them pass today. Local development only: the plugin is loaded from a manifest on disk and is not published to the Figma Community.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
A selection can now hold any number of text layers, so a frame of code
samples is one run instead of one run each. Non-text and empty layers
in the selection are filtered out rather than rejected, so sweeping up
a stray rectangle alongside the code still works.
Each layer is tokenized separately, since each has its own text, but
all of them use the language the picker names — the point being
several samples of the same language at once. Binding is per layer too,
and a layer whose text changed between tokenizing and binding is
skipped and named in the summary rather than failing the whole run.
The panel drops the line that echoed the selected layer's name back;
the layer count now rides on the Highlight button, which is where it
is about to be acted on. What is left of that line is the prompt shown
when there is nothing to work on, now in secondary gray, since asking
for a selection is guidance rather than an error.
The selects draw their own chevron. Chrome pins the platform one hard
against the right edge, which read as cramped; this one is inset, with
room reserved so a long option label cannot run underneath it, and it
is masked with `currentColor` so it follows both Figma themes. Hover
and focus states come along with it.
Collection labels list a mode count instead of every mode name. The
old format was redundant for a single-mode collection ("Light (light)")
and would have overflowed the control for an eight-mode one.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
Stacked on #1049. Base is
mdo/theme-figma-variables, so review that onefirst — this plugin reads the token files it generates.
Summary
A Figma plugin that syntax highlights code in a text layer by binding the Pierre
syntax/*variables to each token's characters. Paste code into a text layer,pick the language, hit Highlight.
Because the colors come from variables rather than hex values, switching the
collection's mode re-colors the code — one layer covers Light, Dark, Soft, and
both CVD variants, which is the payoff for #1049 landing the variables as modes
in the first place.
Local development only: it loads from a manifest on disk and is not published to
the Figma Community.
How it works
Two realms, because Figma requires it.
src/sandbox/code.tsowns thefigmaAPI and does no tokenizing: it reports the selection, lists local collections, and
binds the character ranges it is handed.
src/uiowns Shiki. The build is twoVite passes because the realms need different shapes — a bare IIFE for the
sandbox, and one HTML file with its script and styles inlined for the iframe,
which loads no external resources.
Colors are turned back into role names by probing. Binding a variable needs a
name like
syntax/keyword, but Shiki hands back a resolved color. All eightPierre variants share one scope table and differ only in resolved color, so the UI
always tokenizes with
pierre-darkas a probe, looks the role up by color, andbinds by name; Figma resolves the actual color for whichever mode is active. That
is why there is no theme picker in the UI.
pierre-darkis the probe specifically because its syntax colors are alldistinct — the tritanopia variants collapse five of them into one, which would
make the reverse lookup ambiguous. The lookup table is built from the committed
figma/semantic/dark.json, so it regenerates alongside the variables itdescribes; that is the one new export on
@pierre/theme.Roles do collide even in dark mode (
fg/fg3andsyntax/commentare both#737373), sosrc/shared/roleIndex.tsresolves in a fixed order and excludesbgandborderentirely — those are surface colors that never apply to glyphs.Notes for reviewers
Every bundled language is offered, all 235. Grammars are registered on demand,
so the count costs nothing at startup (0.1 ms to create the highlighter, 0.5 ms
per grammar). Aliases resolve too, so
bashworks and not justshellscript—safe because Shiki's 97 aliases never collide with an id and are never claimed by
two languages.
The WebAssembly constraint is the real risk, and it is tested. The sandbox
cannot load WASM, so Shiki runs on the JavaScript regex engine, and a grammar that
needed Oniguruma would fail in Figma with no other warning.
test/highlight.test.tsruns all 235 through the engine; all of them pass, so no
forgivingmode orerror-path special-casing was needed. That sweep is most of the suite's ~12s.
There is a warm-up call that looks pointless and is not. A fresh highlighter's
very first tokenization comes back coarser than every call after it —
const a = 1;yields
1;as one token instead of separating the number from the semicolon. Onethrowaway pass settles it, and it is genuinely one-time: languages registered later
tokenize correctly on their first real call. There is a test pinning that offset so
nobody deletes it.
dist/ui.htmlis about 8 MB. Inherent — Figma fetches nothing at runtime, soevery grammar has to be in the file. It costs little in practice: the panel loads
in ~200 ms, a first highlight takes ~350 ms, later ones under 100 ms. Minifying was
measured at a 10% saving and skipped, since the bulk is grammar data rather than
code and readable output is worth more on a dev plugin.
Whitespace-only ranges are skipped and touching same-role ranges are merged, to
keep the sandbox's
setRangeFillscalls down. Merging only happens when ranges areliterally adjacent, so a skipped token or a line break breaks the run and it cannot
silently swallow a range.
Test plan
moonx diffs-highlight:test— 32 pass, including the 235-grammar sweepmoonx diffs-highlight:typecheck,moonx theme:test(376 pass)moon run root:format root:lint root:check-licensesmoonx diffs-highlight:build— emits exactly the two files the manifest namesui.htmlin a browser: 235 options render, and TypeScript,Rust, ABAP, and Clojure each produced sensible role bindings, confirming
on-demand grammar registration works in the real bundle
(primitives collection, then semantic as modes), then
Plugins > Development > Import plugin from manifest. Worth trying an
uncommon language to confirm on-demand registration behaves in the sandbox, and
switching modes on the layer afterwards to see it re-color.