mirror of
https://github.com/olehomelchenko/astrolabe.git
synced 2026-08-08 02:02:33 +00:00
393 lines
27 KiB
Markdown
393 lines
27 KiB
Markdown
# 08 · Borrowed Techniques from vega/editor
|
||
|
||
> The official Vega-Lite editor ([vega/editor](https://github.com/vega/editor)) solves the
|
||
> exact "edit a Vega-Lite spec as JSON, validate it, render it live" problem Astrolabe sits
|
||
> on top of — minus the snippet/dataset library. This doc distills the techniques worth
|
||
> borrowing and the gotchas worth avoiding, so we don't rediscover them from scratch in
|
||
> M1/M2.
|
||
>
|
||
> It is a **reference**, not a contract. The behavioral contract is still [`docs/spec/`](../spec/);
|
||
> the patterns are still docs [01](01-state-and-stores.md)–[07](07-naming-and-relationships.md).
|
||
> This doc is the bridge: "here is how the canonical implementation does the editor/renderer
|
||
> plumbing, and here is what we keep vs. improve."
|
||
|
||
## Source of these findings
|
||
|
||
A read-only clone of vega/editor lives at `/Users/oleh/code/reference/vega-editor` (shallow
|
||
clone of `main`, HEAD `4fdbb59`). Re-clone with
|
||
`git clone --depth 1 https://github.com/vega/editor`. Citations below are `file:line` into
|
||
that tree.
|
||
|
||
## Stack delta (read this first — it changes how directly we can borrow)
|
||
|
||
| | vega/editor | Astrolabe |
|
||
| ----------------- | -------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
|
||
| UI framework | **React** | **React** (moved off Preact before build start) |
|
||
| State | Redux-ish single `State` in React context (`useState`) | Zustand **stores** — _not_ Redux |
|
||
| Monaco | `@monaco-editor/react` + `@monaco-editor/loader` (CDN-loaded Monaco, **workers auto-wired**) | **raw `monaco-editor`** via Vite (**we must wire workers ourselves**) |
|
||
| Rendering | **hand-rolled** `vegaLite.compile` → `vega.parse` → `new vega.View().runAsync()` | **`vegaEmbed()`** (wraps that same pipeline) |
|
||
| Schema validation | Monaco JSON worker **+** standalone `ajv ^8` (two independent layers) | same two-layer model planned |
|
||
|
||
Because both apps are now React, vega/editor's **component lifecycle patterns port more or
|
||
less directly** — the friction is only in (a) state (their Redux-flat-state → our Zustand
|
||
stores) and (b) Monaco worker wiring (their CDN loader → our explicit Vite workers).
|
||
|
||
---
|
||
|
||
## Decision · Monaco integration (self-hosted, raw API)
|
||
|
||
> **Decided.** Astrolabe uses **raw `monaco-editor` from npm, bundled and self-hosted**, with
|
||
> workers wired explicitly via Vite `?worker` — **not** vega/editor's
|
||
> `@monaco-editor/react` + `@monaco-editor/loader` (CDN) setup. Two independent axes:
|
||
|
||
**Axis A — sourcing: self-hosted/bundled, not CDN. (Forced by Astrolabe's values.)**
|
||
vega/editor's `@monaco-editor/loader` fetches Monaco's AMD bundle from a CDN at runtime. For
|
||
us that breaks three things at once: (1) **offline** — the CDN bundle is outside Vite's module
|
||
graph, so `vite-plugin-pwa`/Workbox never precaches it and offline silently fails; bundled npm
|
||
assets are hashed files in `dist/` that Workbox precaches automatically; (2) **privacy** — a
|
||
third-party fetch on load contradicts SOUL's "the only outbound requests are user-created
|
||
URL-dataset fetches"; (3) **determinism** — npm + `package-lock` is integrity-pinned and
|
||
reproducible, a runtime CDN resolve is not. This axis is not a close call; vega/editor's CDN
|
||
choice is right _for an online hosted tool_ and wrong for an offline, installable, private app.
|
||
|
||
**Axis B — React integration: raw API, not `@monaco-editor/react`. (A lean, not forced.)**
|
||
The wrapper helps with the easy 80% (mount a JSON editor, lifecycle) and adds nothing to the
|
||
load-bearing 20% this app needs:
|
||
|
||
- **Workers** are still ours — the wrapper never manages `MonacoEnvironment` (see §1 gotcha).
|
||
- The **M2 schema service** (`jsonDefaults.setDiagnosticsOptions`, `fileMatch`) is namespace-level;
|
||
you reach _through_ the wrapper via `onMount`, so it saves nothing there.
|
||
- Its headline **`value`/`onChange` controlled-input model is a hazard**: driving Monaco's
|
||
content from React state causes cursor jumps and undo-stack churn, against §10's "typing
|
||
stays fluid" — you end up using it uncontrolled, i.e. the raw pattern anyway.
|
||
- Its **CDN-by-default** is a standing footgun (works in dev online, fails offline in prod
|
||
unless you remember `loader.config({ monaco })`).
|
||
|
||
Against that, raw costs **one testable `useMonacoEditor` hook** (~50–80 lines: create in
|
||
`useEffect`, `dispose` on unmount, push value, subscribe to `onDidChangeModelContent`, resize).
|
||
That's the **same imperative-teardown discipline already adopted for `vega-embed`** in doc 05
|
||
(`view.finalize()`), and consistent with already using raw `vegaEmbed()` over a React chart
|
||
wrapper — "thin integration layers we own" (SOUL). Lock-in is low either way, so the final
|
||
raw-vs-wrapper call is confirmable at the Monaco spike; what is **not** up for revisiting is
|
||
self-hosting.
|
||
|
||
**Accepted cost:** the explicit worker wiring (§1) is inherent to self-hosting — it is the
|
||
price of offline, paid in any non-CDN setup, and the wrapper would not remove it.
|
||
|
||
### Entry point: `edcore.main`, never `editor.api` (trim languages, not features)
|
||
|
||
Self-hosting raw Monaco forces a choice of ESM entry point, and the granularity matters:
|
||
|
||
| Import | What you get | Use? |
|
||
| --------------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||
| `monaco-editor` (barrel) | All features **+ every basic language** (sql, abap, solidity, …) | ❌ language bloat (~20 dead chunks) |
|
||
| `esm/vs/editor/editor.api` | The API surface only — **zero feature contributions** | ❌ a text box: no folding, suggest widget, `Cmd+Backspace`, find, bracket colorization |
|
||
| `esm/vs/editor/edcore.main` | `editor.all` (all 59 feature contributions) + API, **no languages** | ✅ full editor UX, JSON-only weight |
|
||
|
||
Import **`edcore.main`** and add only the JSON language service
|
||
(`esm/vs/language/json/monaco.contribution`). `edcore.main` ships no `.d.ts` of its own —
|
||
add an ambient `declare module … { export * from '…/editor.api'; }` so types (and Monaco's
|
||
global `MonacoEnvironment` augmentation) resolve. Two editor options worth setting because
|
||
they bite Vega-Lite specs specifically: `showFoldingControls: 'always'` (fold arrows always
|
||
visible), and `quickSuggestions: { strings: true }` (VL enum values like `"bar"` live inside
|
||
JSON strings, where Monaco disables auto-suggest by default).
|
||
|
||
> Reaching for `editor.api` to "drop unused languages" silently strips every editor feature —
|
||
> the languages live elsewhere. This is the concrete case behind AGENTS.md's **"trim content,
|
||
> not capability"** rule: cut the unwanted _content_, keep the _behavior_, and verify the
|
||
> behavior survived by exercising the editor, not by a green build.
|
||
|
||
---
|
||
|
||
> **The single biggest surprise:** vega/editor does **not** use `vega-embed` for its live
|
||
> preview. It builds the compile→parse→View pipeline by hand; `vega-embed` is imported only
|
||
> for types and the exported standalone-HTML snippet. This is _good news_ — `vega-embed` is
|
||
> exactly the wrapper they wrote by hand, so we get it for free. But their hand-rolled
|
||
> version (`src/components/renderer/renderer.tsx`) is the best available documentation of the
|
||
> lifecycle/cleanup discipline `vega-embed` still expects from us.
|
||
|
||
---
|
||
|
||
## 1 · Monaco + Vega-Lite schema wiring (M2 — highest from-scratch risk)
|
||
|
||
All of vega/editor's Monaco setup is one file: `src/utils/monaco.ts`.
|
||
|
||
**What to borrow:**
|
||
|
||
- **Bundle the schema; never fetch it.** They `import vegaLiteSchema from 'vega-lite/vega-lite-schema.json'`,
|
||
resolved by a Vite alias to the package's `build/` output (`monaco.ts:7-8`, `vite.config.ts`).
|
||
The schema version is pinned to the installed `vega-lite` — offline-safe, version-locked,
|
||
no runtime network call. Astrolabe should do the same.
|
||
- **Attach via the JSON language service**, once, globally:
|
||
`monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ schemas, validate:true, ... })`
|
||
(`monaco.ts:51-57`).
|
||
- **`markdownDescription` patch** (`monaco.ts:12-13`, `utils/markdownProps.ts`): recursively
|
||
copy every schema `description` → `markdownDescription` before registering. Monaco renders
|
||
rich hover docs only from `markdownDescription`; without this, hovers are plain text. Do it
|
||
once at setup.
|
||
- **Replace the built-in JSON formatter** with `json-stringify-pretty-compact` via
|
||
`registerDocumentFormattingEditProvider('json', …)` (`monaco.ts:60-61,71-80`) for Vega's
|
||
compact array-on-one-line style.
|
||
- **Editor options worth copying** (`spec-editor/renderer.tsx:263-274`): `folding:true`,
|
||
`minimap.enabled:false`, `scrollBeyondLastLine:false`, `wordWrap:'on'`,
|
||
`quickSuggestions:true` (this is what makes schema completions appear without an explicit
|
||
trigger), `stickyScroll.enabled:false`.
|
||
|
||
**Gotchas / where we improve:**
|
||
|
||
- ⚠️ **Workers are on us.** vega/editor never configures Monaco workers — the CDN loader does.
|
||
With raw `monaco-editor` + Vite we **must** set `self.MonacoEnvironment.getWorker` to return
|
||
the `json.worker` for label `'json'` and `editor.worker` otherwise (via `?worker` imports):
|
||
```ts
|
||
import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
|
||
import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
|
||
self.MonacoEnvironment = {
|
||
getWorker: (_id, label) => (label === 'json' ? new JsonWorker() : new EditorWorker()),
|
||
};
|
||
```
|
||
The `json.worker` runs schema validation + autocomplete. **No worker ⇒ no squiggles, no
|
||
completions.** Upside: dropping the CDN loader makes `monaco` synchronously importable — no
|
||
`await loader.init()` dance, just call `setDiagnosticsOptions(...)` at module load.
|
||
- ⚠️ **`$schema`-based binding vs `fileMatch`.** They register schemas under versioned `uri`s
|
||
(`.../vega-lite/v6.json`) and bind by matching the doc's `$schema` value — **no `fileMatch`**
|
||
(`monaco.ts:15-46`). Consequence: a spec with **no `$schema` gets zero validation/autocomplete.**
|
||
Astrolabe should prefer `fileMatch` against our model URIs so validation works regardless of
|
||
whether the user wrote a `$schema` line.
|
||
- ⚠️ **Set `enableSchemaRequest:false`** for our offline-first app. They set it `true`
|
||
(`monaco.ts:54`), which lets the worker network-fetch any unbundled `$schema` URL — failing
|
||
network calls for an offline app. Register all schema versions locally instead.
|
||
- The schema is multi-MB; register it **once globally**, never per-model.
|
||
|
||
## 2 · Live preview with `vega-embed` (M1 lifecycle, M2 fit-mode)
|
||
|
||
This is doc [05](05-rendering-theming-preview.md)'s territory; these are the concrete details
|
||
vega/editor's hand-rolled renderer (`src/components/renderer/renderer.tsx`) reveals.
|
||
|
||
**What to borrow:**
|
||
|
||
- **Theme = a `vega-themes` config object merged into the spec config.** There is no automatic
|
||
light/dark sync in vega/editor — theme is an explicit choice baked in at compile
|
||
(`config-editor/config-editor-header.tsx:5-37`). For Astrolabe: pass the chosen `theme`/`config`
|
||
to `vegaEmbed`, and when our theme changes, re-embed with the new config.
|
||
- **`"width":"container"` / `"height":"container"` is how VL responsiveness works** — it
|
||
compiles `width`/`height` to signals that re-read `containerSize()` **only on a
|
||
`window:resize` event** (`renderer.tsx:78-90` detects container sizing). Two things a
|
||
from-scratch impl _will_ get wrong (we did): (1) `view.resize().runAsync()` does **not**
|
||
re-measure — it re-runs layout with the stale size; (2) a pane drag fires no window resize, so
|
||
nothing re-fits on its own. The fix is exactly vega/editor's
|
||
`window.dispatchEvent(new Event('resize'))` (`renderer.tsx:101-122`) — **not a hack, the
|
||
actual mechanism** — driven by a `ResizeObserver` on the pane. It also leaves a non-container
|
||
dimension natural for free. Full write-up in doc [05](05-rendering-theming-preview.md) §8.
|
||
- **Reuse the view for cheap changes.** They rebuild the `View` only on spec change; renderer
|
||
(svg/canvas) and tooltip toggles re-`initialize()` the existing view (`renderer.tsx:367-371`).
|
||
- **Capture warnings separately from errors** via a buffering logger (see §4's `LocalLogger`).
|
||
|
||
**Gotchas / where we improve:**
|
||
|
||
- ⚠️ **Finalize before re-embed, or leak.** Every spec change must `view.finalize()` the old
|
||
view _and_ clear the container before mounting the new one (`renderer.tsx:218-226`). `vegaEmbed`
|
||
returns `{ view, finalize }` — call `finalize()` before the next embed and on unmount. This is
|
||
already a Do-rule in doc 05; vega/editor confirms how easy it is to leak otherwise.
|
||
- ⚠️ **Race on rapid edits.** `runAsync` is async; a stale render can resolve after a newer one
|
||
mounts. vega/editor mitigates only with debounce. **We should add a render-generation token**
|
||
and ignore stale resolves (an improvement over the reference).
|
||
- ⚠️ **Wrap `runAsync` in try/catch and finalize on failure** — Vega won't catch runtime errors
|
||
for you, and a half-initialized view leaks if you don't finalize (`renderer.tsx:247-259`).
|
||
- The **CSP-safe expression interpreter** (`vega-interpreter` + `vega.parse(..., {ast:true})`)
|
||
matters only under a strict no-`eval` CSP. A local offline app doesn't need it — keep it
|
||
opt-in.
|
||
|
||
## 3 · Two-tier validation & error surfacing (M2, spec §03E)
|
||
|
||
vega/editor runs **two independent schema-validation systems** with no reconciliation, and
|
||
sorts errors into two tiers. Both are worth copying.
|
||
|
||
**The two layers:**
|
||
|
||
1. **Monaco JSON worker** → inline **squiggles, hovers, autocomplete** in the editor.
|
||
2. **`ajv ^8`** (`src/utils/validate.ts`) → runs at parse time, feeds the **error/log pane**.
|
||
It does _not_ create editor markers.
|
||
|
||
**The two error tiers (keep them separate):**
|
||
|
||
- **Fatal / blocking** — thrown exceptions: JSON syntax error, VL compile error, Vega runtime
|
||
error. These set a single `error` and suppress the chart.
|
||
- **Advisory** — ajv schema-validation findings and `$schema` version mismatch. These are a
|
||
warnings list and do **not** block rendering. (Vega-Lite emits many benign warnings; treating
|
||
ajv output as fatal would wrongly hide specs that render fine.)
|
||
|
||
The orchestration is `app.tsx:188-291`: `parseJSONCOrThrow` → `$schema` semver check (warn) →
|
||
`validateVegaLite` (ajv, warn) → `vegaLite.compile` (throw=fatal) → render (`renderer.tsx`,
|
||
throw=fatal).
|
||
|
||
**ajv setup specifics that _will_ bite a from-scratch impl** (`validate.ts:9-17`):
|
||
|
||
- `new Ajv({ strict: false })` — the VL/Vega schemas fail ajv strict-mode at **compile** time
|
||
otherwise.
|
||
- The VL schema is **draft-06** → must `ajv.addMetaSchema(json-schema-draft-06.json)` (ajv 8
|
||
defaults to draft-07/2020) or `compile` throws.
|
||
- Register a no-op `color-hex` format (`ajv.addFormat('color-hex', () => true)`) plus
|
||
`addFormats(ajv)`; the schema references formats ajv-formats doesn't cover.
|
||
- **Compile the validator once at module load and cache it** — the schema is huge; compiling
|
||
per keystroke is a perf killer.
|
||
|
||
**Where we improve:** ajv errors are shown as JSON-pointer text (e.g. `/encoding/x`) with **no
|
||
editor position** — vega/editor does not map them to markers. Only JSON _syntax_ errors get a
|
||
line/col (via jsonc-parser's visitor, `utils/jsonc-parser.ts:3-17`). If our §03E wants inline
|
||
ajv markers, we map `instancePath` → editor offsets ourselves via jsonc-parser's node tree —
|
||
something the reference does _not_ do.
|
||
|
||
## 4 · Data flow & debouncing (M1/M2 — translate to Zustand stores)
|
||
|
||
vega/editor keeps **`editorString` (the text) as the single source of truth**; the parsed spec
|
||
and compiled Vega spec are _derived_ and recomputed by a subscriber when text/mode/config change
|
||
(`app.tsx:338-365`). Errors don't clobber the last-good derived specs.
|
||
|
||
**The Zustand-store translation (this is the shape to build):**
|
||
|
||
```
|
||
text (store field, debounced writer on editor change)
|
||
└─▶ parsedSpec (derived: JSONC parse + collect syntax/diagnostic errors)
|
||
└─▶ renderInput (derived: prepareSpecForRender — refs, fit-mode)
|
||
└─▶ effect: deep-equal guard → vegaEmbed(); finalize previous view
|
||
```
|
||
|
||
**What to borrow:**
|
||
|
||
- **Debounce only at edit→state**, not state→render. vega/editor debounces the editor at
|
||
**1200 ms** (`spec-editor/renderer.tsx:66`) and guards the render with a `deepEqual` prop
|
||
diff (`renderer.tsx:340-349`). (1200 ms is _their_ number; tune ours — our settings expose a
|
||
render-debounce preference.)
|
||
- **A manual-parse escape hatch** (Ctrl/Cmd+S re-parses without waiting) maps to a future
|
||
live-vs-manual preview toggle (`renderer.tsx:89-111`).
|
||
- **`LocalLogger` pattern** (`utils/logger.ts`): a logger that buffers `errors/warns/infos/debugs`
|
||
into arrays instead of writing to console. This lets a **pure** `src/core` compile/validate
|
||
step _return_ structured diagnostics with zero browser coupling — e.g.
|
||
`validateSpec(spec) → { errors, warns }`. Ideal core-first fit.
|
||
- **`json-stringify-pretty-compact`** for the format action and prettify-on-load — much nicer
|
||
than `JSON.stringify(…, null, 2)` for VL specs.
|
||
|
||
**Persistence note:** vega/editor snapshots its whole state to localStorage on _every_ change,
|
||
stripping non-serializable fields (`view`, `runtime`, editor refs) and restoring via
|
||
`{ ...DEFAULT_STATE, ...parsed }` (`context/app-context.tsx`). Our **debounced auto-save to
|
||
IndexedDB** (doc 01/02) is the better pattern — but the "strip non-serializable, restore with
|
||
defaults-spread" discipline is worth keeping.
|
||
|
||
---
|
||
|
||
## 5 · Editor augmentation (our layer over the borrowed base)
|
||
|
||
Beyond schema validation/completion (§1), the spec editor adds structural refactors and
|
||
data-aware hints — the edits that are awkward in raw JSON and out of reach of the
|
||
single-view visual builder. All transform logic is pure `src/core/`; the Monaco glue is
|
||
thin app-layer services.
|
||
|
||
**Core (pure, portable):**
|
||
|
||
- `spec-transforms` — wrap a view in `layer`/`hconcat`/`vconcat`/`facet`/`repeat`; collapse
|
||
a single-child composition (`unwrapSingleton`). Object-in/object-out.
|
||
- `spec-data` — the Vega-Lite data model: classify a `data` block (`classifyData`, mirroring
|
||
`isNamedData`), the library reference name (`libraryRefName`), and the data binding in scope
|
||
at a cursor path (`dataBindingAtPath`, honoring a view's data inheritance from its parent).
|
||
- `spec-cursor` — `findViewRange` (cursor offset → the enclosing view's byte range),
|
||
`pathAtOffset` (cursor → JSON path), and `valueKeyAtOffset`/`stringValueAtOffset` (the JSON
|
||
context at the cursor), over `jsonc-parser`.
|
||
- `spec-fields` — field names a spec's transforms introduce (their `as`), scoped to a
|
||
cursor's ancestor chain (`derivedFieldNamesAtPath`).
|
||
- `spec-inline-data` — the rows a specific `data` binding carries for profiling
|
||
(`rowsForDataBinding`: inline `values`, or a self-defined `datasets` entry).
|
||
- `spec-insert` — the composition the cursor is in (`compositionTargetAt`), inserting a view
|
||
at an index (`insertView`) and reordering siblings (`moveView`), plus `elementOffset` to
|
||
re-find a view after the edit.
|
||
|
||
**Services (app, store-aware via `getState`):** `spec-transform-actions` (the
|
||
wrap/simplify/add-view operations and their surfaces), `spec-dataset-hints` (completion,
|
||
hover, inlay providers), `active-dataset` (`dataInfoAt(text, offset)` — the columns/types/stats
|
||
plus derived fields the draft sees at the cursor). `SpecEditor` does the wiring.
|
||
|
||
Decision rules:
|
||
|
||
- **Provider lifetime — global-once vs per-editor.** Language providers that need no editor
|
||
handle (code actions, completion, hover, inlay) register **once** for `json`, like the
|
||
schema and formatter; per-editor registration would duplicate them on remount. Pieces that
|
||
need the editor handle — the `addAction` context/F1 commands, and the CodeLens whose command
|
||
runs `executeEdits` — are installed **per editor** and disposed with it.
|
||
- **Transform scope.** A transform targets, in order: an explicit selection → the view the
|
||
cursor sits in (`findViewRange`) → the whole document. A "view" is a composition-array
|
||
element or a facet/repeat `spec` child; a flat unit spec has no inner view, so it scopes to
|
||
the whole document. `jsonc-parser` is error-tolerant, so scoping holds mid-edit; the path
|
||
logic stays in core and only the Monaco `Range` is built in the service.
|
||
- **One edit path.** The lightbulb returns a `WorkspaceEdit` (no editor handle); the toolbar
|
||
and palette use `executeEdits`. Both build the replacement through the same
|
||
serialize-and-reindent step, bracketed by `pushUndoStop`, so ⌘Z restores the prior text.
|
||
- **Composition CodeLens is cursor-scoped.** It follows the view the cursor sits in —
|
||
`+ Add view above/below` at the view's edges and `↑/↓ Move` to reorder among its siblings —
|
||
rather than one fixed button per composition array; an empty composition shows a single `+ Add view`,
|
||
and the F1 palette mirrors all four for the keyboard. The provider reads `editor.getPosition()`
|
||
and refreshes via an `onDidChange` emitter fired on cursor moves (keyed to the enclosing view,
|
||
so typing inside one view doesn't churn the lenses). After an edit the cursor follows the
|
||
affected view (`elementOffset`), so a repeated click keeps acting on it instead of the
|
||
neighbour that slid into place. Cursor-scoping does not strand a load-bearing action
|
||
([arch 10](10-interaction-and-feedback.md) — revealed actions): editing a composition puts
|
||
the cursor in a view exactly when add/reorder is wanted, and the palette is the ever-present
|
||
path for the keyboard.
|
||
- **Field source for hints (view-scoped).** `dataInfoAt(text, offset)` resolves the data
|
||
binding of the cursor's **nearest enclosing view** (`dataBindingAtPath` — a child inherits a
|
||
parent's data unless it declares its own), then its columns: a named **library dataset**
|
||
(matched case-insensitively, like the renderer), else the binding's **inline rows** profiled
|
||
on the fly (`rowsForDataBinding` + `core/profile`) — a "ghost dataset" with nothing stored.
|
||
Derived fields come from that view's and its ancestors' transforms only
|
||
(`derivedFieldNamesAtPath`), so a sibling view's `calculate` does not leak in. Profiling is
|
||
memoized per (draft text, enclosing view), so the inlay provider's many per-line queries
|
||
profile once. A composed spec whose views bind different datasets therefore gets the right
|
||
columns per view. Out of scope: data-dependent derived columns (`pivot`/`lookup` output) and
|
||
`url`/CSV-string inline data, which need the pipeline run or format-aware parsing.
|
||
- **No unknown-field diagnostic.** Hints are additive and forgiving, so over- or
|
||
under-listing costs nothing; a "field not in data" squiggle would false-positive on every
|
||
derived or data-dependent field, so there is deliberately none.
|
||
- **Code-action menu icons are kind-derived** (a wrench for the `refactor.*` kinds) — Monaco's
|
||
`CodeAction` carries no icon field. Custom iconography lives only where it is supported:
|
||
CodeLens titles (`$(codicon)`), completion-item kinds, and glyph-margin decorations.
|
||
|
||
`jsonc-parser` is a direct dependency (Monaco bundles its own copy internally but does not
|
||
re-export it). A standalone `editor-augmentation-demo.html` loads Monaco from a CDN to
|
||
exercise these provider surfaces in isolation.
|
||
|
||
## Borrow list (where each lands)
|
||
|
||
| Technique | Lands in | Milestone |
|
||
| ----------------------------------------------------------------------------------------- | -------------------------------------- | --------- |
|
||
| Bundle VL schema from package `build/`; `setDiagnosticsOptions` | `src/app/infrastructure/` Monaco setup | M2 |
|
||
| `markdownDescription` patch + compact formatter | Monaco setup | M2 |
|
||
| Explicit Vite worker wiring (`MonacoEnvironment.getWorker`) | Monaco setup | M2 |
|
||
| `fileMatch` schema binding (improvement over `$schema`-only) | Monaco setup | M2 |
|
||
| jsonc-parser tolerant parse + line/col syntax errors | `src/core/` | M1/M2 |
|
||
| ajv wrapper (`strict:false`, draft-06, color-hex, compile-once) → structured diagnostics | `src/core/` | M2 |
|
||
| `LocalLogger`-style buffered diagnostics from pure compile | `src/core/` | M2 |
|
||
| Fatal-vs-advisory two-tier error model | rendering/store contract | M1/M2 |
|
||
| `"container"` sizing + `ResizeObserver` → synthetic `window:resize` (not `view.resize()`) | `chart-renderer` + LivePreview | M2 |
|
||
| `finalize()`-before-reembed + **render-generation guard** | LivePreview | M1 |
|
||
| theme = `vega-themes` config merged into `vegaEmbed` | preview + settings | M5 |
|
||
| `json-stringify-pretty-compact` format action | editor | M2 |
|
||
|
||
## Where we deliberately do better than the reference
|
||
|
||
- **Wire Monaco workers explicitly** (they sidestep it via the CDN loader).
|
||
- **Map ajv errors to editor positions** via jsonc-parser offsets (they show pointer text only).
|
||
- **Render-generation guard** against stale async renders (they rely on debounce alone).
|
||
- **`fileMatch`-based schema binding** so validation works without a `$schema` line.
|
||
- **Debounced auto-save to IndexedDB** rather than write-the-whole-state-on-every-change.
|
||
|
||
## Key files in the reference (for deeper reads)
|
||
|
||
- `src/utils/monaco.ts` — all Monaco/schema wiring
|
||
- `src/utils/markdownProps.ts` — the `markdownDescription` patch
|
||
- `src/utils/validate.ts` — ajv setup + cached validators
|
||
- `src/utils/jsonc-parser.ts` — tolerant parse + line/col syntax errors
|
||
- `src/utils/logger.ts` — `LocalLogger` / `DispatchingLogger`
|
||
- `src/components/renderer/renderer.tsx` — the hand-rolled View lifecycle (finalize, sizing, errors)
|
||
- `src/components/app.tsx:188-365` — parse → $schema check → ajv → compile → render orchestration
|
||
- `src/components/error-pane/renderer.tsx` — error/log display
|
||
- `src/constants/default-state.ts` — the full app-state shape
|
||
- `src/components/input-panel/spec-editor/renderer.tsx` — editor component, 1200ms debounce, $schema→mode detection
|