#language

    Backend-neutral contracts for readonly language features. This package is the shape of a language backend, not an implementation and not a registry: it has no moon ide adapter, no LSP client, and no provider table.

    direction: right host: host / backend { grid-columns: 1 a: moon ide, LSP, in-memory... } lang: language (this package) { grid-columns: 1 b: provider traits c: result DTOs d: CancellationToken e: LanguageSelector } reg: viewer/common/languages { grid-columns: 1 f: runtime registration } mk: viewer/common/markers { grid-columns: 1 g: Diagnostic -> decorations } host -> lang: implements traits lang -> reg: registered by hosts lang -> mk: Diagnostic is a shared shape only

    #Surface and behavior

    • Result DTOs: Hover/HoverContent, Diagnostic, Location, DocumentSymbol, and MarkdownCommentBlock. Their Position, Range, and half-open LineRange values use the repository's 1-based UTF-16 convention.
    • Async provider traits: HoverProvider, DefinitionProvider, ReferencesProvider, and DocumentSymbolProvider. Providers receive a readonly TextModel and a cooperative CancellationToken. Definition and reference providers return ordered location arrays; consumers preserve provider order.
    • MarkdownCommentProvider is the synchronous open registration contract for whole-line Markdown comment blocks. Detection and result normalization remain viewer-contribution responsibilities.

    Diagnostic is only a shared data shape; diagnostics enter the viewer through viewer/common/markers. There is currently no diagnostic-provider or semantic-token contract. Definition and References providers are registered and queried by viewer/common/languages through the same ordered, disposable registry and cooperative cancellation boundary.

    Severity and tag values carry their own stable labels, so a host does not invent a second naming scheme when it renders or serializes them.

    ///|
    test "diagnostic severities and tags own their labels" {
    let severities : Array[@language.DiagnosticSeverity] = [
    Error,
    Warning,
    Info,
    Hint,
    ]
    let tags : Array[@language.DiagnosticTag] = [Unnecessary, Deprecated]
    debug_inspect(
    (severities.map(s => s.label()), tags.map(t => t.label())),
    content=(
    #|(["Error", "Warning", "Info", "Hint"], ["Unnecessary", "Deprecated"])
    ),
    )
    }

    A Hover is a range plus ordered content parts; HoverContent distinguishes plain text from Markdown so the renderer never has to guess.

    ///|
    test "hover content keeps plain text and markdown apart" {
    let hover : @language.Hover = {
    range: Range(3, 5, 3, 12),
    contents: [PlainText("fn main"), Markdown("```mbt\nfn main {}\n```")],
    }
    debug_inspect(
    hover,
    content=(
    #|{
    #| range: {
    #| start_line_number: 3,
    #| start_column: 5,
    #| end_line_number: 3,
    #| end_column: 12,
    #| },
    #| contents: [PlainText("fn main"), Markdown("```mbt\nfn main {}\n```")],
    #|}
    ),
    )
    }

    #Cancellation

    CancellationTokenSource owns cancellation. Repeated token reads are identity-stable; cancellation notifies listeners once in registration order, parent cancellation propagates, and dispose(cancel=...) separates listener teardown from cancellation.

    sequenceDiagram participant R as request owner participant S as CancellationTokenSource participant T as CancellationToken participant P as provider R->>S: CancellationTokenSource(parent?) S-->>R: token() (identity-stable) R->>P: provide_hover(model, position, token) P->>T: on_cancellation_requested(abort) alt superseded R->>S: cancel() S->>T: notify once, registration order T-->>P: abort else completed R->>S: dispose() Note over S,T: listeners released, not cancelled end

    The token a source hands out is the same value every time, so a request owner can stash it once and compare identities later.

    ///|
    test "one source hands out one identity-stable token" {
    let source = @language.CancellationTokenSource()
    let first = source.token()
    let second = source.token()
    let fired = []
    first.on_cancellation_requested(() => fired.push("first-listener")) |> ignore
    second.on_cancellation_requested(() => fired.push("second-listener"))
    |> ignore
    source.cancel()
    source.cancel()
    debug_inspect(
    (first.is_cancellation_requested(), fired),
    content=(
    #|(true, ["first-listener", "second-listener"])
    ),
    )
    }

    A listener registered after cancellation still runs. The delivery is scheduled, and the scheduler is injectable so this package stays cross-target: the default delivers inline, while browser request owners pass a clearable zero-delay scheduler.

    ///|
    test "late listeners still fire under the default inline scheduler" {
    let source = @language.CancellationTokenSource()
    let token = source.token()
    source.cancel()
    let fired = []
    token.on_cancellation_requested(() => fired.push("late")) |> ignore
    debug_inspect(
    (token.is_cancellation_requested(), fired),
    content=(
    #|(true, ["late"])
    ),
    )
    }

    Cancelling a parent cancels every child, which is how one model swap retires all of that generation's in-flight provider requests at once.

    ///|
    test "parent cancellation propagates to children" {
    let parent = @language.CancellationTokenSource()
    let child = @language.CancellationTokenSource(parent=parent.token())
    let grandchild = @language.CancellationTokenSource(parent=child.token())
    parent.cancel()
    debug_inspect(
    (
    parent.token().is_cancellation_requested(),
    child.token().is_cancellation_requested(),
    grandchild.token().is_cancellation_requested(),
    ),
    content=(
    #|(true, true, true)
    ),
    )
    }

    dispose releases listeners without cancelling, which is the completion path; dispose(cancel=true) is the abort path. Confusing the two is the bug this split exists to prevent.

    ///|
    test "dispose separates listener teardown from cancellation" {
    let completed = @language.CancellationTokenSource()
    let aborted = @language.CancellationTokenSource()
    let fired = []
    completed.token().on_cancellation_requested(() => fired.push("completed"))
    |> ignore
    aborted.token().on_cancellation_requested(() => fired.push("aborted"))
    |> ignore
    completed.dispose()
    aborted.dispose(cancel=true)
    debug_inspect(
    (
    completed.token().is_cancellation_requested(),
    aborted.token().is_cancellation_requested(),
    fired,
    ),
    content=(
    #|(false, true, ["aborted"])
    ),
    )
    }

    The two constant tokens cover the "never cancelled" and "already cancelled" cases without allocating a source.

    ///|
    test "none and cancelled are the constant tokens" {
    debug_inspect(
    (
    @language.CancellationToken::none().is_cancellation_requested(),
    @language.CancellationToken::cancelled().is_cancellation_requested(),
    ),
    content=(
    #|(false, true)
    ),
    )
    }

    #Selectors

    LanguageSelector scores language ids, filters, and selector lists with the represented subset of Monaco's priority rules: exact fields score 10, language/scheme wildcards score 5, lists take their maximum, and an unconstrained filter scores 0. Filters combine optional language, URI scheme, and path pattern checks.

    ///|
    test "a filter's omitted fields do not constrain its positive score" {
    let model = @model.TextModel(
    @base_common.Uri::parse("file://workspace/src/main.mbt"),
    "main.mbt",
    "moonbit",
    1,
    "rev-1",
    "fn main {}\n",
    )
    debug_inspect(
    (
    @language.LanguageSelector::LanguageId("moonbit").matches(model),
    @language.LanguageSelector::LanguageId("javascript").matches(model),
    @language.LanguageFilter::LanguageFilter().matches(model),
    @language.LanguageFilter::LanguageFilter(scheme="file").matches(model),
    @language.LanguageFilter::LanguageFilter(scheme="memory").matches(model),
    ),
    content=(
    #|(true, false, false, true, false)
    ),
    )
    }

    Pattern matching remains deliberately smaller than Monaco glob matching. It strips one leading / from the URI path, then:

    • an empty pattern does not match, while "*" matches everything;
    • a pattern with no * must equal the path exactly;
    • otherwise the first * splits the pattern into a prefix and a suffix, and the path must start with the prefix and end with the suffix.

    That last rule is why the * may sit anywhere, not just at an end — but also why any further * is matched literally, so ** and * in two places do not mean what a glob library would make them mean. There is no brace expansion and no character class.

    ///|
    test "the first star splits the pattern into a prefix and a suffix" {
    let model = @model.TextModel(
    @base_common.Uri::parse("file://workspace/src/main.mbt"),
    "main.mbt",
    "moonbit",
    1,
    "rev-1",
    "fn main {}\n",
    )
    let matches = pattern => {
    @language.LanguageFilter::LanguageFilter(pattern~).matches(model)
    }
    debug_inspect(
    (
    // exact, no star
    matches("src/main.mbt"),
    matches("src/other.mbt"),
    // prefix and suffix around the first star
    matches("src/*"),
    matches("*.mbt"),
    matches("src/*.mbt"),
    // the second star is a literal character, so this cannot match
    matches("**/*.mbt"),
    ),
    content=(
    #|(true, false, true, true, true, false)
    ),
    )
    }

    A selector list takes its highest member score, so a host registers one provider against several languages without duplicating the registration.

    ///|
    test "a selector list matches if any member matches" {
    let model = @model.TextModel(
    @base_common.Uri::parse("file://workspace/src/main.mbt"),
    "main.mbt",
    "moonbit",
    1,
    "rev-1",
    "fn main {}\n",
    )
    let selector : @language.LanguageSelector = LanguageSelectorList([
    LanguageId("javascript"),
    LanguageId("json"),
    LanguageFilter(LanguageFilter(language="moonbit")),
    ])
    let unmatched : @language.LanguageSelector = LanguageSelectorList([
    LanguageId("javascript"),
    LanguageId("json"),
    ])
    debug_inspect(
    (selector.matches(model), unmatched.matches(model)),
    content=(
    #|(true, false)
    ),
    )
    }

    #Boundaries and Monaco map

    This package depends only on base/common and viewer/common/model. It must not import registries, DOM/browser code, transport, native hosts, servers, or internal/shell. Hosts adapt wire/backend payloads before calling these traits.

    The shapes follow the relevant interfaces in vs/editor/common/languages.ts; this package is the contract layer, not Monaco's LanguageFeaturesService. See pkg.generated.mbti for the complete API and run moon test --target js language for focused coverage.

    CodeLensProvider

    pub(open) trait CodeLensProvider {
    async fn provide_code_lenses(Self,
    TextModel
    , CancellationToken) -> Array[CodeLens]
    }

    Asynchronous CodeLens provider for a readonly text model.

    DefinitionProvider

    Asynchronous definition provider for a readonly text model.

    DocumentSymbolProvider

    pub(open) trait DocumentSymbolProvider {
    async fn provide_document_symbols(Self,
    TextModel
    , CancellationToken) -> Array[DocumentSymbol]
    }

    Document symbol provider for outline/navigation views.

    HoverProvider

    Asynchronous hover provider for a readonly text model and the repository's one-based UTF-16 Position.

    MarkdownCommentProvider

    pub(open) trait MarkdownCommentProvider {
    fn provide_markdown_comments(Self,
    TextModel
    ) -> Array[MarkdownCommentBlock] raise
    }

    Synchronous provider for whole-line Markdown source comments. The caller normalizes ranges and content before rendering.

    ReferencesProvider

    Asynchronous references provider retained as the documented host/protocol contract even though the Viewer does not currently register one.

    CancellationToken

    type CancellationToken

    Cooperative cancellation signal passed to language providers.

    Tokens are stable values: repeated reads from a source return the same token before and after cancellation. Cancellation ownership belongs to CancellationTokenSource; CancellationToken::new and cancel remain as compatibility entry points for existing direct-token callers.

    CancellationToken::CancellationToken

    fn CancellationToken::CancellationToken() -> CancellationToken

    Creates a directly cancellable token for compatibility. New request owners should create a CancellationTokenSource and pass its token instead.

    CancellationToken::cancel

    fn CancellationToken::cancel(self : CancellationToken) -> Unit

    Requests cancellation for a compatibility token. Source-owned and shared tokens keep their source-defined behavior.

    CancellationToken::cancelled

    Shared token whose cancellation has already been requested.

    CancellationToken::is_cancellation_requested

    fn CancellationToken::is_cancellation_requested(self : CancellationToken) -> Bool

    Whether cancellation has been requested.

    CancellationToken::none

    Shared token that never requests cancellation.

    CancellationToken::on_cancellation_requested

    Registers a once-only cancellation listener in registration order.

    CancellationTokenSource

    type CancellationTokenSource

    Owner for one lazily created cancellation token and its optional parent subscription. dispose(cancel=false) releases listeners without requesting cancellation; dispose(cancel=true) cancels first.

    CancellationTokenSource::CancellationTokenSource

    Creates a cancellation source with optional parent propagation and host scheduling.

    CancellationTokenSource::cancel

    Requests cancellation once. Cancellation before the first token read avoids mutable listener state but retains this source's scheduler, so a late listener still has next-turn/disposable delivery semantics.

    CancellationTokenSource::dispose

    fn CancellationTokenSource::dispose(self : CancellationTokenSource, cancel? : Bool) -> Unit

    Releases source listeners, optionally requesting cancellation first.

    CancellationTokenSource::token

    Returns the source's stable cancellation token.

    CodeLens

    pub(all) struct CodeLens {
    range :
    Range

    title : String
    tooltip : String?
    execute : async () -> Unit noraise
    } derive(
    Debug
    )

    A complete inline action. The provider owns execution and its captured data; the editor cancels the request token when this result becomes stale.

    CodeLens::to_repr

    Diagnostic

    pub(all) struct Diagnostic {
    range :
    Range

    severity : DiagnosticSeverity
    message : String
    tags : Array[DiagnosticTag]?
    } derive(Eq,
    Debug
    )

    Problem marker over a source range. range is a line/column Range, matching Monaco's IMarkerData.

    Diagnostic::equal

    fn Diagnostic::equal(Diagnostic, Diagnostic) -> Bool

    Diagnostic::not_equal

    fn Diagnostic::not_equal(x : Diagnostic, y : Diagnostic) -> Bool

    DiagnosticSeverity

    pub(all) enum DiagnosticSeverity {
    Error
    Warning
    Info
    Hint
    } derive(Eq,
    Debug
    )

    Diagnostic severity levels accepted from semantic providers. Hint matches Monaco's MarkerSeverity.Hint.

    DiagnosticSeverity::equal

    DiagnosticSeverity::label

    fn DiagnosticSeverity::label(self : DiagnosticSeverity) -> String

    Human-readable diagnostic severity label.

    DiagnosticSeverity::not_equal

    DiagnosticTag

    pub(all) enum DiagnosticTag {
    Unnecessary
    Deprecated
    } derive(Eq,
    Debug
    )

    Diagnostic tag, mirroring Monaco's MarkerTag (markers.ts:51). Kept local to language rather than reusing viewer/common/markers's MarkerTag, since language sits below viewer in the package graph.

    DiagnosticTag::equal

    DiagnosticTag::label

    fn DiagnosticTag::label(self : DiagnosticTag) -> String

    Diagnostic tag label, mirrored on the wire.

    DiagnosticTag::not_equal

    fn DiagnosticTag::not_equal(x : DiagnosticTag, y : DiagnosticTag) -> Bool

    DocumentSymbol

    pub(all) struct DocumentSymbol {
    name : String
    kind : String
    range :
    Range

    } derive(
    Debug
    )

    Symbol outline item for the current document. range is a line/column Range, matching Monaco's DocumentSymbol.

    Hover

    Hover result anchored to a line/column Range, matching Monaco's Hover.

    Hover::to_repr

    HoverContent

    pub(all) enum HoverContent {
    PlainText(String)
    Markdown(String)
    } derive(Eq,
    Debug
    )

    Hover payload shape. Markdown content is rendered by browser hover participants; plaintext is escaped and shown verbatim.

    HoverContent::equal

    HoverContent::not_equal

    fn HoverContent::not_equal(x : HoverContent, y : HoverContent) -> Bool

    LanguageFilter

    type LanguageFilter derive(
    Debug
    )

    Monaco-shaped language selector filter.

    LanguageFilter::LanguageFilter

    fn LanguageFilter::LanguageFilter(language? : String, scheme? : String, pattern? : String) -> LanguageFilter

    Builds a selector filter with optional language, scheme, and path pattern.

    LanguageFilter::matches

    LanguageSelector

    pub(all) enum LanguageSelector {
    LanguageId(String)
    LanguageFilter(LanguageFilter)
    LanguageSelectorList(Array[LanguageSelector])
    } derive(
    Debug
    )

    Monaco-shaped language selector used during provider dispatch.

    LanguageSelector::matches

    LanguageSelector::score

    Monaco-compatible priority for the selector surface represented locally. Exact language/scheme/pattern matches score 10, wildcard language/scheme matches score 5, selector lists take their highest member, and a filter without any constraining field does not select a model.

    Location

    Cross-document location result for definition and reference providers. range is a line/column Range, matching Monaco's Location.

    Location::to_repr

    MarkdownCommentBlock

    pub(all) struct MarkdownCommentBlock {
    line_range :
    LineRange

    markdown : String
    } derive(Eq,
    Debug
    )

    One whole-line Markdown source-comment block. line_range uses the repository's 1-based half-open line convention.

    MarkdownCommentBlock::equal

    MarkdownCommentBlock::not_equal