///|
/// The line collection for every view model — Monaco's
/// `ViewModelLinesFromProjectedModel` (`viewModelLines.ts:60`), which Monaco
/// uses for all normal editors, wrapping on or off (its
/// `ViewModelLinesFromModelAsIs` is only the too-large-for-tokenization
/// escape hatch, which a readonly viewer never needs). Lines that need no
/// mapping project as `Identity` and stay allocation-free; wrapping and
/// injected text produce per-line `Projected` data.
pub struct ViewModelLinesFromProjectedModel {
model : @model.TextModel
mut font_info : @config.FontInfo
mut tab_size : Int32
mut wrapping_column : Int32
mut wrapping_indent : @editor_api.WrappingIndent
mut model_line_projections : Array[ModelLineProjection]
/// Monaco's `projectedModelLineLineCounts` (a
/// `ConstantTimePrefixSumComputer` over per-line view-line counts). The
/// viewer reconstructs wholesale instead of editing incrementally, so it
/// stores the prefix sums directly: entry `i` is the number of view lines
/// before one-based model line `i + 1`, and the last entry is the total.
mut projected_model_line_line_counts : Array[Int32]
/// Monaco's `hiddenAreasDecorationIds`: the hidden (folded) model ranges
/// live as empty-options model decorations so edits keep them anchored.
mut hidden_areas_decoration_ids : Array[String]
// Package-private conformance counters. They make the source batch/read
// ownership observable to white-box tests without widening the public API.
priv mut batch_call_count : Int32
priv mut passive_token_read_count : Int32
}
///|
/// Keeps the private conformance counters observable to package white-box
/// tests without leaking either field through `pkg.generated.mbti`.
fn ViewModelLinesFromProjectedModel::token_demand_counters(
self : ViewModelLinesFromProjectedModel,
) -> (Int32, Int32) {
(self.batch_call_count, self.passive_token_read_count)
}
///|
let _test_only_token_demand_counters_anchor : Unit = ignore(
ViewModelLinesFromProjectedModel::token_demand_counters,
)
///|
/// A view line as (model line, wrapped-line index) — Monaco's `ViewLineInfo`
/// (`viewModelLines.ts:1054`).
priv struct ViewLineInfo {
model_line_number : Int32
model_line_wrapped_line_idx : Int32
}
///|
/// Builds the collection and its projections — the constructor tail of
/// Monaco's `ViewModelLinesFromProjectedModel` (`viewModelLines.ts:85-112`),
/// minus the DOM/monospace computer-factory split (the viewer is
/// monospace-only, so the monospace factory is the only one).
pub fn ViewModelLinesFromProjectedModel::ViewModelLinesFromProjectedModel(
model : @model.TextModel,
font_info~ : @config.FontInfo,
tab_size~ : Int32,
wrapping_column~ : Int32,
wrapping_indent~ : @editor_api.WrappingIndent,
) -> ViewModelLinesFromProjectedModel {
let lines = {
model,
font_info,
tab_size,
wrapping_column,
wrapping_indent,
model_line_projections: [],
projected_model_line_line_counts: [0],
hidden_areas_decoration_ids: [],
batch_call_count: 0,
passive_token_read_count: 0,
}
lines.construct_lines(reset_hidden_areas=true)
lines
}
///|
/// Releases the owner-0 tracking decorations used for hidden areas —
/// `ViewModelLinesFromProjectedModel.dispose` (`viewModelLines.ts:114-116`).
/// Assigning the returned empty id array makes repeated disposal harmless.
pub fn ViewModelLinesFromProjectedModel::dispose(
self : ViewModelLinesFromProjectedModel,
) -> Unit {
self.hidden_areas_decoration_ids = self.model.delta_decorations(
self.hidden_areas_decoration_ids,
[],
)
}
///|
/// Creates the coordinate converter over this collection —
/// `createCoordinatesConverter` (`viewModelLines.ts:118`).
pub fn ViewModelLinesFromProjectedModel::create_coordinates_converter(
self : ViewModelLinesFromProjectedModel,
) -> CoordinatesConverter {
Projected(self)
}
///|
/// Recomputes every line's projection — Monaco's `_constructLines`
/// (`viewModelLines.ts:122`), minus previous-line-break reuse (the readonly
/// viewer rebuilds wholesale). A `None` projection is the line-breaks
/// computer's null result: a line that neither wraps nor carries injected
/// text needs no mapping and projects as `Identity`. Lines inside a hidden
/// area project invisible (`createModelLineProjection(-, false)`); the
/// hidden areas are read back from their tracking decorations, so a rebuild
/// keeps folded regions folded unless `reset_hidden_areas` clears them.
fn ViewModelLinesFromProjectedModel::construct_lines(
self : ViewModelLinesFromProjectedModel,
reset_hidden_areas~ : Bool,
) -> Unit {
let model_line_projections : Array[ModelLineProjection] = []
let prefix_sums : Array[Int32] = [0]
if reset_hidden_areas {
self.hidden_areas_decoration_ids = self.model.delta_decorations(
self.hidden_areas_decoration_ids,
[],
)
}
let injected_text_buckets = line_injected_text_from_decorations(self.model)
let computer = monospace_line_breaks_computer_factory.create_line_breaks_computer(
font_info=self.font_info,
tab_size=self.tab_size,
wrapping_column=self.wrapping_column,
wrapping_indent=self.wrapping_indent,
)
let line_count = self.model.snapshot.line_count()
for model_line_number in 1..<=line_count {
computer.add_request(
model_line_number,
self.model.snapshot.line_text(model_line_number - 1),
injected_text_buckets[model_line_number - 1],
)
}
let line_break_data = computer.finalize()
// The sorted hidden-area walk of `_constructLines` (`viewModelLines.ts:140`).
let hidden_areas = self.get_hidden_areas()
hidden_areas.sort_by((a, b) => {
@base_common.Range::compare_ranges_using_starts(a, b)
})
let mut hidden_area_start = 1
let mut hidden_area_end = 0
let mut hidden_area_idx = -1
let mut next_line_number_to_update_hidden_area = if hidden_area_idx + 1 <
hidden_areas.length() {
hidden_area_end + 1
} else {
line_count + 2
}
for model_line_number in 1..<=line_count {
if model_line_number == next_line_number_to_update_hidden_area {
hidden_area_idx += 1
hidden_area_start = hidden_areas[hidden_area_idx].start_line_number
hidden_area_end = hidden_areas[hidden_area_idx].end_line_number
next_line_number_to_update_hidden_area = if hidden_area_idx + 1 <
hidden_areas.length() {
hidden_area_end + 1
} else {
line_count + 2
}
}
let is_in_hidden_area = model_line_number >= hidden_area_start &&
model_line_number <= hidden_area_end
let projection = create_model_line_projection(
line_break_data[model_line_number - 1],
!is_in_hidden_area,
)
model_line_projections.push(projection)
prefix_sums.push(
prefix_sums[model_line_number - 1] + projection.get_view_line_count(),
)
}
self.model_line_projections = model_line_projections
self.projected_model_line_line_counts = prefix_sums
self.ensure_at_least_one_visible_line()
}
///|
/// `_ensureAtLeastOneVisibleLine` (`viewModelLines.ts:435`): a document may
/// never hide every line; reveal the first when it would.
fn ViewModelLinesFromProjectedModel::ensure_at_least_one_visible_line(
self : ViewModelLinesFromProjectedModel,
) -> Unit {
if self.get_view_line_count() == 0 && self.model_line_projections.length() > 0 {
self.model_line_projections[0] = self.model_line_projections[0].set_visible(
true,
)
self.rebuild_prefix_sums()
}
}
///|
/// Recomputes the stored prefix sums from the projections' view-line counts
/// — the viewer's stand-in for Monaco's incremental
/// `projectedModelLineLineCounts.setValue` (the collection stores the sums
/// directly and rebuilds at its wholesale granularity).
fn ViewModelLinesFromProjectedModel::rebuild_prefix_sums(
self : ViewModelLinesFromProjectedModel,
) -> Unit {
let prefix_sums : Array[Int32] = [0]
for i, projection in self.model_line_projections {
prefix_sums.push(prefix_sums[i] + projection.get_view_line_count())
}
self.projected_model_line_line_counts = prefix_sums
}
///|
/// Merged one-based hidden model-line ranges — `getHiddenAreas`
/// (`viewModelLines.ts:168`), read back from the tracking decorations.
pub fn ViewModelLinesFromProjectedModel::get_hidden_areas(
self : ViewModelLinesFromProjectedModel,
) -> Array[@base_common.Range] {
self.hidden_areas_decoration_ids.filter_map(decoration_id => {
self.model.get_decoration_range(decoration_id)
})
}
///|
/// Applies a new hidden-area set — `setHiddenAreas` (`viewModelLines.ts:174`).
/// Returns whether the view-line mapping changed. Ranges are validated,
/// normalized into merged whole-line ranges, and diffed against the current
/// decoration-tracked set before any projection is touched.
pub fn ViewModelLinesFromProjectedModel::set_hidden_areas(
self : ViewModelLinesFromProjectedModel,
ranges : Array[@base_common.Range],
) -> Bool {
let validated_ranges = ranges.map(r => self.model.validate_range(r))
let new_ranges = normalize_line_ranges(validated_ranges[:])
// This checks if there really was a change
let old_ranges = self.get_hidden_areas()
old_ranges.sort_by((a, b) => {
@base_common.Range::compare_ranges_using_starts(a, b)
})
if new_ranges == old_ranges {
return false
}
let new_decorations : Array[@model.ModelDeltaDecoration] = new_ranges.map(r => {
range: r,
options: ModelDecorationOptions("", description="empty"),
})
self.hidden_areas_decoration_ids = self.model.delta_decorations(
self.hidden_areas_decoration_ids,
new_decorations,
)
let hidden_areas = new_ranges
let mut hidden_area_start = 1
let mut hidden_area_end = 0
let mut hidden_area_idx = -1
let mut next_line_number_to_update_hidden_area = if hidden_area_idx + 1 <
hidden_areas.length() {
hidden_area_end + 1
} else {
self.model_line_projections.length() + 2
}
let mut has_visible_line = false
for i, projection in self.model_line_projections {
let line_number = i + 1
if line_number == next_line_number_to_update_hidden_area {
hidden_area_idx += 1
hidden_area_start = hidden_areas[hidden_area_idx].start_line_number
hidden_area_end = hidden_areas[hidden_area_idx].end_line_number
next_line_number_to_update_hidden_area = if hidden_area_idx + 1 <
hidden_areas.length() {
hidden_area_end + 1
} else {
self.model_line_projections.length() + 2
}
}
if line_number >= hidden_area_start && line_number <= hidden_area_end {
// Line should be hidden
if projection.is_visible() {
self.model_line_projections[i] = projection.set_visible(false)
}
} else {
has_visible_line = true
// Line should be visible
if !projection.is_visible() {
self.model_line_projections[i] = projection.set_visible(true)
}
}
}
self.rebuild_prefix_sums()
if !has_visible_line {
// Cannot have everything be hidden => reveal everything!
self.set_hidden_areas([]) |> ignore
}
true
}
///|
/// Whether a one-based model line is visible (not folded away) —
/// `modelPositionIsVisible` (`viewModelLines.ts:250`).
pub fn ViewModelLinesFromProjectedModel::model_position_is_visible(
self : ViewModelLinesFromProjectedModel,
model_line_number : Int32,
) -> Bool {
if model_line_number < 1 ||
model_line_number > self.model_line_projections.length() {
// invalid arguments
return false
}
self.model_line_projections[model_line_number - 1].is_visible()
}
///|
/// Sorts and merges touching/overlapping line ranges into whole-line ranges
/// — `normalizeLineRanges` (`viewModelLines.ts:1024`).
fn normalize_line_ranges(
ranges : ArrayView[@base_common.Range],
) -> Array[@base_common.Range] {
if ranges.length() == 0 {
return []
}
let sorted_ranges = ranges.to_owned()
sorted_ranges.sort_by((a, b) => {
@base_common.Range::compare_ranges_using_starts(a, b)
})
let result : Array[@base_common.Range] = []
let (current_range_start, current_range_end) = for
range in sorted_ranges[1:]
current_range_start = sorted_ranges[0].start_line_number,
current_range_end = sorted_ranges[0].end_line_number {
if range.start_line_number > current_range_end + 1 {
result.push(Range(current_range_start, 1, current_range_end, 1))
continue range.start_line_number, range.end_line_number
} else if range.end_line_number > current_range_end {
continue current_range_start, range.end_line_number
}
} nobreak {
(current_range_start, current_range_end)
}
result.push(Range(current_range_start, 1, current_range_end, 1))
result
}
///|
/// Applies changed wrapping settings in place — `setWrappingSettings`
/// (`viewModelLines.ts:277`, which equally takes the `fontInfo` whose
/// full-width/half-width ratio feeds the break widths). Returns whether the
/// view-line mapping changed.
pub fn ViewModelLinesFromProjectedModel::set_wrapping_settings(
self : ViewModelLinesFromProjectedModel,
font_info : @config.FontInfo,
wrapping_column : Int32,
wrapping_indent : @editor_api.WrappingIndent,
) -> Bool {
if self.font_info == font_info &&
self.wrapping_column == wrapping_column &&
self.wrapping_indent == wrapping_indent {
return false
}
self.font_info = font_info
self.wrapping_column = wrapping_column
self.wrapping_indent = wrapping_indent
self.construct_lines(reset_hidden_areas=false)
true
}
///|
/// Applies a changed tab size in place — `setTabSize`
/// (`viewModelLines.ts:266`). Returns whether the view-line mapping changed.
pub fn ViewModelLinesFromProjectedModel::set_tab_size(
self : ViewModelLinesFromProjectedModel,
new_tab_size : Int32,
) -> Bool {
if self.tab_size == new_tab_size {
return false
}
self.tab_size = new_tab_size
self.construct_lines(reset_hidden_areas=false)
true
}
///|
/// Reprojects after the document was replaced — `onModelFlushed`
/// (`viewModelLines.ts:325`): `constructLines(resetHiddenAreas=true)`. A
/// flush replaces the document, so hidden (folded) areas reset; the stale
/// `hidden_areas_decoration_ids` left by the model-side decoration wipe
/// delta-remove as a no-op (`delta_decorations` skips unknown ids), exactly
/// the source's tolerance.
pub fn ViewModelLinesFromProjectedModel::on_model_flushed(
self : ViewModelLinesFromProjectedModel,
) -> Unit {
self.construct_lines(reset_hidden_areas=true)
}
///|
/// Reprojects after an injected-text delta. Deviation: Monaco routes
/// injected-text changes through per-line `onModelLineChanged` events
/// (`viewModelLines.ts:352`), which keep hidden areas; the viewer's
/// granularity is the whole-document reproject, so this is the per-line
/// path's whole-document stand-in — hidden areas survive, unlike
/// `on_model_flushed`.
pub fn ViewModelLinesFromProjectedModel::on_injected_text_changed(
self : ViewModelLinesFromProjectedModel,
) -> Unit {
self.construct_lines(reset_hidden_areas=false)
}
///|
/// Total number of view lines — `getViewLineCount`.
pub fn ViewModelLinesFromProjectedModel::get_view_line_count(
self : ViewModelLinesFromProjectedModel,
) -> Int32 {
let prefix_sums = self.projected_model_line_line_counts
prefix_sums[prefix_sums.length() - 1]
}
///|
/// Clamps a view line number into the document — `_toValidViewLineNumber`.
fn ViewModelLinesFromProjectedModel::to_valid_view_line_number(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> Int32 {
view_line_number.clamp(min=1, max=self.get_view_line_count())
}
///|
/// Decomposes a view line into (model line, wrapped-line index) —
/// `getViewLineInfo` (`viewModelLines.ts:478`), the prefix-sum `getIndexOf`
/// as a binary search over the stored sums.
fn ViewModelLinesFromProjectedModel::get_view_line_info(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> ViewLineInfo {
let view_index = self.to_valid_view_line_number(view_line_number) - 1
let prefix_sums = self.projected_model_line_line_counts
// Greatest model-line index whose first view line is at or before
// `view_index`.
let low = for low = 0, high = self.model_line_projections.length() - 1; low <
high; {
let mid = (low + high + 1) / 2
if prefix_sums[mid] <= view_index {
continue mid, high
} else {
continue low, mid - 1
}
} nobreak {
low
}
{
model_line_number: low + 1,
model_line_wrapped_line_idx: view_index - prefix_sums[low],
}
}
///|
/// Returns text for a one-based view line — `getViewLineContent`.
pub fn ViewModelLinesFromProjectedModel::get_view_line_content(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> String {
let info = self.get_view_line_info(view_line_number)
self.model_line_projections[info.model_line_number - 1].get_view_line_content(
self.model,
info.model_line_number,
info.model_line_wrapped_line_idx,
)
}
///|
/// Minimum one-based column for a view line — `getViewLineMinColumn`.
pub fn ViewModelLinesFromProjectedModel::get_view_line_min_column(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> Int32 {
let info = self.get_view_line_info(view_line_number)
self.model_line_projections[info.model_line_number - 1].get_view_line_min_column(
info.model_line_wrapped_line_idx,
)
}
///|
/// Maximum one-based column for a view line — `getViewLineMaxColumn`.
pub fn ViewModelLinesFromProjectedModel::get_view_line_max_column(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> Int32 {
let info = self.get_view_line_info(view_line_number)
self.model_line_projections[info.model_line_number - 1].get_view_line_max_column(
self.model,
info.model_line_number,
info.model_line_wrapped_line_idx,
)
}
///|
/// Maps a one-based view line to its one-based model line — the line-number
/// half of Monaco's `getViewLineInfo`, kept as the viewer's shorthand for
/// consumers that only need the line.
pub fn ViewModelLinesFromProjectedModel::model_line_number_of_view_line(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> Int32 {
self.get_view_line_info(view_line_number).model_line_number
}
///|
/// The view-line data of a one-based view line — `getViewLineData`.
pub fn ViewModelLinesFromProjectedModel::get_view_line_data(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
) -> ViewLineData {
let info = self.get_view_line_info(view_line_number)
let base_view_line_number = self.projected_model_line_line_counts[info.model_line_number -
1] +
1
self.model_line_projections[info.model_line_number - 1].get_view_line_data(
self.model,
info.model_line_number,
info.model_line_wrapped_line_idx,
base_view_line_number,
)
}
///|
/// Batch view-line data (`viewModelLines.ts:763-795`). The needed mask is
/// indexed from `view_start_line_number`; hidden model lines are skipped and
/// each participating projected model line shares one passive token read.
pub fn ViewModelLinesFromProjectedModel::get_view_lines_data(
self : ViewModelLinesFromProjectedModel,
view_start_line_number : Int32,
view_end_line_number : Int32,
needed : Array[Bool],
) -> Array[ViewLineData?] {
self.batch_call_count += 1
let view_start_line_number = self.to_valid_view_line_number(
view_start_line_number,
)
let view_end_line_number = self.to_valid_view_line_number(
view_end_line_number,
)
let count = view_end_line_number - view_start_line_number + 1
if needed.length() != count {
abort("get_view_lines_data needed mask length mismatch")
}
let result : Array[ViewLineData?] = [ for _ in 0.. None ]
let start = self.get_view_line_info(view_start_line_number)
let start_model_line_index = start.model_line_number - 1
let mut view_line_number = view_start_line_number
for
model_line_index in start_model_line_index.. view_end_line_number {
last_line = true
remaining_view_line_count = view_end_line_number - view_line_number + 1
}
let base_view_line_number = self.projected_model_line_line_counts[model_line_index] +
1
let read_tokens = projection.get_view_lines_data(
self.model,
model_line_index + 1,
from_view_line_index,
remaining_view_line_count,
base_view_line_number,
view_line_number - view_start_line_number,
needed,
result,
)
if read_tokens {
self.passive_token_read_count += 1
}
view_line_number += remaining_view_line_count
if last_line {
break
}
}
result
}
///|
/// Builds line-renderer input data for a one-based view line — the viewer's
/// bridge to `ViewLineRenderingData` (Monaco builds it in
/// `ViewModel.getViewLineRenderingData`).
pub fn ViewModelLinesFromProjectedModel::view_line_rendering_data(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
line_decorations~ : Array[@view_layout.LineDecoration] = [],
) -> @view_layout.ViewLineRenderingData {
view_line_rendering_data_from_view_line_data(
self.get_view_line_data(view_line_number),
line_decorations~,
tab_size=self.tab_size,
)
}
///|
/// The injected-text source index at a 0-based view column of a
/// view line, or `-1` for source text — the index-valued form of Monaco's
/// `getInjectedTextAt` (`viewModelLines.ts:991`).
pub fn ViewModelLinesFromProjectedModel::injected_text_index_at(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
column : Int32,
) -> Int32 {
let info = self.get_view_line_info(view_line_number)
self.model_line_projections[info.model_line_number - 1].injected_text_index_at(
info.model_line_wrapped_line_idx,
column,
)
}
///|
/// Snaps a view position at injected-text/wrap boundaries per the affinity —
/// `normalizePosition` (`viewModelLines.ts:996`).
pub fn ViewModelLinesFromProjectedModel::normalize_position(
self : ViewModelLinesFromProjectedModel,
position : @base_common.Position,
affinity : @model.PositionAffinity,
) -> @base_common.Position {
let info = self.get_view_line_info(position.line_number)
self.model_line_projections[info.model_line_number - 1].normalize_position(
info.model_line_wrapped_line_idx,
position,
affinity,
)
}
///|
/// Converts a view position to model coordinates —
/// `convertViewPositionToModelPosition` (`viewModelLines.ts:834`).
pub fn ViewModelLinesFromProjectedModel::convert_view_position_to_model_position(
self : ViewModelLinesFromProjectedModel,
view_line_number : Int32,
view_column : Int32,
) -> @base_common.Position {
let info = self.get_view_line_info(view_line_number)
let input_column = self.model_line_projections[info.model_line_number - 1].get_model_column_of_view_position(
info.model_line_wrapped_line_idx,
view_column,
)
self.model.validate_position(Position(info.model_line_number, input_column))
}
///|
/// Converts a view range to model coordinates —
/// `convertViewRangeToModelRange` (`viewModelLines.ts:842`).
pub fn ViewModelLinesFromProjectedModel::convert_view_range_to_model_range(
self : ViewModelLinesFromProjectedModel,
view_range : @base_common.Range,
) -> @base_common.Range {
let start = self.convert_view_position_to_model_position(
view_range.start_line_number,
view_range.start_column,
)
let end = self.convert_view_position_to_model_position(
view_range.end_line_number,
view_range.end_column,
)
Range(start.line_number, start.column, end.line_number, end.column)
}
///|
/// Converts a model position to view coordinates —
/// `convertModelPositionToViewPosition` (`viewModelLines.ts:848`). A position
/// on a hidden (folded) line walks up to the nearest visible line's end —
/// or, with `below_hidden_ranges`, down to the next visible line's start.
/// The affinity picks the boundary side at injected text and wrap breaks;
/// absent it behaves as Monaco's default `None`.
pub fn ViewModelLinesFromProjectedModel::convert_model_position_to_view_position(
self : ViewModelLinesFromProjectedModel,
model_line_number : Int32,
model_column : Int32,
affinity? : @model.PositionAffinity,
allow_zero_line_number~ : Bool = false,
below_hidden_ranges~ : Bool = false,
) -> @base_common.Position {
let valid_position = self.model.validate_position(
Position(model_line_number, model_column),
)
// `TextModel::set_value` swaps its snapshot before it flushes registered
// view models. Monaco suppresses the matching token-change notification
// until `_lines.onModelFlushed()` has rebuilt this array, but the viewer's
// current whole-document tokenizer can synchronously expose that narrow
// window. Keep the source validation above, and address only projections
// that are committed so an early observer cannot index past the old array.
let input_line_number = valid_position.line_number.clamp(
min=1,
max=self.model_line_projections.length(),
)
let input_column = valid_position.column
let original_line_index = input_line_number - 1
let line_index = if below_hidden_ranges {
for line_index = original_line_index; line_index <
self.model_line_projections.length() &&
!self.model_line_projections[line_index].is_visible(); {
continue line_index + 1
} nobreak {
line_index
}
} else {
for line_index = original_line_index; line_index > 0 &&
!self.model_line_projections[line_index].is_visible(); {
continue line_index - 1
} nobreak {
line_index
}
}
let line_index_changed = line_index != original_line_index
if line_index == 0 && !self.model_line_projections[line_index].is_visible() {
// Could not reach a real line
return Position(if allow_zero_line_number { 0 } else { 1 }, 1)
}
let delta_line_number = 1 + self.projected_model_line_line_counts[line_index]
if line_index_changed {
if below_hidden_ranges {
self.model_line_projections[line_index].get_view_position_of_model_position(
delta_line_number,
1,
affinity?,
)
} else {
self.model_line_projections[line_index].get_view_position_of_model_position(
delta_line_number,
self.model.snapshot.get_line_max_column(line_index + 1),
affinity?,
)
}
} else {
self.model_line_projections[input_line_number - 1].get_view_position_of_model_position(
delta_line_number,
input_column,
affinity?,
)
}
}
///|
/// Converts a model range to view coordinates —
/// `convertModelRangeToViewRange` (`viewModelLines.ts:892-901`): the default
/// affinity is `Left` and only applies to empty ranges; a non-empty range
/// excludes injected text at its edges (start `Right`, end `Left`).
pub fn ViewModelLinesFromProjectedModel::convert_model_range_to_view_range(
self : ViewModelLinesFromProjectedModel,
model_range : @base_common.Range,
affinity? : @model.PositionAffinity,
) -> @base_common.Range {
if model_range.is_empty() {
let affinity = affinity.unwrap_or(Left)
let start = self.convert_model_position_to_view_position(
model_range.start_line_number,
model_range.start_column,
affinity~,
)
Range(start.line_number, start.column, start.line_number, start.column)
} else {
let start = self.convert_model_position_to_view_position(
model_range.start_line_number,
model_range.start_column,
affinity=Right,
)
let end = self.convert_model_position_to_view_position(
model_range.end_line_number,
model_range.end_column,
affinity=Left,
)
Range(start.line_number, start.column, end.line_number, end.column)
}
}
///|
/// Derives per-model-line injected text from the backing model's
/// injected-text decorations — `LineInjectedText.fromDecorations`
/// (`textModelEvents.ts`): `before` lands at the range start (order 0),
/// `after` at the range end (order 1); sorted by line, column, order.
/// Positions convert to the 0-based line/column space
/// `ProjectedTextLine::from_injected_text` consumes.
fn line_injected_text_from_decorations(
model : @model.TextModel,
) -> Array[Array[InjectedText]] {
let snapshot = model.snapshot
let buckets : Array[Array[InjectedText]] = []
for _ in 0.. 0 {
entries.push(
(
decoration.range.start_line_number,
decoration.range.start_column,
0,
before,
),
)
}
if decoration.options.after is Some(after) && after.content().length() > 0 {
entries.push(
(
decoration.range.end_line_number,
decoration.range.end_column,
1,
after,
),
)
}
}
entries.sort_by((left, right) => {
let (l_line, l_column, l_order, _) = left
let (r_line, r_column, r_order, _) = right
if l_line != r_line {
l_line - r_line
} else if l_column != r_column {
l_column - r_column
} else {
l_order - r_order
}
})
for entry in entries {
let (line_number, column, _, options) = entry
let line = (line_number - 1).clamp(min=0, max=snapshot.line_count() - 1)
let line_text = snapshot.line_text(line)
let column0 = (column - 1).clamp(min=0, max=line_text.length())
buckets[line].push(
InjectedText(
Position(line, column0),
options.content(),
options.inline_class_name().unwrap_or(""),
source_index=options.attached_index(),
),
)
}
buckets
}
///|
/// The injected texts of one 1-based model line — the single-line form of
/// `line_injected_text_from_decorations` (same derivation, same clamping,
/// filtered to the line and sorted by column then before/after order).
fn injected_text_for_model_line(
model : @model.TextModel,
model_line_number : Int32,
) -> Array[InjectedText] {
let snapshot = model.snapshot
if snapshot.line_count() == 0 {
return []
}
let target_line0 = (model_line_number - 1).clamp(
min=0,
max=snapshot.line_count() - 1,
)
let entries : Array[(Int32, Int32, @model.InjectedTextOptions)] = []
for decoration in model.get_injected_text_decorations() {
if decoration.options.before is Some(before) &&
before.content().length() > 0 &&
(decoration.range.start_line_number - 1).clamp(
min=0,
max=snapshot.line_count() - 1,
) ==
target_line0 {
entries.push((decoration.range.start_column, 0, before))
}
if decoration.options.after is Some(after) &&
after.content().length() > 0 &&
(decoration.range.end_line_number - 1).clamp(
min=0,
max=snapshot.line_count() - 1,
) ==
target_line0 {
entries.push((decoration.range.end_column, 1, after))
}
}
entries.sort_by((left, right) => {
let (l_column, l_order, _) = left
let (r_column, r_order, _) = right
if l_column != r_column {
l_column - r_column
} else {
l_order - r_order
}
})
let line_text = snapshot.line_text(target_line0)
let out : Array[InjectedText] = []
for entry in entries {
let (column, _, options) = entry
let column0 = (column - 1).clamp(min=0, max=line_text.length())
out.push(
InjectedText(
Position(target_line0, column0),
options.content(),
options.inline_class_name().unwrap_or(""),
source_index=options.attached_index(),
),
)
}
out
}