///|
pub(all) enum Mode {
  Normal
  Insert
  Select
  Command
} derive(Eq, Debug)

///|
pub(all) enum EditorCommand {
  Key(String)
  Ex(String)
  Quit
} derive(Eq, Debug)

///|
pub(all) struct Cursor {
  mut row : Int
  mut col : Int
} derive(Eq, Debug)

///|
pub(all) struct Document {
  mut path : String?
  mut lines : Array[String]
  mut dirty : Bool
} derive(Eq, Debug)

///|
pub(all) struct Editor {
  mut doc : Document
  mut mode : Mode
  mut cursor : Cursor
  mut selection_anchor : Cursor?
  mut register : String
  mut register_is_line : Bool
  mut status : String
  mut should_quit : Bool
  undo_stack : Array[String]
  redo_stack : Array[String]
} derive(Eq, Debug)

///|
pub(all) struct DriverResult {
  editor : Editor
  log : Array[String]
} derive(Eq, Debug)

///|
fn clamp(value : Int, low : Int, high : Int) -> Int {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn line_len(line : String) -> Int {
  line.to_array().length()
}

///|
fn char_string(ch : Char) -> String {
  String::from_array([ch])
}

///|
fn hex_digit(value : Int) -> Char {
  match value {
    0 => '0'
    1 => '1'
    2 => '2'
    3 => '3'
    4 => '4'
    5 => '5'
    6 => '6'
    7 => '7'
    8 => '8'
    9 => '9'
    10 => 'A'
    11 => 'B'
    12 => 'C'
    13 => 'D'
    14 => 'E'
    _ => 'F'
  }
}

///|
fn hex_code(value : Int) -> String {
  let chars : Array[Char] = []
  let mut n = value
  if n == 0 {
    chars.push('0')
  } else {
    while n > 0 {
      chars.insert(0, hex_digit(n % 16))
      n = n / 16
    }
  }
  while chars.length() < 4 {
    chars.insert(0, '0')
  }
  String::from_array(chars)
}

///|
enum WordCategory {
  WhitespaceChar
  WordChar
  PunctuationChar
} derive(Eq, Debug)

///|
fn is_ascii_punctuation_char(ch : Char) -> Bool {
  let code = ch.to_int()
  (code >= 33 && code <= 47) ||
  (code >= 58 && code <= 64) ||
  (code >= 91 && code <= 96) ||
  (code >= 123 && code <= 126)
}

///|
fn is_unicode_punctuation_char(ch : Char) -> Bool {
  let code = ch.to_int()
  (code >= 0x2000 && code <= 0x206F) ||
  (code >= 0x3000 && code <= 0x303F) ||
  (code >= 0xFF01 && code <= 0xFF0F) ||
  (code >= 0xFF1A && code <= 0xFF20) ||
  (code >= 0xFF3B && code <= 0xFF40) ||
  (code >= 0xFF5B && code <= 0xFF65)
}

///|
fn char_category(ch : Char) -> WordCategory {
  if ch.is_whitespace() {
    WhitespaceChar
  } else if is_ascii_punctuation_char(ch) || is_unicode_punctuation_char(ch) {
    PunctuationChar
  } else {
    WordChar
  }
}

///|
fn movement_category(ch : Char, long : Bool) -> WordCategory {
  if long {
    if char_category(ch) == WhitespaceChar {
      WhitespaceChar
    } else {
      WordChar
    }
  } else {
    char_category(ch)
  }
}

///|
fn is_word_char(ch : Char) -> Bool {
  char_category(ch) == WordChar
}

///|
fn is_blank_char(ch : Char) -> Bool {
  ch == ' ' || ch.to_int() == 9
}

///|
fn lower_char(ch : Char) -> Char {
  let code = ch.to_int()
  if code >= 65 && code <= 90 {
    match (code + 32).to_char() {
      Some(value) => value
      None => ch
    }
  } else {
    ch
  }
}

///|
fn upper_char(ch : Char) -> Char {
  let code = ch.to_int()
  if code >= 97 && code <= 122 {
    match (code - 32).to_char() {
      Some(value) => value
      None => ch
    }
  } else {
    ch
  }
}

///|
fn switch_case_char(ch : Char) -> Char {
  let lower = lower_char(ch)
  if lower != ch {
    lower
  } else {
    upper_char(ch)
  }
}

///|
fn transform_case_char(ch : Char, kind : Int) -> Char {
  if kind == 1 {
    lower_char(ch)
  } else if kind == 2 {
    upper_char(ch)
  } else {
    switch_case_char(ch)
  }
}

///|
fn is_digit_char(ch : Char) -> Bool {
  let code = ch.to_int()
  code >= 48 && code <= 57
}

///|
fn digit_value(ch : Char) -> Int {
  ch.to_int() - 48
}

///|
fn int_to_string(value : Int) -> String {
  if value == 0 {
    return "0"
  }
  let chars : Array[Char] = []
  let mut n = value
  let negative = n < 0
  if negative {
    n = 0 - n
  }
  while n > 0 {
    chars.insert(0, hex_digit(n % 10))
    n = n / 10
  }
  if negative {
    chars.insert(0, '-')
  }
  String::from_array(chars)
}

///|
fn first_non_whitespace_col(line : String) -> Int {
  let chars = line.to_array()
  let mut col = 0
  while col < chars.length() && is_blank_char(chars[col]) {
    col += 1
  }
  col
}

///|
fn normalize_lines(lines : Array[String]) -> Array[String] {
  if lines.length() == 0 {
    [""]
  } else {
    lines
  }
}

///|
fn split_lines(text : String) -> Array[String] {
  let lines : Array[String] = []
  for line in text.split("\n") {
    lines.push(line.to_owned())
  }
  normalize_lines(lines)
}

///|
pub fn document(path? : String, text? : String = "") -> Document {
  { path, lines: split_lines(text), dirty: false }
}

///|
pub fn empty_editor() -> Editor {
  {
    doc: document(),
    mode: Normal,
    cursor: { row: 0, col: 0 },
    selection_anchor: None,
    register: "",
    register_is_line: false,
    status: "meolix ready",
    should_quit: false,
    undo_stack: [],
    redo_stack: [],
  }
}

///|
pub fn open_text(path : String, text : String) -> Editor {
  {
    doc: { path: Some(path), lines: split_lines(text), dirty: false },
    mode: Normal,
    cursor: { row: 0, col: 0 },
    selection_anchor: None,
    register: "",
    register_is_line: false,
    status: "opened \{path}",
    should_quit: false,
    undo_stack: [],
    redo_stack: [],
  }
}

///|
fn Editor::current_line(self : Editor) -> String {
  self.doc.lines[self.cursor.row]
}

///|
fn cursor_copy(cursor : Cursor) -> Cursor {
  { row: cursor.row, col: cursor.col }
}

///|
fn cursor_before(a : Cursor, b : Cursor) -> Bool {
  a.row < b.row || (a.row == b.row && a.col <= b.col)
}

///|
fn cursor_strict_before(a : Cursor, b : Cursor) -> Bool {
  a.row < b.row || (a.row == b.row && a.col < b.col)
}

///|
fn Editor::remember(self : Editor) -> Unit {
  self.undo_stack.push(self.buffer_text())
  self.redo_stack.clear()
}

///|
fn Editor::sync_cursor(self : Editor) -> Unit {
  self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
  self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}

///|
pub fn Editor::set_status(self : Editor, value : String) -> Unit {
  self.status = value
}

///|
pub fn Editor::move_left(self : Editor) -> Unit {
  if self.cursor.col > 0 {
    self.cursor.col -= 1
  }
  self.sync_cursor()
}

///|
pub fn Editor::move_right(self : Editor) -> Unit {
  self.cursor.col = clamp(self.cursor.col + 1, 0, line_len(self.current_line()))
  self.sync_cursor()
}

///|
pub fn Editor::move_up(self : Editor) -> Unit {
  if self.cursor.row > 0 {
    self.cursor.row -= 1
  }
  self.sync_cursor()
}

///|
pub fn Editor::move_down(self : Editor) -> Unit {
  if self.cursor.row + 1 < self.doc.lines.length() {
    self.cursor.row += 1
  }
  self.sync_cursor()
}

///|
pub fn Editor::move_line_start(self : Editor) -> Unit {
  self.cursor.col = 0
}

///|
pub fn Editor::move_line_end(self : Editor) -> Unit {
  self.cursor.col = line_len(self.current_line())
}

///|
pub fn Editor::move_to_column(self : Editor, column : Int) -> Unit {
  self.cursor.col = clamp(column - 1, 0, line_len(self.current_line()))
  self.sync_cursor()
  self.set_status("column \{self.cursor.col + 1}")
}

///|
pub fn Editor::move_line_first_non_whitespace(self : Editor) -> Unit {
  self.cursor.col = first_non_whitespace_col(self.current_line())
  self.sync_cursor()
}

///|
pub fn Editor::character_info(self : Editor) -> String {
  let chars = self.current_line().to_array()
  if chars.length() == 0 {
    "empty line"
  } else {
    let col = clamp(self.cursor.col, 0, chars.length() - 1)
    let ch = chars[col]
    "'\{char_string(ch)}' U+\{hex_code(ch.to_int())} decimal \{ch.to_int()}"
  }
}

///|
pub fn Editor::move_file_start(self : Editor) -> Unit {
  self.cursor.row = 0
  self.cursor.col = 0
}

///|
pub fn Editor::move_file_end(self : Editor) -> Unit {
  self.cursor.row = self.doc.lines.length() - 1
  self.cursor.col = 0
  self.sync_cursor()
}

///|
fn Editor::category_at(
  self : Editor,
  row : Int,
  col : Int,
  long : Bool,
) -> WordCategory {
  if row < 0 || row >= self.doc.lines.length() {
    WhitespaceChar
  } else {
    let chars = self.doc.lines[row].to_array()
    if col < 0 || col >= chars.length() {
      WhitespaceChar
    } else {
      movement_category(chars[col], long)
    }
  }
}

///|
fn Editor::find_next_word_start(self : Editor, long : Bool) -> Cursor {
  let start = self.category_at(self.cursor.row, self.cursor.col, long)
  let mut row = self.cursor.row
  let mut col = self.cursor.col + 1
  let mut skipping_start_segment = true
  while row < self.doc.lines.length() {
    let chars = self.doc.lines[row].to_array()
    if skipping_start_segment {
      while col < chars.length() && movement_category(chars[col], long) == start {
        col += 1
      }
      skipping_start_segment = false
    }
    while col < chars.length() &&
          movement_category(chars[col], long) == WhitespaceChar {
      col += 1
    }
    if col < chars.length() {
      return { row, col }
    }
    row += 1
    col = 0
  }
  {
    row: self.doc.lines.length() - 1,
    col: line_len(self.doc.lines[self.doc.lines.length() - 1]),
  }
}

///|
fn Editor::find_prev_char(self : Editor, row : Int, col : Int) -> Cursor? {
  let mut row = row
  let mut col = col
  while row >= 0 {
    let len = line_len(self.doc.lines[row])
    if col > len {
      col = len
    }
    if col > 0 {
      return Some({ row, col: col - 1 })
    }
    row -= 1
    if row >= 0 {
      col = line_len(self.doc.lines[row])
    }
  }
  None
}

///|
fn Editor::segment_start_at(
  self : Editor,
  row : Int,
  col : Int,
  long : Bool,
) -> Cursor {
  let cat = self.category_at(row, col, long)
  let mut start = col
  let chars = self.doc.lines[row].to_array()
  while start > 0 && movement_category(chars[start - 1], long) == cat {
    start -= 1
  }
  { row, col: start }
}

///|
fn Editor::segment_end_at(
  self : Editor,
  row : Int,
  col : Int,
  long : Bool,
) -> Cursor {
  let cat = self.category_at(row, col, long)
  let chars = self.doc.lines[row].to_array()
  let mut finish = col
  while finish + 1 < chars.length() &&
        movement_category(chars[finish + 1], long) == cat {
    finish += 1
  }
  { row, col: finish }
}

///|
fn Editor::find_prev_word_start(self : Editor, long : Bool) -> Cursor {
  let chars = self.current_line().to_array()
  if self.cursor.col < chars.length() {
    let cat = movement_category(chars[self.cursor.col], long)
    if cat != WhitespaceChar &&
      self.cursor.col > 0 &&
      movement_category(chars[self.cursor.col - 1], long) == cat {
      return self.segment_start_at(self.cursor.row, self.cursor.col, long)
    }
  }
  let mut pos = self.find_prev_char(self.cursor.row, self.cursor.col)
  while pos is Some(cursor) {
    let cat = self.category_at(cursor.row, cursor.col, long)
    if cat != WhitespaceChar {
      return self.segment_start_at(cursor.row, cursor.col, long)
    }
    pos = self.find_prev_char(cursor.row, cursor.col)
  }
  { row: 0, col: 0 }
}

///|
fn Editor::find_next_word_end(self : Editor, long : Bool) -> Cursor {
  let chars = self.current_line().to_array()
  if self.cursor.col < chars.length() {
    let cat = movement_category(chars[self.cursor.col], long)
    if cat != WhitespaceChar &&
      self.cursor.col + 1 < chars.length() &&
      movement_category(chars[self.cursor.col + 1], long) == cat {
      return self.segment_end_at(self.cursor.row, self.cursor.col, long)
    }
  }
  let start = self.find_next_word_start(long)
  self.segment_end_at(start.row, start.col, long)
}

///|
pub fn Editor::move_next_word(self : Editor) -> Unit {
  let target = self.find_next_word_start(false)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_prev_word(self : Editor) -> Unit {
  let target = self.find_prev_word_start(false)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_next_word_end(self : Editor) -> Unit {
  let target = self.find_next_word_end(false)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_next_long_word(self : Editor) -> Unit {
  let target = self.find_next_word_start(true)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_prev_long_word(self : Editor) -> Unit {
  let target = self.find_prev_word_start(true)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_next_long_word_end(self : Editor) -> Unit {
  let target = self.find_next_word_end(true)
  self.cursor.row = target.row
  self.cursor.col = target.col
  self.sync_cursor()
}

///|
pub fn Editor::move_paragraph_next(self : Editor) -> Unit {
  let mut row = self.cursor.row + 1
  while row < self.doc.lines.length() && self.doc.lines[row].trim() != "" {
    row += 1
  }
  while row < self.doc.lines.length() && self.doc.lines[row].trim() == "" {
    row += 1
  }
  self.cursor.row = clamp(row, 0, self.doc.lines.length() - 1)
  self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}

///|
pub fn Editor::move_paragraph_prev(self : Editor) -> Unit {
  let mut row = self.cursor.row - 1
  while row >= 0 && self.doc.lines[row].trim() != "" {
    row -= 1
  }
  while row >= 0 && self.doc.lines[row].trim() == "" {
    row -= 1
  }
  self.cursor.row = clamp(row + 1, 0, self.doc.lines.length() - 1)
  self.cursor.col = clamp(self.cursor.col, 0, line_len(self.current_line()))
}

///|
fn Editor::replace_current_line(self : Editor, value : String) -> Unit {
  self.doc.lines[self.cursor.row] = value
  self.doc.dirty = true
  self.sync_cursor()
}

///|
pub fn Editor::insert_text(self : Editor, text : String) -> Unit {
  for part_index, part in text.split("\n").iter2() {
    if part_index > 0 {
      self.split_line()
    }
    for ch in part {
      self.insert_char(ch)
    }
  }
}

///|
pub fn Editor::insert_completion(self : Editor, text : String) -> Unit {
  self.remember()
  let chars = self.current_line().to_array()
  let col = clamp(self.cursor.col, 0, chars.length())
  let mut start = col
  while start > 0 && is_word_char(chars[start - 1]) {
    start -= 1
  }
  for _ in start.. ignore
  }
  for ch in text {
    chars.insert(start, ch)
    start += 1
  }
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = start
  self.doc.dirty = true
}

///|
pub fn Editor::replace_range(
  self : Editor,
  start_row : Int,
  start_col : Int,
  end_row : Int,
  end_col : Int,
  text : String,
) -> Unit {
  self.remember()
  let row1 = clamp(start_row, 0, self.doc.lines.length() - 1)
  let row2 = clamp(end_row, row1, self.doc.lines.length() - 1)
  let line1 = self.doc.lines[row1].to_array()
  let line2 = self.doc.lines[row2].to_array()
  let col1 = clamp(start_col, 0, line1.length())
  let col2 = clamp(end_col, 0, line2.length())
  let before = String::from_array(line1[:col1].to_owned())
  let after = String::from_array(line2[col2:].to_owned())
  let parts = split_lines(text)
  for _ in row1..<(row2 + 1) {
    self.doc.lines.remove(row1) |> ignore
  }
  if parts.length() == 0 {
    self.doc.lines.insert(row1, before + after)
    self.cursor.row = row1
    self.cursor.col = line_len(before)
  } else if parts.length() == 1 {
    self.doc.lines.insert(row1, before + parts[0] + after)
    self.cursor.row = row1
    self.cursor.col = line_len(before + parts[0])
  } else {
    self.doc.lines.insert(row1, before + parts[0])
    for index in 1..<(parts.length() - 1) {
      self.doc.lines.insert(row1 + index, parts[index])
    }
    self.doc.lines.insert(
      row1 + parts.length() - 1,
      parts[parts.length() - 1] + after,
    )
    self.cursor.row = row1 + parts.length() - 1
    self.cursor.col = line_len(parts[parts.length() - 1])
  }
  if self.doc.lines.length() == 0 {
    self.doc.lines.push("")
  }
  self.selection_anchor = None
  self.mode = Normal
  self.doc.dirty = true
  self.sync_cursor()
  self.set_status("applied edit")
}

///|
pub fn Editor::insert_char(self : Editor, ch : Char) -> Unit {
  self.remember()
  let chars = self.current_line().to_array()
  let col = clamp(self.cursor.col, 0, chars.length())
  chars.insert(col, ch)
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = col + 1
}

///|
pub fn Editor::split_line(self : Editor) -> Unit {
  self.remember()
  let chars = self.current_line().to_array()
  let col = clamp(self.cursor.col, 0, chars.length())
  let left = chars[0:col].to_owned()
  let right = chars[col:].to_owned()
  self.doc.lines[self.cursor.row] = String::from_array(left)
  self.doc.lines.insert(self.cursor.row + 1, String::from_array(right))
  self.cursor.row += 1
  self.cursor.col = 0
  self.doc.dirty = true
}

///|
pub fn Editor::delete_char(self : Editor) -> Unit {
  let chars = self.current_line().to_array()
  if chars.length() == 0 {
    if self.doc.lines.length() > 1 {
      self.remember()
      self.register = self.doc.lines.remove(self.cursor.row)
      self.register_is_line = true
      self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
      self.cursor.col = 0
      self.doc.dirty = true
    }
    return
  }
  if self.cursor.col >= chars.length() {
    if self.cursor.row + 1 < self.doc.lines.length() {
      self.remember()
      let next = self.doc.lines.remove(self.cursor.row + 1)
      self.doc.lines[self.cursor.row] = self.current_line() + next
      self.doc.dirty = true
      self.sync_cursor()
    }
    return
  }
  self.remember()
  let col = self.cursor.col
  let removed = chars.remove(col)
  self.register = String::from_array([removed])
  self.register_is_line = false
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = clamp(col, 0, line_len(self.current_line()))
}

///|
pub fn Editor::delete_line(self : Editor) -> Unit {
  self.remember()
  self.register = self.doc.lines.remove(self.cursor.row)
  self.register_is_line = true
  if self.doc.lines.length() == 0 {
    self.doc.lines.push("")
  }
  self.cursor.row = clamp(self.cursor.row, 0, self.doc.lines.length() - 1)
  self.cursor.col = 0
  self.doc.dirty = true
  self.set_status("deleted line")
}

///|
pub fn Editor::yank_line(self : Editor) -> Unit {
  self.register = self.current_line()
  self.register_is_line = true
  self.set_status("yanked line")
}

///|
pub fn Editor::paste_after(self : Editor) -> Unit {
  guard self.register != "" else {
    self.set_status("register is empty")
    return
  }
  self.remember()
  if self.register_is_line {
    self.doc.lines.insert(self.cursor.row + 1, self.register)
    self.cursor.row += 1
    self.cursor.col = 0
  } else if self.register.contains("\n") {
    for line in self.register.split("\n") {
      self.doc.lines.insert(self.cursor.row + 1, line.to_owned())
      self.cursor.row += 1
    }
    self.cursor.col = 0
  } else {
    self.cursor.col = clamp(
      self.cursor.col + 1,
      0,
      line_len(self.current_line()),
    )
    self.insert_text(self.register)
  }
  self.doc.dirty = true
  self.set_status("pasted")
}

///|
pub fn Editor::paste_before(self : Editor) -> Unit {
  guard self.register != "" else {
    self.set_status("register is empty")
    return
  }
  self.remember()
  if self.register_is_line {
    self.doc.lines.insert(self.cursor.row, self.register)
    self.cursor.col = 0
  } else if self.register.contains("\n") {
    for line in self.register.split("\n") {
      self.doc.lines.insert(self.cursor.row, line.to_owned())
      self.cursor.row += 1
    }
    self.cursor.col = 0
  } else {
    self.insert_text(self.register)
  }
  self.doc.dirty = true
  self.set_status("pasted before")
}

///|
pub fn Editor::open_below(self : Editor) -> Unit {
  self.remember()
  self.doc.lines.insert(self.cursor.row + 1, "")
  self.cursor.row += 1
  self.cursor.col = 0
  self.mode = Insert
  self.doc.dirty = true
}

///|
pub fn Editor::open_above(self : Editor) -> Unit {
  self.remember()
  self.doc.lines.insert(self.cursor.row, "")
  self.cursor.col = 0
  self.mode = Insert
  self.doc.dirty = true
}

///|
pub fn Editor::add_newline_below(self : Editor) -> Unit {
  self.remember()
  self.doc.lines.insert(self.cursor.row + 1, "")
  self.doc.dirty = true
  self.set_status("added line below")
}

///|
pub fn Editor::add_newline_above(self : Editor) -> Unit {
  self.remember()
  self.doc.lines.insert(self.cursor.row, "")
  self.doc.dirty = true
  self.set_status("added line above")
}

///|
pub fn Editor::backspace(self : Editor) -> Unit {
  if self.cursor.col > 0 {
    self.remember()
    let chars = self.current_line().to_array()
    chars.remove(self.cursor.col - 1) |> ignore
    self.cursor.col -= 1
    self.replace_current_line(String::from_array(chars))
  } else if self.cursor.row > 0 {
    self.remember()
    let previous_len = line_len(self.doc.lines[self.cursor.row - 1])
    let line = self.doc.lines.remove(self.cursor.row)
    self.cursor.row -= 1
    self.cursor.col = previous_len
    self.doc.lines[self.cursor.row] = self.doc.lines[self.cursor.row] + line
    self.doc.dirty = true
  }
}

///|
pub fn Editor::delete_word_backward(self : Editor) -> Unit {
  if self.cursor.col == 0 {
    self.backspace()
    return
  }
  self.remember()
  let chars = self.current_line().to_array()
  let end = clamp(self.cursor.col, 0, chars.length())
  let mut start = end - 1
  while start >= 0 && !is_word_char(chars[start]) {
    start -= 1
  }
  while start > 0 && is_word_char(chars[start - 1]) {
    start -= 1
  }
  let from = clamp(start, 0, end)
  for _ in from.. ignore
  }
  self.cursor.col = from
  self.replace_current_line(String::from_array(chars))
  self.set_status("deleted word backward")
}

///|
pub fn Editor::delete_word_forward(self : Editor) -> Unit {
  let chars = self.current_line().to_array()
  if self.cursor.col >= chars.length() {
    self.delete_char()
    return
  }
  self.remember()
  let start = clamp(self.cursor.col, 0, chars.length())
  let mut end = start
  while end < chars.length() && !is_word_char(chars[end]) {
    end += 1
  }
  while end < chars.length() && is_word_char(chars[end]) {
    end += 1
  }
  for _ in start.. ignore
  }
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = start
  self.set_status("deleted word forward")
}

///|
pub fn Editor::delete_to_line_start(self : Editor) -> Unit {
  if self.cursor.col == 0 {
    return
  }
  self.remember()
  let chars = self.current_line().to_array()
  let end = clamp(self.cursor.col, 0, chars.length())
  for _ in 0.. ignore
  }
  self.cursor.col = 0
  self.replace_current_line(String::from_array(chars))
  self.set_status("deleted to line start")
}

///|
pub fn Editor::delete_to_line_end(self : Editor) -> Unit {
  let chars = self.current_line().to_array()
  let start = clamp(self.cursor.col, 0, chars.length())
  if start == chars.length() {
    return
  }
  self.remember()
  while chars.length() > start {
    chars.remove(start) |> ignore
  }
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = start
  self.set_status("deleted to line end")
}

///|
pub fn Editor::undo(self : Editor) -> Unit {
  if self.undo_stack.length() == 0 {
    self.set_status("nothing to undo")
    return
  }
  self.redo_stack.push(self.buffer_text())
  let previous = self.undo_stack.remove(self.undo_stack.length() - 1)
  self.doc.lines = split_lines(previous)
  self.doc.dirty = true
  self.sync_cursor()
  self.set_status("undo")
}

///|
pub fn Editor::redo(self : Editor) -> Unit {
  if self.redo_stack.length() == 0 {
    self.set_status("nothing to redo")
    return
  }
  self.undo_stack.push(self.buffer_text())
  let next = self.redo_stack.remove(self.redo_stack.length() - 1)
  self.doc.lines = split_lines(next)
  self.doc.dirty = true
  self.sync_cursor()
  self.set_status("redo")
}

///|
pub fn Editor::enter_insert(self : Editor) -> Unit {
  self.mode = Insert
  self.selection_anchor = None
  self.set_status("-- INSERT --")
}

///|
pub fn Editor::enter_normal(self : Editor) -> Unit {
  self.mode = Normal
  self.selection_anchor = None
  self.sync_cursor()
  self.set_status("-- NORMAL --")
}

///|
pub fn Editor::enter_select(self : Editor) -> Unit {
  self.mode = Select
  self.selection_anchor = Some({ row: self.cursor.row, col: self.cursor.col })
  self.set_status("-- SELECT --")
}

///|
pub fn Editor::insert_at_line_start(self : Editor) -> Unit {
  self.move_line_first_non_whitespace()
  self.enter_insert()
}

///|
pub fn Editor::insert_at_line_end(self : Editor) -> Unit {
  self.move_line_end()
  self.enter_insert()
}

///|
pub fn Editor::selection_bounds(self : Editor) -> (Cursor, Cursor)? {
  match self.selection_anchor {
    None => None
    Some(anchor) =>
      if cursor_before(anchor, self.cursor) {
        Some((cursor_copy(anchor), cursor_copy(self.cursor)))
      } else {
        Some((cursor_copy(self.cursor), cursor_copy(anchor)))
      }
  }
}

///|
pub fn Editor::select_line(self : Editor) -> Unit {
  let (start_row, target_row) = match self.selection_bounds() {
    Some((start, finish)) if self.mode == Select =>
      (start.row, clamp(finish.row + 1, 0, self.doc.lines.length() - 1))
    _ => (self.cursor.row, self.cursor.row)
  }
  self.mode = Select
  self.selection_anchor = Some({ row: start_row, col: 0 })
  self.cursor.row = target_row
  self.cursor.col = clamp(
    line_len(self.current_line()) - 1,
    0,
    line_len(self.current_line()),
  )
  self.set_status("selected line")
}

///|
pub fn Editor::extend_selection_to_line_bounds(self : Editor) -> Unit {
  let (start_row, finish_row) = match self.selection_bounds() {
    None => (self.cursor.row, self.cursor.row)
    Some((start, finish)) => (start.row, finish.row)
  }
  self.mode = Select
  self.selection_anchor = Some({ row: start_row, col: 0 })
  self.cursor.row = finish_row
  self.cursor.col = clamp(
    line_len(self.doc.lines[finish_row]) - 1,
    0,
    line_len(self.doc.lines[finish_row]),
  )
  self.set_status("extended to line bounds")
}

///|
pub fn Editor::select_all(self : Editor) -> Unit {
  self.mode = Select
  self.selection_anchor = Some({ row: 0, col: 0 })
  self.cursor.row = self.doc.lines.length() - 1
  let last_len = line_len(self.current_line())
  self.cursor.col = clamp(last_len - 1, 0, last_len)
  self.set_status("selected file")
}

///|
pub fn Editor::collapse_selection(self : Editor) -> Unit {
  self.selection_anchor = None
  self.mode = Normal
  self.set_status("collapsed selection")
}

///|
pub fn Editor::flip_selection(self : Editor) -> Unit {
  match self.selection_anchor {
    None => self.set_status("no selection to flip")
    Some(anchor) => {
      self.selection_anchor = Some({
        row: self.cursor.row,
        col: self.cursor.col,
      })
      self.cursor = anchor
      self.mode = Select
      self.set_status("flipped selection")
    }
  }
}

///|
pub fn Editor::is_selected(self : Editor, row : Int, col : Int) -> Bool {
  match self.selection_bounds() {
    None => false
    Some((start, finish)) =>
      if row < start.row || row > finish.row {
        false
      } else if start.row == finish.row {
        col >= start.col && col <= finish.col
      } else if row == start.row {
        col >= start.col
      } else if row == finish.row {
        col <= finish.col
      } else {
        true
      }
  }
}

///|
fn Editor::selection_is_full_lines(self : Editor) -> Bool {
  match self.selection_bounds() {
    None => false
    Some((start, finish)) =>
      start.col == 0 &&
      finish.col >=
      clamp(
        line_len(self.doc.lines[finish.row]) - 1,
        0,
        line_len(self.doc.lines[finish.row]),
      )
  }
}

///|
fn Editor::target_line_range(self : Editor) -> (Int, Int) {
  match self.selection_bounds() {
    None => (self.cursor.row, self.cursor.row)
    Some((start, finish)) => (start.row, finish.row)
  }
}

///|
pub fn Editor::indent_selection(self : Editor) -> Unit {
  self.remember()
  let (start_row, finish_row) = self.target_line_range()
  for row in start_row..<(finish_row + 1) {
    self.doc.lines[row] = "  " + self.doc.lines[row]
  }
  if self.cursor.row >= start_row && self.cursor.row <= finish_row {
    self.cursor.col += 2
  }
  match self.selection_anchor {
    None => ()
    Some(anchor) =>
      if anchor.row >= start_row && anchor.row <= finish_row {
        self.selection_anchor = Some({ row: anchor.row, col: anchor.col + 2 })
      }
  }
  self.doc.dirty = true
  self.set_status("indented")
}

///|
fn leading_indent_width(line : String, spaces : Int) -> Int {
  let chars = line.to_array()
  if chars.length() == 0 {
    0
  } else if chars[0].to_int() == 9 {
    1
  } else {
    let mut count = 0
    while count < chars.length() && count < spaces && chars[count] == ' ' {
      count += 1
    }
    count
  }
}

///|
pub fn Editor::unindent_selection(self : Editor) -> Unit {
  self.remember()
  let (start_row, finish_row) = self.target_line_range()
  for row in start_row..<(finish_row + 1) {
    let chars = self.doc.lines[row].to_array()
    let remove = leading_indent_width(self.doc.lines[row], 2)
    for _ in 0.. ignore
    }
    self.doc.lines[row] = String::from_array(chars)
    if row == self.cursor.row {
      self.cursor.col = clamp(
        self.cursor.col - remove,
        0,
        line_len(self.current_line()),
      )
    }
    match self.selection_anchor {
      None => ()
      Some(anchor) =>
        if row == anchor.row {
          self.selection_anchor = Some({
            row: anchor.row,
            col: clamp(anchor.col - remove, 0, line_len(self.doc.lines[row])),
          })
        }
    }
  }
  self.doc.dirty = true
  self.set_status("unindented")
}

///|
pub fn Editor::trim_selection(self : Editor) -> Unit {
  match self.selection_bounds() {
    None => self.set_status("no selection to trim")
    Some((start, finish)) => {
      let mut trimmed_start : Cursor? = None
      for row in start.row..<(finish.row + 1) {
        let chars = self.doc.lines[row].to_array()
        let from = if row == start.row {
          clamp(start.col, 0, chars.length())
        } else {
          0
        }
        let to = if row == finish.row {
          clamp(finish.col + 1, from, chars.length())
        } else {
          chars.length()
        }
        for col in from..= start.row {
        let chars = self.doc.lines[row].to_array()
        let from = if row == start.row {
          clamp(start.col, 0, chars.length())
        } else {
          0
        }
        let mut col = if row == finish.row {
          clamp(finish.col + 1, from, chars.length()) - 1
        } else {
          chars.length() - 1
        }
        while col >= from {
          if !chars[col].is_whitespace() && trimmed_finish is None {
            trimmed_finish = Some({ row, col })
          }
          col -= 1
        }
        row -= 1
      }
      match (trimmed_start, trimmed_finish) {
        (Some(new_start), Some(new_finish)) => {
          self.selection_anchor = Some(new_start)
          self.cursor = new_finish
          self.mode = Select
          self.set_status("trimmed selection")
        }
        _ => self.set_status("selection is whitespace")
      }
    }
  }
}

///|
pub fn Editor::change_number_under_cursor(self : Editor, delta : Int) -> Unit {
  let chars = self.current_line().to_array()
  if chars.length() == 0 {
    self.set_status("no number on line")
    return
  }
  let mut at = clamp(self.cursor.col, 0, chars.length() - 1)
  if !is_digit_char(chars[at]) {
    if chars[at] == '-' &&
      at + 1 < chars.length() &&
      is_digit_char(chars[at + 1]) {
      at += 1
    } else {
      while at < chars.length() && !is_digit_char(chars[at]) {
        at += 1
      }
      if at >= chars.length() {
        self.set_status("no number on line")
        return
      }
    }
  }
  let mut start = at
  while start > 0 && is_digit_char(chars[start - 1]) {
    start -= 1
  }
  let sign_start = if start > 0 && chars[start - 1] == '-' {
    start - 1
  } else {
    start
  }
  let mut finish = at + 1
  while finish < chars.length() && is_digit_char(chars[finish]) {
    finish += 1
  }
  let mut value = 0
  for col in start.. ignore
  }
  let mut insert_at = sign_start
  for ch in replacement {
    chars.insert(insert_at, ch)
    insert_at += 1
  }
  self.replace_current_line(String::from_array(chars))
  self.cursor.col = clamp(insert_at - 1, 0, line_len(self.current_line()))
  self.set_status("number \{value + delta}")
}

///|
fn find_substring(line : String, query : String) -> Int? {
  find_substring_from(line, query, 0)
}

///|
fn find_substring_from(line : String, query : String, from : Int) -> Int? {
  let haystack = line.to_array()
  let needle = query.to_array()
  if needle.length() == 0 || needle.length() > haystack.length() {
    return None
  }
  let first = clamp(from, 0, haystack.length())
  if first + needle.length() > haystack.length() {
    return None
  }
  for start in first..<(haystack.length() - needle.length() + 1) {
    let mut matched = true
    for offset in 0.. Int? {
  let haystack = line.to_array()
  let needle = query.to_array()
  if needle.length() == 0 || needle.length() > haystack.length() {
    return None
  }
  if before < 0 {
    return None
  }
  let max_end = clamp(before, 0, haystack.length() - 1)
  if max_end + 1 < needle.length() {
    return None
  }
  let last_start = clamp(
    max_end - needle.length() + 1,
    0,
    haystack.length() - needle.length(),
  )
  let mut found : Int? = None
  for start in 0..<(last_start + 1) {
    let mut matched = true
    for offset in 0.. String {
  match self.selection_bounds() {
    None => {
      let chars = self.current_line().to_array()
      if chars.length() == 0 {
        ""
      } else {
        let col = clamp(self.cursor.col, 0, chars.length() - 1)
        String::from_array([chars[col]])
      }
    }
    Some((start, finish)) =>
      if self.selection_is_full_lines() {
        let parts : Array[String] = []
        for row in start.row..<(finish.row + 1) {
          parts.push(self.doc.lines[row])
        }
        parts.join("\n")
      } else if start.row == finish.row {
        let chars = self.doc.lines[start.row].to_array()
        let start_col = clamp(start.col, 0, chars.length())
        let end_col = clamp(finish.col + 1, start_col, chars.length())
        String::from_array(chars[start_col:end_col].to_owned())
      } else {
        let parts : Array[String] = []
        let first_chars = self.doc.lines[start.row].to_array()
        let first_col = clamp(start.col, 0, first_chars.length())
        parts.push(String::from_array(first_chars[first_col:].to_owned()))
        for row in (start.row + 1).. String {
  match self.selection_bounds() {
    Some(_) => self.selected_text().trim().to_owned()
    None => {
      let chars = self.current_line().to_array()
      if chars.length() == 0 {
        return ""
      }
      let col = clamp(self.cursor.col, 0, chars.length() - 1)
      if chars[col].is_whitespace() {
        ""
      } else if is_word_char(chars[col]) {
        let mut start = col
        while start > 0 && is_word_char(chars[start - 1]) {
          start -= 1
        }
        let mut finish = col + 1
        while finish < chars.length() && is_word_char(chars[finish]) {
          finish += 1
        }
        String::from_array(chars[start:finish].to_owned())
      } else {
        char_string(chars[col])
      }
    }
  }
}

///|
pub fn Editor::search_word_under_cursor(self : Editor) -> String? {
  let query = self.word_under_cursor()
  if query == "" {
    self.set_status("no word under cursor")
    None
  } else {
    self.search_forward(query) |> ignore
    Some(query)
  }
}

///|
pub fn Editor::yank_selection(self : Editor) -> Unit {
  let text = self.selected_text()
  if text == "" {
    self.set_status("nothing selected")
  } else {
    self.register = text
    self.register_is_line = self.selection_is_full_lines()
    self.set_status("yanked selection")
  }
}

///|
fn Editor::replace_selection_text(
  self : Editor,
  text : String,
  linewise : Bool,
) -> Unit {
  if linewise {
    self.delete_selection()
    self.register = text
    self.register_is_line = true
    let lines = split_lines(text)
    for line in lines {
      self.doc.lines.insert(self.cursor.row, line)
      self.cursor.row += 1
    }
    self.cursor.row = clamp(self.cursor.row - 1, 0, self.doc.lines.length() - 1)
    self.cursor.col = 0
  } else {
    self.delete_selection()
    self.register = text
    self.register_is_line = false
    self.insert_text(text)
  }
  self.mode = Normal
  self.selection_anchor = None
  self.doc.dirty = true
}

///|
pub fn Editor::change_selection(self : Editor) -> Unit {
  match self.selection_bounds() {
    Some((start, finish)) if self.selection_is_full_lines() => {
      self.register = self.selected_text()
      self.register_is_line = true
      self.remember()
      for _ in start.row..<(finish.row + 1) {
        self.doc.lines.remove(start.row) |> ignore
      }
      self.doc.lines.insert(start.row, "")
      self.cursor.row = clamp(start.row, 0, self.doc.lines.length() - 1)
      self.cursor.col = 0
      self.selection_anchor = None
      self.mode = Insert
      self.doc.dirty = true
      self.set_status("-- INSERT change --")
    }
    _ => {
      self.delete_selection()
      self.enter_insert()
      self.set_status("-- INSERT change --")
    }
  }
}

///|
pub fn Editor::delete_selection_without_yank(self : Editor) -> Unit {
  let saved = self.register
  let saved_line = self.register_is_line
  self.delete_selection()
  self.register = saved
  self.register_is_line = saved_line
  self.set_status("deleted selection")
}

///|
pub fn Editor::change_selection_without_yank(self : Editor) -> Unit {
  let saved = self.register
  let saved_line = self.register_is_line
  self.change_selection()
  self.register = saved
  self.register_is_line = saved_line
}

///|
pub fn Editor::replace_with_yanked(self : Editor) -> Unit {
  guard self.register != "" else {
    self.set_status("register is empty")
    return
  }
  let saved = self.register
  let saved_line = self.register_is_line
  self.replace_selection_text(saved, saved_line)
  self.register = saved
  self.register_is_line = saved_line
  self.set_status("replaced with yanked text")
}

///|
pub fn Editor::replace_selection_with_char(self : Editor, ch : Char) -> Unit {
  self.remember()
  match self.selection_bounds() {
    None => {
      let chars = self.current_line().to_array()
      if chars.length() > 0 {
        let col = clamp(self.cursor.col, 0, chars.length() - 1)
        chars[col] = ch
        self.replace_current_line(String::from_array(chars))
      }
    }
    Some((start, finish)) => {
      for row in start.row..<(finish.row + 1) {
        let chars = self.doc.lines[row].to_array()
        let from = if row == start.row {
          clamp(start.col, 0, chars.length())
        } else {
          0
        }
        let to = if row == finish.row {
          clamp(finish.col + 1, from, chars.length())
        } else {
          chars.length()
        }
        for col in from.. Unit {
  self.remember()
  match self.selection_bounds() {
    None => {
      let chars = self.current_line().to_array()
      if chars.length() > 0 {
        let col = clamp(self.cursor.col, 0, chars.length() - 1)
        chars[col] = transform_case_char(chars[col], kind)
        self.replace_current_line(String::from_array(chars))
      }
    }
    Some((start, finish)) =>
      for row in start.row..<(finish.row + 1) {
        let chars = self.doc.lines[row].to_array()
        let from = if row == start.row {
          clamp(start.col, 0, chars.length())
        } else {
          0
        }
        let to = if row == finish.row {
          clamp(finish.col + 1, from, chars.length())
        } else {
          chars.length()
        }
        for col in from.. Unit {
  self.transform_selection_case(0, "switched case")
}

///|
pub fn Editor::lower_selection(self : Editor) -> Unit {
  self.transform_selection_case(1, "lowercase")
}

///|
pub fn Editor::upper_selection(self : Editor) -> Unit {
  self.transform_selection_case(2, "uppercase")
}

///|
fn join_two_lines(left : String, right : String) -> String {
  let l = left.trim().to_owned()
  let r = right.trim().to_owned()
  if l == "" {
    r
  } else if r == "" {
    l
  } else {
    l + " " + r
  }
}

///|
pub fn Editor::join_lines(self : Editor) -> Unit {
  if self.doc.lines.length() <= 1 {
    self.set_status("nothing to join")
    return
  }
  self.remember()
  let (start_row, finish_row) = match self.selection_bounds() {
    None =>
      (
        self.cursor.row,
        clamp(self.cursor.row + 1, 0, self.doc.lines.length() - 1),
      )
    Some((start, finish)) => (start.row, finish.row)
  }
  if start_row == finish_row {
    if start_row + 1 < self.doc.lines.length() {
      let next = self.doc.lines.remove(start_row + 1)
      self.doc.lines[start_row] = join_two_lines(
        self.doc.lines[start_row],
        next,
      )
    }
  } else {
    for _ in start_row.. Unit {
  self.remember()
  let (start_row, finish_row) = match self.selection_bounds() {
    None => (self.cursor.row, self.cursor.row)
    Some((start, finish)) => (start.row, finish.row)
  }
  for row in start_row..<(finish_row + 1) {
    let chars = self.doc.lines[row].to_array()
    let col = first_non_whitespace_col(self.doc.lines[row])
    if col + 1 < chars.length() && chars[col] == '/' && chars[col + 1] == '/' {
      chars.remove(col) |> ignore
      chars.remove(col) |> ignore
      if col < chars.length() && chars[col] == ' ' {
        chars.remove(col) |> ignore
      }
    } else {
      chars.insert(col, '/')
      chars.insert(col + 1, '/')
      chars.insert(col + 2, ' ')
    }
    self.doc.lines[row] = String::from_array(chars)
  }
  self.doc.dirty = true
  self.set_status("toggled line comment")
}

///|
fn starts_with_chars(chars : Array[Char], prefix : Array[Char]) -> Bool {
  if chars.length() < prefix.length() {
    false
  } else {
    let mut ok = true
    for index in 0.. Bool {
  if chars.length() < suffix.length() {
    false
  } else {
    let offset = chars.length() - suffix.length()
    let mut ok = true
    for index in 0.. Int {
  let chars = line.to_array()
  let mut index = chars.length()
  while index > 0 && chars[index - 1].is_whitespace() {
    index -= 1
  }
  index
}

///|
fn toggle_block_comment_text(text : String) -> String {
  let chars = text.to_array()
  if starts_with_chars(chars, ['/', '*', ' ']) &&
    ends_with_chars(chars, [' ', '*', '/']) {
    String::from_array(chars[3:chars.length() - 3].to_owned())
  } else if starts_with_chars(chars, ['/', '*']) &&
    ends_with_chars(chars, ['*', '/']) {
    String::from_array(chars[2:chars.length() - 2].to_owned())
  } else {
    "/* " + text + " */"
  }
}

///|
pub fn Editor::toggle_block_comment(self : Editor) -> Unit {
  match self.selection_bounds() {
    Some((start, finish)) => {
      let replacement = toggle_block_comment_text(self.selected_text())
      self.replace_range(
        start.row,
        start.col,
        finish.row,
        finish.col + 1,
        replacement,
      )
      self.set_status("toggled block comment")
    }
    None => {
      let row = self.cursor.row
      let line = self.doc.lines[row]
      let start = first_non_whitespace_col(line)
      let finish = last_non_whitespace_exclusive(line)
      let chars = line.to_array()
      let text = String::from_array(chars[start:finish].to_owned())
      self.replace_range(
        row,
        start,
        row,
        finish,
        toggle_block_comment_text(text),
      )
      self.set_status("toggled block comment")
    }
  }
}

///|
pub fn Editor::delete_selection(self : Editor) -> Unit {
  match self.selection_bounds() {
    None => self.delete_char()
    Some((start, finish)) => {
      self.register = self.selected_text()
      self.register_is_line = self.selection_is_full_lines()
      self.remember()
      if self.register_is_line {
        for _ in start.row..<(finish.row + 1) {
          self.doc.lines.remove(start.row) |> ignore
        }
        if self.doc.lines.length() == 0 {
          self.doc.lines.push("")
        }
      } else if start.row == finish.row {
        let chars = self.doc.lines[start.row].to_array()
        let start_col = clamp(start.col, 0, chars.length())
        let end_col = clamp(finish.col + 1, start_col, chars.length())
        for _ in start_col.. ignore
        }
        self.doc.lines[start.row] = String::from_array(chars)
      } else {
        let first_chars = self.doc.lines[start.row].to_array()
        let last_chars = self.doc.lines[finish.row].to_array()
        let start_col = clamp(start.col, 0, first_chars.length())
        let end_col = clamp(finish.col + 1, 0, last_chars.length())
        let merged = String::from_array(first_chars[:start_col].to_owned()) +
          String::from_array(last_chars[end_col:].to_owned())
        self.doc.lines[start.row] = merged
        for _ in start.row.. ignore
        }
      }
      if self.doc.lines.length() == 0 {
        self.doc.lines.push("")
      }
      self.cursor.row = clamp(start.row, 0, self.doc.lines.length() - 1)
      self.cursor.col = clamp(start.col, 0, line_len(self.current_line()))
      self.selection_anchor = None
      self.mode = Normal
      self.doc.dirty = true
      self.set_status("deleted selection")
    }
  }
}

///|
fn bracket_match(ch : Char) -> (Char, Int)? {
  match ch {
    '(' => Some((')', 1))
    '[' => Some((']', 1))
    '{' => Some(('}', 1))
    ')' => Some(('(', -1))
    ']' => Some(('[', -1))
    '}' => Some(('{', -1))
    _ => None
  }
}

///|
fn surround_pair(ch : Char) -> (Char, Char) {
  match ch {
    '(' | ')' => ('(', ')')
    '[' | ']' => ('[', ']')
    '{' | '}' => ('{', '}')
    '<' | '>' => ('<', '>')
    _ => (ch, ch)
  }
}

///|
fn find_surround_pair_on_line(
  line : String,
  cursor : Int,
  marker : Char,
) -> (Int, Int)? {
  let chars = line.to_array()
  if chars.length() < 2 {
    return None
  }
  let (open, close) = surround_pair(marker)
  let at = clamp(cursor, 0, chars.length() - 1)
  let mut open_index : Int? = None
  let mut col = at
  while col >= 0 {
    if chars[col] == open {
      open_index = Some(col)
      break
    }
    col -= 1
  }
  match open_index {
    None => None
    Some(left) => {
      let mut right = left + 1
      while right < chars.length() {
        if chars[right] == close {
          return Some((left, right))
        }
        right += 1
      }
      None
    }
  }
}

///|
pub fn Editor::surround_selection(self : Editor, marker : Char) -> Unit {
  let (open, close) = surround_pair(marker)
  self.remember()
  match self.selection_bounds() {
    None => {
      let chars = self.current_line().to_array()
      if chars.length() == 0 {
        chars.insert(0, open)
        chars.insert(1, close)
        self.replace_current_line(String::from_array(chars))
        self.cursor.col = 1
      } else {
        let col = clamp(self.cursor.col, 0, chars.length() - 1)
        chars.insert(col + 1, close)
        chars.insert(col, open)
        self.replace_current_line(String::from_array(chars))
        self.cursor.col = col + 2
      }
    }
    Some((start, finish)) =>
      if start.row == finish.row {
        let chars = self.doc.lines[start.row].to_array()
        let start_col = clamp(start.col, 0, chars.length())
        let finish_col = clamp(finish.col + 1, start_col, chars.length())
        chars.insert(finish_col, close)
        chars.insert(start_col, open)
        self.doc.lines[start.row] = String::from_array(chars)
        self.cursor.row = start.row
        self.cursor.col = finish_col + 1
      } else {
        let finish_chars = self.doc.lines[finish.row].to_array()
        let finish_col = clamp(finish.col + 1, 0, finish_chars.length())
        finish_chars.insert(finish_col, close)
        self.doc.lines[finish.row] = String::from_array(finish_chars)
        let start_chars = self.doc.lines[start.row].to_array()
        let start_col = clamp(start.col, 0, start_chars.length())
        start_chars.insert(start_col, open)
        self.doc.lines[start.row] = String::from_array(start_chars)
        self.cursor = { row: finish.row, col: finish_col }
      }
  }
  self.selection_anchor = None
  self.mode = Normal
  self.doc.dirty = true
  self.set_status("surrounded")
}

///|
pub fn Editor::delete_surround(self : Editor, marker : Char) -> Unit {
  match
    find_surround_pair_on_line(self.current_line(), self.cursor.col, marker) {
    None => self.set_status("surround not found")
    Some((left, right)) => {
      self.remember()
      let chars = self.current_line().to_array()
      chars.remove(right) |> ignore
      chars.remove(left) |> ignore
      self.replace_current_line(String::from_array(chars))
      self.cursor.col = clamp(left, 0, line_len(self.current_line()))
      self.set_status("deleted surround")
    }
  }
}

///|
pub fn Editor::replace_surround(
  self : Editor,
  old : Char,
  replacement : Char,
) -> Unit {
  match find_surround_pair_on_line(self.current_line(), self.cursor.col, old) {
    None => self.set_status("surround not found")
    Some((left, right)) => {
      let (open, close) = surround_pair(replacement)
      self.remember()
      let chars = self.current_line().to_array()
      chars[left] = open
      chars[right] = close
      self.replace_current_line(String::from_array(chars))
      self.cursor.col = clamp(right, 0, line_len(self.current_line()))
      self.set_status("replaced surround")
    }
  }
}

///|
fn Editor::select_range(
  self : Editor,
  start : Cursor,
  finish : Cursor,
  status : String,
) -> Unit {
  self.mode = Select
  self.selection_anchor = Some({
    row: clamp(start.row, 0, self.doc.lines.length() - 1),
    col: clamp(start.col, 0, line_len(self.doc.lines[start.row])),
  })
  self.cursor = {
    row: clamp(finish.row, 0, self.doc.lines.length() - 1),
    col: clamp(finish.col, 0, line_len(self.doc.lines[finish.row])),
  }
  self.set_status(status)
}

///|
fn Editor::next_position(self : Editor, cursor : Cursor) -> Cursor {
  let len = line_len(self.doc.lines[cursor.row])
  if cursor.col + 1 < len {
    { row: cursor.row, col: cursor.col + 1 }
  } else if cursor.row + 1 < self.doc.lines.length() {
    { row: cursor.row + 1, col: 0 }
  } else {
    cursor
  }
}

///|
fn Editor::prev_position(self : Editor, cursor : Cursor) -> Cursor {
  if cursor.col > 0 {
    { row: cursor.row, col: cursor.col - 1 }
  } else if cursor.row > 0 {
    let row = cursor.row - 1
    {
      row,
      col: clamp(
        line_len(self.doc.lines[row]) - 1,
        0,
        line_len(self.doc.lines[row]),
      ),
    }
  } else {
    cursor
  }
}

///|
fn Editor::select_word_textobject(self : Editor, long : Bool) -> Bool {
  let chars = self.current_line().to_array()
  if chars.length() == 0 {
    self.set_status("textobject not found")
    return false
  }
  let col = clamp(self.cursor.col, 0, chars.length() - 1)
  let category = movement_category(chars[col], long)
  if category == WhitespaceChar {
    self.set_status("textobject not found")
    return false
  }
  let mut start = col
  while start > 0 && movement_category(chars[start - 1], long) == category {
    start -= 1
  }
  let mut finish = col
  while finish + 1 < chars.length() &&
        movement_category(chars[finish + 1], long) == category {
    finish += 1
  }
  self.select_range(
    { row: self.cursor.row, col: start },
    { row: self.cursor.row, col: finish },
    "selected textobject",
  )
  true
}

///|
fn Editor::select_paragraph_textobject(self : Editor) -> Bool {
  let blank = self.current_line().trim() == ""
  let mut start = self.cursor.row
  while start > 0 && (self.doc.lines[start - 1].trim() == "") == blank {
    start -= 1
  }
  let mut finish = self.cursor.row
  while finish + 1 < self.doc.lines.length() &&
        (self.doc.lines[finish + 1].trim() == "") == blank {
    finish += 1
  }
  self.select_range(
    { row: start, col: 0 },
    {
      row: finish,
      col: clamp(
        line_len(self.doc.lines[finish]) - 1,
        0,
        line_len(self.doc.lines[finish]),
      ),
    },
    "selected textobject",
  )
  true
}

///|
fn Editor::matching_close_from(
  self : Editor,
  start : Cursor,
  open : Char,
  close : Char,
) -> Cursor? {
  let mut depth = 1
  let mut row = start.row
  let mut col = start.col + 1
  while row < self.doc.lines.length() {
    let chars = self.doc.lines[row].to_array()
    while col < chars.length() {
      if chars[col] == open {
        depth += 1
      } else if chars[col] == close {
        depth -= 1
        if depth == 0 {
          return Some({ row, col })
        }
      }
      col += 1
    }
    row += 1
    col = 0
  }
  None
}

///|
fn Editor::surrounding_pair_textobject(
  self : Editor,
  marker : Char,
) -> (Cursor, Cursor)? {
  let (open, close) = surround_pair(marker)
  if open == close {
    let chars = self.current_line().to_array()
    if chars.length() < 2 {
      return None
    }
    let at = clamp(self.cursor.col, 0, chars.length() - 1)
    let mut left : Int? = None
    let mut col = at
    while col >= 0 {
      if chars[col] == open {
        left = Some(col)
        break
      }
      col -= 1
    }
    match left {
      None => None
      Some(left_col) => {
        let mut right = left_col + 1
        while right < chars.length() {
          if chars[right] == close {
            return Some(
              (
                { row: self.cursor.row, col: left_col },
                { row: self.cursor.row, col: right },
              ),
            )
          }
          right += 1
        }
        None
      }
    }
  } else {
    let target = cursor_copy(self.cursor)
    let mut best : (Cursor, Cursor)? = None
    for row in 0..<(target.row + 1) {
      let chars = self.doc.lines[row].to_array()
      let max_col = if row == target.row {
        clamp(target.col, 0, chars.length() - 1)
      } else {
        chars.length() - 1
      }
      if max_col >= 0 {
        for col in 0..<(max_col + 1) {
          if chars[col] == open {
            let start = { row, col }
            match self.matching_close_from(start, open, close) {
              Some(finish) =>
                if cursor_before(target, finish) {
                  best = Some((start, finish))
                }
              None => ()
            }
          }
        }
      }
    }
    best
  }
}

///|
pub fn Editor::select_textobject(
  self : Editor,
  marker : Char,
  inside : Bool,
) -> Unit {
  match marker {
    'w' => ignore(self.select_word_textobject(false))
    'W' => ignore(self.select_word_textobject(true))
    'p' => ignore(self.select_paragraph_textobject())
    _ =>
      match self.surrounding_pair_textobject(marker) {
        None => self.set_status("textobject not found")
        Some((open, close)) => {
          let start = if inside { self.next_position(open) } else { open }
          let finish = if inside { self.prev_position(close) } else { close }
          if cursor_strict_before(finish, start) {
            self.set_status("textobject is empty")
          } else {
            self.select_range(start, finish, "selected textobject")
          }
        }
      }
  }
}

///|
pub fn Editor::match_bracket(self : Editor) -> Bool {
  let chars = self.current_line().to_array()
  if chars.length() == 0 {
    self.set_status("no bracket under cursor")
    return false
  }
  let origin_col = clamp(self.cursor.col, 0, chars.length() - 1)
  let origin = chars[origin_col]
  match bracket_match(origin) {
    None => {
      self.set_status("no bracket under cursor")
      false
    }
    Some((target, direction)) => {
      let mut depth = 1
      if direction > 0 {
        let mut row = self.cursor.row
        let mut col = origin_col + 1
        while row < self.doc.lines.length() {
          let scan = self.doc.lines[row].to_array()
          while col < scan.length() {
            if scan[col] == origin {
              depth += 1
            } else if scan[col] == target {
              depth -= 1
              if depth == 0 {
                self.cursor.row = row
                self.cursor.col = col
                self.set_status("matched bracket")
                return true
              }
            }
            col += 1
          }
          row += 1
          col = 0
        }
      } else {
        let mut row = self.cursor.row
        let mut col = origin_col - 1
        while row >= 0 {
          let scan = self.doc.lines[row].to_array()
          if col >= scan.length() {
            col = scan.length() - 1
          }
          while col >= 0 {
            if scan[col] == origin {
              depth += 1
            } else if scan[col] == target {
              depth -= 1
              if depth == 0 {
                self.cursor.row = row
                self.cursor.col = col
                self.set_status("matched bracket")
                return true
              }
            }
            col -= 1
          }
          row -= 1
          if row >= 0 {
            col = line_len(self.doc.lines[row]) - 1
          }
        }
      }
      self.set_status("matching bracket not found")
      false
    }
  }
}

///|
pub fn Editor::find_char_forward(self : Editor, ch : Char, till : Bool) -> Bool {
  let mut row = self.cursor.row
  let mut col = self.cursor.col + 1
  while row < self.doc.lines.length() {
    let chars = self.doc.lines[row].to_array()
    while col < chars.length() {
      if chars[col] == ch {
        self.cursor.row = row
        self.cursor.col = if till {
          clamp(col - 1, 0, chars.length())
        } else {
          col
        }
        self.set_status("found char")
        return true
      }
      col += 1
    }
    row += 1
    col = 0
  }
  self.set_status("char not found")
  false
}

///|
pub fn Editor::find_char_backward(
  self : Editor,
  ch : Char,
  till : Bool,
) -> Bool {
  let mut row = self.cursor.row
  let mut col = self.cursor.col - 1
  while row >= 0 {
    let chars = self.doc.lines[row].to_array()
    if col >= chars.length() {
      col = chars.length() - 1
    }
    while col >= 0 {
      if chars[col] == ch {
        self.cursor.row = row
        self.cursor.col = if till {
          clamp(col + 1, 0, chars.length())
        } else {
          col
        }
        self.set_status("found char")
        return true
      }
      col -= 1
    }
    row -= 1
    if row >= 0 {
      col = line_len(self.doc.lines[row]) - 1
    }
  }
  self.set_status("char not found")
  false
}

///|
pub fn Editor::search_forward(self : Editor, query : String) -> Bool {
  guard query != "" else { return false }
  for row in self.cursor.row.. Bool {
  guard query != "" else { return false }
  let mut row = self.cursor.row
  while row >= 0 {
    let before = if row == self.cursor.row {
      match self.selection_bounds() {
        Some((start, finish)) if start.row == finish.row && start.row == row =>
          start.col - 1
        _ => self.cursor.col - 1
      }
    } else {
      line_len(self.doc.lines[row])
    }
    if find_substring_before(self.doc.lines[row], query, before) is Some(col) {
      self.cursor.row = row
      self.cursor.col = col + line_len(query) - 1
      self.mode = Select
      self.selection_anchor = Some({ row, col })
      self.set_status("found: \{query}")
      return true
    }
    row -= 1
  }
  row = self.doc.lines.length() - 1
  while row >= self.cursor.row {
    if find_substring_before(
        self.doc.lines[row],
        query,
        line_len(self.doc.lines[row]),
      )
      is Some(col) {
      self.cursor.row = row
      self.cursor.col = col + line_len(query) - 1
      self.mode = Select
      self.selection_anchor = Some({ row, col })
      self.set_status("found: \{query}")
      return true
    }
    row -= 1
  }
  self.set_status("not found: \{query}")
  false
}

///|
pub fn Editor::buffer_text(self : Editor) -> String {
  self.doc.lines.join("\n")
}

///|
fn mode_text(mode : Mode) -> String {
  match mode {
    Normal => "NOR"
    Insert => "INS"
    Select => "SEL"
    Command => "CMD"
  }
}

///|
fn path_text(path : String?) -> String {
  match path {
    None => "[scratch]"
    Some(value) => value
  }
}

///|
pub fn Editor::render(self : Editor, height? : Int = 18) -> String {
  let out = StringBuilder::new()
  out.write_string("meolix - terminal Helix-like editor in MoonBit\n")
  let max_rows = clamp(height, 1, self.doc.lines.length())
  for row in 0.." } else { " " }
    let number = (row + 1).to_string().pad_start(4, ' ')
    out.write_string("\{marker} \{number} \u{2502} ")
    out.write_string(self.doc.lines[row])
    out.write_string("\n")
  }
  let dirty = if self.doc.dirty { "[+]" } else { "[ ]" }
  out.write_string(
    "\{mode_text(self.mode)} \{dirty} \{path_text(self.doc.path)}:\{self.cursor.row + 1}:\{self.cursor.col + 1}  \{self.status}\n",
  )
  out.to_string()
}

///|
pub fn Editor::apply_key(self : Editor, key : String) -> Unit {
  match self.mode {
    Insert =>
      match key {
        "esc" => self.enter_normal()
        "enter" => self.split_line()
        "backspace" => self.backspace()
        _ => self.insert_text(key)
      }
    _ =>
      match key {
        "h" => self.move_left()
        "j" => self.move_down()
        "k" => self.move_up()
        "l" => self.move_right()
        "w" => self.move_next_word()
        "b" => self.move_prev_word()
        "e" => self.move_next_word_end()
        "W" => self.move_next_long_word()
        "B" => self.move_prev_long_word()
        "E" => self.move_next_long_word_end()
        "gg" => self.move_file_start()
        "G" => self.set_status("G expects a line number; use :go N or ge")
        "ge" => self.move_file_end()
        "gh" => self.move_line_start()
        "gl" => self.move_line_end()
        "gs" => self.move_line_first_non_whitespace()
        "g|" => self.move_to_column(1)
        "ga" =>
          self.set_status("goto alternate file is only available in the TUI")
        "i" => self.enter_insert()
        "a" => {
          self.move_right()
          self.enter_insert()
        }
        "I" => self.insert_at_line_start()
        "A" => self.insert_at_line_end()
        "v" =>
          if self.mode == Select {
            self.enter_normal()
          } else {
            self.enter_select()
          }
        "esc" => self.enter_normal()
        "x" => self.select_line()
        "X" => self.extend_selection_to_line_bounds()
        "%" => self.select_all()
        ";" => self.collapse_selection()
        "_" => self.trim_selection()
        "d" => self.delete_selection()
        "alt-d" => self.delete_selection_without_yank()
        "c" => self.change_selection()
        "alt-c" => self.change_selection_without_yank()
        "y" => {
          self.yank_selection()
          self.enter_normal()
        }
        "dd" => self.delete_line()
        "yy" => self.yank_line()
        "p" => self.paste_after()
        "P" => self.paste_before()
        "R" => self.replace_with_yanked()
        "J" => self.join_lines()
        ">" => self.indent_selection()
        "<" => self.unindent_selection()
        "*" => self.search_word_under_cursor() |> ignore
        "ctrl-a" => self.change_number_under_cursor(1)
        "ctrl-x" => self.change_number_under_cursor(-1)
        "~" => self.switch_selection_case()
        "`" => self.lower_selection()
        "o" => self.open_below()
        "O" => self.open_above()
        "u" => self.undo()
        "U" => self.redo()
        _ if key.has_prefix("ms") => {
          let chars = key.to_array()
          if chars.length() >= 3 {
            self.surround_selection(chars[2])
          } else {
            self.set_status("surround expects a delimiter")
          }
        }
        _ if key.has_prefix("md") => {
          let chars = key.to_array()
          if chars.length() >= 3 {
            self.delete_surround(chars[2])
          } else {
            self.set_status("delete surround expects a delimiter")
          }
        }
        _ if key.has_prefix("mr") => {
          let chars = key.to_array()
          if chars.length() >= 4 {
            self.replace_surround(chars[2], chars[3])
          } else {
            self.set_status("replace surround expects two delimiters")
          }
        }
        _ if key.has_prefix("ma") => {
          let chars = key.to_array()
          if chars.length() >= 3 {
            self.select_textobject(chars[2], false)
          } else {
            self.set_status("around textobject expects an object key")
          }
        }
        _ if key.has_prefix("mi") => {
          let chars = key.to_array()
          if chars.length() >= 3 {
            self.select_textobject(chars[2], true)
          } else {
            self.set_status("inside textobject expects an object key")
          }
        }
        _ => self.set_status("unknown key: \{key}")
      }
  }
}

///|
pub fn Editor::apply_command(self : Editor, command : String) -> Unit {
  let command = command.trim().to_owned()
  match command {
    "" => ()
    "q" | "quit" =>
      if self.doc.dirty {
        self.set_status("buffer is dirty; use :q! to quit")
      } else {
        self.should_quit = true
      }
    "q!" | "quit!" => self.should_quit = true
    "w" | "write" => self.set_status("write requested")
    "wq" => {
      self.set_status("write requested")
      self.should_quit = true
    }
    "mode normal" => self.enter_normal()
    "mode insert" => self.enter_insert()
    "mode select" => self.enter_select()
    _ if command.has_prefix("insert ") => {
      self.enter_insert()
      self.insert_text(command[7:].to_owned())
    }
    _ if command.has_prefix("append ") => {
      self.move_line_end()
      self.enter_insert()
      self.insert_text(command[7:].to_owned())
    }
    _ if command.has_prefix("go ") => {
      let target = command[3:].to_owned()
      let line = @string.parse_int(target) catch { _ => 1 }
      self.cursor.row = clamp(line - 1, 0, self.doc.lines.length() - 1)
      self.sync_cursor()
    }
    _ => self.set_status("unknown command: :\{command}")
  }
}

///|
pub fn Editor::apply_editor_command(
  self : Editor,
  command : EditorCommand,
) -> Unit {
  match command {
    Key(key) => self.apply_key(key)
    Ex(ex) => self.apply_command(ex)
    Quit => self.should_quit = true
  }
}

///|
pub async fn load(path : String) -> Editor {
  if @fs.exists(path) {
    try @fs.kind(path) catch {
      _ => {
        let data = @fs.read_file(path)
        open_text(path, data.text())
      }
    } noraise {
      Directory => {
        let editor = empty_editor()
        editor.set_status("\{path} is a directory")
        editor
      }
      _ => {
        let data = @fs.read_file(path)
        open_text(path, data.text())
      }
    }
  } else {
    {
      doc: { path: Some(path), lines: [""], dirty: false },
      mode: Normal,
      cursor: { row: 0, col: 0 },
      selection_anchor: None,
      register: "",
      register_is_line: false,
      status: "new file \{path}",
      should_quit: false,
      undo_stack: [],
      redo_stack: [],
    }
  }
}

///|
pub async fn Editor::save(self : Editor) -> Unit {
  match self.doc.path {
    None => self.set_status("no path; use a file path when starting meolix")
    Some(path) => {
      @fs.write_file(path, self.buffer_text(), create_mode=CreateOrTruncate)
      self.doc.dirty = false
      self.set_status("wrote \{path}")
    }
  }
}

///|
pub async fn Editor::save_as(self : Editor, path : String) -> Unit {
  @fs.write_file(path, self.buffer_text(), create_mode=CreateOrTruncate)
  self.doc.path = Some(path)
  self.doc.dirty = false
  self.set_status("wrote \{path}")
}

///|
async fn command_producer(
  queue : @aqueue.Queue[EditorCommand],
  commands : Array[EditorCommand],
) -> Unit {
  for command in commands {
    queue.put(command)
    @async.pause()
  }
  queue.put(Quit)
}

///|
async fn command_consumer(
  editor : Editor,
  queue : @aqueue.Queue[EditorCommand],
  log : Array[String],
  save_on_write~ : Bool,
) -> Editor {
  let mut running = true
  while running {
    let command = queue.get()
    match command {
      Ex("w") | Ex("write") if save_on_write => editor.save()
      Ex("wq") if save_on_write => {
        editor.save()
        editor.should_quit = true
      }
      _ => editor.apply_editor_command(command)
    }
    log.push(editor.render(height=8))
    if editor.should_quit {
      running = false
    }
  }
  editor
}

///|
pub async fn run_commands(
  editor : Editor,
  commands : Array[EditorCommand],
  save_on_write? : Bool = true,
) -> DriverResult {
  let log : Array[String] = []
  let result = @async.with_task_group() <| group => {
    let queue = @aqueue.Queue(kind=Unbounded)
    group.spawn_bg() <| () => { command_producer(queue, commands) }
    command_consumer(editor, queue, log, save_on_write~)
  }
  { editor: result, log }
}

///|
pub fn parse_script_line(line : String) -> EditorCommand? {
  let trimmed = line.trim().to_owned()
  match trimmed {
    "" => None
    _ if trimmed.has_prefix("#") => None
    _ if trimmed.has_prefix(":") => Some(Ex(trimmed[1:].to_owned()))
    _ if trimmed.has_prefix("key ") => Some(Key(trimmed[4:].to_owned()))
    _ if trimmed.has_prefix("text ") => Some(Key(trimmed[5:].to_owned()))
    _ if trimmed == "quit" => Some(Quit)
    _ => Some(Key(trimmed))
  }
}

///|
pub fn parse_script(text : String) -> Array[EditorCommand] {
  let commands : Array[EditorCommand] = []
  for line in text.split("\n") {
    if parse_script_line(line.to_owned()) is Some(command) {
      commands.push(command)
    }
  }
  commands
}