///|
/// Text inserted into a rendered line without existing in the source document.
pub struct InjectedText {
position : @base_common.Position
text : String
class_name : String
source_index : Int32
} derive(Eq, Debug)
///|
/// Builds injected text at a model position.
pub fn InjectedText::InjectedText(
position : @base_common.Position,
text : String,
class_name : String,
source_index~ : Int32 = -1,
) -> InjectedText {
{ position, text, class_name, source_index, }
}
///|
/// Segment kind inside a projected line: source text or injected text.
pub enum ProjectedLinePartKind {
SourceSegment(model_start~ : Int32, model_end~ : Int32)
InjectedSegment(
model_column~ : Int32,
injected_text_index~ : Int32,
class_name~ : String
)
} derive(Eq, Debug)
///|
/// Half-open projected text segment with its source/injected identity.
pub struct ProjectedLinePart {
start : Int32
end : Int32
kind : ProjectedLinePartKind
} derive(Eq, Debug)
///|
/// One model line after injected text has been projected into display text,
/// with column maps in both directions.
///
/// Monaco's `ModelLineProjectionData` maps columns with offset arithmetic and
/// binary search (`translateToOutputPosition` / `translateToInputOffset`);
/// this type precomputes per-column lookup tables
/// (`model_to_projected_columns`, `model_columns`) for O(1) `.get()`. The
/// tables are first-write-wins, so a boundary column resolves to the *left*
/// of the injections anchored there — exactly Monaco's non-`Right` affinity
/// behavior; `model_column_to_projected_column` surfaces the `Right` side and
/// `normalize_projected_column_around_injections` ports the normalization
/// branches.
pub struct ProjectedTextLine {
text : String
model_line_length : Int32
model_columns : Array[Int32]
injected_text_indices : Array[Int32]
model_to_projected_columns : Array[Int32]
parts : Array[ProjectedLinePart]
} derive(Eq, Debug)
///|
/// Projection for a line with no injected text.
pub fn ProjectedTextLine::identity(line_text : String) -> ProjectedTextLine {
ProjectedTextLine::from_injected_text(line_text, [])
}
///|
/// Builds projected text and column maps for one model line plus injected text.
pub fn ProjectedTextLine::from_injected_text(
line_text : String,
injected_text : Array[InjectedText],
) -> ProjectedTextLine {
let normalized = normalize_injected_text(line_text, injected_text[:])
if normalized.length() == 0 {
return identity_projected_text_line(line_text)
}
let builder = StringBuilder()
let model_columns : Array[Int32] = [0]
let injected_text_indices : Array[Int32] = []
let model_to_projected_columns = initial_model_to_projected_columns(
line_text.length(),
)
let parts : Array[ProjectedLinePart] = []
let (source_cursor, view_cursor) = for
item in normalized
source_cursor = 0, view_cursor = 0 {
let column = item.position.column.clamp(min=0, max=line_text.length())
let (source_cursor, view_cursor) = if column > source_cursor {
append_source_text(
builder, line_text, source_cursor, column, model_columns, injected_text_indices,
model_to_projected_columns, parts, view_cursor,
)
(column, view_cursor + column - source_cursor)
} else {
(source_cursor, view_cursor)
}
append_injected_text(
builder, item, column, model_columns, injected_text_indices, parts, view_cursor,
)
continue source_cursor, view_cursor + item.text.length()
} nobreak {
(source_cursor, view_cursor)
}
let view_cursor = if source_cursor < line_text.length() {
append_source_text(
builder,
line_text,
source_cursor,
line_text.length(),
model_columns,
injected_text_indices,
model_to_projected_columns,
parts,
view_cursor,
)
view_cursor + line_text.length() - source_cursor
} else {
view_cursor
}
fill_unmapped_model_columns(model_to_projected_columns, view_cursor)
{
text: builder.to_string(),
model_line_length: line_text.length(),
model_columns,
injected_text_indices,
model_to_projected_columns,
parts,
}
}
///|
/// Maps a model column to a projected display column.
///
/// The injection branch of `translateToOutputPosition`
/// (`modelLineProjectionData.ts:111-128`): only `Right` resolves a boundary
/// column to the far side of every injection anchored there; every other
/// affinity (the `None` default included) stays left of them, which is what
/// the first-write-wins lookup table already encodes.
pub fn ProjectedTextLine::model_column_to_projected_column(
self : ProjectedTextLine,
model_column : Int32,
affinity? : @model.PositionAffinity,
) -> Int32 {
let column = model_column.clamp(min=0, max=self.model_line_length)
// Deliberately not `unwrap_or`: the clamped column always hits the lookup
// table, so the fallback must stay off the hot path of this mapping method.
let projected = match self.model_to_projected_columns.get(column) {
Some(projected_column) => projected_column
None => self.text.length()
}
match affinity {
Some(Right) =>
// Parts are in ascending projected order, so one pass walks every
// injection anchored at this model column.
for part in self.parts; result = projected {
if part.start == result &&
part.kind is InjectedSegment(model_column=anchor, ..) &&
anchor == column {
continue part.end
}
} nobreak {
result
}
_ => projected
}
}
///|
/// Snaps a projected column that touches injected text to one of its sides,
/// `normalizeOffsetInInputWithInjectionsAroundInjections`
/// (`modelLineProjectionData.ts:204-252`). Containment mirrors
/// `getInjectedTextAtOffset` (`:255-284`): inclusive on both ends, first
/// match wins. The viewer's injected text carries no `cursorStops`, so `None`
/// behaves as Monaco's default `InjectedTextCursorStops.Both`: only
/// strictly-inside columns snap (to the injection start); boundary columns
/// already are cursor stops.
pub fn ProjectedTextLine::normalize_projected_column_around_injections(
self : ProjectedTextLine,
projected_column : Int32,
affinity : @model.PositionAffinity,
) -> Int32 {
let parts = self.parts
let found = for index, part in parts {
guard part.kind is InjectedSegment(..) else { continue }
if part.start > projected_column {
// All later injections start even further right.
break -1
}
if projected_column <= part.end {
break index
}
} nobreak {
-1
}
if found < 0 {
return projected_column
}
let part = parts[found]
match affinity {
None =>
// cursorStops = Both: the injection's end is a right cursor stop, its
// start a left one.
if projected_column == part.end {
projected_column
} else {
part.start
}
Right | RightOfInjectedText => {
// Past every injection anchored at the same model column.
let anchor = injected_part_anchor(part)
for index = found + 1, result = part.end; index < parts.length() &&
parts[index].kind is InjectedSegment(model_column=next_anchor, ..) &&
next_anchor == anchor; {
continue index + 1, parts[index].end
} nobreak {
result
}
}
Left | LeftOfInjectedText => {
// Before every injection anchored at the same model column.
let anchor = injected_part_anchor(part)
for index = found - 1, result = part.start; index >= 0 &&
parts[index].kind is InjectedSegment(model_column=prev_anchor, ..) &&
prev_anchor == anchor; {
continue index - 1, parts[index].start
} nobreak {
result
}
}
}
}
///|
fn injected_part_anchor(part : ProjectedLinePart) -> Int32 {
match part.kind {
InjectedSegment(model_column~, ..) => model_column
SourceSegment(..) => -1
}
}
///|
/// Maps a projected display column back to a model column.
pub fn ProjectedTextLine::projected_column_to_model_column(
self : ProjectedTextLine,
projected_column : Int32,
) -> Int32 {
let column = projected_column.clamp(min=0, max=self.text.length())
self.model_columns.get(column).unwrap_or(self.model_line_length)
}
///|
/// Returns the injected-text source index at a projected display column, or
/// `-1` for source text.
pub fn ProjectedTextLine::injected_text_index_at_projected_column(
self : ProjectedTextLine,
projected_column : Int32,
) -> Int32 {
let column = projected_column.clamp(min=0, max=self.text.length())
self.injected_text_indices.get(column).unwrap_or(-1)
}
///|
fn identity_projected_text_line(line_text : String) -> ProjectedTextLine {
let length = line_text.length()
let model_columns = Array::makei(length + 1, column => column)
let injected_text_indices = Array::make(length, -1)
let model_to_projected_columns = Array::makei(length + 1, column => column)
let parts : Array[ProjectedLinePart] = []
if length > 0 {
parts.push({
start: 0,
end: length,
kind: SourceSegment(model_start=0, model_end=length),
})
}
{
text: line_text,
model_line_length: length,
model_columns,
injected_text_indices,
model_to_projected_columns,
parts,
}
}
///|
fn normalize_injected_text(
line_text : String,
injected_text : ArrayView[InjectedText],
) -> Array[InjectedText] {
let normalized : Array[InjectedText] = []
for item in injected_text {
if item.text.length() > 0 {
let column = item.position.column.clamp(min=0, max=line_text.length())
normalized.push({ ..item, position: { ..item.position, column, }, })
}
}
normalized.sort_by((left, right) => {
if left.position.column != right.position.column {
left.position.column - right.position.column
} else {
left.source_index - right.source_index
}
})
normalized
}
///|
fn initial_model_to_projected_columns(line_length : Int32) -> Array[Int32] {
let columns = Array::make(line_length + 1, -1)
columns[0] = 0
columns
}
///|
fn append_source_text(
builder : StringBuilder,
line_text : String,
start_column : Int32,
end_column : Int32,
model_columns : Array[Int32],
injected_text_indices : Array[Int32],
model_to_projected_columns : Array[Int32],
parts : Array[ProjectedLinePart],
view_cursor : Int32,
) -> Unit {
builder.write_view(line_text[start_column:end_column])
let mut view = view_cursor
for column in start_column.. Unit {
builder.write_string(item.text)
let mut view = view_cursor
for _ in 0.. Unit {
for index, projected_column in model_to_projected_columns; last = fallback {
if projected_column >= 0 {
continue projected_column
} else {
model_to_projected_columns[index] = last
}
}
}
///|
pub extend InjectedText with Debug::{to_repr}
///|
pub extend InjectedText with Eq::{equal, not_equal}
///|
pub extend ProjectedLinePart with Debug::{to_repr}
///|
pub extend ProjectedLinePart with Eq::{equal, not_equal}
///|
pub extend ProjectedLinePartKind with Debug::{to_repr}
///|
pub extend ProjectedLinePartKind with Eq::{equal, not_equal}
///|
pub extend ProjectedTextLine with Debug::{to_repr}
///|
pub extend ProjectedTextLine with Eq::{equal, not_equal}