Skip to content

AvaCodeEditor v12.0.0-preview.1

Choose a tag to compare

@pavel-zheltiakov pavel-zheltiakov released this 18 Aug 05:38
· 2 commits to main since this release

The first published version: the two packages, the site, and a demo application you can run.
A preview because the API surface is still forming — see the notes below for what a host plugs
into, and expect those interfaces to be the ones that settle first.

The demo runs in a browser

demo/AvaCodeEditor.Demo.Browser publishes the same demo to WebAssembly, and the release drops it
into the site — so the landing page runs the editor rather than describing it. Needs the
wasm-tools workload to build.

A demo application ships with the release

demo/AvaCodeEditor.Demo — a themed sample with eight scenarios, one per thing a host plugs in:
editing, syntax highlighting, a gutter of your own, actions anchored to a block of lines, an
annotation column that arrives asynchronously, line and sub-line backgrounds, a hundred thousand
lines, and themes with a host overriding them.

It is published in the public repository alongside the packages, and it references
AvaCodeEditor as a package there — the same way your own project does. One demo source
serves both repositories: here it references the projects, so it exercises the working tree; there
AvaCodeEditorFromPackage switches it to the published NuGets. scripts/release.sh builds that
public copy against the freshly packed packages and runs its --smoke check before anything is
pushed, so a release that would not compile for a consumer fails here first.

--smoke builds every scenario in both appearances and exits — it is what CI and the release
script run, because nothing else finds out that the sixth scenario throws without clicking it.

Sub-line backgrounds — ISegmentBackgroundProvider, CodeEditor.SegmentBackgrounds

Runs of characters painted under the text inside one line — in-line (word-level) diff
highlighting being the case it was built for. The counterpart of ILineBackgroundProvider: that
one paints a whole line, this one paints inside it, and either may be set without the other.

  • A run is (Start, Length, Brush) in character offsets, never visual columns. The editor
    maps them to cells itself, through the same code the caret and the selection use — a host
    cannot do it, because a tab is one character and up to TabSize cells and a grapheme cluster
    is several characters and one cell.
  • Painting order is band → runs → selection → glyphs. A run stays visible against the band it
    sits in; a selection stays visible over everything.
  • Offsets are clamped to the line: they describe the document as you last read it, and the
    buffer moves on under them. A run of no width paints nothing. Nothing is sorted, merged or
    de-overlapped — two runs over one character paint twice.
  • Changed may be raised from any thread, like ILineBackgroundProvider.Changed; the editor
    marshals and repaints. Subscriptions are dropped when the control leaves the visual tree.

Line annotations — ILineAnnotationProvider, LineAnnotationMargin

Per-line metadata in a gutter column of its own — git blame being the case it was built for.
The host implements the provider; the editor knows nothing about git.

  • Annotations may arrive late: raise AnnotationsChanged when a lookup completes and the
    column repaints. Nothing blocks the render pass waiting for data.
  • The column's width is declared, not measuredMaxCharacters (clamped 1..64), in character
    cells. A column measured from whatever annotations happen to be visible would slide the whole
    text area sideways as the user scrolled onto a longer author name.
  • What does not fit is ellipsised, one cell short of the edge, rather than clipped: a hard cut
    hides that anything was removed, and pzh 2026-07-2 reads as a date rather than as a truncation.
  • A run of consecutive lines carrying the same annotation is labelled once (CollapseRepeats,
    on by default), at the run's first visible line — so a commit taller than the viewport is still
    labelled while you are reading the middle of it. That row is nudged into view when the scroll
    offset lands part-way down it, which a trackpad does constantly, unless the row below carries a
    label of its own and the nudge would print one over the other.
  • The caret's line is answered at full contrast by its run's label, wherever inside the run the
    caret is — including from above the top of the viewport, which is where a caret ends up when you
    scroll down through a long commit. Which label that is costs the viewport, not the document: it
    is chosen from the labels on screen, so the provider is asked about the visible rows and the
    caret's line, and nothing else.
  • GetTooltip answers per line, including on rows whose label was collapsed away or cut short,
    so the full text is always reachable. An empty Tooltip falls back to the label rather than
    popping an empty balloon.
  • The column consumes no press: clicks and drags pass through to the caret.

Margins that fail no longer take the editor with them

Margins are host code called from inside the editor's render pass, which made a host bug a crash
that loses the unsaved document.

  • IEditorMargin.Render and GetWidth are wrapped per margin, per call. One faulting margin
    costs one column; the text, selection and caret are already on the frame when gutters draw, and
    the remaining columns still draw.
  • A throwing GetWidth keeps the width it last reported. A margin that has never measured
    successfully has no width to keep and gets zero — that includes one added back to Margins after
    being removed, since what the editor remembers about a margin goes with it. A width that is not
    finite counts as a fault: NaN would otherwise take the text area's origin with it.
  • Faults surface on CodeEditor.MarginFailed, raised once per fault rather than once per
    frame, and again when a margin starts working. It arrives after the frame the fault happened
    in, so handling it by removing the margin, replacing it, or re-laying out is safe — and a handler
    that throws costs neither the frame nor the other handlers. Margin is always a margin still in
    Margins, on an editor still attached: a report whose margin was removed, or whose editor was
    detached, in the moment between the frame and the delivery is dropped rather than handed to a
    handler with nothing left to act on. It is still logged.
  • Pointer callbacks (OnPointerPressed and friends) are left to propagate, as a change
    action's Execute is: they run once, because of something the user did, so swallowing them
    hides a real bug rather than surviving a frame. The exception is the hover refresh something
    other than the pointer drives — a scroll, or a re-measure that moved the columns under a
    motionless pointer: nobody touched the gutter, and the throw would land in a layout pass, so that
    path reports like a margin that cannot draw, naming the margin whose call actually threw.
  • A change to Margins cancels a held press, and the margin is told. Any change: the press is
    aimed at a COLUMN, and an add, a move or a removal anywhere renumbers the columns under a button
    already down. Sliding off is a cancel; so is the gutter being rebuilt underneath.

Gutter text

  • MarginContext.GutterText(scale) is the shared arithmetic for text drawn at a fraction of the
    row — the size, the per-cell advance and the baseline. The row it scales is bounded at 1.32 em,
    because past that a row is leading rather than text: sizing from all of it gives a cell more room
    than the column reserved, and the label runs past its own edge where the clip removes the ellipsis
    that said it was truncated. Gutter text therefore stays a notch below the code whatever the
    typeface's line height is — a CJK monospace fallback included, which is the case this is for.

Change actions — IChangeActionProvider, ChangeActionMargin

  • Actions run on pointer release, and only when the release lands on the same action, by Id,
    that the press began on — a push is destructive and a click begun by accident must be takeable
    back. A Changed from the provider cancels a held press, so a re-diff cannot slide a different
    block's identically-named action under the button.
  • ChangeAction.Id now carries a written contract: it identifies the action and its target.
    Run collapsing and the press/release match both depend on it.
  • A block taller than the viewport keeps its button (drawn at the first visible row of each run);
    the column reserves the width the provider declares (MaxActionsPerLine, clamped 1..4); only
    buttons that were actually drawn are hit-test targets.

Highlighting

  • Tokenization runs on a time budget off the render pass, per line, with equatable end-states,
    so a large file stays interactive while it colours in.
  • A line that exceeds the budget is retried a bounded number of times
    (MaxTimeoutRetries) and then left un-tokenized rather than re-attempted forever. Those attempts
    belong to the LINE and travel with it over an edit that inserts or deletes lines above it, so a
    line never inherits another's exhausted budget (which left it plain for good) and a pathological
    one never gets a fresh budget out of a paste somewhere else.

Also in this release

The rest of the 12.0.0 surface, unchanged in shape since it was built: canvas glyph-grid rendering
on a fixed character cell; the document model with undo; IEditorMargin composition with
LineNumberMargin; TextMate grammars and VS Code-style JSON themes, both drop-in
(AvaCodeEditor.Highlighting); light/dark theming that tracks ActualThemeVariant and
never re-tokenizes on a variant switch.

Requires Avalonia 12 and .NET 10. The package major tracks the Avalonia major.