#viewer/common/cursor

    Backend-neutral single-cursor state for the readonly viewer.

    The cursor retains both model and projected-view coordinates. Callers supply one side through PartialCursorState; CursorsController validates it and derives the other side through CursorContext.

    flowchart LR P["PartialCursorState<br>model or view"] --> C["CursorsController"] C --> M["model CursorState"] C --> V["view CursorState"] X["CursorContext conversions"] --> C

    #Cursor values

    SingleCursorState holds the anchor range, anchor kind, active position, and leftover visible-column residues. selection, has_selection, and moved derive the oriented viewer/common/core.Selection.

    ///|
    fn collapsed(position : @base_common.Position) -> @cursor.SingleCursorState {
    SingleCursorState(position, position, Simple, position)
    }

    ///|
    test "moved either extends the anchor or starts a new caret" {
    let start = collapsed(Position(2, 1))
    let extended = start.moved(true, Position(4, 7))
    let restarted = start.moved(false, Position(4, 7))
    debug_inspect(
    (
    (extended.has_selection(), extended.selection()),
    (restarted.has_selection(), restarted.selection()),
    ),
    content=(
    #|(
    #| (
    #| true,
    #| {
    #| selection_start: { line_number: 2, column: 1 },
    #| position: { line_number: 4, column: 7 },
    #| },
    #| ),
    #| (
    #| false,
    #| {
    #| selection_start: { line_number: 4, column: 7 },
    #| position: { line_number: 4, column: 7 },
    #| },
    #| ),
    #|)
    ),
    )
    }

    Word and line anchors remain ranges while dragging, so extending past an anchor keeps the entire original anchor covered.

    ///|
    test "a word anchor retains its shape" {
    let state = @cursor.SingleCursorState(
    Position(2, 5),
    Position(2, 10),
    Word,
    Position(2, 10),
    )
    debug_inspect(
    (
    state.moved(true, Position(4, 1)).selection(),
    state.moved(true, Position(1, 1)).selection(),
    ),
    content=(
    #|(
    #| {
    #| selection_start: { line_number: 2, column: 5 },
    #| position: { line_number: 4, column: 1 },
    #| },
    #| {
    #| selection_start: { line_number: 2, column: 10 },
    #| position: { line_number: 1, column: 1 },
    #| },
    #|)
    ),
    )
    }

    #Controller transitions

    ///|
    fn identity_context() -> @cursor.CursorContext {
    CursorContext(
    view_to_model=position => position,
    view_range_to_model=range => range,
    model_to_view=position => position,
    )
    }

    ///|
    fn controller_for(
    text : String,
    ) -> (@cursor.CursorsController, @model.TextModel) raise {
    let model = @model.TextModel(
    @base_common.Uri::parse("file:///cursor-doc.mbt"),
    "cursor-doc.mbt",
    "moonbit",
    1,
    "rev-1",
    text,
    )
    (CursorsController(model, identity_context()), model)
    }

    ///|
    fn set_selection(
    controller : @cursor.CursorsController,
    selection : @core.Selection,
    ) -> @cursor.CursorStateChange? {
    controller.set_cursor_state(
    Some(@cursor.CursorState::from_model_selection(selection)),
    )
    }

    ///|
    test "an identical transition produces no second change" {
    let (controller, model) = controller_for("fn main {\n println(1)\n}\n")
    let selection = @core.Selection(Position(2, 3), Position(2, 3))
    let first = set_selection(controller, selection)
    let repeat = set_selection(controller, selection)
    debug_inspect(
    (first is Some(_), repeat is Some(_), controller.get_model_selection()),
    content=(
    #|(
    #| true,
    #| false,
    #| {
    #| selection_start: { line_number: 2, column: 3 },
    #| position: { line_number: 2, column: 3 },
    #| },
    #|)
    ),
    )
    model.dispose()
    }

    Positions are validated against the model. A caller cannot park the caret past the final line or line end.

    ///|
    test "out-of-range positions are clamped" {
    let (controller, model) = controller_for("ab\ncd\n")
    set_selection(controller, Selection(Position(99, 99), Position(99, 99)))
    |> ignore
    debug_inspect(
    controller.get_model_selection(),
    content=(
    #|{
    #| selection_start: { line_number: 3, column: 1 },
    #| position: { line_number: 3, column: 1 },
    #|}
    ),
    )
    model.dispose()
    }

    CursorStateChange carries both model versions, selection snapshots, source, reason, and whether the transition should emit an outgoing event. An exact paired-state/version no-op returns None.

    #Boundaries and checks

    The package depends on base/common, viewer/common/editor_api, viewer/common/core, and viewer/common/model; it has no view-model, DOM, or FFI dependency. Implementation carriers remain abstract outside the package.

    moon test --target all viewer/common/cursor

    CursorContext

    type CursorContext

    The conversion context a cursor needs, the readonly-viewer reduction of Monaco's cursorContext.ts CursorContext. Monaco's holds the model, view model, coordinates converter, and cursor config. The local context needs only the converter to bridge model and view positions; the owning CursorsController separately holds the mutable TextModel for validation and content-version tracking. The port stores the three exact conversion functions it consumes instead of turning Monaco's structural interface into a runtime MoonBit trait object.

    CursorContext::CursorContext

    Builds a cursor context from the active view model's required conversions.

    CursorModelState

    type CursorModelState derive(Eq)

    Internal equality snapshot used by CursorsController to distinguish a full no-op, a view-only change, and a public model-state change.

    CursorModelState::equal

    CursorModelState::not_equal

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

    CursorState

    pub struct CursorState {
    model_state : SingleCursorState
    view_state : SingleCursorState
    } derive(Eq,
    Debug
    )

    The paired model/view cursor state. Both sides are committed atomically; live callers provide one side and the cursor derives the other.

    CursorState::equal

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

    CursorState::from_model_selection

    Builds the source's collapsed Simple anchor from one model selection.

    CursorState::from_model_selections

    Converts model selections in source order.

    CursorState::from_model_state

    fn CursorState::from_model_state(state : SingleCursorState) -> PartialCursorState

    CursorState::from_view_state

    fn CursorState::from_view_state(state : SingleCursorState) -> PartialCursorState

    CursorState::not_equal

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

    CursorStateChange

    pub struct CursorStateChange {
    old_selections : Array[
    Selection
    ]?
    selections : Array[
    Selection
    ]
    old_model_version_id : Int
    model_version_id : Int
    source : String
    reason :
    CursorChangeReason

    reached_max_cursor_count : Bool
    has_outgoing_event : Bool
    } derive(
    Debug
    )

    One accepted full cursor transition. has_outgoing_event is false for a view-only reprojection: the view changed and must render, but public model cursor listeners must remain silent. A content flush deliberately carries old_selections=None and old version 0 even when both visible states are the origin.

    CursorsController

    type CursorsController

    Owns the readonly editor's primary cursor and the single transition spine shared by API, pointer, keyboard, and model-content changes. This is the single-cursor reduction of Monaco's CursorsController plus CursorCollection: every accepted transition commits a paired model/view state and returns one fact for the ViewModel's outgoing dispatcher.

    CursorsController::CursorsController

    Builds a controller at the document origin and captures the model's internal content version, never its caller-owned host metadata version.

    CursorsController::get_model_selection

    The selection in model coordinates.

    CursorsController::get_primary_cursor_state

    fn CursorsController::get_primary_cursor_state(self : CursorsController) -> CursorState

    Returns the paired primary state.

    CursorsController::get_view_selection

    The selection in view coordinates.

    CursorsController::model_state

    The current model-coordinate state.

    CursorsController::on_model_content_changed

    Model-content transition. A flush updates the known internal version, rebuilds the cursor at the origin, and deliberately compares against a null old snapshot so it emits even when the visible cursor was already there.

    CursorsController::set_context

    fn CursorsController::set_context(self : CursorsController, context : CursorContext) -> Unit

    Installs a changed converter and reprojects only from the validated model side. The source's simultaneous model/view cross-validation surface is an approved deferral; this local reduced seam therefore emits no public model cursor event.

    CursorsController::set_cursor_state

    Applies one source-shaped partial state and returns a change fact only when the full paired state or internal version changed. None is the source's both-sides-null early return. A full state follows the model-authoritative reprojection seam documented by ONE-028; live callers otherwise provide exactly one side.

    CursorsController::view_state

    The current view-coordinate state.

    PartialCursorState

    type PartialCursorState derive(Eq,
    Debug
    )

    Source-shaped partial cursor-state union. Live callers normally provide exactly one side; FullCursorState represents the retained complete-state member used by snapshots/reprojection tests.

    PartialCursorState::equal

    PartialCursorState::not_equal

    SelectionStartKind

    pub(all) enum SelectionStartKind {
    Simple
    Word
    Line
    } derive(Eq,
    Debug
    )

    How a selection was anchored, mirroring Monaco's SelectionStartKind. A plain click anchors Simple; double/triple-click anchors Word/Line, and continuation gestures preserve and extend the corresponding anchor range.

    SelectionStartKind::equal

    SelectionStartKind::not_equal

    SingleCursorState

    pub struct SingleCursorState {
    selection_start_start :
    Position

    selection_start_end :
    Position

    selection_start_kind : SelectionStartKind
    selection_start_leftover_visible_columns : Int
    position :
    Position

    leftover_visible_columns : Int
    } derive(Eq,
    Debug
    )

    The cursor state on either the model or the view model, a faithful port of Monaco's cursorCommon.ts SingleCursorState. selection_start is the anchor range (a region for word/line selection, collapsed for simple) and position is the moving active end; the derived selection carries the orientation. Positions are one-based (line_number, UTF-16 column), matching Monaco's Position.

    The two leftover-visible-column fields retain the desired visual column while vertical movement crosses lines of different widths. The anchor-side value is preserved while extending a selection; collapsing a selection replaces both values with the active movement residue.

    SingleCursorState::SingleCursorState

    fn SingleCursorState::SingleCursorState(selection_start_start :
    Position
    , selection_start_end :
    Position
    , selection_start_kind : SelectionStartKind, position :
    Position
    , selection_start_leftover_visible_columns? : Int, leftover_visible_columns? : Int) -> SingleCursorState

    Builds a cursor state from an explicit anchor range and active position.

    SingleCursorState::equal

    SingleCursorState::has_selection

    fn SingleCursorState::has_selection(self : SingleCursorState) -> Bool

    Whether this state selects any text (mirrors Monaco hasSelection).

    SingleCursorState::moved

    fn SingleCursorState::moved(self : SingleCursorState, in_selection_mode : Bool, position :
    Position
    , leftover_visible_columns? : Int) -> SingleCursorState

    Moves the cursor, transcribing Monaco's SingleCursorState.move (move is a reserved word here). In selection mode only the active position moves (the anchor is kept); out of selection mode the whole state collapses to the new point.

    SingleCursorState::not_equal

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

    SingleCursorState::selection

    The derived selection (anchor, active) carrying orientation. Transcribes Monaco's SingleCursorState._computeSelection: an active end strictly after the anchor's start uses that start as the selection anchor; equality or a position before the start (selecting backward over a word/line) uses the anchor range's end.