///|
/// Faithful 1:1 port of Monaco's `LinesLayout` (and `EditorWhitespace`) from
/// `vscode/src/vs/editor/common/viewLayout/linesLayout.ts` in the checked-in submodule.
///
/// Layouts objects that take vertical space (lines and the whitespaces between
/// them, i.e. view zones). Lines are addressed with Monaco's 1-based line
/// numbers; whitespaces are addressed by an opaque `id`, sorted by
/// `(afterLineNumber, ordinal)`.
///
/// DEVIATION: variable line heights (`LineHeightsManager` /
/// `lineHeights.test.ts`) are out of scope (Conditional row in the conformance
/// plan), so the port keeps a single uniform `default_line_height`. Every
/// `linesLayout.test.ts` case constructs the layout with an empty
/// `customLineHeightData` (`[]`), so this matches the upstream tests exactly;
/// the accumulated-line-height helper collapses to `lineNumber * height`.
///|
/// A whitespace (view zone) that displaces the lines after `after_line_number`.
pub struct EditorWhitespace {
id : String
mut after_line_number : Int32
ordinal : Int32
mut height : Int32
mut min_width : Int32
mut prefix_sum : Double
} derive(Eq, Debug)
///|
/// Constructs a whitespace; `prefix_sum` starts at 0 (lazily computed).
pub fn EditorWhitespace::EditorWhitespace(
id : String,
after_line_number : Int32,
ordinal : Int32,
height : Int32,
min_width : Int32,
) -> EditorWhitespace {
{ id, after_line_number, ordinal, height, min_width, prefix_sum: 0.0, }
}
///|
/// Lines and relative offsets positioned between two vertical offsets — the
/// Monaco `IPartialViewLinesViewportData` shape.
pub struct PartialViewLinesViewportData {
big_numbers_delta : Int32
start_line_number : Int32
end_line_number : Int32
relative_vertical_offset : Array[Double]
centered_line_number : Int32
completely_visible_start_line_number : Int32
completely_visible_end_line_number : Int32
line_height : Int32
} derive(Eq, Debug)
///|
/// One whitespace positioned relative to a viewport — the Monaco
/// `IViewWhitespaceViewportData` shape.
pub struct ViewWhitespaceViewportData {
id : String
after_line_number : Int32
vertical_offset : Double
height : Int32
} derive(Eq, Debug)
///|
priv struct PendingChange {
id : String
new_after_line_number : Int32
new_height : Int32
new_min_width : Int32?
}
///|
priv struct PendingRemove {
id : String
}
///|
priv struct PendingChanges {
mut has_pending : Bool
mut inserts : Array[EditorWhitespace]
mut changes : Array[PendingChange]
mut removes : Array[PendingRemove]
}
///|
fn PendingChanges::PendingChanges() -> PendingChanges {
{ has_pending: false, inserts: [], changes: [], removes: [], }
}
///|
fn PendingChanges::insert(self : PendingChanges, x : EditorWhitespace) -> Unit {
self.has_pending = true
self.inserts.push(x)
}
///|
fn PendingChanges::change(self : PendingChanges, x : PendingChange) -> Unit {
self.has_pending = true
self.changes.push(x)
}
///|
fn PendingChanges::remove(self : PendingChanges, x : PendingRemove) -> Unit {
self.has_pending = true
self.removes.push(x)
}
///|
fn PendingChanges::commit(
self : PendingChanges,
lines_layout : LinesLayout,
) -> Unit {
if !self.has_pending {
return
}
let inserts = self.inserts
let changes = self.changes
let removes = self.removes
self.has_pending = false
self.inserts = []
self.changes = []
self.removes = []
lines_layout.commit_pending_changes(inserts, changes, removes)
}
///|
/// Global instance counter, mirroring Monaco's `LinesLayout.INSTANCE_COUNT`.
let lines_layout_instance_count : Ref[Int32] = Ref(0)
///|
/// ECMAScript ToInt32, used by LinesLayout's public-number boundary. MoonBit's
/// `Double::to_int` saturates; JavaScript instead truncates, reduces modulo
/// 2^32, and interprets the high half as negative. NaN, infinities, and both
/// signed zeros canonicalize to integer zero.
fn javascript_to_int32(value : Double) -> Int32 {
let modulus = 4294967296.0
let signed_boundary = 2147483648.0
let mut wrapped = value.trunc() % modulus
if wrapped.is_nan() || wrapped == 0.0 {
return 0
}
if wrapped < 0.0 {
wrapped += modulus
}
if wrapped >= signed_boundary {
(wrapped - modulus).to_int()
} else {
wrapped.to_int()
}
}
///|
/// Mutable accessor handed to `change_whitespace`, mirroring Monaco's
/// `IWhitespaceChangeAccessor`.
struct WhitespaceChangeAccessor {
layout : LinesLayout
mut had_a_change : Bool
mut min_width_update_requested : Bool
}
///|
/// Queues a whitespace insertion; returns its freshly minted id.
pub fn WhitespaceChangeAccessor::insert_whitespace(
self : WhitespaceChangeAccessor,
after_line_number : Int32,
ordinal : Int32,
height_in_px : Double,
min_width : Double,
) -> String {
self.had_a_change = true
// JavaScript's `| 0` is ToInt32, not MoonBit's saturating `Double::to_int`.
// Keep public number carriers raw until this exact LinesLayout boundary.
let height_in_px = javascript_to_int32(height_in_px)
let min_width = javascript_to_int32(min_width)
self.layout.last_whitespace_id += 1
let id = "\{self.layout.instance_id}\{self.layout.last_whitespace_id}"
self.layout.pending_changes.insert(
EditorWhitespace(id, after_line_number, ordinal, height_in_px, min_width),
)
id
}
///|
/// Queues a whitespace `afterLineNumber`/`height` change.
pub fn WhitespaceChangeAccessor::change_one_whitespace(
self : WhitespaceChangeAccessor,
id : String,
new_after_line_number : Int32,
new_height : Double,
new_min_width? : Double,
) -> Unit {
self.had_a_change = true
if new_min_width is Some(_) {
self.min_width_update_requested = true
}
let new_height = javascript_to_int32(new_height)
self.layout.pending_changes.change({
id,
new_after_line_number,
new_height,
new_min_width: new_min_width.map(javascript_to_int32),
})
}
///|
/// Queues a whitespace removal.
pub fn WhitespaceChangeAccessor::remove_whitespace(
self : WhitespaceChangeAccessor,
id : String,
) -> Unit {
self.had_a_change = true
self.layout.pending_changes.remove({ id, })
}
///|
/// Vertical line layout: lines (uniform `default_line_height`) and the
/// whitespaces (view zones) that push them down.
struct LinesLayout {
instance_id : String
pending_changes : PendingChanges
mut last_whitespace_id : Int32
mut arr : Array[EditorWhitespace]
mut prefix_sum_valid_index : Int32
mut min_width : Int32
mut line_count : Int32
mut padding_top : Int32
mut padding_bottom : Int32
mut default_line_height : Int32
}
///|
/// Creates a lines layout. `custom_line_height_data` is accepted for shape
/// parity with Monaco but ignored (uniform heights — see file deviation).
pub fn LinesLayout::LinesLayout(
line_count : Int32,
default_line_height : Int32,
padding_top : Int32,
padding_bottom : Int32,
) -> LinesLayout {
lines_layout_instance_count.val += 1
{
instance_id: @base_common.single_letter_hash(
lines_layout_instance_count.val,
),
pending_changes: PendingChanges(),
last_whitespace_id: 0,
arr: [],
prefix_sum_valid_index: -1,
min_width: -1,
line_count,
padding_top,
padding_bottom,
default_line_height,
}
}
///|
/// Find the insertion index for a new whitespace inside a sorted array. If the
/// value is already present, the index is after the existing one.
pub fn LinesLayout::find_insertion_index(
arr : Array[EditorWhitespace],
after_line_number : Int32,
ordinal : Int32,
) -> Int32 {
for low = 0, high = arr.length(); low < high; {
let mid = (low + high) / 2
if after_line_number == arr[mid].after_line_number {
if ordinal < arr[mid].ordinal {
continue low, mid
} else {
continue mid + 1, high
}
} else if after_line_number < arr[mid].after_line_number {
continue low, mid
} else {
continue mid + 1, high
}
} nobreak {
low
}
}
///|
/// Change the uniform line height in pixels.
pub fn LinesLayout::set_default_line_height(
self : LinesLayout,
line_height : Int32,
) -> Unit {
self.default_line_height = line_height
}
///|
/// Changes the padding used to calculate vertical offsets.
pub fn LinesLayout::set_padding(
self : LinesLayout,
padding_top : Int32,
padding_bottom : Int32,
) -> Unit {
self.padding_top = padding_top
self.padding_bottom = padding_bottom
}
///|
/// Set the number of lines (Monaco's `onFlushed`).
pub fn LinesLayout::on_flushed(self : LinesLayout, line_count : Int32) -> Unit {
self.line_count = line_count
}
///|
/// Run `callback` with a whitespace-change accessor, then commit the batch.
/// Returns whether anything changed.
pub fn LinesLayout::change_whitespace(
self : LinesLayout,
callback : (WhitespaceChangeAccessor) -> Unit raise?,
) -> Bool raise? {
let (had_a_change, _) = self.change_whitespace_with_width(callback)
had_a_change
}
///|
fn LinesLayout::change_whitespace_with_width(
self : LinesLayout,
callback : (WhitespaceChangeAccessor) -> Unit raise?,
) -> (Bool, Bool) raise? {
let accessor = {
layout: self,
had_a_change: false,
min_width_update_requested: false,
}
// Monaco commits in `finally`: queued work survives a callback failure, and
// the queues are cleared before that failure continues to the caller.
defer self.pending_changes.commit(self)
callback(accessor)
(accessor.had_a_change, accessor.min_width_update_requested)
}
///|
fn LinesLayout::commit_pending_changes(
self : LinesLayout,
inserts : Array[EditorWhitespace],
changes : Array[PendingChange],
removes : Array[PendingRemove],
) -> Unit {
if inserts.length() > 0 ||
removes.length() > 0 ||
changes.any(change => change.new_min_width is Some(_)) {
self.min_width = -1
}
if inserts.length() + changes.length() + removes.length() <= 1 {
// when only one thing happened, handle it "delicately"
for insert in inserts {
self.insert_whitespace(insert)
}
for change in changes {
self.change_one_whitespace(
change.id,
change.new_after_line_number,
change.new_height,
change.new_min_width,
)
}
for remove in removes {
let index = self.find_whitespace_index(remove.id)
if index == -1 {
continue
}
self.remove_whitespace(index)
}
return
}
// simply rebuild the entire datastructure
let to_remove : Map[String, Bool] = Map([])
for remove in removes {
to_remove[remove.id] = true
}
let to_change : Map[String, PendingChange] = Map([])
for change in changes {
to_change[change.id] = change
}
let apply_remove_and_change = fn(
whitespaces : Array[EditorWhitespace],
) -> Array[EditorWhitespace] {
let result : Array[EditorWhitespace] = []
for whitespace in whitespaces {
if to_remove.contains(whitespace.id) {
continue
}
if to_change.get(whitespace.id) is Some(change) {
whitespace.after_line_number = change.new_after_line_number
whitespace.height = change.new_height
if change.new_min_width is Some(min_width) {
whitespace.min_width = min_width
}
}
result.push(whitespace)
}
result
}
let result = apply_remove_and_change(self.arr)
for w in apply_remove_and_change(inserts) {
result.push(w)
}
// ECMAScript Array.prototype.sort is stable. Preserve the source array's
// old-before-inserts sequence explicitly because MoonBit's backend sort is
// not required to keep equal-key elements in their original order.
let original_sequence : Map[String, Int32] = Map([])
for index, whitespace in result {
original_sequence[whitespace.id] = index
}
result.sort_by((a, b) => {
match a.after_line_number.compare(b.after_line_number) {
0 =>
match a.ordinal.compare(b.ordinal) {
0 => original_sequence[a.id].compare(original_sequence[b.id])
ordinal_order => ordinal_order
}
line_order => line_order
}
})
self.arr = result
self.prefix_sum_valid_index = -1
}
///|
fn LinesLayout::insert_whitespace(
self : LinesLayout,
whitespace : EditorWhitespace,
) -> Unit {
let insert_index = LinesLayout::find_insertion_index(
self.arr,
whitespace.after_line_number,
whitespace.ordinal,
)
self.arr.insert(insert_index, whitespace)
self.prefix_sum_valid_index = self.prefix_sum_valid_index.min(
insert_index - 1,
)
}
///|
fn LinesLayout::find_whitespace_index(self : LinesLayout, id : String) -> Int32 {
self.arr.search_by(whitespace => whitespace.id == id).unwrap_or(-1)
}
///|
fn LinesLayout::change_one_whitespace(
self : LinesLayout,
id : String,
new_after_line_number : Int32,
new_height : Int32,
new_min_width : Int32?,
) -> Unit {
let index = self.find_whitespace_index(id)
if index == -1 {
return
}
if self.arr[index].height != new_height {
self.arr[index].height = new_height
self.prefix_sum_valid_index = self.prefix_sum_valid_index.min(index - 1)
}
if new_min_width is Some(min_width) && self.arr[index].min_width != min_width {
self.arr[index].min_width = min_width
self.min_width = -1
}
if self.arr[index].after_line_number != new_after_line_number {
// `afterLineNumber` changed: removing then re-inserting may reorder it.
let whitespace = self.arr[index]
self.remove_whitespace(index)
whitespace.after_line_number = new_after_line_number
self.insert_whitespace(whitespace)
}
}
///|
fn LinesLayout::remove_whitespace(
self : LinesLayout,
remove_index : Int32,
) -> Unit {
self.arr.remove(remove_index) |> ignore
self.prefix_sum_valid_index = self.prefix_sum_valid_index.min(
remove_index - 1,
)
}
///|
/// Notify that a continuous zone of lines was deleted (inclusive bounds).
pub fn LinesLayout::on_lines_deleted(
self : LinesLayout,
from_line_number : Int32,
to_line_number : Int32,
) -> Unit {
self.line_count -= to_line_number - from_line_number + 1
for i in 0.. move whitespace to before first deleted line
self.arr[i].after_line_number = from_line_number - 1
} else if after_line_number > to_line_number {
// The line this whitespace was after has been moved up
// => move whitespace up
self.arr[i].after_line_number -= to_line_number - from_line_number + 1
}
}
}
///|
/// Notify that a continuous zone of lines was inserted (inclusive bounds).
pub fn LinesLayout::on_lines_inserted(
self : LinesLayout,
from_line_number : Int32,
to_line_number : Int32,
) -> Unit {
self.line_count += to_line_number - from_line_number + 1
for i in 0.. Double {
if self.arr.length() == 0 {
return 0.0
}
self.get_whitespaces_accumulated_height(self.arr.length() - 1)
}
///|
/// Sum of the heights of whitespaces in `[0..index]` (inclusive of `index`).
pub fn LinesLayout::get_whitespaces_accumulated_height(
self : LinesLayout,
index : Int32,
) -> Double {
let mut start_index = (0).max(self.prefix_sum_valid_index + 1)
if start_index == 0 {
self.arr[0].prefix_sum = self.arr[0].height.to_double()
start_index += 1
}
for i in start_index..<=index {
self.arr[i].prefix_sum = self.arr[i - 1].prefix_sum +
self.arr[i].height.to_double()
}
self.prefix_sum_valid_index = self.prefix_sum_valid_index.max(index)
self.arr[index].prefix_sum
}
///|
/// The sum of heights for all objects (lines + whitespaces + padding).
pub fn LinesLayout::get_lines_total_height(self : LinesLayout) -> Double {
let lines_height = self.accumulated_line_heights_including_line_number(
self.line_count,
)
let whitespaces_height = self.get_whitespaces_total_height()
lines_height +
whitespaces_height +
self.padding_top.to_double() +
self.padding_bottom.to_double()
}
///|
/// Accumulated height of whitespaces strictly before `line_number`.
pub fn LinesLayout::get_whitespace_accumulated_height_before_line_number(
self : LinesLayout,
line_number : Int32,
) -> Double {
let last_whitespace_before = self.find_last_whitespace_before_line_number(
line_number,
)
if last_whitespace_before == -1 {
return 0.0
}
self.get_whitespaces_accumulated_height(last_whitespace_before)
}
///|
fn LinesLayout::find_last_whitespace_before_line_number(
self : LinesLayout,
line_number : Int32,
) -> Int32 {
let arr = self.arr
for low = 0, high = arr.length() - 1; low <= high; {
let delta = high - low
let half_delta = delta / 2
let mid = low + half_delta
if arr[mid].after_line_number < line_number {
if mid + 1 >= arr.length() ||
arr[mid + 1].after_line_number >= line_number {
break mid
} else {
continue mid + 1, high
}
} else {
continue low, mid - 1
}
} nobreak {
-1
}
}
///|
fn LinesLayout::find_first_whitespace_after_line_number(
self : LinesLayout,
line_number : Int32,
) -> Int32 {
let last_whitespace_before = self.find_last_whitespace_before_line_number(
line_number,
)
let first_whitespace_after = last_whitespace_before + 1
if first_whitespace_after < self.arr.length() {
return first_whitespace_after
}
-1
}
///|
/// Index of the first whitespace with `afterLineNumber` >= `line_number`,
/// or -1 if there is none.
pub fn LinesLayout::get_first_whitespace_index_after_line_number(
self : LinesLayout,
line_number : Int32,
) -> Int32 {
self.find_first_whitespace_after_line_number(line_number)
}
///|
/// Vertical offset (sum of heights of everything above) of `line_number`.
pub fn LinesLayout::get_vertical_offset_for_line_number(
self : LinesLayout,
line_number : Int32,
include_view_zones~ : Bool = false,
) -> Double {
let previous_lines_height = if line_number > 1 {
self.accumulated_line_heights_including_line_number(line_number - 1)
} else {
0.0
}
let previous_whitespaces_height = self.get_whitespace_accumulated_height_before_line_number(
line_number - (if include_view_zones { 1 } else { 0 }),
)
previous_lines_height +
previous_whitespaces_height +
self.padding_top.to_double()
}
///|
/// The (uniform) height of `line_number`.
pub fn LinesLayout::get_line_height_for_line_number(
self : LinesLayout,
_line_number : Int32,
) -> Int32 {
self.default_line_height
}
///|
/// Vertical offset including `line_number`'s own height.
pub fn LinesLayout::get_vertical_offset_after_line_number(
self : LinesLayout,
line_number : Int32,
include_view_zones~ : Bool = false,
) -> Double {
let previous_lines_height = self.accumulated_line_heights_including_line_number(
line_number,
)
let previous_whitespaces_height = self.get_whitespace_accumulated_height_before_line_number(
line_number + (if include_view_zones { 1 } else { 0 }),
)
previous_lines_height +
previous_whitespaces_height +
self.padding_top.to_double()
}
///|
/// Whether there is any whitespace in the document.
pub fn LinesLayout::has_whitespace(self : LinesLayout) -> Bool {
self.get_whitespaces_count() > 0
}
///|
/// The maximum min width over all whitespaces.
pub fn LinesLayout::get_whitespace_min_width(self : LinesLayout) -> Int32 {
if self.min_width == -1 {
self.min_width = self.arr.fold(init=0, (widest, whitespace) => {
widest.max(whitespace.min_width)
})
}
self.min_width
}
///|
/// Whether `vertical_offset` is below all lines.
pub fn LinesLayout::is_after_lines(
self : LinesLayout,
vertical_offset : Double,
) -> Bool {
vertical_offset > self.get_lines_total_height()
}
///|
/// Whether `vertical_offset` falls in the top padding.
pub fn LinesLayout::is_in_top_padding(
self : LinesLayout,
vertical_offset : Double,
) -> Bool {
if self.padding_top == 0 {
return false
}
vertical_offset < self.padding_top.to_double()
}
///|
/// Whether `vertical_offset` falls in the bottom padding.
pub fn LinesLayout::is_in_bottom_padding(
self : LinesLayout,
vertical_offset : Double,
) -> Bool {
if self.padding_bottom == 0 {
return false
}
vertical_offset >=
self.get_lines_total_height() - self.padding_bottom.to_double()
}
///|
/// First line number at or after vertical offset `vertical_offset`.
pub fn LinesLayout::get_line_number_at_or_after_vertical_offset(
self : LinesLayout,
vertical_offset : Double,
) -> Int32 {
let vertical_offset = javascript_to_int32(vertical_offset)
if vertical_offset < 0 {
return 1
}
let lines_count = self.line_count
let min_line_number = for min_line_number = 1, max_line_number = lines_count; min_line_number <
max_line_number; {
let mid_line_number = min_line_number +
(max_line_number - min_line_number) / 2
let line_height = self.get_line_height_for_line_number(mid_line_number)
let mid_line_number_vertical_offset = javascript_to_int32(
self.get_vertical_offset_for_line_number(mid_line_number),
)
if vertical_offset.to_double() >=
mid_line_number_vertical_offset.to_double() + line_height.to_double() {
// vertical offset is after mid line number
continue mid_line_number + 1, max_line_number
} else if vertical_offset >= mid_line_number_vertical_offset {
// Hit
break mid_line_number
} else {
// vertical offset is before mid line number, but it could still be it
continue min_line_number, mid_line_number
}
} nobreak {
min_line_number
}
if min_line_number > lines_count {
return lines_count
}
min_line_number
}
///|
/// All lines (and their relative vertical offsets) positioned between
/// `vertical_offset1` and `vertical_offset2`.
pub fn LinesLayout::get_lines_viewport_data(
self : LinesLayout,
vertical_offset1 : Double,
vertical_offset2 : Double,
) -> PartialViewLinesViewportData {
let vertical_offset1 = javascript_to_int32(vertical_offset1)
let vertical_offset2 = javascript_to_int32(vertical_offset2)
// Find first line number (it might start before or after verticalOffset1).
let start_line_number = self.get_line_number_at_or_after_vertical_offset(
vertical_offset1.to_double(),
)
let start_line_number_vertical_offset = javascript_to_int32(
self.get_vertical_offset_for_line_number(start_line_number),
)
let mut end_line_number = self.line_count
// Also keep track of what whitespace we've got.
let mut whitespace_index = self.get_first_whitespace_index_after_line_number(
start_line_number,
)
let whitespace_count = self.get_whitespaces_count()
let mut current_whitespace_height = 0
let mut current_whitespace_after_line_number : Int32? = None
if whitespace_index == -1 {
whitespace_index = whitespace_count
current_whitespace_height = 0
} else {
current_whitespace_after_line_number = Some(
self.get_after_line_number_for_whitespace_index(whitespace_index),
)
current_whitespace_height = self.get_height_for_whitespace_index(
whitespace_index,
)
}
let mut current_vertical_offset = start_line_number_vertical_offset.to_double()
let mut current_line_relative_offset = current_vertical_offset
// IE cannot handle units above ~1.5M px, so step down past 500k px.
let step_size = 500000
let mut big_numbers_delta = 0
if start_line_number_vertical_offset >= step_size {
big_numbers_delta = start_line_number_vertical_offset /
step_size *
step_size
big_numbers_delta = big_numbers_delta /
self.default_line_height *
self.default_line_height
current_line_relative_offset -= big_numbers_delta.to_double()
}
let lines_offsets : Array[Double] = []
let vertical_center = vertical_offset1.to_double() +
(vertical_offset2.to_double() - vertical_offset1.to_double()) / 2.0
let mut centered_line_number = -1
// Figure out how far the lines go.
let mut line_number = start_line_number
while line_number <= end_line_number {
let line_height = self.get_line_height_for_line_number(line_number)
if centered_line_number == -1 {
let current_line_top = current_vertical_offset
let current_line_bottom = current_vertical_offset +
line_height.to_double()
if (
current_line_top <= vertical_center &&
vertical_center < current_line_bottom
) ||
current_line_top > vertical_center {
centered_line_number = line_number
}
}
// Count current line height in the vertical offsets.
current_vertical_offset += line_height.to_double()
lines_offsets.push(current_line_relative_offset)
// Next line starts immediately after this one.
current_line_relative_offset += line_height.to_double()
while current_whitespace_after_line_number is Some(matched) &&
matched == line_number {
// Push down next line with the height of the current whitespace.
current_line_relative_offset += current_whitespace_height.to_double()
// Count current whitespace in the vertical offsets.
current_vertical_offset += current_whitespace_height.to_double()
whitespace_index += 1
if whitespace_index >= whitespace_count {
current_whitespace_after_line_number = None
} else {
current_whitespace_after_line_number = Some(
self.get_after_line_number_for_whitespace_index(whitespace_index),
)
current_whitespace_height = self.get_height_for_whitespace_index(
whitespace_index,
)
}
}
if current_vertical_offset >= vertical_offset2.to_double() {
// We have covered the entire viewport area, time to stop.
end_line_number = line_number
break
}
if line_number == end_line_number {
break
}
line_number += 1
}
if centered_line_number == -1 {
centered_line_number = end_line_number
}
let end_line_number_vertical_offset = javascript_to_int32(
self.get_vertical_offset_for_line_number(end_line_number),
)
let mut completely_visible_start_line_number = start_line_number
let mut completely_visible_end_line_number = end_line_number
if completely_visible_start_line_number < completely_visible_end_line_number {
if start_line_number_vertical_offset < vertical_offset1 {
completely_visible_start_line_number += 1
}
}
if completely_visible_start_line_number < completely_visible_end_line_number {
let end_line_height = self.get_line_height_for_line_number(end_line_number)
if end_line_number_vertical_offset.to_double() + end_line_height.to_double() >
vertical_offset2.to_double() {
completely_visible_end_line_number -= 1
}
}
{
big_numbers_delta,
start_line_number,
end_line_number,
relative_vertical_offset: lines_offsets,
centered_line_number,
completely_visible_start_line_number,
completely_visible_end_line_number,
line_height: self.default_line_height,
}
}
///|
/// Vertical offset of the whitespace at `whitespace_index`.
pub fn LinesLayout::get_vertical_offset_for_whitespace_index(
self : LinesLayout,
whitespace_index : Int32,
) -> Double {
let after_line_number = self.get_after_line_number_for_whitespace_index(
whitespace_index,
)
let previous_lines_height = if after_line_number >= 1 {
self.accumulated_line_heights_including_line_number(after_line_number)
} else {
0.0
}
let previous_whitespaces_height = if whitespace_index > 0 {
self.get_whitespaces_accumulated_height(whitespace_index - 1)
} else {
0.0
}
previous_lines_height +
previous_whitespaces_height +
self.padding_top.to_double()
}
///|
/// Index of the first whitespace at or after vertical offset `vertical_offset`,
/// or -1 if there is none.
pub fn LinesLayout::get_whitespace_index_at_or_after_vertical_offset(
self : LinesLayout,
vertical_offset : Double,
) -> Int32 {
let vertical_offset = javascript_to_int32(vertical_offset).to_double()
let max_whitespace_index = self.get_whitespaces_count() - 1
if max_whitespace_index < 0 {
return -1
}
// Special case: nothing to be found.
let max_whitespace_vertical_offset = self.get_vertical_offset_for_whitespace_index(
max_whitespace_index,
)
let max_whitespace_height = self.get_height_for_whitespace_index(
max_whitespace_index,
)
if vertical_offset >=
max_whitespace_vertical_offset + max_whitespace_height.to_double() {
return -1
}
for min_whitespace_index = 0, max_whitespace_index = max_whitespace_index; min_whitespace_index <
max_whitespace_index; {
let mid_whitespace_index = min_whitespace_index +
(max_whitespace_index - min_whitespace_index) / 2
let mid_whitespace_vertical_offset = self.get_vertical_offset_for_whitespace_index(
mid_whitespace_index,
)
let mid_whitespace_height = self.get_height_for_whitespace_index(
mid_whitespace_index,
)
if vertical_offset >=
mid_whitespace_vertical_offset + mid_whitespace_height.to_double() {
// vertical offset is after whitespace
continue mid_whitespace_index + 1, max_whitespace_index
} else if vertical_offset >= mid_whitespace_vertical_offset {
// Hit
break mid_whitespace_index
} else {
// before whitespace, but mid index could still be the answer
continue min_whitespace_index, mid_whitespace_index
}
} nobreak {
min_whitespace_index
}
}
///|
/// Exactly the whitespace layouted at `vertical_offset`, or `None`.
pub fn LinesLayout::get_whitespace_at_vertical_offset(
self : LinesLayout,
vertical_offset : Double,
) -> ViewWhitespaceViewportData? {
let vertical_offset = javascript_to_int32(vertical_offset)
let candidate_index = self.get_whitespace_index_at_or_after_vertical_offset(
vertical_offset.to_double(),
)
if candidate_index < 0 {
return None
}
if candidate_index >= self.get_whitespaces_count() {
return None
}
let candidate_top = self.get_vertical_offset_for_whitespace_index(
candidate_index,
)
if candidate_top > vertical_offset.to_double() {
return None
}
let candidate_height = self.get_height_for_whitespace_index(candidate_index)
let candidate_id = self.get_id_for_whitespace_index(candidate_index)
let candidate_after_line_number = self.get_after_line_number_for_whitespace_index(
candidate_index,
)
Some({
id: candidate_id,
after_line_number: candidate_after_line_number,
vertical_offset: candidate_top,
height: candidate_height,
})
}
///|
/// All whitespaces positioned between `vertical_offset1` and `vertical_offset2`.
pub fn LinesLayout::get_whitespace_viewport_data(
self : LinesLayout,
vertical_offset1 : Double,
vertical_offset2 : Double,
) -> Array[ViewWhitespaceViewportData] {
let vertical_offset1 = javascript_to_int32(vertical_offset1)
let vertical_offset2 = javascript_to_int32(vertical_offset2)
let start_index = self.get_whitespace_index_at_or_after_vertical_offset(
vertical_offset1.to_double(),
)
let end_index = self.get_whitespaces_count() - 1
if start_index < 0 {
return []
}
let result : Array[ViewWhitespaceViewportData] = []
for i in start_index..<=end_index {
let top = self.get_vertical_offset_for_whitespace_index(i)
let height = self.get_height_for_whitespace_index(i)
if top >= vertical_offset2.to_double() {
break
}
result.push({
id: self.get_id_for_whitespace_index(i),
after_line_number: self.get_after_line_number_for_whitespace_index(i),
vertical_offset: top,
height,
})
}
result
}
///|
/// All whitespaces (a copy).
pub fn LinesLayout::get_whitespaces(
self : LinesLayout,
) -> Array[EditorWhitespace] {
self.arr.copy()
}
///|
/// The number of whitespaces.
pub fn LinesLayout::get_whitespaces_count(self : LinesLayout) -> Int32 {
self.arr.length()
}
///|
/// The `id` of the whitespace at `index`.
pub fn LinesLayout::get_id_for_whitespace_index(
self : LinesLayout,
index : Int32,
) -> String {
self.arr[index].id
}
///|
/// The `afterLineNumber` of the whitespace at `index`.
pub fn LinesLayout::get_after_line_number_for_whitespace_index(
self : LinesLayout,
index : Int32,
) -> Int32 {
self.arr[index].after_line_number
}
///|
/// The `height` of the whitespace at `index`.
pub fn LinesLayout::get_height_for_whitespace_index(
self : LinesLayout,
index : Int32,
) -> Int32 {
self.arr[index].height
}
///|
/// Uniform-height accumulation: sum of line heights for lines `1..=line_number`
/// (`0` for `line_number <= 0`). Replaces Monaco's `LineHeightsManager`.
fn LinesLayout::accumulated_line_heights_including_line_number(
self : LinesLayout,
line_number : Int32,
) -> Double {
if line_number <= 0 {
0.0
} else {
line_number.to_double() * self.default_line_height.to_double()
}
}
///|
pub extend EditorWhitespace with Debug::{to_repr}
///|
pub extend EditorWhitespace with Eq::{equal, not_equal}
///|
pub extend PartialViewLinesViewportData with Debug::{to_repr}
///|
pub extend PartialViewLinesViewportData with Eq::{equal, not_equal}
///|
pub extend ViewWhitespaceViewportData with Debug::{to_repr}
///|
pub extend ViewWhitespaceViewportData with Eq::{equal, not_equal}