Skip to content

Commit df5f6aa

Browse files
serpentbladeclaude
andcommitted
fix(codemirror): decoration-unfilled returns [] not Decoration.none (G5 runtime crash)
makeDecorationExt returned Decoration.none (a DecorationSet/RangeSet, NOT an Extension) when the decoration slot was unfilled. Placed in the extensions array via decorationCompartment.of(...), it made EditorState.create throw at runtime — the editor never mounted, so ALL 6 CodeMirrorScreenshot VR cells timed out waiting for .cm-content. The gutter branch already returned [] correctly. Build/typecheck passed (CM facet types are loose); only the browser/VR surfaced it — the build-green != runtime-green gap. Return [] to match the gutter unfilled branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f4f4fbf commit df5f6aa

7 files changed

Lines changed: 42 additions & 7 deletions

File tree

packages/ui/codemirror/packages/angular/src/CodeMirror.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,12 @@ export class CodeMirror {
598598
// The WidgetType class captures $portals.decoration, so it is defined inside
599599
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
600600
const makeDecorationExt = (dv: any) => {
601-
if (!(this.decorationTpl ?? this.templates()?.['decoration'])) return Decoration.none;
601+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
602+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
603+
// in the extensions array (via `decorationCompartment.of(...)`) makes
604+
// EditorState.create throw at runtime — the editor never mounts. Only the
605+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
606+
if (!(this.decorationTpl ?? this.templates()?.['decoration'])) return [];
602607
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
603608
// but its per-widget state (`from`/`to`, the live portal handle) lives in
604609
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/packages/lit/src/CodeMirror.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -582,7 +582,12 @@ private _portalContainers = new Set<HTMLElement>();
582582
// The WidgetType class captures $portals.decoration, so it is defined inside
583583
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
584584
const makeDecorationExt = (dv: any) => {
585-
if (!(this.decoration !== undefined)) return Decoration.none;
585+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
586+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
587+
// in the extensions array (via `decorationCompartment.of(...)`) makes
588+
// EditorState.create throw at runtime — the editor never mounts. Only the
589+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
590+
if (!(this.decoration !== undefined)) return [];
586591
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
587592
// but its per-widget state (`from`/`to`, the live portal handle) lives in
588593
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/packages/react/src/CodeMirror.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -551,7 +551,12 @@ const CodeMirror = forwardRef<CodeMirrorHandle, CodeMirrorProps>(function CodeMi
551551
// The WidgetType class captures $portals.decoration, so it is defined inside
552552
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
553553
const makeDecorationExt = (dv: any) => {
554-
if (!(props.renderDecoration ?? props.slots?.["decoration"])) return Decoration.none;
554+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
555+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
556+
// in the extensions array (via `decorationCompartment.of(...)`) makes
557+
// EditorState.create throw at runtime — the editor never mounts. Only the
558+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
559+
if (!(props.renderDecoration ?? props.slots?.["decoration"])) return [];
555560
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
556561
// but its per-widget state (`from`/`to`, the live portal handle) lives in
557562
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/packages/solid/src/CodeMirror.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,12 @@ export default function CodeMirror(_props: CodeMirrorProps): JSX.Element {
444444
// The WidgetType class captures $portals.decoration, so it is defined inside
445445
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
446446
const makeDecorationExt = (dv: any) => {
447-
if (!(_props.decorationSlot ?? _props.slots?.["decoration"])) return Decoration.none;
447+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
448+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
449+
// in the extensions array (via `decorationCompartment.of(...)`) makes
450+
// EditorState.create throw at runtime — the editor never mounts. Only the
451+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
452+
if (!(_props.decorationSlot ?? _props.slots?.["decoration"])) return [];
448453
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
449454
// but its per-widget state (`from`/`to`, the live portal handle) lives in
450455
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/packages/svelte/src/CodeMirror.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,12 @@ onMount(() => {
634634
// The WidgetType class captures $portals.decoration, so it is defined inside
635635
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
636636
const makeDecorationExt = (dv: any) => {
637-
if (!decoration) return Decoration.none;
637+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
638+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
639+
// in the extensions array (via `decorationCompartment.of(...)`) makes
640+
// EditorState.create throw at runtime — the editor never mounts. Only the
641+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
642+
if (!decoration) return [];
638643
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
639644
// but its per-widget state (`from`/`to`, the live portal handle) lives in
640645
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/packages/vue/src/CodeMirror.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,7 +623,12 @@ onMounted(() => {
623623
// The WidgetType class captures $portals.decoration, so it is defined inside
624624
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
625625
const makeDecorationExt = (dv: any) => {
626-
if (!slots.decoration) return Decoration.none;
626+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
627+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
628+
// in the extensions array (via `decorationCompartment.of(...)`) makes
629+
// EditorState.create throw at runtime — the editor never mounts. Only the
630+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
631+
if (!slots.decoration) return [];
627632
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
628633
// but its per-widget state (`from`/`to`, the live portal handle) lives in
629634
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

packages/ui/codemirror/src/CodeMirror.rozie

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,12 @@ $onMount(() => {
396396
// The WidgetType class captures $portals.decoration, so it is defined inside
397397
// this $onMount-invoked factory (the bundled-leaf typecheck discipline).
398398
const makeDecorationExt = (dv) => {
399-
if (!$slots.decoration) return Decoration.none
399+
// Unfilled slot → EMPTY EXTENSION (`[]`), NOT `Decoration.none`. The latter
400+
// is a DecorationSet (a RangeSet), which is NOT a valid Extension; placing it
401+
// in the extensions array (via `decorationCompartment.of(...)`) makes
402+
// EditorState.create throw at runtime — the editor never mounts. Only the
403+
// browser surfaces this (CM's facet types are loose, so build/typecheck pass).
404+
if (!$slots.decoration) return []
400405
// The WidgetType subclass is declared inline (WidgetType REQUIRES subclassing)
401406
// but its per-widget state (`from`/`to`, the live portal handle) lives in
402407
// CLOSURE — `makeWidget(from, to)` captures them — NOT in `this` fields, for

0 commit comments

Comments
 (0)