flowchart TB
M["model lines 1..n<br>viewer/common/model"]
M -->|"wrap at wrapping_column"| P["projection"]
M -->|"hidden areas (folding)"| P
M -->|"injected text decorations"| P
P --> V["view lines 1..m"]
V -->|convert_view_position_to_model_position| M
M -->|convert_model_position_to_view_position| V///|
fn projection_model(text : String) -> @model.TextModel raise {
TextModel(
@base_common.Uri::parse("file:///view-model-doc.mbt"),
"view-model-doc.mbt",
"moonbit",
1,
"rev-1",
text,
)
}
///|
fn projection(
text : String,
wrapping_column~ : Int32,
) -> @view_model.ViewModelLinesFromProjectedModel raise {
ViewModelLinesFromProjectedModel(
projection_model(text),
font_info=@config.FontInfo::default(),
tab_size=4,
wrapping_column~,
wrapping_indent=None,
)
}
///|
test "with no wrapping, view lines and model lines coincide" {
let lines = projection("alpha\nbeta\ngamma\n", wrapping_column=200)
debug_inspect(
(
lines.get_view_line_count(),
lines.get_view_line_content(2),
lines.model_line_number_of_view_line(2),
lines.convert_view_position_to_model_position(2, 1),
),
content=(
#|(4, "beta", 2, { line_number: 2, column: 1 })
),
)
}///|
test "wrapping splits one model line into several view lines" {
let lines = projection(
"aaaa bbbb cccc dddd eeee\nshort\n",
wrapping_column=10,
)
debug_inspect(
(
lines.get_view_line_count(),
[
for view_line in 1..<=lines.get_view_line_count() => {
(
view_line,
lines.get_view_line_content(view_line),
lines.model_line_number_of_view_line(view_line),
)
}
],
),
content=(
#|(
#| 5,
#| [
#| (1, "aaaa bbbb ", 1),
#| (2, "cccc dddd ", 1),
#| (3, "eeee", 1),
#| (4, "short", 2),
#| (5, "", 3),
#| ],
#|)
),
)
}///|
test "model and view positions convert in both directions" {
let lines = projection("aaaa bbbb cccc dddd eeee\n", wrapping_column=10)
let view = lines.convert_model_position_to_view_position(1, 18)
debug_inspect(
(
view,
lines.convert_view_position_to_model_position(
view.line_number,
view.column,
),
),
content=(
#|({ line_number: 2, column: 8 }, { line_number: 1, column: 18 })
),
)
}///|
test "a hidden area removes view lines without touching the model" {
let lines = projection("one\ntwo\nthree\nfour\n", wrapping_column=200)
let before = lines.get_view_line_count()
let changed = lines.set_hidden_areas([Range(2, 1, 3, 6)])
debug_inspect(
(
before,
changed,
lines.get_view_line_count(),
lines.get_view_line_content(2),
(
lines.model_position_is_visible(1),
lines.model_position_is_visible(2),
lines.model_position_is_visible(4),
),
lines.get_hidden_areas(),
),
content=(
#|(
#| 5,
#| true,
#| 3,
#| "four",
#| (true, false, true),
#| [
#| {
#| start_line_number: 2,
#| start_column: 1,
#| end_line_number: 3,
#| end_column: 1,
#| },
#| ],
#|)
),
)
}///|
test "re-setting identical hidden areas is not a change" {
let lines = projection("one\ntwo\nthree\n", wrapping_column=200)
let first = lines.set_hidden_areas([Range(2, 1, 2, 4)])
let again = lines.set_hidden_areas([Range(2, 1, 2, 4)])
debug_inspect(
(first, again),
content=(
#|(true, false)
),
)
}///|
test "a live ViewModel exposes line reads and a converter" {
let model = projection_model("alpha\nbeta\ngamma\n")
let view_model = @view_model.ViewModel(model)
debug_inspect(
(
view_model.line_count(),
view_model.get_line_content(2),
view_model.get_line_max_column(2),
view_model.language_id(),
view_model.model_position_is_visible(Position(2, 1)),
),
content=(
#|(4, "beta", 5, "moonbit", true)
),
)
}///|
test "a view range maps to the model ranges it really covers" {
let model = projection_model("one\ntwo\nthree\nfour\n")
let view_model = @view_model.ViewModel(model)
debug_inspect(
view_model.to_model_visible_ranges(Range(1, 1, 4, 5)),
content=(
#|[
#| {
#| start_line_number: 1,
#| start_column: 1,
#| end_line_number: 4,
#| end_column: 5,
#| },
#|]
),
)
}///|
test "positions are validated against the current model" {
let view_model = @view_model.ViewModel(projection_model("ab\ncd\n"))
debug_inspect(
(
view_model.validate_model_position(Position(1, 1)),
view_model.validate_model_position(Position(99, 99)),
),
content=(
#|({ line_number: 1, column: 1 }, { line_number: 3, column: 1 })
),
)
}///|
test "a plain document has no injected text anywhere" {
let view_model = @view_model.ViewModel(projection_model("let x = 1\n"))
debug_inspect(
(
view_model.get_injected_text_at(Position(1, 5)),
view_model.injected_text_index_at(Position(1, 5)),
),
content=(
#|(None, -1)
),
)
}model line + grammar tokens + injected-text decorations
-> ProjectedTextLine
-> soft-wrap ModelLineProjection segments
-> hidden-area filtering
-> ViewLineData + model/view coordinate conversion
-> viewport_data_from_view_model -> view_layout.ViewportDatamoon test --target js viewer/common/view_model/inline_decorations_reference_wbtest.mbt
moon test --target native viewer/common/view_model/inline_decorations_reference_wbtest.mbtmoon test --target js viewer/common/view_model/inline_decorations_matrix_wbtest.mbt
moon test --target native viewer/common/view_model/inline_decorations_matrix_wbtest.mbtfn CoordinatesConverter::model_position_is_visible(self : CoordinatesConverter, position : Position) -> Boolfn CoordinatesConverter::model_position_to_view_position(self : CoordinatesConverter, position : Position, affinity? : PositionAffinity) -> Positionfn CoordinatesConverter::model_range_to_view_range(self : CoordinatesConverter, range : Range, affinity? : PositionAffinity) -> Rangefn CoordinatesConverter::view_position_to_model_position(self : CoordinatesConverter, position : Position) -> Positionfn CoordinatesConverter::view_range_to_model_range(self : CoordinatesConverter, range : Range) -> Rangetype CursorEventDispatcherpub(all) struct CursorStateChangedEvent {
kind : OutgoingViewModelEventKind
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
} derive(Debug)fn HiddenAreasChangedOutgoingEvent::HiddenAreasChangedOutgoingEvent() -> HiddenAreasChangedOutgoingEventfn InjectedText::InjectedText(position : Position, text : String, class_name : String, source_index? : Int) -> InjectedTexttype LineBreaksComputerfn LineBreaksComputer::add_request(self : LineBreaksComputer, model_line_number : Int, line_text : String, injected_text : Array[InjectedText]) -> Unitpub enum ModelLineProjection {
Identity
Hidden
Projected(ModelLineProjectionData, is_visible~ : Bool)
}fn ModelLineProjection::get_model_column_of_view_position(self : ModelLineProjection, output_line_index : Int, output_column : Int) -> Intfn ModelLineProjection::get_view_line_content(self : ModelLineProjection, model : TextModel, model_line_number : Int, output_line_index : Int) -> Stringfn ModelLineProjection::get_view_line_data(self : ModelLineProjection, model : TextModel, model_line_number : Int, output_line_index : Int, base_view_line_number : Int) -> ViewLineDatafn ModelLineProjection::get_view_line_max_column(self : ModelLineProjection, model : TextModel, model_line_number : Int, output_line_index : Int) -> Intfn ModelLineProjection::get_view_line_min_column(self : ModelLineProjection, output_line_index : Int) -> Intfn ModelLineProjection::get_view_position_of_model_position(self : ModelLineProjection, delta_line_number : Int, input_column : Int, affinity? : PositionAffinity) -> Positionfn ModelLineProjection::injected_text_index_at(self : ModelLineProjection, output_line_index : Int, column : Int) -> Intfn ModelLineProjection::normalize_position(self : ModelLineProjection, output_line_index : Int, position : Position, affinity : PositionAffinity) -> Positionfn ModelLineProjection::set_visible(self : ModelLineProjection, is_visible : Bool) -> ModelLineProjectionpub struct ModelLineProjectionData {
model_line_number : Int
line_length : Int
projected_line : ProjectedTextLine
segments : Array[ModelLineProjectionSegment]
} derive(Eq, Debug)fn ModelLineProjectionData::ModelLineProjectionData(model_line_number : Int, line_length : Int, projected_line : ProjectedTextLine, segments : Array[ModelLineProjectionSegment]) -> ModelLineProjectionDatafn ModelLineProjectionData::not_equal(x : ModelLineProjectionData, y : ModelLineProjectionData) -> Boolfn ModelLineProjectionData::segment_at_view_line_index(self : ModelLineProjectionData, index : Int) -> ModelLineProjectionSegmentfn ModelLineProjectionData::view_line_index_for_model_column(self : ModelLineProjectionData, column : Int) -> Intfn ModelLineProjectionData::view_line_index_for_projected_column(self : ModelLineProjectionData, column : Int, affinity? : PositionAffinity) -> Intfn ModelLineProjectionSegment::equal(ModelLineProjectionSegment, ModelLineProjectionSegment) -> Boolfn ModelLineProjectionSegment::not_equal(x : ModelLineProjectionSegment, y : ModelLineProjectionSegment) -> Boolpub struct ModelTokensChangedOutgoingEvent {
kind : OutgoingViewModelEventKind
event : ModelTokensChangedEvent
}fn ModelTokensChangedOutgoingEvent::ModelTokensChangedOutgoingEvent(event : ModelTokensChangedEvent) -> ModelTokensChangedOutgoingEventtype MonospaceLineBreaksComputerFactoryfn MonospaceLineBreaksComputerFactory::MonospaceLineBreaksComputerFactory(break_before_chars : String, break_after_chars : String) -> MonospaceLineBreaksComputerFactoryfn MonospaceLineBreaksComputerFactory::create_line_breaks_computer(self : MonospaceLineBreaksComputerFactory, font_info~ : FontInfo, tab_size~ : Int, wrapping_column~ : Int, wrapping_indent~ : WrappingIndent, word_break? : WordBreak) -> LineBreaksComputerfn OutgoingViewModelEventKind::equal(OutgoingViewModelEventKind, OutgoingViewModelEventKind) -> Boolfn OutgoingViewModelEventKind::not_equal(x : OutgoingViewModelEventKind, y : OutgoingViewModelEventKind) -> Boolpub struct ProjectedLinePart {
start : Int
end : Int
kind : ProjectedLinePartKind
} derive(Eq, Debug)fn ProjectedTextLine::from_injected_text(line_text : String, injected_text : Array[InjectedText]) -> ProjectedTextLinefn ProjectedTextLine::injected_text_index_at_projected_column(self : ProjectedTextLine, projected_column : Int) -> Intfn ProjectedTextLine::model_column_to_projected_column(self : ProjectedTextLine, model_column : Int, affinity? : PositionAffinity) -> Intfn ProjectedTextLine::normalize_projected_column_around_injections(self : ProjectedTextLine, projected_column : Int, affinity : PositionAffinity) -> Intfn ProjectedTextLine::projected_column_to_model_column(self : ProjectedTextLine, projected_column : Int) -> Intpub struct SimpleMoveArguments {
direction : SimpleMoveDirection
unit : CursorMoveUnit
select : Bool
value : Int
} derive(Eq, Debug)fn SimpleMoveArguments::SimpleMoveArguments(direction : SimpleMoveDirection, unit : CursorMoveUnit, select : Bool, value : Int) -> SimpleMoveArgumentspub struct ViewLineData {
content : String
continues_with_wrapped_line : Bool
min_column : Int
max_column : Int
start_visible_column : Int
tokens : ViewLineTokens
inline_decorations : Array[InlineDecoration]
}pub struct ViewModel {
model : TextModel
options : ViewModelOptions
lines : ViewModelLinesFromProjectedModel
cursor_event_dispatcher : CursorEventDispatcher
cursor : CursorsController
view_layout : ViewLayout
decorations : ViewModelDecorations
coordinates_converter : CoordinatesConverter
// private fields
}fn ViewModel::change_whitespace(self : ViewModel, callback : (WhitespaceChangeAccessor) -> Unit) -> Boolfn ViewModel::model_position_to_view_position(self : ViewModel, position : Position, affinity? : PositionAffinity, allow_zero_line_number? : Bool) -> Positionfn ViewModel::move_cursor(self : ViewModel, direction : SimpleMoveDirection, in_selection_mode : Bool, value? : Int, unit? : CursorMoveUnit, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_direction(self : ViewModel, direction : CursorMoveDirection, in_selection_mode : Bool, value? : Int, unit? : CursorMoveUnit, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_line(self : ViewModel, model_position : Position, in_selection_mode : Bool, view_position? : Position, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_line_select_to(self : ViewModel, view_position : Position, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_page(self : ViewModel, direction : SimpleMoveDirection, in_selection_mode : Bool, page_size? : Int, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_to(self : ViewModel, view_position : Position, in_selection_mode : Bool, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_to_line_end(self : ViewModel, in_selection_mode : Bool, sticky? : Bool, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_to_line_start(self : ViewModel, in_selection_mode : Bool, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_to_model_position(self : ViewModel, model_position : Position, in_selection_mode : Bool, view_position? : Position, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_with_arguments(self : ViewModel, args : SimpleMoveArguments, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::move_cursor_word(self : ViewModel, model_position : Position, in_selection_mode : Bool, source? : String, reason? : CursorChangeReason) -> Boolfn ViewModel::normalize_position(self : ViewModel, position : Position, affinity : PositionAffinity) -> Positionfn ViewModel::on_cursor_state_changed(self : ViewModel, listener : (CursorStateChangedEvent) -> Unit) -> Disposablefn ViewModel::on_did_change_content_or_injected_text(self : ViewModel, e? : InternalModelContentChangeEvent) -> Unitfn ViewModel::on_hidden_areas_changed(self : ViewModel, listener : (HiddenAreasChangedOutgoingEvent) -> Unit) -> Disposablefn ViewModel::on_model_tokens_changed(self : ViewModel, listener : (ModelTokensChangedOutgoingEvent) -> Unit) -> Disposablefn ViewModel::on_view_zones_changed(self : ViewModel, listener : (ViewZonesChangedEvent) -> Unit) -> Disposablefn ViewModel::set_cursor_state(self : ViewModel, state : PartialCursorState?, source : String, reason : CursorChangeReason) -> Boolflowchart LR
A[Replace one<br>source range set] --> B[Merge every source]
B --> C{Union changed<br>or forced?}
C -->|no| D[Return false]
C -->|yes| E[Capture top anchor,<br>apply projected ranges]
E --> F{Line mapping<br>changed?}
F -->|yes| G[Re-point cursor,<br>decorations, layout]
F -->|no| H[Keep mapping]
G --> I[Restore visible<br>viewport anchor]
H --> I
I --> J[Emit changed-only<br>outgoing event]fn ViewModel::set_model_cursor_state(self : ViewModel, state : SingleCursorState, source : String, reason : CursorChangeReason) -> Boolfn ViewModel::set_view_cursor_state(self : ViewModel, state : SingleCursorState, source : String, reason : CursorChangeReason) -> Boolfn ViewModel::with_options(model : TextModel, options : ViewModelOptions, smooth_scrolling? : Bool, schedule_at_next_animation_frame? : (() -> Unit) -> Disposable, now? : () -> Double, attached_view? : AttachedViewImpl, emit_view_tokens_changed? : (Array[ViewTokensChangedRange]) -> Unit) -> ViewModeltype ViewModelDecorationsfn ViewModelDecorations::get_decorations_viewport_data(self : ViewModelDecorations, view_range : Range) -> ViewDecorationsCollectionpub struct ViewModelLinesFromProjectedModel {
model : TextModel
font_info : FontInfo
tab_size : Int
wrapping_column : Int
wrapping_indent : WrappingIndent
model_line_projections : Array[ModelLineProjection]
projected_model_line_line_counts : Array[Int]
hidden_areas_decoration_ids : Array[String]
// private fields
}fn ViewModelLinesFromProjectedModel::ViewModelLinesFromProjectedModel(model : TextModel, font_info~ : FontInfo, tab_size~ : Int, wrapping_column~ : Int, wrapping_indent~ : WrappingIndent) -> ViewModelLinesFromProjectedModelfn ViewModelLinesFromProjectedModel::convert_model_position_to_view_position(self : ViewModelLinesFromProjectedModel, model_line_number : Int, model_column : Int, affinity? : PositionAffinity, allow_zero_line_number? : Bool, below_hidden_ranges? : Bool) -> Positionfn ViewModelLinesFromProjectedModel::convert_model_range_to_view_range(self : ViewModelLinesFromProjectedModel, model_range : Range, affinity? : PositionAffinity) -> Rangefn ViewModelLinesFromProjectedModel::convert_view_position_to_model_position(self : ViewModelLinesFromProjectedModel, view_line_number : Int, view_column : Int) -> Positionfn ViewModelLinesFromProjectedModel::convert_view_range_to_model_range(self : ViewModelLinesFromProjectedModel, view_range : Range) -> Rangefn ViewModelLinesFromProjectedModel::create_coordinates_converter(self : ViewModelLinesFromProjectedModel) -> CoordinatesConverterfn ViewModelLinesFromProjectedModel::get_hidden_areas(self : ViewModelLinesFromProjectedModel) -> Array[Range]fn ViewModelLinesFromProjectedModel::get_view_line_content(self : ViewModelLinesFromProjectedModel, view_line_number : Int) -> Stringfn ViewModelLinesFromProjectedModel::get_view_line_count(self : ViewModelLinesFromProjectedModel) -> Intfn ViewModelLinesFromProjectedModel::get_view_line_data(self : ViewModelLinesFromProjectedModel, view_line_number : Int) -> ViewLineDatafn ViewModelLinesFromProjectedModel::get_view_line_max_column(self : ViewModelLinesFromProjectedModel, view_line_number : Int) -> Intfn ViewModelLinesFromProjectedModel::get_view_line_min_column(self : ViewModelLinesFromProjectedModel, view_line_number : Int) -> Intfn ViewModelLinesFromProjectedModel::get_view_lines_data(self : ViewModelLinesFromProjectedModel, view_start_line_number : Int, view_end_line_number : Int, needed : Array[Bool]) -> Array[ViewLineData?]fn ViewModelLinesFromProjectedModel::injected_text_index_at(self : ViewModelLinesFromProjectedModel, view_line_number : Int, column : Int) -> Intfn ViewModelLinesFromProjectedModel::model_line_number_of_view_line(self : ViewModelLinesFromProjectedModel, view_line_number : Int) -> Intfn ViewModelLinesFromProjectedModel::model_position_is_visible(self : ViewModelLinesFromProjectedModel, model_line_number : Int) -> Boolfn ViewModelLinesFromProjectedModel::normalize_position(self : ViewModelLinesFromProjectedModel, position : Position, affinity : PositionAffinity) -> Positionfn ViewModelLinesFromProjectedModel::on_injected_text_changed(self : ViewModelLinesFromProjectedModel) -> Unitfn ViewModelLinesFromProjectedModel::on_model_flushed(self : ViewModelLinesFromProjectedModel) -> Unitfn ViewModelLinesFromProjectedModel::set_hidden_areas(self : ViewModelLinesFromProjectedModel, ranges : Array[Range]) -> Boolfn ViewModelLinesFromProjectedModel::set_tab_size(self : ViewModelLinesFromProjectedModel, new_tab_size : Int) -> Boolfn ViewModelLinesFromProjectedModel::set_wrapping_settings(self : ViewModelLinesFromProjectedModel, font_info : FontInfo, wrapping_column : Int, wrapping_indent : WrappingIndent) -> Boolfn ViewModelLinesFromProjectedModel::view_line_rendering_data(self : ViewModelLinesFromProjectedModel, view_line_number : Int, line_decorations? : Array[LineDecoration]) -> ViewLineRenderingDatapub struct ViewModelOptions {
font_info : FontInfo
soft_wrap_column : Int
tab_size : Int
wrapping_indent : WrappingIndent
editor_id : Int
render_validation_decorations : RenderValidationDecorations
} derive(Eq, Debug)fn ViewModelOptions::ViewModelOptions(font_info? : FontInfo, soft_wrap_column? : Int, tab_size? : Int, wrapping_indent? : WrappingIndent, editor_id? : Int, render_validation_decorations? : RenderValidationDecorations) -> ViewModelOptionsfn ViewModelOptions::soft_wrap(column : Int, tab_size? : Int, wrapping_indent? : WrappingIndent) -> ViewModelOptionsfn ViewTokensChangedRange::not_equal(x : ViewTokensChangedRange, y : ViewTokensChangedRange) -> Boolfn create_model_line_projection(data : ModelLineProjectionData?, is_visible : Bool) -> ModelLineProjectionfn dedup_margin_decorations(visible_start_line_number : Int, visible_end_line_number : Int, decorations : Array[DecorationToRender]) -> Array[Array[String]]fn lines_decorations_to_render(decorations : Array[ViewModelDecoration]) -> Array[DecorationToRender]fn render_line_input_from_view_line_data(line : ViewLineData, line_decorations? : Array[LineDecoration], space_width? : Double, render_whitespace? : RenderWhitespace, render_control_characters? : Bool, vertical_scrollbar_size? : Double) -> RenderLineInputfn view_line_rendering_data_from_view_line_data(line : ViewLineData, line_decorations? : Array[LineDecoration], tab_size? : Int) -> ViewLineRenderingDatafn viewport_data_from_view_model(view_model : ViewModel, window : LineWindow, line_height~ : Int, layout? : ViewLayout, partial_viewport? : PartialViewLinesViewportData) -> ViewportDataInstall
Download zipReadonly MoonBit code viewer harness inspired by Monaco and CodeMirror.