///| Behavior port of vscode's `LineTokens` / `SliceLineTokens`.
///
/// Source: `src/vs/editor/common/tokens/lineTokens.ts` (pinned `vscode/`
/// submodule). The binary token store is a flat `Array[UInt]` mirroring
/// Monaco's `Uint32Array`: even slots hold a token's exclusive end offset,
/// odd slots its packed metadata word (see `encoded_token_attributes.mbt`).
/// The codec parameter is `@services.LanguageIdCodec`
/// (`services/languages_registry.mbt`), the `ILanguageIdCodec` role
/// `lineTokens.ts` takes.
///
/// Deviations from the source, none of which the conformance suite exercises:
/// - `ViewLineTokens` omits `equals`, the `languageIdCodec` field accessor,
/// and `getInlineStyle` stays via `color_map : Array[String]`.
/// - `LineTokens` omits `getTokensInRange` / `toString` (these pull in the
/// unported `TokenArray` family).
/// - The constructor skips Monaco's soft token-length/text-length mismatch
/// warning (`onUnexpectedError`); the readonly viewer has no such reporter.
/// - `with_inserted` guards the middle-insert metadata read against a
/// past-end token index (Monaco reads `undefined`); the read is unreachable
/// for valid in-bounds insert offsets.
/// - MoonBit's scalar-validating `StringView` cannot represent Monaco's raw
/// UTF-16 slices at lone-surrogate or pair-interior boundaries, so every
/// token-content slice uses `String::unsafe_substring`.
///|
/// A line's full or sliced token view. Package consumers see an abstract type
/// and use the common read operations below.
enum ViewLineTokens {
Full(LineTokens)
Slice(SliceLineTokens)
}
///|
/// A token sequence backed by interleaved end-offset and metadata words.
///
/// For token `i`, `tokens[i << 1]` is its exclusive UTF-16 end offset and
/// `tokens[(i << 1) + 1]` is its packed metadata. Its start offset is `0` for
/// the first token and the previous token's end offset otherwise. For example,
/// `[3U, a, 4U, b, 5U, c]` over `"let x"` describes `"let"` at `[0, 3)`, a
/// space at `[3, 4)`, and `"x"` at `[4, 5)`.
///
/// Direct-constructor callers must supply an even number of words with
/// nondecreasing end offsets. For a complete line with at least one token, the
/// last end offset is the text's UTF-16 length.
struct LineTokens {
tokens : Array[UInt32]
tokens_count : Int32
text : String
language_id_codec : @services.LanguageIdCodec
} derive(Debug)
///|
pub fn LineTokens::LineTokens(
tokens : Array[UInt32],
text : String,
decoder : @services.LanguageIdCodec,
) -> LineTokens {
{
tokens,
tokens_count: tokens.length() >> 1,
text,
language_id_codec: decoder,
}
}
///|
/// The default metadata value (no font style, default fg/bg colors).
pub fn LineTokens::default_token_metadata() -> TokenMetadata {
TokenMetadata(
((FONT_STYLE_NONE << METADATA_FONT_STYLE_OFFSET) |
(COLOR_ID_DEFAULT_FOREGROUND << METADATA_FOREGROUND_OFFSET) |
(COLOR_ID_DEFAULT_BACKGROUND << METADATA_BACKGROUND_OFFSET)).reinterpret_as_uint(),
)
}
///|
/// Rewrites `tokens` start-offset slots in place to be end offsets.
pub fn LineTokens::convert_to_end_offset(
tokens : Array[UInt32],
line_text_length : Int32,
) -> Unit {
let token_count = tokens.length() >> 1
let last_token_index = token_count - 1
for token_index in 0.. Int32 {
if tokens.length() <= 2 {
return 0
}
for low = 0, high = (tokens.length() >> 1) - 1; low < high; {
let mid = low + (high - low) / 2
let end_offset = tokens[mid << 1].reinterpret_as_int()
if end_offset == desired_index {
break mid + 1
} else if end_offset < desired_index {
continue mid + 1, high
} else {
continue low, mid
}
} nobreak {
low
}
}
///|
pub fn LineTokens::get_start_offset(
self : LineTokens,
token_index : Int32,
) -> Int32 {
if token_index > 0 {
self.tokens[(token_index - 1) << 1].reinterpret_as_int()
} else {
0
}
}
///|
/// `inflate()` returns the tokens unchanged — already a view.
pub fn LineTokens::inflate(self : LineTokens) -> ViewLineTokens {
Full(self)
}
///|
pub fn LineTokens::slice_and_inflate(
self : LineTokens,
start_offset : Int32,
end_offset : Int32,
delta_offset : Int32,
) -> ViewLineTokens {
Slice(SliceLineTokens(self, start_offset, end_offset, delta_offset))
}
///|
/// A token to splice into the line. `insert_tokens` must be sorted by offset.
struct InsertedToken {
offset : Int32
text : String
token_metadata : TokenMetadata
}
///|
pub fn InsertedToken::InsertedToken(
offset : Int32,
text : String,
token_metadata : TokenMetadata,
) -> InsertedToken {
{ offset, text, token_metadata, }
}
///|
/// Returns a new `LineTokens` with `insert_tokens` spliced into the text and
/// token boundaries. Pure; the receiver is unchanged.
pub fn LineTokens::with_inserted(
self : LineTokens,
insert_tokens : Array[InsertedToken],
) -> LineTokens {
if insert_tokens.length() == 0 {
return self
}
let mut next_original_token_idx = 0
let mut next_insert_token_idx = 0
let mut text = ""
let new_tokens : Array[UInt32] = []
let mut original_end_offset = 0
while true {
let next_original_token_end_offset = if next_original_token_idx <
self.tokens_count {
self.tokens[next_original_token_idx << 1].reinterpret_as_int()
} else {
-1
}
let has_insert = next_insert_token_idx < insert_tokens.length()
if next_original_token_end_offset != -1 &&
(
!has_insert ||
next_original_token_end_offset <=
insert_tokens[next_insert_token_idx].offset
) {
// original token ends before the next insert token
text += substr(
self.text,
original_end_offset,
next_original_token_end_offset,
)
let metadata = self.tokens[(next_original_token_idx << 1) + 1]
new_tokens.push(text.length().reinterpret_as_uint())
new_tokens.push(metadata)
next_original_token_idx += 1
original_end_offset = next_original_token_end_offset
} else if has_insert {
let next_insert_token = insert_tokens[next_insert_token_idx]
if next_insert_token.offset > original_end_offset {
// insert token is in the middle of the next token
text += substr(self.text, original_end_offset, next_insert_token.offset)
let metadata = if next_original_token_idx < self.tokens_count {
self.tokens[(next_original_token_idx << 1) + 1]
} else {
LineTokens::default_token_metadata().to_uint()
}
new_tokens.push(text.length().reinterpret_as_uint())
new_tokens.push(metadata)
original_end_offset = next_insert_token.offset
}
text += next_insert_token.text
new_tokens.push(text.length().reinterpret_as_uint())
new_tokens.push(next_insert_token.token_metadata.to_uint())
next_insert_token_idx += 1
} else {
break
}
}
LineTokens(new_tokens, text, self.language_id_codec)
}
///|
pub fn LineTokens::get_count(self : LineTokens) -> Int32 {
self.tokens_count
}
///|
pub fn LineTokens::get_line_content(self : LineTokens) -> String {
self.text
}
///|
fn LineTokens::get_end_offset(self : LineTokens, token_index : Int32) -> Int32 {
self.tokens[token_index << 1].reinterpret_as_int()
}
///|
pub fn LineTokens::get_metadata(
self : LineTokens,
token_index : Int32,
) -> TokenMetadata {
TokenMetadata(self.tokens[(token_index << 1) + 1])
}
///|
fn LineTokens::get_class_name(self : LineTokens, token_index : Int32) -> String {
self.get_metadata(token_index).get_class_name()
}
///|
fn LineTokens::get_inline_style(
self : LineTokens,
token_index : Int32,
color_map : Array[String],
) -> String {
self.get_metadata(token_index).get_inline_style(color_map)
}
///|
fn LineTokens::find_token_index_at_offset(
self : LineTokens,
offset : Int32,
) -> Int32 {
LineTokens::find_index_in_tokens_array(self.tokens, offset)
}
///|
fn LineTokens::get_token_text(self : LineTokens, token_index : Int32) -> String {
substr(
self.text,
self.get_start_offset(token_index),
self.get_end_offset(token_index),
)
}
///|
/// A read-only window onto a `LineTokens`, offset by `delta_offset`. Mirrors
/// Monaco's `SliceLineTokens`.
priv struct SliceLineTokens {
source : LineTokens
start_offset : Int32
end_offset : Int32
delta_offset : Int32
first_token_index : Int32
tokens_count : Int32
}
///|
fn SliceLineTokens::SliceLineTokens(
source : LineTokens,
start_offset : Int32,
end_offset : Int32,
delta_offset : Int32,
) -> SliceLineTokens {
let first_token_index = source.find_token_index_at_offset(start_offset)
let tokens_count = for i in first_token_index..= end_offset {
break count
}
continue count + 1
} nobreak {
count
}
{
source,
start_offset,
end_offset,
delta_offset,
first_token_index,
tokens_count,
}
}
///|
fn SliceLineTokens::get_count(self : SliceLineTokens) -> Int32 {
self.tokens_count
}
///|
fn SliceLineTokens::get_line_content(self : SliceLineTokens) -> String {
substr(self.source.get_line_content(), self.start_offset, self.end_offset)
}
///|
fn SliceLineTokens::get_end_offset(
self : SliceLineTokens,
token_index : Int32,
) -> Int32 {
let token_end_offset = self.source.get_end_offset(
self.first_token_index + token_index,
)
self.end_offset.min(token_end_offset) - self.start_offset + self.delta_offset
}
///|
fn SliceLineTokens::get_class_name(
self : SliceLineTokens,
token_index : Int32,
) -> String {
self.source.get_class_name(self.first_token_index + token_index)
}
///|
fn SliceLineTokens::get_inline_style(
self : SliceLineTokens,
token_index : Int32,
color_map : Array[String],
) -> String {
self.source.get_inline_style(self.first_token_index + token_index, color_map)
}
///|
fn SliceLineTokens::get_token_text(
self : SliceLineTokens,
token_index : Int32,
) -> String {
let adjusted_token_index = self.first_token_index + token_index
let token_start_offset = self.source.get_start_offset(adjusted_token_index)
let token_end_offset = self.source.get_end_offset(adjusted_token_index)
let mut text = self.source.get_token_text(adjusted_token_index)
if token_start_offset < self.start_offset {
text = substr(text, self.start_offset - token_start_offset, text.length())
}
if token_end_offset > self.end_offset {
text = substr(text, 0, text.length() - (token_end_offset - self.end_offset))
}
text
}
///|
pub fn ViewLineTokens::get_count(self : ViewLineTokens) -> Int32 {
match self {
Full(tokens) => tokens.get_count()
Slice(tokens) => tokens.get_count()
}
}
///|
pub fn ViewLineTokens::get_line_content(self : ViewLineTokens) -> String {
match self {
Full(tokens) => tokens.get_line_content()
Slice(tokens) => tokens.get_line_content()
}
}
///|
pub fn ViewLineTokens::get_end_offset(
self : ViewLineTokens,
token_index : Int32,
) -> Int32 {
match self {
Full(tokens) => tokens.get_end_offset(token_index)
Slice(tokens) => tokens.get_end_offset(token_index)
}
}
///|
pub fn ViewLineTokens::get_class_name(
self : ViewLineTokens,
token_index : Int32,
) -> String {
match self {
Full(tokens) => tokens.get_class_name(token_index)
Slice(tokens) => tokens.get_class_name(token_index)
}
}
///|
pub fn ViewLineTokens::get_inline_style(
self : ViewLineTokens,
token_index : Int32,
color_map : Array[String],
) -> String {
match self {
Full(tokens) => tokens.get_inline_style(token_index, color_map)
Slice(tokens) => tokens.get_inline_style(token_index, color_map)
}
}
///|
pub fn ViewLineTokens::get_token_text(
self : ViewLineTokens,
token_index : Int32,
) -> String {
match self {
Full(tokens) => tokens.get_token_text(token_index)
Slice(tokens) => tokens.get_token_text(token_index)
}
}
///|
/// Raw UTF-16 code-unit substring `[start, end)`. Unlike `StringView`, this
/// preserves lone surrogates and either half of a valid pair.
fn substr(s : String, start : Int32, end : Int32) -> String {
s.unsafe_substring(start~, end~)
}