This editor source contains a reusable MoonBit readonly viewer and an editor-owned reference host/backend:
vieweris the js-only public editor facade and browser surface.internal/shellis the demo, development, and end-to-end host. It is not an external import surface.- OpenSeek's
desktop/frontend/fileeditoris the primary downstream, user-facing host in the surrounding monorepo. It consumes the public Viewer surface and owns its product-specific composition.
For exact APIs use the owning package's pkg.generated.mbti; for exact
dependencies use moon.pkg.
Monaco/VS Code is the primary behavioral reference and a source of tested
algorithmic and ownership patterns. It is not a required type, class, package,
or inheritance template: local representation follows MoonBit capabilities,
lifetime boundaries, and dependency direction. CodeMirror is secondary.
vscode/ and codemirror/ are pinned research trees only; product code never
imports them and public names remain MoonBit-owned.
flowchart TB
A[host document] --> B[TextModel<br>common/model]
B --> C[ViewModel<br>common/view_model]
C --> D[ViewLayout<br>common/view_layout]
D --> E[BrowserPresentation<br>closed family]
E --> F[Code: View<br>browser/view]
E --> G[Markdown: document view<br>+ hover and definition bridges]
F --> H[DOM]
G --> H
The stages after the host document live in viewer/common/model,
viewer/common/view_model, and viewer/common/view_layout. The Viewer-owned
closed BrowserPresentation family then selects
internal/viewer/browser/view.View for Code or
internal/viewer/browser/markdown_document.MarkdownDocumentView plus the
presentation-local hover and definition bridges for Markdown.
Viewer keeps cross-domain ordering at the root and delegates private state to
five concrete owners: EditorConfigurationState, ViewerModelSlot,
ViewerMount, EditorContributions, and CursorEventDelivery.
ViewerModelSlot.current is the one nullable ModelData; its optional closed
BrowserPresentation makes active-root dispatch explicit. It contains exactly
Code(CodeBrowserData) and Markdown(MarkdownBrowserData); root lookup, focus,
theme, layout, scroll, reveal, and lifecycle dispatch exhaustively over that
family instead of assuming a hidden Code View. The Code payload pairs a real
View with its retained mouse handler and view-scoped render/reveal facts. The
Markdown payload pairs a contribution-free document view with
presentation-local hover and definition bridges over the original model.
Public construction seeds
EditorConfigurationState synchronously from the host client box before model
attachment. Each ModelData then carries one generation-scoped initialization
boundary: the host invokes Viewer::handle_initialized after model, view-state,
and option setup, publishing stable visible-token demand outside attach_model
and independently of the animation-frame render loop. Repeated calls
re-stabilize the current model's demand instead of consuming one-shot state.
Presentation selection belongs to the root Viewer. An exact lowercase URI-path
.md suffix, including .mbt.md, or the exact markdown language id selects
Markdown; every other model selects Code. URI matching excludes query and
fragment text and remains case-sensitive. Shells and embedders always supply an
ordinary caller-owned TextModel; they do not parse Markdown or select a
presentation. Both variants retain that original model and ViewModel as
source truth.
Headless means ViewerMount::Headless: there is no caller host, placeholder,
browser presentation, DOM focus state, or root animation frame. A headless
Viewer may still have a model and ViewModel; ModelData.presentation_kind
retains the same automatic selection while ModelData.browser=None. On
supported construction paths, a mounted model always has
Some(BrowserPresentation). ViewerMount installs and removes the active
presentation root without knowing its concrete payload. This is distinct from
a mounted Viewer with no model, which owns and paints one complete placeholder
pair. Mounting is one-way; disposal removes Viewer-owned DOM and clears mounted
placeholder/frame/focus state, but retains the caller's host for container
lookup.
The host owns files, transport, persistence, reload policy, shell chrome, and error presentation. The viewer owns readonly rendering, selection, scrolling, widgets, language-feature presentation, and editor events.
Dependencies point strictly downward through the tiers; no lower tier imports an upper one:
direction: right
product_host: OpenSeek desktop file editor
reference_host: editor reference composition {
grid-columns: 1
workbench: internal/shell/workbench
backend: moonbitlang/editor-server
}
facade: viewer facade
inner: internal/viewer {
grid-columns: 1
view: browser/view
contrib: contrib/*
markdown: markdown
}
common: viewer/common {
grid-columns: 1
model: model
view_model: view_model
view_layout: view_layout
}
foundations: shared foundations {
grid-columns: 1
base: base/*
language: language
syntax: syntax
log: platform/log
}
product_host -> facade
reference_host -> facade
facade -> inner
inner -> common
common -> foundationsbase/common: URI/path, positions/ranges, events, and disposables.base/browser: canonical browser runtime and DOM primitives (including untransformedclientWidth/clientHeightreads), mouse events, global pointer-move monitoring, and the one realm-global animation-frame coordinator. Its strict-next and current-or-next queues share one native rAF, sort by descending priority with stable FIFO ties, and return independently cancellable registrations.language: backend-neutral diagnostic, hover, location, and symbol provider contracts.syntaxandsyntax/lang_*: stateful line-tokenization contracts and concrete compile-time lexers.syntax/examples/*contains opt-in reference implementations for examples and test-mode consumers.platform/log: host-neutral logging.
viewer/common/** is DOM-free and multi-target:
model: immutable text snapshots, model identity, decorations, and the complete model-owned tokenization cluster. Its shared live model state, stores, queues, backends, attached-view aggregation, and scheduling machinery are package-private.servicesandtokens: language-id encoding and compact line tokens.editor_api: the single owner of public cursor/model/scroll event values and editor-option enums shared by root, cursor, layout, view-model, and browser consumers.agent_feedback_api,navigation_api, andquick_diff_api: host-facing DTO/callback handles. Navigation exposes only location-opening intent and caller-owned target-model leases. Current/Side opening, resource loading, and model lifetime remain host policy; concrete feature implementations remain contributions below their callers.languagesandmarkers: runtime provider registration and diagnostics-to-decoration flow. Definition and References providers share selector scoring, newer-first ties, ordered aggregation, failure isolation, and cancellation. Their opaqueLanguageHandle,MarkerServiceHandle, andMarkerDecorationsHandleexpose only the reviewed Viewer capability floor while hosts retain the concrete registries/stores. The marker-decoration handle can resolve the live, exact-model occurrences used by browser presentations; Markdown does not create a second marker store or decorate a synthetic model.core,cursor,config: coordinates, selection/cursor state, and editor configuration values.view_model: injected text, wrapping/folding projection, model/view conversion, and concrete inline/model decoration resolution.view_layout: scrolling, viewport, zones, and view-line rendering data.diff: line diff contracts used by quick diff.- the root
viewer/commonpackage is a small compatibility surface for line HTML helpers.
Common code never imports browser or contribution packages.
The animation-frame coordinator is one process value for the supported
single-JavaScript-realm product. Monaco's explicit per-window maps and its
cross-editor phased prepare/render batching are not local contracts. Common
ViewModel/layout code stays browser-free and receives scheduling only through
root injection; base/browser never imports upward into Viewer packages.
The root viewer facade and public viewer/browser contract package are
js-only. Concrete browser runtime packages live below the module-private
internal/viewer/** boundary:
vieweris the opaque public facade, the MonacoCodeEditorWidgetandeditor.api.tsrole. PublicViewerconstruction is onlyViewer::create; precomputed reference locations enter only throughViewer::show_references, while the provider-backed References contribution consumes only the opaqueLanguageHandle;ViewerOptions,ViewerServices, andViewerViewStateexpose no public layout. Root generated interfaces may reference language/common values, browser contracts, and common capability handles, never private view or contribution implementations.vieweralso exposes the independent opaqueUnifiedDiffViewfacade.UnifiedDiffView::createborrows one stable caller-owned host and owns only its installed subtree. Hosts may supply an optional typed line-comment callback; omitting it keeps the surface read-only.clearretains the host binding and idempotentdisposeremoves the subtree without removing the host.set_diffaccepts caller-owned strings, bounds their combined UTF-16 size before snapshot allocation and their logical line count before diff computation, then routes through the replaceableviewer/common/unified_diffseam. Its concrete DOM renderer remains underinternal/viewer/browser/unified_diff, while the public stylesheet source isviewer/browser/unified_diff/unified_diff.cssand is assembled intoviewer.cssfor hosts to ship and size.viewer/browserowns canonical editor mouse events, target kinds, DOM coordinates, the mutable live ViewZone descriptor/opaque accessor contract, and the opaque unmanaged overlay-widget handle.internal/viewer/browser/testingis a JS-only Viewer-id-keyed workbench/browser-test registry. Root publishes callback-backed build/render/hover facts downward plus a disabled-by-default scroll state/render-phase trace. Observation records are constructed only while the matching Viewer has a listener; the package never imports root Viewer and is not an external API.internal/viewer/browser/markdown_documentowns the focusable Markdown root, native scroll viewport, replaceable article, retained overlay mount, and the same-parse source projection. Only compiler-recognized fenced rows receive semantic source boundaries; cross-line tokenization state is preserved, and decoded-text or row-cardinality mismatches fail closed. The package owns no model, provider, marker store, or request policy. It also owns semantic DOM caret-to-source mapping, exact projected-range span construction, and one shared diagram-viewport lifetime for its replaceable article; root and the hover browser package own the higher-level feature contracts.internal/viewer/markdownis the multi-target safe-cmark boundary. It owns plaintext fallback, a cmark-independent code-block value, conversion facts, the shared editor-token HTML override, and the private synchronous adapter that renders exact lowercasediagofences throughMilky2018/diagowhile returning failures and every other code block to the existing fallback;internal/viewer/browser/markdownadds MoonBit-owned DOM retention, URI/media policy, activation listeners, size notification, per-target disposal, and the reusable pan/zoom/fit/resize diagram viewport controller. Its emitted diagram-control stylesheet remains atinternal/viewer/browser/markdown/diagram_viewport.css. Narrow JS bindings provide inert-template parsing, browser URL resolution, realm registries, dynamic import, and Mermaid API calls. Its diagram-wheel listener rechecks wrapper classes for every event:moonbit-viewer-markdown-diagram-viewportdeclines the generic native-scroll handoff so ordinary wheel input reaches the editor, while unmarked hover and agent-feedback diagrams retain their existing inner-scroll behavior. Its opt-in Mermaid lifetime lazily imports the pinned official module from the build-staged same-originmermaid/tree. Its realm-wide MoonBit runtime caches the API, serializes global configuration with rendering, and rejects stale asynchronous DOM commits through per-diagram epochs and target ownership. Successful full-document and Markdown-comment Mermaid commits refresh the same pan/zoom/fit/resize viewport owner used by synchronous D2/Diago output, including after theme replacement. The full Markdown document and root Markdown-comment contribution emit the exact lowercasemermaidmarker and enable this lifetime; hover and agent feedback remain ordinary tokenized code consumers. Browser contributions consume these packages instead of owning private Markdown-to-innerHTMLpipelines.internal/viewer/browser/configmeasures fonts and browser geometry.internal/viewer/browser/controllerowns hit testing, mouse selection, drag scrolling, and scrollbar input. Touch inertia uses the shared strict-next frame coordinator; its disposable remains owned by the per-model handler.internal/viewer/browser/viewis one package owning the DOM tree, render/event ordering, recycler, content/overlay widgets, current-line and model decorations, margin, selections, cursor, virtualized lines, zones, and the scrollbar view part. Its focused.mbtfiles preserve source-unit responsibilities; they are not package or namespace boundaries.viewer/browser/view_parts/*contains CSS assets at their stable build paths. These asset-only directories have no MoonBit package or executable ownership.internal/viewer/ui/scrollbarowns custom scrollbar DOM and pointer behavior; its emitted stylesheet remains atviewer/ui/scrollbar/scrollable_element.css, and its arithmetic lives inviewer/common/view_layout.
internal/viewer/browser/view owns closed event/render handle enums, rendering
contexts, concrete view parts, and their lifecycle wiring. Files in that
directory share private declarations as one compilation unit. Root
viewer/*.mbt contains the Viewer:: composition glue: the internal browser
packages are lower-level dependencies and cannot import the root facade
without reversing the dependency and creating a package cycle.
Concrete contributions live below internal/viewer/contrib/**. They depend on
editor common/browser layers; editor common never depends on them.
internal/viewer/contrib/hoveris the DOM-free state/computation layer;internal/viewer/contrib/hover/browserowns its widget and browser controller. Its single-owner row renderer is shared by Code hover and the Markdown document bridge without sharing widget state. The Markdown bridge resolves real DOM carets through retained UTF-16 source boundaries, queries providers against the original model, and projects exact-model marker occurrences into the semantic rows. Its freshness stamp covers request, model, content, URI/revision, attach, projection, block, source-offset, and cancellation identities before any DOM commit. Projected diagnostics reuse resolved class, range, and z-index policy. Thesquiggly-inline-unnecessaryopacity andsquiggly-inline-deprecatedstrike-through effects are explicitly deferred because they mutate source glyphs; the overlay does not approximate them. The emitted hover stylesheet remains atviewer/contrib/hover/hover.css.internal/viewer/contrib/definitionowns DOM-free definition-result normalization, token fingerprints, and Ctrl/Cmd-link/request state.internal/viewer/contrib/definition/browserowns only Definition's non-destructive-message DOM shell. Root Definition composition owns provider requests, cancellation, Definition-specific no-result/open-rejection feedback, and Code decorations or projected Markdown link spans. It populates the shared References controller after an Alt+F12 result set or multiple ordinary results are already known. The Markdown adapter resolves native pointer geometry through the document-owned source map and never creates a virtual model. The emitted stylesheet remains atviewer/contrib/definition/browser/definition.css.internal/viewer/contrib/referencesowns the DOM-free grouped result, snippet, navigation, mode, phase, and exact source-session values shared by Definition and References Peek. Its browser sibling owns only the detached mode-labelled shell and feature-local ARIA tree. The tree is native content inside the sharedinternal/viewer/ui/scrollbarDOM/input lifetime, while retaining tree focus and keyboard ownership. Rootviewerowns the one shared per-Viewer controller, the Shift+F12/context-menu provider action, provider-query cancellation and freshness,Viewer::show_references, per-group lazy cancellation/reference slots, the selected nested Viewer/reference and decorations, presentation mounts, Current/Side opener dispatch, and atomic teardown. Browser callbacks carry typed result identities upward; neither internal package imports the root facade or resolves a model. The shared Peek/tree stylesheet remains atviewer/contrib/references/browser/references.css.internal/viewer/contrib/contextmenu/browserowns the reusable detached HTML menu shell, focus return, temporary document/window listeners, submenu timers, ARIA state, and viewport fitting. Rootviewerowns the per-Viewer contribution, live command/menu resolution, target filtering, cursor/selection policy, and semantic Markdown source anchor. Code text or empty-content hits and valid semantic Markdown rows use the custom menu; injected text, editor widgets, margins, scrollbars, ordinary Markdown, and unavailable command sets retain the browser-native menu. The emitted stylesheet remains atviewer/contrib/contextmenu/browser/contextmenu.css.internal/viewer/contrib/agent_feedbackowns concrete feedback storage/service projection; host DTOs and the callback handle live inviewer/common/agent_feedback_api, whileinternal/viewer/contrib/agent_feedback/browserowns input and bubble widgets. Its emitted stylesheet remains atviewer/contrib/agent_feedback/browser/agent_feedback.css.internal/viewer/contrib/quick_diff/commonowns baseline state and diff contracts; its public host handle lives inviewer/common/quick_diff_api, andinternal/viewer/contrib/quick_diff/browserproduces gutter decorations. Its emitted stylesheet remains atviewer/contrib/quick_diff/browser/quick_diff.css.internal/viewer/contrib/folding/browsercurrently owns the complete js-only folding implementation, including ranges, hidden areas, decorations, controller, and the language-neutral outline-body range presentation used by explicit host outlines. The root Viewer computes MoonBit function-body ranges without importing a concrete syntax package; ordinary provider/user folding remains the Monaco behavior port. Its emitted stylesheet remains atviewer/contrib/folding/browser/folding.css.internal/viewer/contrib/markdown_commentsowns multi-target whole-line comment detection, provider/configuration resolution, and normalized block ranges. Its browser sibling owns the stable ViewZone DOM pair and coalesced visible/offscreen size observer. The root contribution owns one instance of the shared Markdown diagram-viewport group per rendered target and disposes it before renderer replacement. Inline viewport-height changes report only through a narrow callback into the existing size observer. The rootviewercontribution owns reconciliation, renderer and viewport lifetimes, exact model-source extraction, zone ids, and the one hidden-area source. Its emitted stylesheet remains atviewer/contrib/markdown_comments/browser/markdown_comments.css.
The content ViewZones container is presentation-only but not globally hidden
from accessibility. Registration applies aria-hidden=true to each caller
node by default; the Markdown contribution removes it from its own zone after
registration so links and the source/documentation-fold controls are discoverable,
while unrelated ViewZones and the margin container retain their hidden default.
Removal restores the caller node's original aria-hidden presence and value.
The root editor registry has two distinct ownership layers. Its process-wide
contribution-description table contains constructors only; the adjacent
command, keybinding, and closed menu-placement tables likewise contain no
per-Viewer state.
Viewer.contributions is an EditorContributions owner whose instances map
is the sole lookup table for that Viewer's eight concrete contribution entries;
feature packages do not keep second maps keyed by editor id. Root Viewer::
helpers recover typed controllers by matching both the fixed id and central
entry variant, mirroring Monaco's typed view over
CodeEditorContributions._instances.
Contribution construction is two-phase: reject a duplicate id before side
effects, construct the bare controller, insert the actual entry, then install
listeners and run initial synchronization. The content-hover entry's
ContentHoverContributionState owns its controller, lazy widget and logical
widget view, and timeout/async launch policy across model swaps. During
disposal, the complete map remains installed while entry behavior tears down;
the owner then becomes non-lookuppable, and retained hover browser resources
are released at their later root cleanup slot before the map is cleared.
Declared instantiation modes are retained for source parity, but all modes
currently instantiate eagerly.
The Markdown hover bridge is not a central Code contribution. It is
owned by MarkdownBrowserData, is activated only after the corresponding
ModelData becomes current, and is cancelled before any source/theme
projection replacement. Detach retires the bridge and renderer while the root
is still mounted, removes the now-inert presentation root, and only then
releases model listeners, the attached-view handle, and the ViewModel.
The Markdown-comment entry owns Viewer-lifetime model/content subscriptions and
a model-scoped stable-key map. A mounted Viewer replaces normalized comment
lines only in the view projection; headless Viewers keep source visible because
they have no replacement DOM. Model detach disposes renderers and size
observers, removes zones, and clears the contribution's hidden source before
the outgoing browser View is destroyed. Same-model flushes rebuild that
source with force_update=true because the projected line collection was
recreated even when normalized ranges did not change. Viewer theme changes
rerender retained Mermaid SVGs in place and send accepted size changes through
the existing observer and generation-checked ViewZone height writer.
Each rendered entry retains the full and optional preview shared-Markdown
renderers, an initially empty source target that extracts and tokenizes exact
model lines only while source is selected, its source and expanded-state toggle lifetimes, its diagram-viewport
group, and the existing size observer. The source toggle is per block, keeps
the model projection unchanged, survives same-range content replacement, and
restores the block's prior documentation-fold state when rendering resumes.
Its stable accessible name and pressed state describe the selected
presentation; rendered content reserves the control's trailing hit area, and
the control remains bounded by even a one-line separator ViewZone.
Newly mounted item-delimited multi-line
API documentation from a foldable provider registration starts on the
first-line preview; ordinary Markdown and
separator-only or one-line API blocks remain expanded without a control.
The fold state has two affordances sharing one listener lifetime: a
mouse-only chevron on the ViewZone's margin node, aligned with the
code-folding column inside the aria-hidden gutter, and an accessible
in-content button revealed by hover or keyboard focus.
Same-key body replacement preserves the user's expanded state while disposing
the old toggle and viewport group before the renderers replace their targets
and mount fresh browser lifetimes. Entry
teardown orders toggle, viewport, renderers, and observer disposal before zone
removal. The only diagram-to-ViewZone invalidation chain runs from the
viewport-height callback through
MarkdownCommentSizeObserver::request_measure to
Viewer::apply_markdown_comment_height; the final step keeps the existing
generation and zone-id checks and remains the sole writer of the live ViewZone
height.
Feature-local instance tables must not duplicate this central ownership.
OpenSeek and internal/shell intentionally serve different roles. OpenSeek's
desktop file editor is the primary real downstream host used by end users. It
owns product services, document lifecycle, host RPC, surrounding UI, and
packaging under desktop/**. The reference shell is the faster editor-owned
host for UI preview, browser scenarios, and end-to-end development; it is not a
model of OpenSeek's product chrome.
Both hosts consume the public viewer and viewer/common/** boundaries. This
gives editor UI work a two-stage delivery path:
- Develop and visually verify Viewer-owned UI in
internal/shell. A change to existing UI should normally flow into OpenSeek through the established public surface with no or only small host changes. Requiring a large adapter rewrite for such a change is evidence that the host boundary should be inspected. - Unless explicitly scoped to editor-only exploration, treat new user-facing
Viewer UI as intended for OpenSeek. Add any required generic service, event,
model-lifetime, or capability contract to the public Viewer surface,
exercise it in the reference shell, and implement the corresponding
desktop/frontend/fileeditoradapter in the same feature task. Known downstream wiring is part of the feature, not an unspecified follow-up.
OpenSeek-specific RPC, storage, workspace, and product-shell behavior stays in
OpenSeek. OpenSeek must not import internal/shell/**; the slower full desktop
build and preview is a downstream integration tool, not the editor's routine UI
iteration loop.
The direct embedding proof is:
internal/shell/examples/embedded_viewer
-> viewer
-> viewer/common/{model,languages,editor_api,capability APIs}
The full reference app is:
internal/shell/web
-> internal/shell/workbench
-> viewer
-> internal/shell/widgets/file_tree
-> shell/remote_protocol
server/host/main (module moonbitlang/editor-server)
-> server/host
-> server/server
-> shell/{remote_protocol,workspace}
-> language + viewer/common/model
The host server is its own moon.work member. It supports Wasm and native, with
preferred_target = "wasm"; the editor justfile likewise defaults to
TARGET=wasm, while TARGET=native is an explicit opt-in. Consequently,
unqualified server commands such as moon run server/host/main and
moon test server/... select Wasm. shell/workspace and
shell/remote_protocol are the shared
host-shell contracts both the browser workbench and the server module import;
they live outside internal/ because workspace members cannot import another
module's internal packages. They ship in the published package: once
moonbitlang/editor-server is consumed from the registry rather than from
this workspace, it resolves these two packages through moonbitlang/editor,
so excluding them would leave it unbuildable.
workspacedefines host-side source paths, document snapshots, filesystem, and tree-provider contracts. ItsDocumentSnapshotis not the editor model.remote_protocolis the reference app's versioned WebSocket JSON protocol, not LSP.servervalidates/caches documents and routes file and language requests. Each protocol connection owns one session-local document cache and watch map; host/provider services and document-sync delivery remain server-scoped.server/hostprovides filesystem/watch/HTTP/WebSocket effects and the currentmoon ide hover,moon ide find-references, andmoon checkbackend. It also owns host-directory browsing and workspace switching. With no explicit browse root, that capability is unrestricted only for loopback listeners and disabled for non-loopback listeners;--browse-rootgrants a realpath-confined subtree on either bind.workbenchadapts protocol payloads intoTextModel, Definition, References, and Hover providers, markers, the file tree, theme, and harness events. Its private MoonBit Markdown-comment provider adapts exact///|item anchors and their following///documentation into the Viewer's language-neutral provider contract. It installs every document through the sameViewer::set_modelpath;.mdand.mbt.mdrequire no shell-side presentation branch. Remote hover and diagnostics are accepted only for the exact current model identity, version, URI, revision, and content generation, so the Markdown bridge receives the same original-model freshness guarantees as Code.
moon check --target all enforces package cycles and target compatibility.
The remaining rules are intentionally kept visible in moon.pkg, generated
interfaces, and code review instead of duplicated in an architecture-lint
script:
- Product code does not import
vscode/orcodemirror/. - Reusable packages do not import the
internal/shell/**reference host. - The OpenSeek product host consumes public Viewer packages and does not import
internal/shell/**. viewer/common/**and DOM-free internal contribution packages stay multi-target; publicviewer/browser, internal browser runtime and contribution-browser packages, and rootviewerstay js-only.- Viewer packages may use
rabbita/domandrabbita/js, not Rabbita's TEA framework packages. The reference shell owns the app framework. - Browser helper packages do not import the parent
viewerfacade. viewer/common/**does not importinternal/viewer/**.- The root
viewerpackage is the external browser facade; embedders use it and publicviewer/common/**contracts rather than private browser/contribution implementation packages. The embedded example keeps this surface compiling. - Concrete
syntax/lang_*packages are selected by hosts, examples, or tests, not by the reusable viewer core.
Position,Range, andLineRangeuse 1-based UTF-16 line/column values.OffsetRangeis a 0-based half-open UTF-16 span.TextSnapshotowns offset/position conversion.- Markdown DOM projection boundaries and remote hover wire offsets are 0-based UTF-16 offsets. Provider positions and returned ranges remain 1-based and always refer to the original model.
- Keep conversions at model/view, protocol, and DOM boundaries; do not pass an unlabelled integer between coordinate spaces.
Hover is a cross-package query over the original TextModel. The DOM neither
stores a filesystem path nor chooses a language provider or CLI command. Its
only job is to identify a rendered character boundary. Presentation-specific
code maps that boundary back to the original model; the reference host later
maps the model URI to a server-side workspace path.
flowchart LR
subgraph PM["Presentation-specific pointer mapping"]
PTR["Pointer coordinates"] --> PRES{"Active presentation"}
PRES -->|Code| CODE["Browser caret hit<br>+ retained CharacterMapping"]
PRES -->|Markdown| MD["Browser caret hit<br>+ retained semantic source boundaries"]
CODE --> VP["1-based view Position"]
VP --> CONV["CoordinatesConverter"]
CONV --> MP["1-based model Position"]
MD --> MP
end
MP --> OFF["0-based TextModel UTF-16 offset"]
OFF --> REG["Languages provider registry"]
REG --> RLP["readonly-remote HoverProvider"]
RLP --> WIRE["Wire request<br>URI + revision + UTF-16 offset"]
WIRE --> SRV["Server cached TextModel"]
SRV --> LOC["Safe relative path<br>+ 1-based line:column"]
LOC --> CLI["moon ide hover --loc ... --output-json"]
CLI --> RESULT["Hover contents + original-model range"]
RESULT --> FRESH["Freshness gates"]
FRESH --> UI["Hover widget + range decoration"]
There are two presentation-specific entrances and one shared semantic path:
- Code uses the browser caret API and the
CharacterMappingretained by each renderedViewLine. It first produces a view position, then converts that position through theViewModelprojection. - Markdown uses UTF-16 source boundaries retained on semantic rendered
rows. Its presentation-local bridge maps the DOM caret directly into the
original model. It does not manufacture a Markdown-only model and does not
pass through Code's
ViewLinemapping. - Both paths query the same
ContentHoverComputer,Languagesregistry, and providers against the original caller-ownedTextModel.
Every conversion changes either the coordinate owner, the origin, or both.
The names below are therefore semantic types, even where the runtime value is
represented by an ordinary integer or Position.
| Space | Shape and unit | Owner | Conversion boundary |
|---|---|---|---|
| Pointer geometry | client/page/editor-relative CSS pixels | browser controller | Browser event coordinates are normalized before hit testing. |
| DOM caret | token span plus a normalized 0-based UTF-16 text offset | active browser presentation | The browser caret API answers a DOM boundary, not a source location. |
| View position | 1-based UTF-16 Position in rendered view lines |
Code View/ViewModel |
CharacterMapping maps the token span and DOM offset to a view column. Markdown does not use this space. |
| Model position | 1-based UTF-16 Position in the original source model |
TextModel |
CoordinatesConverter removes wrapping, folding, and injected-text projection. |
| Model offset | 0-based UTF-16 offset from the start of the original document | TextSnapshot |
TextModel::get_offset_at and get_position_at are the only position/offset authorities. |
| Remote document position | URI, document revision, and 0-based UTF-16 offset | shell/remote_protocol |
The browser sends document identity, not a trusted filesystem path. |
| Native CLI location | safe root-relative path plus 1-based UTF-16 line and column | server/host |
The server resolves the URI and reconstructs the position from its revision-matched model. |
| Returned hover range | 1-based UTF-16 Range in the original model |
language.Hover |
Presentation code projects it back to visible rows only for painting. |
The UTF-16 choice is deliberate: MoonBit String, browser text-node offsets,
editor positions, snapshots, and the remote protocol must agree even when a
source line contains non-BMP characters represented by surrogate pairs. A
Unicode character can therefore occupy two columns/offset units. No stage may
silently reinterpret these values as UTF-8 byte offsets or Unicode scalar
indices.
For Code, the pointer controller performs the following steps:
EditorMouseEventFactoryrecords page, client, and editor-relative pointer coordinates.MouseHandlerasksMouseTargetFactoryto classify the hit.- For ordinary text, the factory calls the available browser caret API
(
caretRangeFromPoint, with the browser-specific caret-position path as a fallback). The result is normalized to a token span and a UTF-16 offset in that span. Margin, widget, scrollbar, empty-content, and outside-editor hits follow separate target branches and cannot accidentally become text hover. ViewLineswalks from the token span to its retained.view-line, finds the corresponding rendered view line, and asks that line'sCharacterMappingfor the view column. The mapping was produced together with the token-span HTML and retained with theViewLine; it is not reconstructed from CSS geometry or token text after the event.- The resulting
MouseTargetis still in view coordinates. At the root Viewer event boundary,convert_view_to_model_mouse_targetrebuilds every position-bearing target arm through the activeCoordinatesConverter. This is where wrapped view rows, folded source lines, and injected text collapse back to a position in the original model. - Hover anchor discovery accepts content-text hits, plus the narrow near-end-of-line empty-content case, and creates a collapsed model-space range at the pointer position. The anchor is the query position; it is not yet the semantic token range eventually returned by the provider.
The critical invariant is that a public editor mouse target and a hover anchor are already in model space. Code above the Viewer boundary must never treat them as view coordinates or apply the conversion a second time.
Markdown is rendered as a semantic document rather than as Code ViewLine
rows, so reusing Code hit testing would invent the wrong coordinate space. A
successful cmark projection instead retains exact UTF-16 source boundaries for
source-bearing semantic rows. The Markdown hover bridge:
- accepts a real DOM caret over a source-bearing row;
- resolves the caret within the rendered text tree;
- maps that boundary through the retained projection to the original model position and 0-based model offset; and
- launches the same hover computer against the original
TextModel.
Ordinary prose, synthetic indentation, mismatched projections, and unsafe DOM zones fail closed rather than guessing a source location. The bridge is owned by the active Markdown presentation and is cancelled before source, theme, or projection replacement.
MarkdownHoverParticipant queries providers at the start of the model-space
anchor range. Here, "Markdown" describes the hover content format, not the
Markdown document presentation: the same participant is used after either
presentation has found a model anchor. The Languages query surface is
offset-addressed, so it first calls TextModel::get_offset_at. The registry
then calls TextModel::get_position_at before invoking the provider because
the two contracts intentionally differ:
- the registry/query boundary carries a document offset; and
language.HoverProviderreceives a model plus a modelPosition.
The apparent offset-to-position round trip is therefore a checked boundary,
not an assumption that the two integers are interchangeable. Languages
snapshots providers whose language/scheme/path selector matches the model,
preserves registry priority, forwards cancellation, and returns the first
non-empty live hover result.
The reference workbench registers RemoteLanguageClient for the
readonly-remote URI scheme. Consequently the model URI selects the remote
provider; neither the DOM class name nor the rendered token type selects it.
An embedder may register a different provider for another language or URI
scheme without changing pointer mapping or hover rendering.
The reference host crosses the browser/native boundary using a revision-bound UTF-16 offset rather than a line/column pair or client-supplied disk path. The sequence below expands the Code entrance. Markdown replaces the initial DOM-to-anchor messages with its retained semantic projection and joins the same sequence at the hover computer.
sequenceDiagram
participant D as Browser DOM
participant C as Pointer controller
participant V as View and ViewModel
participant H as Hover computer
participant L as Languages registry
participant W as RemoteLanguageClient
participant S as RemoteServer
participant N as MoonWorkspaceLanguageProvider
participant M as moon CLI
D->>C: mousemove(clientX, clientY)
C->>D: caret hit test at pointer
D-->>C: token span + DOM text offset
C->>V: resolve rendered character boundary
V-->>C: view Position
C->>V: convert view target to model target
V-->>H: model-space range anchor
H->>L: hover_at(model, UTF-16 offset)
L->>W: provide_hover(model, model Position)
W->>S: Hover(uri, revision, UTF-16 offset)
S->>S: require matching cached document
S->>S: rebuild TextModel and offset -> Position
S->>N: provide_hover(model, Position)
N->>N: URI -> safe path; capture workspace root
N->>N: require disk snapshot == model snapshot
N->>M: ide hover --loc path:line:column --output-json
M-->>N: JSON hover or no-hover result
N->>N: recheck disk and model snapshots
N-->>S: Hover(contents, original-model range)
S->>S: require cached document still current
S-->>W: HoverResult(uri, revision, hover)
W->>W: require captured client freshness stamp
W-->>L: accepted Hover
L-->>H: first non-empty live result
H-->>D: render rows and paint projected range
RemoteLanguageClient captures the current model freshness stamp, converts
the provider position back to the model's UTF-16 offset, and sends:
Hover {
id,
uri,
document_revision,
offset
}
The reference workbench sends the model's non-empty revision. The server
requires that revision and URI to match its session-local cached document. It
rebuilds a TextModel from that exact cached snapshot and obtains the provider
position with get_position_at(offset). This prevents the client and server
from independently calculating line/column against different text. The server
API permits an empty expected revision for other callers, but the reference
hover path does not use that compatibility case.
MoonWorkspaceLanguageProvider then performs the host-only conversion:
-
resolve a URI such as
readonly-remote://workspace/src/main.mbtthrough the server's containment policy to the safe root-relative pathsrc/main.mbt; -
capture the current workspace root, which becomes the process working directory;
-
require the disk signature and normalized text to match the request model;
-
construct
src/main.mbt:<line>:<column>from the resolved path and the server-reconstructed, 1-based model position; and -
run:
moon ide hover --loc src/main.mbt:<line>:<column> --output-json
The browser is never trusted to supply src/main.mbt, an absolute path, or the
workspace root. URI containment and filesystem ownership remain native-server
responsibilities.
The CLI adapter parses Markdown/plain hover contents plus its optional source
range. If a valid payload omits a range, the adapter supplies a one-column
range at the requested model position. The range always belongs to the
original TextModel; Code projects it through the ViewModel for decoration
painting, while Markdown projects it into semantic row fragments.
Hover is asynchronous, so correctness requires more than translating the initial pointer accurately. A result is displayed only while all applicable guards still hold:
- the hover operation and cancellation token are current;
- the active model identity, version, URI, revision, and content generation still match the request captured by the workbench;
- the server session still caches the same document revision and normalized text;
- the native disk snapshot matches the model before the CLI call and remains unchanged after it; and
- for Markdown, the attach, projection, block, source-offset, and presentation generations still identify the same rendered source boundary.
A failed hit test, unsupported presentation location, unmatched provider, cancelled request, stale generation, URI containment failure, disk/model mismatch, nonzero CLI exit, or empty hover is an ordinary no-hover result. No layer should compensate by guessing a nearby source position or displaying a result produced for older text.
| Stage | Owning package | Architectural responsibility |
|---|---|---|
| Code DOM classification and caret hit testing | internal/viewer/browser/controller |
Turns pointer geometry into a typed view-space MouseTarget. |
| Retained Code DOM/source rendering map | internal/viewer/browser/view |
Owns ViewLine DOM and its renderer-produced CharacterMapping. |
| Model/view projection | viewer/common/view_model |
Converts wrapping, folding, and injected-text view coordinates to original-model coordinates. |
| Markdown caret/source projection | internal/viewer/contrib/hover/browser |
Maps semantic Markdown DOM boundaries directly to the original model. |
| Hover computation and anchoring | internal/viewer/contrib/hover |
Owns hover anchors, participant scheduling, cancellation, and normalized results. |
| Provider selection | viewer/common/languages and language |
Owns selector matching, provider lifetime, and backend-neutral hover values. |
| Browser protocol adapter | internal/shell/workbench |
Registers the remote provider and guards client-side model freshness. |
| Wire coordinate contract | shell/remote_protocol |
Carries URI, revision, and 0-based UTF-16 offset; it is not LSP. |
| Server cache and URI containment | server/server |
Reconstructs the revision-matched model position and rejects unsafe paths. |
| Native Moon CLI adapter | server/host |
Owns workspace root, disk coherence checks, --loc construction, process execution, and output parsing. |
As an illustrative wrapped-line example, a caret at DOM offset 3 may map to
view position 18:4, then through the projection to original-model position
12:47, and finally to document offset 642. The wire request carries the
model URI, its revision, and 642; the server reconstructs 12:47 against the
same revision, resolves the URI to src/main.mbt, and runs the CLI at
src/main.mbt:12:47. The numbers are illustrative, but the ownership and
conversion order are mandatory.