The pack hosts two Monaco editors: the single code editor (MonacoEditorView, in
MeshWeaver.Blazor) and the side-by-side diff editor (DiffEditorView, here). Both wrap a
BlazorMonaco component, and both depend on a detail that is easy to miss and silent when missed.
The contract
BlazorMonaco renders its editor host as <div id="…" class="@CssClass"> and writes no style
attribute. Verified against the shipped BlazorMonaco 3.5 assembly: the compiled markup carries the
class and id attribute literals and no style at all. So the class the view passes as
CssClass is the only handle on that element — if no stylesheet defines a rule for it, the
element is a plain block div of height: auto containing nothing but Monaco's own
absolutely-positioned children, i.e. zero pixels tall.
monaco.editor.create / createDiffEditor then lay the editor out at the container's measured
size. With automaticLayout: true that measurement is repeated on every resize — so a zero-height
container stays zero-height forever, and there is no error anywhere: no exception, no console
warning, no failed request. The page renders, the host box occupies its declared height, and the
editor inside it is invisible.
Wrapping the component in a sized <div> is not enough. height does not inherit, and a plain
block child does not stretch to a parent's height. The rule has to name the container itself:
/* DiffEditorView.razor.css */
::deep .diff-editor-view {
width: 100%;
height: 100%;
}
::deep because the element belongs to the child component, not to this view's own markup — the
scope attribute lands on the view's root <div>, and the rule compiles to
[b-xxxxxxxxxx] .diff-editor-view.
What went wrong (MeshWeaver#3288)
DiffEditorView shipped from the day it was written with CssClass="diff-editor-view" and no
stylesheet. Across both repos and every served CSS bundle, that class name appeared exactly once —
in the markup that names it. The version comparison of a node whose content is structured data
(the report was a Claims/Claim, whose developments array routes it to the JSON side-by-side
editor rather than the markdown redline) therefore rendered as a 600px gap between the "Changes
from v3 to v4" heading and the Restore button.
It survived that long because the common case never reaches this editor: a markdown node's version
comparison renders the tracked-change redline, and the diff editor only appears for non-markdown
content or behind ?view=source.
Measured in a headless browser against the shipped BlazorMonaco assets, on the exact DOM the view emits:
| host box | Monaco container | original pane | modified pane | |
|---|---|---|---|---|
| without the stylesheet | 1280×600 | 1280×0 | 625×0 | 625×0 |
| with the stylesheet | 1280×600 | 1280×600 | 625×600 | 625×600 |
Only the CONTAINER is sized. Inside a side-by-side diff Monaco positions .editor.original and
.editor.modified itself from the split ratio, so forcing those to 100% — which the
single-editor sheet legitimately does for its one editor — would break the split.
The second half: wait for Monaco
Blazor.start() is deliberately not gated on Monaco (gating it left Safari staring at an empty
page while the 3.6MB AMD bundle loaded). The gate moved into the editor components instead: each
one waits for window.monacoReady before rendering the BlazorMonaco component, because
BlazorMonaco calls monaco.editor.create… on its first render and 3.5 console.errors and
returns when monaco is still undefined — no editor, and nothing retries.
MonacoEditorView was given that gate; DiffEditorView was not, and now is. Both treat a failed or
absent readiness promise as "ready", so the gate can only ever delay an editor, never withhold one.
The guard
MonacoEditorContainerSizingGuard (in MeshWeaver.Blazor.Views.Test) discovers every
<Standalone…Editor> in both view assemblies' Monaco folders — from the sources, not a hand-written
list — and fails when one names a CssClass its component stylesheet does not give a height. It
also asserts that the scan found the known hosts, so a rename or a move cannot turn it into a test
that checks an empty list and reports green.
A stylesheet contract is necessary, not sufficient: it cannot see an ancestor that collapses the host box. The pixels are confirmed by measuring the real Monaco layout in a browser, as above.
The other direction: the overflow guard must clip
A box that is too SMALL for its content is the ordinary case — a notebook cell clamps its editor
at 480px (CodeViews.CellEditorHeight), so any cell longer than about 24 lines scrolls inside it.
Monaco handles that by clipping, and only by clipping: it sizes the line-number gutter
(.margin and its .margin-view-overlays) to the content's FULL scroll height — the shipped
bundle carries setHeight(Math.min(scrollHeight, 1e6)) — and relies on .overflow-guard's
overflow: hidden to cut it at the editor's bottom edge.
MonacoEditorView.razor.css used to set .overflow-guard { overflow: visible !important }
(the comment said: so autocomplete can overflow). The gutter of every long cell then painted
down past the editor, over the cell toolbar that sits directly beneath it, and its left column
landed exactly on the ▶ Run button. Clicks on Run hit the gutter instead. The Education e2e
journeys measured it 336 times in one run (MeshWeaver.Education#414), with the Playwright message
<div class="margin-view-overlays"> from <div class="monaco-editor-container with-border"> subtree intercepts pointer events. A learner's mouse hit the same gutter.
Measured in headless Chromium on the cell's own DOM (480px box, toolbar directly below), with the
shipped monaco.js/monaco.css and this stylesheet:
| code lines | guard visible: element at the Run button's centre |
guard hidden |
|---|---|---|
| 3, 10, 15, 20, 24 | the button | the button |
| 30 | margin-view-overlays (gutter bottom 642px, box bottom 511px) |
the button |
| 80 | — | the button |
Autocomplete never needed the visible guard: with FixedOverflowWidgets the suggest, hover and
parameter-hint widgets render outside .overflow-guard (measured: the suggest widget renders and
has no .overflow-guard ancestor). MonacoGutterStaysInsideTheEditorGuard (same test project)
fails when any Monaco view stylesheet makes .overflow-guard visible again.