///|
/// Immutable LF-normalized text plus cached line starts, the readonly analog
/// of Monaco's text buffer: the field pair is the piece tree's leaf
/// (`StringBuffer { buffer, lineStarts }`, `pieceTreeBase.ts:147`) and the
/// 1-based methods are the `IReadonlyTextBuffer` read surface
/// (`model.ts:1524`). Offsets and columns are UTF-16 code units so browser,
/// Monaco, and LSP-facing code share one convention. Document identity
/// (language id, version) lives on `TextModel`, not the buffer — same split as
/// Monaco.
struct TextSnapshot {
  text : String
  line_starts : Array[Int32]
} derive(Debug)

///|
/// Builds a snapshot by replacing every CRLF pair, lone CR, or LF with one LF
/// before deriving line starts. U+FEFF, U+2028/U+2029, surrogate code units,
/// and all other UTF-16 content are preserved verbatim.
pub fn TextSnapshot::TextSnapshot(text : String) -> TextSnapshot {
  let text = normalize_eol(text)
  let line_starts = build_line_starts(text)
  { text, line_starts, }
}

///|
/// Snapshot length in the repo's JavaScript-compatible UTF-16 indexing model.
/// Mirrors `IReadonlyTextBuffer.getLength`.
fn TextSnapshot::get_length(self : TextSnapshot) -> Int32 {
  self.text.length()
}

///|
/// Number of logical lines, including the final empty line after a trailing
/// newline.
pub fn TextSnapshot::line_count(self : TextSnapshot) -> Int32 {
  self.line_starts.length()
}

///|
/// Start offset for `line`, clamped into the snapshot's line range.
fn TextSnapshot::line_start_offset(self : TextSnapshot, line : Int32) -> Int32 {
  let line = line.clamp(min=0, max=self.line_count() - 1)
  self.line_starts[line]
}

///|
/// End offset for `line`, clamped into the snapshot's line range and excluding
/// the stored LF line break.
fn TextSnapshot::line_end_offset(self : TextSnapshot, line : Int32) -> Int32 {
  let line = line.clamp(min=0, max=self.line_count() - 1)
  if line + 1 < self.line_count() {
    self.line_starts[line + 1] - 1
  } else {
    self.get_length()
  }
}

///|
/// Text of one logical line without its line break. Zero-based, part of the
/// offset-centric internal layer (see the README parity ledger); the
/// `IReadonlyTextBuffer` surface is `get_line_content`.
pub fn TextSnapshot::line_text(self : TextSnapshot, line : Int32) -> String {
  let start = self.line_start_offset(line)
  let end = self.line_end_offset(line)
  code_unit_slice(self.text, start, end)
}

///|
/// Text of the one-based `line_number` without its line break, clamped into
/// the snapshot. Mirrors `IReadonlyTextBuffer.getLineContent` (the viewer
/// clamps where Monaco throws `Illegal value for lineNumber`).
pub fn TextSnapshot::get_line_content(
  self : TextSnapshot,
  line_number : Int32,
) -> String {
  self.line_text(line_number - 1)
}

///|
/// Full stored document text. Construction has already normalized every line
/// ending to LF, so this is the direct fixed-LF `getValue()` result.
pub fn TextSnapshot::get_value(self : TextSnapshot) -> String {
  self.text
}

///|
/// Stored model end-of-line sequence. Viewer snapshots have one fixed LF EOL
/// and expose no TextDefined/CRLF preference.
fn TextSnapshot::get_eol(_self : TextSnapshot) -> String {
  "\n"
}

///|
/// Text inside a 1-based line/column `Range`, interior line breaks emitted as
/// `\n`. Mirrors Monaco's `getValueInRange` at its default LF EOL. Columns are
/// clamped into each line.
fn TextSnapshot::get_value_in_range(
  self : TextSnapshot,
  range : @base_common.Range,
) -> String {
  let start_line = (range.start_line_number - 1).clamp(
    min=0,
    max=self.line_count() - 1,
  )
  let end_line = (range.end_line_number - 1).clamp(
    min=0,
    max=self.line_count() - 1,
  )
  if start_line == end_line {
    let line = self.line_text(start_line)
    let a = (range.start_column - 1).clamp(min=0, max=line.length())
    let b = (range.end_column - 1).clamp(min=a, max=line.length())
    return code_unit_slice(line, a, b)
  }
  let sb = StringBuilder()
  let first = self.line_text(start_line)
  let a = (range.start_column - 1).clamp(min=0, max=first.length())
  sb.write_string(code_unit_slice(first, a, first.length()))
  for line in (start_line + 1).. Int32 {
  self.get_value_in_range(range).length()
}

///|
/// Whole-buffer equality — `PieceTreeTextBuffer.equals` behind
/// `TextModel.equalsTextBuffer` (`textModel.ts:457-459`). The source compares
/// its selected-EOL representation; the Viewer compares its coherent
/// LF-normalized representation, including U+FEFF as ordinary content. Hosts
/// use it for the refresh no-op early return (`modelService.ts:370-373`).
pub fn TextSnapshot::equals(self : TextSnapshot, other : TextSnapshot) -> Bool {
  self.text == other.text
}

///|
/// Returns a clamped half-open slice of the snapshot text.
pub fn TextSnapshot::slice(
  self : TextSnapshot,
  range : @base_common.OffsetRange,
) -> String {
  let start = range.start.clamp(min=0, max=self.get_length())
  let end = range.end_exclusive.clamp(min=start, max=self.get_length())
  code_unit_slice(self.text, start, end)
}

///|
/// Zero-based line containing `offset`, with out-of-range offsets clamped to
/// the snapshot.
fn TextSnapshot::line_at_offset(self : TextSnapshot, offset : Int32) -> Int32 {
  let offset = offset.clamp(min=0, max=self.get_length())
  let mut found = 0
  for i in 0.. @base_common.Position {
  let offset = offset.clamp(min=0, max=self.get_length())
  let line = self.line_at_offset(offset)
  { line_number: line + 1, column: offset - self.line_starts[line] + 1, }
}

///|
/// Converts an offset span to a line/column `Range` (the boundary helper for
/// code that carries `Range` but holds offsets, e.g. the hover widget).
pub fn TextSnapshot::range_of(
  self : TextSnapshot,
  offset_range : @base_common.OffsetRange,
) -> @base_common.Range {
  @base_common.Range::from_positions(
    self.get_position_at(offset_range.start),
    self.get_position_at(offset_range.end_exclusive),
  )
}

///|
/// Converts a line/column `Range` to an offset span (inverse of `range_of`).
pub fn TextSnapshot::offset_range_of(
  self : TextSnapshot,
  range : @base_common.Range,
) -> @base_common.OffsetRange {
  OffsetRange(
    self.get_offset_at(range.start_line_number, range.start_column),
    self.get_offset_at(range.end_line_number, range.end_column),
  )
}

///|
/// Converts a one-based line/column pair to a clamped document offset,
/// undoing the +1 this boundary added in `get_position_at`. Mirrors
/// `IReadonlyTextBuffer.getOffsetAt` (same arity; the `Position`-taking
/// form is `TextModel::get_offset_at`).
pub fn TextSnapshot::get_offset_at(
  self : TextSnapshot,
  line_number : Int32,
  column : Int32,
) -> Int32 {
  let line = (line_number - 1).clamp(min=0, max=self.line_count() - 1)
  let start = self.line_start_offset(line)
  let end = self.line_end_offset(line)
  (start + column - 1).clamp(min=start, max=end)
}

///|
/// Length, in UTF-16 code units, of `line`'s content (1-based, EOL excluded).
/// Mirrors `IReadonlyTextBuffer.getLineLength`.
fn TextSnapshot::get_line_length(self : TextSnapshot, line : Int32) -> Int32 {
  self.line_text(line - 1).length()
}

///|
/// Max (1-based) column for `line`: content length + 1. Mirrors
/// `getLineMaxColumn`.
pub fn TextSnapshot::get_line_max_column(
  self : TextSnapshot,
  line : Int32,
) -> Int32 {
  self.get_line_length(line) + 1
}

///|
/// Code unit at 0-based `index` of `line`. The content endpoint of a non-final
/// line is its LF. A true-empty or non-empty final-line endpoint keeps the
/// pinned source's terminal failure.
fn TextSnapshot::get_line_char_code(
  self : TextSnapshot,
  line : Int32,
  index : Int32,
) -> Int32 {
  let content = self.line_text(line - 1)
  if line >= 1 && line < self.line_count() && index == content.length() {
    return 10
  }
  content[index].to_int()
}

///|
/// 1-based column of the first non-whitespace character on `line`, or `0` if the
/// line is empty/whitespace-only. Mirrors `getLineFirstNonWhitespaceColumn`.
pub fn TextSnapshot::get_line_first_non_whitespace_column(
  self : TextSnapshot,
  line : Int32,
) -> Int32 {
  let result = @base_common.first_non_whitespace_index(self.line_text(line - 1))
  if result == -1 {
    0
  } else {
    result + 1
  }
}

///|
/// 1-based column just past the last non-whitespace character on `line`, or `0`
/// if the line is empty/whitespace-only. Mirrors
/// `getLineLastNonWhitespaceColumn`.
fn TextSnapshot::get_line_last_non_whitespace_column(
  self : TextSnapshot,
  line : Int32,
) -> Int32 {
  let result = @base_common.last_non_whitespace_index(self.line_text(line - 1))
  if result == -1 {
    0
  } else {
    result + 2
  }
}

///|
/// Replaces every CRLF pair or lone CR with one LF. Existing LF and all other
/// UTF-16 units are preserved. Raw code-unit substrings deliberately avoid the
/// Unicode-scalar boundary checks of `StringView`, so lone low/high surrogates
/// beside an EOL are retained exactly once on both JS and native targets.
fn normalize_eol(text : String) -> String {
  let len = text.length()
  let sb = StringBuilder()
  let mut segment_start = 0
  let mut i = 0
  let mut changed = false
  while i < len {
    if text[i].to_int() == 13 {
      changed = true
      if segment_start < i {
        sb.write_string(code_unit_slice(text, segment_start, i))
      }
      sb.write_string("\n")
      if i + 1 < len && text[i + 1].to_int() == 10 {
        i += 2
      } else {
        i += 1
      }
      segment_start = i
    } else {
      i += 1
    }
  }
  if !changed {
    return text
  }
  if segment_start < len {
    sb.write_string(code_unit_slice(text, segment_start, len))
  }
  sb.to_string()
}

///|
/// Copies the half-open UTF-16 code-unit interval without requiring Unicode
/// scalar boundaries. Bounds are validated/clamped by each caller. Monaco's
/// JavaScript `substring` likewise permits ranges that begin/end at either
/// half of a surrogate pair and returns the corresponding lone code unit.
fn code_unit_slice(text : String, start : Int32, end : Int32) -> String {
  text.unsafe_substring(start~, end~)
}

///|
/// Offsets where each line begins in the normalized stored text (0-based).
/// Construction has already reduced every source EOL to LF, so only LF can
/// create a start. The raw workspace `DocumentSnapshot` scanner intentionally
/// remains separate on the provider side of this boundary.
fn build_line_starts(text : String) -> Array[Int32] {
  let starts = [0]
  for i in 0..