///|
/// Return the number of logical lines in a source file.
pub fn SourceFile::line_count(self : SourceFile) -> Int {
  let mut count = 1
  for character in self.text {
    if character == '\n' {
      count += 1
    }
  }
  count
}

///|
/// Return the number of characters in the source text.
pub fn SourceFile::character_count(self : SourceFile) -> Int {
  self.text.to_array().length()
}

///|
/// Return whether an offset can identify a position in the source text.
pub fn SourceFile::contains_offset(self : SourceFile, offset : Int) -> Bool {
  offset >= 0 && offset <= self.character_count()
}

///|
/// Return the complete line containing an offset, without its newline.
pub fn SourceFile::line_text(self : SourceFile, offset : Int) -> String {
  let chars = self.text.to_array()
  let safe_offset = if offset < 0 {
    0
  } else if offset > chars.length() {
    chars.length()
  } else {
    offset
  }
  let mut line_start = safe_offset
  while line_start > 0 && chars[line_start - 1] != '\n' {
    line_start -= 1
  }
  let mut line_end = safe_offset
  while line_end < chars.length() && chars[line_end] != '\n' {
    line_end += 1
  }
  String::from_array(chars[line_start:line_end])
}

///|
/// Return all source lines in order, retaining empty lines.
pub fn SourceFile::lines(self : SourceFile) -> Array[String] {
  let result : Array[String] = []
  let current = StringBuilder()
  for character in self.text {
    if character == '\n' {
      result.push(current.to_string())
      current.reset()
    } else {
      current.write_char(character)
    }
  }
  result.push(current.to_string())
  result
}