#viewer/common/core

    Selection state and the column arithmetic that sits between a model column and what the reader actually sees on screen.

    #Two kinds of column

    A model column counts UTF-16 code units. A visible column counts rendered cells, so a tab is worth however many cells it takes to reach the next tab stop. Everything that positions a caret, draws a selection, or reports a status-bar position has to know which one it is holding.

    flowchart LR M["model column<br>(UTF-16 code units, 1-based)"] V["visible column<br>(rendered cells, 0-based)"] M -->|"visible_column_from_column(line, column, tab_size)"| V V -->|"column_from_visible_column(line, visible, tab_size)"| M M -->|"to_statusbar_column"| S["status bar column<br>(1-based, tab = 1 cell)"]

    Both directions take the line content, because the answer depends on which characters precede the column.

    ///|
    test "tabs make the visible column diverge from the model column" {
    // "\tab" — one tab then three characters, tab size 4.
    let line = "\tabc"
    debug_inspect(
    (
    // model column 1 is before the tab, so nothing is rendered yet
    @core.visible_column_from_column(line, 1, 4),
    // model column 2 is after the tab: it has consumed a full tab stop
    @core.visible_column_from_column(line, 2, 4),
    @core.visible_column_from_column(line, 3, 4),
    // and back again
    @core.column_from_visible_column(line, 4, 4),
    @core.column_from_visible_column(line, 5, 4),
    ),
    content=(
    #|(0, 4, 5, 2, 3)
    ),
    )
    }

    Without a tab the two agree, which is why the distinction is easy to forget.

    ///|
    test "with no tabs the two column spaces coincide" {
    let line = "let x = 1"
    debug_inspect(
    (
    @core.visible_column_from_column(line, 5, 4),
    @core.column_from_visible_column(line, 4, 4),
    ),
    content=(
    #|(4, 5)
    ),
    )
    }

    to_statusbar_column is a third space again: it is the 1-based position a human reads, where a tab counts as a single character rather than as its rendered width.

    ///|
    test "the status bar counts a tab as one character" {
    let line = "\tabc"
    debug_inspect(
    (
    @core.to_statusbar_column(line, 2, 4),
    @core.visible_column_from_column(line, 2, 4),
    ),
    content=(
    #|(5, 4)
    ),
    )
    }

    #Selection

    A Selection is an ordered pair: where the gesture started and where the caret is. That is strictly more information than a Range, because it also records which end the user is dragging.

    flowchart LR subgraph ltr["LTR — anchor before caret"] A1["selection_start (2,1)"] --> P1["position (4,3)"] end subgraph rtl["RTL — caret before anchor"] P2["position (2,1)"] --> A2["selection_start (4,3)"] end

    start/end normalize the pair into document order, while get_selection_start/get_position preserve the gesture.

    ///|
    test "direction is preserved while start and end are normalized" {
    let anchor = @base_common.Position(2, 1)
    let caret = @base_common.Position(4, 3)
    let forward = @core.Selection(anchor, caret)
    let backward = @core.Selection(caret, anchor)
    debug_inspect(
    (
    (forward.direction(), forward.start(), forward.end()),
    (backward.direction(), backward.start(), backward.end()),
    // the gesture endpoints are not normalized
    (backward.get_selection_start(), backward.get_position()),
    ),
    content=(
    #|(
    #| (LTR, { line_number: 2, column: 1 }, { line_number: 4, column: 3 }),
    #| (RTL, { line_number: 2, column: 1 }, { line_number: 4, column: 3 }),
    #| ({ line_number: 4, column: 3 }, { line_number: 2, column: 1 }),
    #|)
    ),
    )
    }

    An empty selection is a bare caret: both endpoints coincide.

    ///|
    test "an empty selection is a caret" {
    let at = @base_common.Position(3, 7)
    debug_inspect(
    (
    @core.Selection(at, at).is_empty(),
    @core.Selection(at, Position(3, 8)).is_empty(),
    ),
    content=(
    #|(true, false)
    ),
    )
    }

    set_start_position and set_end_position move an endpoint in document order and return a new value; they never mutate the receiver.

    ///|
    test "endpoint updates are copies in document order" {
    let original = @core.Selection(Position(2, 1), Position(4, 3))
    let moved = original.set_end_position(9, 1)
    debug_inspect(
    (original, moved),
    content=(
    #|(
    #| {
    #| selection_start: { line_number: 2, column: 1 },
    #| position: { line_number: 4, column: 3 },
    #| },
    #| {
    #| selection_start: { line_number: 2, column: 1 },
    #| position: { line_number: 9, column: 1 },
    #| },
    #|)
    ),
    )
    }

    #Metrics

    editor_vertical_scrollbar_size and editor_horizontal_scrollbar_size are the shared scrollbar constants, kept here so layout and browser packages cannot drift to different numbers.

    ///|
    test "layout constants have one definition" {
    debug_inspect(
    (
    @core.editor_vertical_scrollbar_size, @core.editor_horizontal_scrollbar_size,
    ),
    content=(
    #|(14, 12)
    ),
    )
    }

    #Boundaries and checks

    This package maps to vs/editor/common/core/{cursorColumns,selection}.ts. It may depend only on base/common; it must not import model, view-model, layout, DOM, or host packages. The complete API is pkg.generated.mbti.

    moon test --target js viewer/common/core

    Selection

    type Selection derive(Eq,
    Debug
    )

    A selection in the editor: a range that carries an orientation. Mirrors Monaco's core/selection.ts Selection. Both endpoints are model positions (one-based line_number, UTF-16 column); selection_start is the fixed anchor and position is the moving active end (the cursor).

    This is pure geometry, like core/selection.ts. The model/clipboard boundary — normalizing to document offsets and extracting copy text — lives in the controller layer (viewer/selection.mbt), not on this type, matching Monaco's split (clipboard belongs to the editor controller, not the value type).

    Selection::Selection

    Builds a selection from its anchor (selection_start) and active (position) endpoints without reordering them; the orientation is kept.

    Selection::direction

    fn Selection::direction(self : Selection) -> SelectionDirection

    The orientation of the selection. Mirrors Selection.getDirection.

    Selection::end

    The ordered end position (bottom-right). Mirrors Range.getEndPosition.

    Selection::equal

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

    Selection::from_positions

    Builds a collapsed-or-oriented selection from two model positions, anchor first. Mirrors Monaco's Selection.fromPositions.

    Selection::get_position

    The active end of the selection (the cursor). Mirrors getPosition.

    Selection::get_selection_start

    The fixed anchor of the selection. Mirrors getSelectionStart.

    Selection::is_empty

    fn Selection::is_empty(self : Selection) -> Bool

    Whether the selection covers no text (anchor and active coincide).

    Selection::not_equal

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

    Selection::set_end_position

    fn Selection::set_end_position(self : Selection, end_line_number : Int, end_column : Int) -> Selection

    A copy with the ordered end moved. Mirrors Selection.setEndPosition: the orientation is kept.

    Selection::set_start_position

    fn Selection::set_start_position(self : Selection, start_line_number : Int, start_column : Int) -> Selection

    A copy with the ordered start moved. Mirrors Selection.setStartPosition: the orientation is kept.

    Selection::start

    The ordered start position (top-left). Mirrors Range.getStartPosition.

    SelectionDirection

    pub(all) enum SelectionDirection {
    LTR
    RTL
    } derive(Eq,
    Debug
    )

    The orientation of a selection: whether the active position is at the end (LTR) or the start (RTL) of the selected range. Mirrors Monaco's SelectionDirection.

    SelectionDirection::equal

    SelectionDirection::not_equal

    column_from_visible_column

    fn column_from_visible_column(line_content : String, visible_column : Int, tab_size : Int) -> Int

    Returns a (1-based) column from a visible column.

    editor_horizontal_scrollbar_size

    let editor_horizontal_scrollbar_size : Double

    editor_vertical_scrollbar_size

    let editor_vertical_scrollbar_size : Double

    Editor scrollbar defaults from Monaco's EditorScrollbar option.

    to_statusbar_column

    fn to_statusbar_column(line_content : String, column : Int, tab_size : Int) -> Int

    Returns the value to display as "Col" in the status bar (1-based, code points, tabs snap to tab stops).

    visible_column_from_column

    fn visible_column_from_column(line_content : String, column : Int, tab_size : Int) -> Int

    Returns a visible column from a (1-based) column.