Skip to content

feat: a VS Code custom editor for .apollon files, and the theming fixes it exposed#806

Merged
FelixTJDietrich merged 11 commits into
mainfrom
feat/vscode-custom-editor-and-embed-theming
Jul 9, 2026
Merged

feat: a VS Code custom editor for .apollon files, and the theming fixes it exposed#806
FelixTJDietrich merged 11 commits into
mainfrom
feat/vscode-custom-editor-and-embed-theming

Conversation

@FelixTJDietrich

@FelixTJDietrich FelixTJDietrich commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Summary

The VS Code extension is rewritten as a CustomTextEditorProvider over .apollon files. Rather than a bespoke webview that owned the document, VS Code now owns it — so save, auto-save, undo, dirty state, hot exit, diffs, split editors and branch switches all work because they are the editor's, not because we reimplemented them.

Embedding the library in a real host immediately exposed three bugs in it. All three only appear when the theme is scoped to the editor's own mount nodedataTheme, a theme override, or a host stylesheet — and are invisible to the webapp, which themes the document root. They are fixed here because the extension is unusable without them. Separately, fitView never reserved the device safe area, so on a phone the camera framed nodes under the notch and the home indicator.

Area Commits
The custom-editor rewrite feat(vscode-extension)
Library fixes it exposed fix(library) × 3 — safe area, scoped theme, popover titles
Correctness found in review fix(vscode-extension) × 2 — lifecycle holes, PNG export
Structure refactor(vscode-extension) — split the provider
Plumbing build, build(webapp), test(library), style

Release note

Apollon diagrams now open in a real VS Code editor, with saving, undo and diffs working the way they do for any other file — and embedded editors correctly follow a dark or custom theme in their popups, inputs and drag previews.

Implementation notes

Why the extension shrank by two workspaces. It previously ran two extra pnpm workspaces (editor/, menu/) and mutated the user's global files.associations on every activation. Both are gone: one webview workspace, and no writes to user settings. The extension has never been published (CHANGELOG.md: "First Marketplace release pending"), so there is no installed base holding a stale files.associations entry and nothing to migrate. That is the only reason it was safe to simply delete that code rather than clean up after it.

Writes are range edits, not rewrites. The document is edited through jsonc-parser's modify/applyEdits at the model's own path, so a hand-annotated .apollon file keeps its comments, trailing commas, sibling keys, indentation and line endings, and a text editor open on the same document keeps its cursor and a sane undo granularity. detectFormatting reads the document's own indent and EOL rather than imposing ours. Tests pin the wiring, not just the detection — deleting formattingOptions from modelEdits turns three of them red.

An empty file is a state, not a failure. readDocument returns empty | model | invalid. An empty .apollon offers a diagram-type picker; an unparseable one explains itself instead of throwing. parseModel checks only the type discriminator — structural validation belongs to the library, which rejects a bad model on mount with a better message than this extension could reconstruct from a JSON shape.

The three library theming bugs. Each has a one-line root cause worth stating precisely, because each is easy to "fix" somewhere that doesn't work:

  1. An unregistered custom property substitutes its var() at the element that declares it, then inherits the computed result. The --home-* aliases the shared @tumaet/ui primitives paint from were declared only at :root, so under a scoped mount they had already frozen to the light document. Re-declared on .apollon-editor, exactly as the chrome ramp already was.
  2. The UA stylesheet's input { color: fieldtext } is a declaration on the element, which beats inheritance, and fieldtext follows color-scheme — the OS appearance, not the editor's theme. Tailwind's Preflight resets this; the Tailwind-free bundle the embeds ship does not, so the primitives now reset color themselves.
  3. Body-portaled popups copy resolved token values and never recomputed them, so a theme switch left an open menu — and every later one on the same anchor — painting the old palette.

For (3) the popups now share one MutationObserver behind useSyncExternalStore instead of one per popup (an editor keeps dozens of tooltips mounted). It deliberately watches class but not style on <body>, because a palette drag writes body.style.overflow on every grab and would otherwise repaint every popup. The drag ghost, which portals to <body>, carries both the data-theme attribute and the resolved values — the attribute alone is not enough for a mount themed by inline properties or a host stylesheet, which is precisely the VS Code case.

Nothing tied the bridge to tokens.css, so an alias added to one and not the other was invisible until an embed rendered light input borders on a dark surface. A test now asserts the bridge is a superset of what tokens.css scopes to .apollon-editor.

PNG export produced 0-byte files for large diagrams. The webview rasterized its own PNGs through <img><canvas>toBlob — the exact pipeline the library abandoned in #667. Past the browser's canvas-area cap (~16 MP on WebKit, ~268 MP on Chrome) toBlob returns null with no exception, so the export silently wrote nothing. It now goes through @tumaet/apollon/export, which renders into resvg's own wasm buffer, as the webapp already does. This was a duplicate of library code, and the known-broken variant of it.

The renderer and its wasm binary load on first export, so the canvas keeps its startup path. Instantiating the module needs wasm-unsafe-eval and fetching it needs connect-src; a nonce does not propagate to import()-ed chunks, so script-src also carries cspSource. This was verified against the CSP the host actually emits, rendered from webviewHtml.ts rather than retyped: with wasm-unsafe-eval a real PNG comes back, and without it the module fails to compile — so the check can fail.

860 kB of dead weight left the VSIX. @tumaet/apollon/export also exports svgToPdf, and a bundler keeps every export of a dynamically imported module, since nothing constrains what the namespace object is read for. So the PNG import dragged jsPDF, svg2pdf, html2canvas and dompurify into a code path the webview can never take. pngRenderer.ts narrows the namespace to one binding and they shake out; an eslint rule stops a future import from silently re-inflating it. Separately, .vscodeignore's node_modules/** was anchored to the root and matched no nested tree.

The provider was doing three jobs. ApollonEditorProvider was 481 lines. Two subsystems moved out, leaving 253:

  • DocumentSync — the document↔canvas engine, previously five interlocking mutable variables in a closure. It is the code where a bug loses a user's work, and it now has a name, a lifecycle and a doc comment that fits it.
  • DiagramExporter — render request/response correlation, the workspace-trust guard, the file write, cancellation.

protocol.ts and diagramTypes.ts moved to src/shared/, which the webview imports too. That boundary was real but unenforced: nothing stopped a vscode import from breaking the webview bundle. An eslint rule enforces it now, and both rules were checked by writing the violating import and watching them fire.

Naming, while the files were open: the config message carried one field and promised a bag → autoExportChanged. The customEditor viewType apollon.diagram differed from the tree view id apollon.diagrams by one character → apollon.diagramEditor.

What review caught, and what it means. Three defects in the first push, all now covered by tests that fail without their fix:

  • Closing a tab inside the 300 ms commit debounce cleared the timer and dropped the edit with it. VS Code never saw a dirty document, so there was no save prompt — the change was silently gone. This was a data-loss bug. The pending model is now written on disposal, unless the document is already closed (closing without saving is a choice; resurrecting it would undo that).
  • An external write to a transiently invalid document — mid-typing in a split JSON editor — returned before cancelling the pending canvas write, so the debounce fired 300 ms later and overwrote what had just been typed.
  • The manual export command bypassed the workspace-trust guard that auto-export applied, even though the manifest promises image exports stay off until the workspace is trusted. The guard now sits on the shared write path.

The extension had no coverage of the provider at all — only of the pure document functions — which is precisely why these survived. tests/vscodeStub.ts now stands in for the vscode module: events fire synchronously and applyEdit really rewrites the document, so tests assert on the text the document ends up holding rather than on which API happened to be called. Auto-export writes files to a user's disk on every save and nothing asserted a file was ever written; the round trip, both payload encodings, the failure path, the trust refusal, the scaffold and its non-empty guard are now pinned too. 44 extension tests, up from 31.

Every one was falsified: eleven targeted mutations of the source, each turning red exactly the tests that guard the behaviour it broke, and green again on revert.

CI gap closed. pr-health-checks.yml linted vscode-extension but never typechecked, tested or built it, so its unit tests were decorative. A vscode-extension-checks job now runs typecheck, test and build:all against a built library, and it is wired into the PR Health Gate.

Safe area. The overlay grid's padding is the safe area, and chrome insets are measured from each control's own box, which begins inside that padding — so the two stack without double-counting. respectInsets: false and a per-side padding override both still opt out of chrome; neither can opt out of the safe area, which is a hardware constraint rather than breathing room.

Trade-off — the CSS size budget. standalone/webapp capped gzipped CSS at 35 kB and main had grown to exactly 35,001 B, so the next CSS change of any size would have failed the check on its own. This PR adds 56 B. Raised to 36 kB rather than shaving bytes out of a correctness fix; the current 35.06 kB still leaves the budget able to catch a real regression.

Marketplace metadata. The manifest had no icon, which the Marketplace requires as a PNG of at least 128×128 (media/ held only the monochrome activity-bar glyph, which is recoloured by the theme and would have listed as a black square). media/icon.png is the project's own brand asset downscaled to 256×256. apollon.autoExport gains enumDescriptions and scope: resource, since export writes are per-folder. Other leaves categories.

Deliberately not in this PR

An audit for duplication turned up four more things. None are this branch's doing, and folding them in would make it harder to review, not better:

  • standalone/webapp's diagramTypes identity map re-declares all 13 diagram types purely to hand back string constants identical to the library's runtime UMLDiagramType. Unlike the extension — where importing the runtime value would drag the browser editor into the Node host bundle, which is why src/shared/diagramTypes.ts legitimately declares its own — the webapp already ships the editor, so this is free to delete. Its sibling shortLabelsByType and tilesByType are typed Partial<Record<…>>, so a 14th diagram type would silently render with no label rather than failing the build.
  • The model schema version is a bare string in five places. "4.0.0" is the repo-wide convention (the v3→v4 converter, the webapp store, every fixture) and the extension follows it; the outlier is library/lib/apollon-editor.tsx, whose model getter emits "4.1.0". UMLModel["version"] is typed `4.${number}.${number}`, so nothing catches the disagreement. The library should export the constant.
  • DocumentSync.write() rolls written back if applyEdit is refused. If an unrelated external edit lands inside that await, the rollback restores text the document no longer holds. This behaviour is carried over verbatim from the pre-refactor closure, not introduced here. Refused applyEdit is rare and the window is a microtask; still worth a ticket.
  • knip runs in no workflow, so the config in knip.json only gates whoever runs it locally. (It is green here, for the first time — the dead nodeTypeLabel export it was flagging turned out to be a stale duplicate of the i18n label table, missing two diagram prefixes the live one also lacked. That is the fix(library) popover-titles commit.)

vsce is deliberately not a workspace dependency — the release workflow installs it into a temp prefix with --ignore-scripts, keeping its native credential store and Azure SDK out of every contributor's pnpm install. Recorded in README.dev.md so it doesn't get "fixed".

Steps for testing

The extension

  1. pnpm install && pnpm run build:lib
  2. pnpm --filter apollon-vscode run build:all, then F5 to launch an Extension Development Host.
  3. Create an empty diagram.apollon and open it — you should get a diagram-type picker, not an error. Pick one, then Cmd/Ctrl+Z: you are back to an empty file.
  4. Drag a node. Hit Cmd/Ctrl+S, then Cmd/Ctrl+Z — undo, dirty state and save behave like any text file.
  5. Add a // comment and a sibling key to the JSON via Reopen with Text Editor, go back to the canvas, move a node, save. The comment, the sibling key and your indentation survive.
  6. Set apollon.autoExport to png and save a large diagram. You get a real PNG next to the file, not a 0-byte one.
  7. Switch VS Code between Light Modern, Dark Modern and both high-contrast themes with the canvas open. Popovers, inputs, tooltips and the palette follow, live.

The library fixes (these are the ones with regression coverage)

  • pnpm --filter @tumaet/apollon run testportalTheme, portalThemeReactivity, draggableGhostTheme, nodeTypeLabel, fitView.
  • pnpm --filter @tumaet/webapp run test:e2e -- scoped-theme-form-controls — asserts the rendered border-color and color of an input inside a body-portaled popover under an inline dark theme.
  • pnpm --filter apollon-vscode run test — the provider lifecycle, including the data-loss path above.

Every new test was falsified against the unfixed code before being kept: reverting the .apollon-editor { --home-* } block turns the border assertion red (it reads 0.84 luminance — a light border on a dark surface), and removing text-inherit turns the ink assertion red (rgb(0, 0, 0)).

Two caveats worth stating, because both are cases where a test passed for the wrong reason until it was checked:

  • An early vscode stub modelled applyEdit as refusing a closed document. That made the "don't resurrect a closed document" test pass whether or not the guard existed. Real VS Code applies a WorkspaceEdit to files that aren't open — that is how cross-file refactors work — so the stub was corrected and the test now actually fails without the guard.
  • The scoped-theme e2e test first used data-theme="dark", which also sets color-scheme: dark and therefore made fieldtext white on its own — the fix was unfalsifiable. Rewritten to an inline theme; it then still passed, because the webapp loads Tailwind Preflight, which already resets input { color }. The bug is only observable in the Preflight-free components.css the embeds ship, which the test now loads directly.

Screenshots / screencasts

⚠️ Not attached. This is a UI change and screenshots are required — they need to be captured from a running Extension Development Host (the editor across the four VS Code themes, the empty-file picker, and a popover with an input under a dark theme) and added before merge. Flagging rather than silently checking the box.

Checklist

  • Linked to a related issue (if applicable)
  • Added a changeset whose summary is written in the user's voice (pnpm changeset, how) — or this PR doesn't touch a Changesets-tracked package (@tumaet/apollon, @tumaet/webapp, @tumaet/server)
  • PR title's Conventional Commit type (feat/fix/…) matches the kind of change — it groups the release note
  • Tests added or updated
  • Ran pnpm lint && pnpm format:check && pnpm build && pnpm test locally — green
  • Documentation updated (if applicable)
  • Screenshots or screencasts attached (if a UI change)

Five changesets ship with this PR: four @tumaet/apollon patches (safe area, scoped-theme form controls, drag ghost, popover titles), of which the form-controls one also bumps @tumaet/ui; plus one @tumaet/webapp patch (iOS keyboard). apollon-vscode is on the Changesets ignore list and versions through its own release workflow.

Note for the reviewer: the e2e suite is flaky under parallel load with retries disabled — main fails 1–2 different tests per run in that mode, as does this branch, on a different test each time. CI runs with retries, where this branch is 211 passed / 1 flaky. The palette-drag ghost tests were A/B'd specifically (this branch's DraggableGhost vs main's, 18 repeats each across 4 workers): both 18/18.

`fitView` reserved the overlay insets a host's chrome declares, but nothing
reserved the notch, Dynamic Island, home indicator or the mobile soft keyboard.
On a phone the camera framed nodes underneath them.

The overlay grid already pads itself by the safe area, so it is measured
independently of the chrome insets (which come from each control's own box) and
the two stack without double-counting. `respectInsets: false` and a per-side
`padding` override both still opt out of chrome; neither can opt out of the safe
area, which is a hardware constraint rather than breathing room.

Consolidates the `--safe-area-inset-*` declaration — previously duplicated in
the library and the webapp, with a unitless `0` fallback that invalidated the
`calc()` expressions it fed — into a single block in the shared tokens.

Also: the zoom cluster honours prefers-reduced-transparency and
prefers-contrast, floating chrome uses a cheaper backdrop blur on touch devices,
and the iOS shell no longer strands the viewport after the keyboard closes.
…ghost

Every embed that themes the editor's own mount node — `dataTheme`, a `theme`
override, or a host stylesheet like the VS Code webview's — hit three separate
bugs that a document-root theme (the webapp) hides completely.

An unregistered custom property substitutes its `var()` at the element that
declares it and then inherits the *computed* result. The `--home-*` aliases the
shared @tumaet/ui form primitives paint from were declared only at `:root`, so
they resolved against the light document and merely inherited down: a dark embed
drew light input borders and light placeholder ink. They are now re-declared on
`.apollon-editor`, exactly as the chrome ramp already was.

Text fields inherited nothing and fell through to the UA's `input { color:
fieldtext }` — a system color keyed to `color-scheme`, i.e. the OS appearance
rather than the editor's theme. Tailwind's Preflight resets this, but the
Tailwind-free bundle the embeds ship does not, so the primitives reset `color`
themselves.

Popups portal to `<body>` and copy resolved token values, but never recomputed
them, so a theme switch left an open menu — and every later one anchored to the
same element — painting the old palette. They now subscribe to theme changes
through one shared observer rather than one per popup, and the palette drag
ghost carries the theme it was grabbed under out to `<body>` with it.
…files

The extension previously opened a diagram in a bespoke webview backed by two
extra pnpm workspaces (`editor/`, `menu/`) and mutated the user's global
`files.associations` on every activation.

It is now a single `CustomTextEditorProvider` over one webview workspace. VS Code
owns the document, so save, auto-save, undo, dirty state, hot exit, diffs and
branch switches all work by default rather than being reimplemented. Writes go
through `jsonc-parser` as a minimal range edit, so a hand-annotated `.apollon`
file keeps its comments, sibling keys, indentation and line endings, and a text
editor open on the same document keeps its cursor.

Also contributes an `apollon` language with a grammar and icon, a diagram tree
view of the workspace, an empty-file diagram-type picker instead of a parse
error, and optional export-on-save. The webview maps the host's `--vscode-*`
colors onto the editor's public theme tokens, verified against Light/Dark Modern
and both high-contrast themes.

The extension has never been published, so nothing migrates.

CI never typechecked, tested or built this workspace. It does now.
The stylesheet had grown to exactly the 35 kB ceiling, so the next CSS change of
any size would fail the check regardless of its merit — the two theming fixes in
this branch add 56 B gzipped. Raised to 36 kB, which the current 35.06 kB clears
with room to still catch a real regression.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The custom-editor rewrite is headed in the right direction, but I found a data-loss window in the debounce lifecycle, stale Dockerfile workspace paths that are breaking Docker CI, and a trust guard gap on manual export. Codacy also has a few CSS style annotations to clear, and the required UI screenshots still need to be attached.

Comment thread vscode-extension/src/apollonEditorProvider.ts Outdated
Comment thread vscode-extension/src/apollonEditorProvider.ts Outdated
Comment thread pnpm-workspace.yaml
Comment thread vscode-extension/src/apollonEditorProvider.ts Outdated

function DiagramPicker() {
return (
<div className="apollon-vscode-notice">

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich [medium] This PR changes visible VS Code editor states and scoped-theme UI behavior, but there are no real screenshots or screencasts attached. The Apollon review rules require captures for UI changes, so please add the Extension Development Host screenshots for the empty picker, the editor across VS Code themes, and a themed popover/input before merge.

🤖 Prompt for AI agents

For PR #806, visible UI changes lack the required review screenshots. Capture and attach real screenshots or screencasts from a running VS Code Extension Development Host covering the empty-file picker, editor theme variants, and a scoped dark popover/input; do not use generated or mock screenshots.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich Still open: I do not see screenshots in the PR body or comments yet, and the PR body still says they are not attached.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, and I have not attached them — I cannot capture a running Extension Development Host from here, and I would rather leave the checklist box unticked than fill it with anything generated. Called out explicitly in the PR body under Screenshots / screencasts: the empty-file picker, the editor across the four VS Code themes, and a scoped-dark popover with an input still need real captures before merge.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich Still open then. I agree generated screenshots would be worse, but the review requirement is real captures before merge, so I’m leaving this thread unresolved.

Comment thread library/lib/styles/app.css Outdated
Comment thread packages/ui/src/styles/components.css
Comment thread vscode-extension/webview/src/index.css
@github-project-automation github-project-automation Bot moved this from Backlog to In progress in Apollon Development Jul 9, 2026
Both Dockerfiles copied `vscode-extension/{menu,editor}/package.json` so pnpm
could read them as workspace manifests. Those two workspaces are gone, replaced
by a single `webview`, and the `COPY` failed to resolve. `.dockerignore` still
dropped the same two source trees.

Verified by building both images locally.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich One Docker issue is fixed and I resolved that thread, but the debounce/external-edit data-loss paths, manual export trust guard, missing screenshots, and Codacy CSS items are still open in the existing threads.

…aries

Three lifecycle holes, all found in review, all now covered by tests that fail
without their fix. The extension had no coverage of the provider at all, so
`tests/vscodeStub.ts` stands in for the `vscode` module: events fire
synchronously and `applyEdit` really rewrites the document, which lets the tests
assert on the text the document ends up holding rather than on which API was
called.

Closing a tab inside the 300ms debounce cleared the timer and dropped the edit
with it. VS Code never saw a dirty document, so there was no save prompt and the
change was simply gone. The pending model is now written on disposal — unless
the document is already closed, because closing without saving is a choice and
resurrecting it would undo that.

An external write to a *transiently invalid* document (mid-typing in a split
JSON editor) returned before cancelling the pending canvas write, so the
debounce fired 300ms later and overwrote what had just been typed. Every
external change now supersedes the pending write, before validity is considered.

The manual export command bypassed the workspace-trust guard that auto-export
applied, even though the manifest promises image exports stay off until the
workspace is trusted. The guard now sits on the shared write path.

Also clears the Codacy stylelint findings: direct `padding` fallbacks take a
unitless `0` (the typed `0px` stays where the value feeds `calc()`), plus two
blank-line nits.
Clears the last Codacy stylelint finding (`comment-empty-line-before`). Verified
by running Codacy's three rules locally over every stylesheet this branch
touches: nothing else it reports sits on a line this PR adds.

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The code issues I had flagged are addressed and CI is green now. I'm keeping changes requested only because the required real screenshots or screencasts for the VS Code UI/scoped-theme changes are still missing.

FelixTJDietrich and others added 4 commits July 9, 2026 14:31
…tax-tree popover titles

`nodeTypeLabel` strips a diagram-type prefix so a popover reads "Marking", not
"Reachability Graph Marking". Two copies of the prefix table existed: a dead one
in `nodeUtils.ts` that listed every diagram, and the live one in `i18n/labels.ts`
that was missing `reachabilityGraph` and `syntaxTree`.

Delete the dead copy and complete the live one. knip was already red on the dead
export.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`tokens.css` scopes the `--home-*` aliases to `.apollon-editor`; a popup portaled
to `<body>` leaves that subtree and reads them off the bridge instead. Nothing
tied the two lists together, so an alias added to one and not the other went
unnoticed until an embed rendered light input borders on a dark surface.

Assert the bridge is a superset of what `tokens.css` scopes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The webview rasterized its own PNGs through `<img>` → `<canvas>` → `toBlob`,
the exact pipeline the library abandoned in #667: past the browser's canvas-area
cap (~16 MP on WebKit, ~268 MP on Chrome) `toBlob` yields `null` with no
exception, so a large diagram exported as a 0-byte file. Route the export through
`@tumaet/apollon/export`, which renders into resvg's own wasm buffer, as the
webapp already does.

The renderer and its wasm binary load on first export, so the canvas keeps its
startup path. Instantiating the module needs `wasm-unsafe-eval` and fetching it
needs `connect-src`; the nonce does not propagate to `import()`-ed chunks, so
`script-src` gains `cspSource`. Verified against the CSP the host emits: without
`wasm-unsafe-eval` the export fails to compile, with it a real PNG comes back.

`@tumaet/apollon/export` also exports `svgToPdf`, and a bundler keeps every
export of a dynamically imported module. Narrowing the namespace through
`pngRenderer.ts` sheds jsPDF, svg2pdf and html2canvas — about 860 kB the webview
could never reach.

Also stop the VSIX from swallowing a nested `node_modules`: the `node_modules/**`
pattern was anchored to the root and matched neither subdirectory.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
`ApollonEditorProvider` was 481 lines holding three unrelated jobs. Two of them
move out:

- `DocumentSync` — the document↔canvas engine, previously five interlocking
  mutable variables inside a closure. It is the code where a bug loses a user's
  work, and it now has a name and a lifecycle.
- `DiagramExporter` — render request/response correlation, the workspace-trust
  guard, the file write and cancellation.

What is left is the webview's lifecycle and the messages it answers, at 253
lines.

`protocol.ts` and `diagramTypes.ts` move to `src/shared/`, which the webview
imports too. The boundary was real but unenforced: nothing stopped a `vscode`
import from breaking the webview bundle. An eslint rule stops it now.

Naming: the `config` message carried one field and promised a bag, so it is
`autoExportChanged`. The customEditor viewType `apollon.diagram` differed from
the tree view `apollon.diagrams` by one character; it is `apollon.diagramEditor`.

Tests cover what the branch newly relies on. Auto-export writes files to a user's
disk on every save and nothing asserted a file was written — now the round trip,
both encodings, the failure path, the trust refusal, the scaffold and its
non-empty guard are all pinned. Each was checked against the unfixed code.

The manifest gains the Marketplace icon it had no `icon` field for, per-value
descriptions for `apollon.autoExport`, and `scope: resource` since export writes
are per-folder. `Other` leaves `categories`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@Claudia-Anthropica Claudia-Anthropica left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@FelixTJDietrich The code feedback I checked is addressed, and the targeted VS Code extension tests pass locally. I’m keeping changes requested because the required real screenshots or screencasts for the VS Code UI and scoped-theme changes are still missing, and the GitHub checks for this SHA are still pending.

@FelixTJDietrich
FelixTJDietrich merged commit ecad49e into main Jul 9, 2026
17 of 21 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the feat/vscode-custom-editor-and-embed-theming branch July 9, 2026 12:47
@github-project-automation github-project-automation Bot moved this from In progress to Done in Apollon Development Jul 9, 2026
@github-actions github-actions Bot mentioned this pull request Jul 9, 2026
@FelixTJDietrich

Copy link
Copy Markdown
Contributor Author

Correction to this PR's description, found while debugging the release run it triggered (#808).

Under Implementation notes I wrote that the extension "has never been published (CHANGELOG.md: First Marketplace release pending), so there is no installed base holding a stale files.associations entry and nothing to migrate."

That is wrong. I trusted the changelog instead of querying the Marketplace. tumaet.apollon-vscode has been listed since 2024-11-18, last at 0.0.17. I downloaded that VSIX and read its bundle: it writes "files.associations": { "*.apollon": "json" } into global user settings on every activation. An installed base exists, and it carries that line.

The conclusion mostly survives, but not for the reason I gave:

  • .apollon files still open in the Apollon editor — the custom editor matches on filenamePattern, not on language, so the stale setting cannot hijack it.
  • Reopening one as text shows it as JSON rather than under this extension's apollon language, grammar and file icon.
  • Nothing removes the setting. Deleting it restores both.

Deleting the code that wrote it was still right; claiming there was nobody to migrate was not. #808 corrects CHANGELOG.md and asks whether we want a one-time, self-deleting migration.

Two related things #808 fixes, both surfaced by merging this PR:

  1. Release VS Code Extension fired on merge and tried to publish 1.0.0, because it wakes on any edit to vscode-extension/package.json and this PR edited the icon, categories and settings schema. Nothing was published — the publish job crashed first, on a missing .nvmrc.
  2. That crash (the publish job never checks the repo out) is also fixed there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants