///|
/// Physical lines include their exact terminator, excluding a leading BOM.
pub(all) struct Line {
  start : Int
  content_end : Int
  end : Int
  number : Int
} derive(Eq, Debug)

///|
pub fn scan_lines(source : String) -> Array[Line] {
  let lines : Array[Line] = []
  let mut i = if source.length() > 0 && source[0] == '\uFEFF' { 1 } else { 0 }
  let mut number = 1
  while i < source.length() {
    let start = i
    while i < source.length() && source[i] != '\r' && source[i] != '\n' {
      i += 1
    }
    let content_end = i
    if i < source.length() {
      if source[i] == '\r' && i + 1 < source.length() && source[i + 1] == '\n' {
        i += 2
      } else {
        i += 1
      }
    }
    lines.push({ start, content_end, end: i, number })
    number += 1
  }
  lines
}

///|
fn horizontal(c : UInt16) -> Bool {
  c == ' ' || c == '\t'
}

///|
fn trim_bounds(source : String, start : Int, end : Int) -> (Int, Int) {
  let mut a = start
  let mut b = end
  while a < b && horizontal(source[a]) {
    a += 1
  }
  while b > a && horizontal(source[b - 1]) {
    b -= 1
  }
  (a, b)
}

///|
fn text(source : String, start : Int, end : Int) -> String {
  source[start:end].to_owned()
}

///|
fn line_span(line : Line, start : Int, end : Int) -> Span {
  span(start, end, line.number, start - line.start + 1)
}