///|
fn markdown_editor_transform_current_block(
  source : String,
  command : MarkdownEditorCommand,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  if source == "" {
    return markdown_editor_append_snippet(source, command)
  }
  match markdown_editor_active_selection(selection.selection) {
    Some(_) =>
      match command {
        Paragraph | BulletList | TaskList | OrderedList | Quote =>
          return markdown_editor_transform_selected_lines(
            source, command, selection,
          )
        HorizontalRule =>
          return markdown_editor_replace_selected_lines_with_rule(
            source, selection,
          )
        Table =>
          return markdown_editor_replace_selected_lines_with_table(
            source, selection,
          )
        _ => ()
      }
    None => ()
  }
  match markdown_editor_setext_block_at_selection(source, selection) {
    Some(block) =>
      match command {
        Paragraph
        | Heading(_)
        | BulletList
        | TaskList
        | OrderedList
        | Quote
        | HorizontalRule
        | Table =>
          return markdown_editor_transform_setext_block(
            source, command, selection, block,
          )
        _ => ()
      }
    None => ()
  }
  let range = markdown_editor_current_line_range(source, selection)
  let chars = source.to_array()
  let line = String::from_array(chars[range.start:range.end])
  let stripped = markdown_editor_strip_block_marker(line)
  let prefix = markdown_editor_block_prefix(command)
  let parts = markdown_editor_line_block_parts(line)
  if command == Paragraph {
    let replacement = markdown_editor_paragraph_line_text(line, parts)
    let next_source = String::from_array(chars[:range.start]) +
      replacement +
      String::from_array(chars[range.end:])
    let paragraph_offset = if markdown_editor_line_block_parts_has_inner_marker(
        parts,
      ) &&
      markdown_editor_block_command_context_has_quote(parts.context_prefix) {
      markdown_editor_text_length(parts.context_prefix) +
      markdown_editor_caret_offset_in_line_block_parts(
        parts,
        selection.caret - range.start,
      )
    } else {
      markdown_editor_caret_offset_in_stripped_line(
        line,
        selection.caret - range.start,
      )
    }
    let next_caret = range.start + paragraph_offset
    { source: next_source, caret: next_caret, selection: None }
  } else if command == HorizontalRule {
    let next_source = String::from_array(chars[:range.start]) +
      "---" +
      String::from_array(chars[range.end:])
    { source: next_source, caret: range.start + 3, selection: None }
  } else if command == Table {
    let snippet = markdown_editor_command_snippet(Table)
    let next_source = String::from_array(chars[:range.start]) +
      snippet +
      String::from_array(chars[range.end:])
    { source: next_source, caret: range.start + 2, selection: None }
  } else if prefix == "" {
    markdown_editor_append_snippet(source, command)
  } else if markdown_editor_block_command_context_has_quote(
      parts.context_prefix,
    ) &&
    (
      command == BulletList ||
      command == TaskList ||
      command == OrderedList ||
      command is Heading(_)
    ) {
    let has_same_marker = markdown_editor_line_has_same_block_marker_after_context(
      line,
      command,
      parts.context_prefix,
    )
    let marker = if has_same_marker { "" } else { prefix }
    let replacement = parts.context_prefix + marker + parts.content
    let next_source = String::from_array(chars[:range.start]) +
      replacement +
      String::from_array(chars[range.end:])
    let next_caret = range.start +
      markdown_editor_text_length(parts.context_prefix) +
      markdown_editor_text_length(marker) +
      markdown_editor_caret_offset_in_line_block_parts(
        parts,
        selection.caret - range.start,
      )
    { source: next_source, caret: next_caret, selection: None }
  } else {
    let replacement = if markdown_editor_line_has_same_block_marker(
        line, command,
      ) {
      stripped
    } else {
      prefix + stripped
    }
    let next_source = String::from_array(chars[:range.start]) +
      replacement +
      String::from_array(chars[range.end:])
    let replacement_prefix_length = markdown_editor_text_length(replacement) -
      markdown_editor_text_length(stripped)
    let next_caret = range.start +
      replacement_prefix_length +
      markdown_editor_caret_offset_in_stripped_line(
        line,
        selection.caret - range.start,
      )
    { source: next_source, caret: next_caret, selection: None }
  }
}

///|
fn markdown_editor_setext_block_at_selection(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorBlock? {
  let anchor = markdown_editor_clamp_int(
    selection.caret,
    0,
    markdown_editor_text_length(source),
  )
  for block in parse_markdown_editor_blocks(source) {
    if !(block.block.kind is MarkdownBlockKind::Heading(_)) ||
      anchor < block.source_range.start ||
      anchor > block.content_range.end ||
      block.source_range.end <= block.content_range.end {
      continue
    }
    return Some(block)
  }
  None
}

///|
fn markdown_editor_transform_setext_block(
  source : String,
  command : MarkdownEditorCommand,
  selection : MarkdownEditorSelection,
  block : MarkdownEditorBlock,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let text = block.block.text
  let replacement = markdown_editor_setext_block_replacement(text, command)
  let next_source = String::from_array(chars[:block.source_range.start]) +
    replacement +
    String::from_array(chars[block.source_range.end:])
  let caret = if command == Table {
    block.source_range.start + 2
  } else if command == HorizontalRule {
    block.source_range.start + 3
  } else {
    let offset = markdown_editor_clamp_int(
      selection.caret - block.content_range.start,
      0,
      markdown_editor_text_length(text),
    )
    block.source_range.start +
    markdown_editor_text_length(replacement) -
    markdown_editor_text_length(text) +
    offset
  }
  { source: next_source, caret, selection: None }
}

///|
fn markdown_editor_setext_block_replacement(
  text : String,
  command : MarkdownEditorCommand,
) -> String {
  match command {
    Paragraph => text
    Heading(level) =>
      markdown_editor_heading_marker(level) +
      " " +
      markdown_editor_setext_atx_heading_text(text)
    BulletList | TaskList | OrderedList | Quote =>
      markdown_editor_transform_line_segment(text, command)
    HorizontalRule => "---"
    Table => markdown_editor_command_snippet(Table)
    _ => text
  }
}

///|
fn markdown_editor_setext_atx_heading_text(text : String) -> String {
  let parts : Array[String] = []
  for line in markdown_split_lines(text) {
    let trimmed = line.trim().to_owned()
    if trimmed != "" {
      parts.push(trimmed)
    }
  }
  parts.join(" ")
}

///|
fn markdown_editor_replace_selected_lines_with_table(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let range = markdown_editor_selected_line_range(source, selection)
  let snippet = markdown_editor_command_snippet(Table)
  let text = String::from_array(chars[:range.start]) +
    snippet +
    String::from_array(chars[range.end:])
  let next_selection = @core.TextRange::new(
    start=range.start,
    end=range.start + markdown_editor_text_length(snippet),
  )
  { source: text, caret: range.start + 2, selection: Some(next_selection) }
}

///|
fn markdown_editor_replace_selected_lines_with_rule(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let range = markdown_editor_selected_line_range(source, selection)
  let text = String::from_array(chars[:range.start]) +
    "---" +
    String::from_array(chars[range.end:])
  let next_selection = @core.TextRange::new(
    start=range.start,
    end=range.start + 3,
  )
  { source: text, caret: range.start + 3, selection: Some(next_selection) }
}

///|
fn markdown_editor_line_has_same_block_marker(
  line : String,
  command : MarkdownEditorCommand,
) -> Bool {
  let marker_length = markdown_editor_marker_prefix_length(line)
  let indent_length = markdown_editor_line_indent_prefix_length(line)
  match command {
    Heading(level) => markdown_editor_heading_marker_level(line) == level
    BulletList =>
      marker_length == indent_length + 2 &&
      markdown_editor_string_ends_with(
        markdown_editor_continuation_marker(line),
        "- ",
      )
    TaskList => marker_length == indent_length + 6
    OrderedList => markdown_editor_ordered_marker_length(line.to_array()) > 0
    Quote =>
      marker_length == indent_length + 2 &&
      markdown_editor_string_ends_with(
        markdown_editor_continuation_marker(line),
        "> ",
      )
    _ => false
  }
}

///|
fn markdown_editor_heading_marker_level(line : String) -> Int {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  let mut index = indent
  while index < chars.length() && chars[index] == '#' {
    index = index + 1
  }
  if index > 0 && index < chars.length() && chars[index] == ' ' {
    index - indent
  } else {
    0
  }
}

///|
fn markdown_editor_caret_offset_in_stripped_line(
  line : String,
  caret_offset : Int,
) -> Int {
  let marker_length = markdown_editor_marker_prefix_length(line)
  let stripped_length = markdown_editor_text_length(line) - marker_length
  markdown_editor_clamp_int(caret_offset - marker_length, 0, stripped_length)
}

///|
fn markdown_editor_transform_selected_lines(
  source : String,
  command : MarkdownEditorCommand,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  let chars = source.to_array()
  let range = markdown_editor_selected_line_range(source, selection)
  let segment = String::from_array(chars[range.start:range.end])
  let transformed = markdown_editor_transform_line_segment(segment, command)
  let text = String::from_array(chars[:range.start]) +
    transformed +
    String::from_array(chars[range.end:])
  let delta = markdown_editor_text_length(transformed) -
    (range.end - range.start)
  let next_selection = Some(
    @core.TextRange::new(
      start=range.start,
      end=range.start + markdown_editor_text_length(transformed),
    ),
  )
  let caret = if selection.caret <= range.start {
    selection.caret
  } else if selection.caret <= range.end {
    range.start + markdown_editor_text_length(transformed)
  } else {
    selection.caret + delta
  }
  { source: text, caret, selection: next_selection }
}

///|
fn markdown_editor_transform_line_segment(
  segment : String,
  command : MarkdownEditorCommand,
) -> String {
  let lines = markdown_editor_lines(segment)
  let transformed : Array[String] = []
  let ordered_contexts : Array[MarkdownEditorOrderedRenumberContext] = []
  for line in lines {
    let parts = markdown_editor_line_block_parts(line.text)
    let content_is_blank = parts.content.trim().to_owned() == ""
    match command {
      Paragraph =>
        transformed.push(markdown_editor_paragraph_line_text(line.text, parts))
      BulletList =>
        if content_is_blank {
          transformed.push(markdown_editor_block_command_blank_line(parts))
        } else {
          transformed.push(parts.context_prefix + "- " + parts.content)
        }
      TaskList =>
        if content_is_blank {
          transformed.push(markdown_editor_block_command_blank_line(parts))
        } else {
          transformed.push(parts.context_prefix + "- [ ] " + parts.content)
        }
      OrderedList =>
        if content_is_blank {
          transformed.push(markdown_editor_block_command_blank_line(parts))
        } else {
          let number = markdown_editor_next_selected_ordered_number(
            ordered_contexts,
            parts.context_prefix,
          )
          transformed.push(
            parts.context_prefix + number.to_string() + ". " + parts.content,
          )
        }
      Quote =>
        if markdown_editor_block_command_context_has_quote(parts.context_prefix) {
          transformed.push(line.text)
        } else {
          transformed.push(parts.context_prefix + "> " + parts.content)
        }
      _ => transformed.push(line.text)
    }
  }
  markdown_join_lines(transformed)
}

///|
pub(all) struct MarkdownEditorLineBlockParts {
  context_prefix : String
  content_start : Int
  content : String
}

///|
fn markdown_editor_line_block_parts(
  line : String,
) -> MarkdownEditorLineBlockParts {
  let chars = line.to_array()
  let context_length = markdown_editor_block_command_context_prefix_length(line)
  let marker_length = markdown_editor_marker_prefix_length_after_context(
    line, context_length,
  )
  let content_start = if marker_length > 0 {
    marker_length
  } else {
    context_length
  }
  {
    context_prefix: String::from_array(chars[:context_length]),
    content_start,
    content: String::from_array(chars[content_start:]),
  }
}

///|
fn markdown_editor_caret_offset_in_line_block_parts(
  parts : MarkdownEditorLineBlockParts,
  caret_offset : Int,
) -> Int {
  markdown_editor_clamp_int(
    caret_offset - parts.content_start,
    0,
    markdown_editor_text_length(parts.content),
  )
}

///|
fn markdown_editor_paragraph_line_text(
  line : String,
  parts : MarkdownEditorLineBlockParts,
) -> String {
  if markdown_editor_line_block_parts_has_inner_marker(parts) &&
    markdown_editor_block_command_context_has_quote(parts.context_prefix) {
    parts.context_prefix + parts.content
  } else {
    markdown_editor_strip_block_marker(line)
  }
}

///|
fn markdown_editor_line_block_parts_has_inner_marker(
  parts : MarkdownEditorLineBlockParts,
) -> Bool {
  parts.content_start > markdown_editor_text_length(parts.context_prefix)
}

///|
fn markdown_editor_block_command_blank_line(
  parts : MarkdownEditorLineBlockParts,
) -> String {
  if markdown_editor_block_command_context_has_quote(parts.context_prefix) {
    parts.context_prefix
  } else {
    ""
  }
}

///|
fn markdown_editor_block_command_context_has_quote(prefix : String) -> Bool {
  let chars = prefix.to_array()
  for index in 0..' {
      return true
    }
  }
  false
}

///|
fn markdown_editor_line_has_same_block_marker_after_context(
  line : String,
  command : MarkdownEditorCommand,
  context_prefix : String,
) -> Bool {
  let chars = line.to_array()
  let context_length = markdown_editor_text_length(context_prefix)
  markdown_editor_line_has_same_block_marker(
    String::from_array(chars[context_length:]),
    command,
  )
}

///|
fn markdown_editor_block_command_context_prefix_length(line : String) -> Int {
  let chars = line.to_array()
  let mut index = 0
  let mut scanning = true
  while scanning {
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    if index + 1 < chars.length() &&
      chars[index] == '>' &&
      chars[index + 1] == ' ' {
      index = index + 2
    } else {
      scanning = false
    }
  }
  index
}

///|
fn markdown_editor_marker_prefix_length_after_context(
  line : String,
  context_length : Int,
) -> Int {
  let chars = line.to_array()
  let nested_marker_length = markdown_editor_marker_prefix_length(
    String::from_array(chars[context_length:]),
  )
  if nested_marker_length > 0 {
    context_length + nested_marker_length
  } else {
    0
  }
}

///|
fn markdown_editor_next_selected_ordered_number(
  contexts : Array[MarkdownEditorOrderedRenumberContext],
  context_prefix : String,
) -> Int {
  let indent = markdown_editor_text_length(context_prefix)
  markdown_editor_trim_ordered_renumber_contexts(contexts, indent)
  if contexts.length() > 0 &&
    contexts[contexts.length() - 1].indent == indent &&
    contexts[contexts.length() - 1].context_prefix == context_prefix {
    let context = contexts[contexts.length() - 1]
    ignore(contexts.pop())
    contexts.push({ indent, context_prefix, expected: context.expected + 1 })
    context.expected
  } else {
    while contexts.length() > 0 &&
          contexts[contexts.length() - 1].indent >= indent {
      ignore(contexts.pop())
    }
    contexts.push({ indent, context_prefix, expected: 2 })
    1
  }
}

///|
pub(all) struct MarkdownEditorBlockPosition {
  block : MarkdownEditorBlock
  index : Int
}

///|
pub(all) struct MarkdownEditorBlockGroup {
  start_index : Int
  end_index : Int
  source_start : Int
  source_end : Int
}

///|
fn markdown_editor_noop_result(
  source : String,
  selection : MarkdownEditorSelection?,
) -> MarkdownEditorEditResult {
  match selection {
    Some(selection) =>
      { source, caret: selection.caret, selection: selection.selection }
    None =>
      { source, caret: markdown_editor_text_length(source), selection: None }
  }
}

///|
fn markdown_editor_duplicate_current_block(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorEditResult {
  match markdown_editor_active_edit_group(source, selection) {
    Some(group) => {
      let blocks = parse_markdown_editor_blocks(source)
      let chars = source.to_array()
      let block_text = String::from_array(
        chars[group.source_start:group.source_end],
      )
      if block_text == "" {
        return markdown_editor_noop_result(source, Some(selection))
      }
      let separator = markdown_editor_duplicate_group_separator(blocks, group)
      let duplicate_start = group.source_end +
        markdown_editor_text_length(separator)
      let next_source = String::from_array(chars[:group.source_end]) +
        separator +
        block_text +
        String::from_array(chars[group.source_end:])
      {
        source: next_source,
        caret: markdown_editor_map_offset_to_group(
          selection.caret,
          group,
          duplicate_start,
        ),
        selection: markdown_editor_map_selection_to_group(
          selection.selection,
          group,
          duplicate_start,
        ),
      }
    }
    None => markdown_editor_noop_result(source, Some(selection))
  }
}

///|
fn markdown_editor_duplicate_group_separator(
  blocks : Array[MarkdownEditorBlock],
  group : MarkdownEditorBlockGroup,
) -> String {
  if group.start_index < 0 ||
    group.end_index >= blocks.length() ||
    group.start_index > group.end_index {
    return "\n"
  }
  let first = blocks[group.start_index]
  let last = blocks[group.end_index]
  if markdown_editor_duplicate_kind_needs_blank_separator(first.block.kind) ||
    markdown_editor_duplicate_kind_needs_blank_separator(last.block.kind) {
    "\n\n"
  } else {
    "\n"
  }
}

///|
fn markdown_editor_duplicate_kind_needs_blank_separator(
  kind : MarkdownBlockKind,
) -> Bool {
  match kind {
    Paragraph | Table => true
    _ => false
  }
}

///|
fn markdown_editor_move_current_block(
  source : String,
  selection : MarkdownEditorSelection,
  up~ : Bool,
) -> MarkdownEditorEditResult {
  match markdown_editor_active_edit_group(source, selection) {
    Some(group) => {
      let blocks = parse_markdown_editor_blocks(source)
      let chars = source.to_array()
      if up {
        match markdown_editor_previous_edit_block(blocks, group.start_index) {
          Some(previous) => {
            let previous_text = String::from_array(
              chars[previous.source_range.start:previous.source_range.end],
            )
            let group_text = String::from_array(
              chars[group.source_start:group.source_end],
            )
            let gap = String::from_array(
              chars[previous.source_range.end:group.source_start],
            )
            let next_source = String::from_array(
                chars[:previous.source_range.start],
              ) +
              group_text +
              gap +
              previous_text +
              String::from_array(chars[group.source_end:])
            markdown_editor_moved_group_result(
              next_source,
              selection,
              group,
              new_start=previous.source_range.start,
            )
          }
          None => markdown_editor_noop_result(source, Some(selection))
        }
      } else {
        match markdown_editor_next_edit_block(blocks, group.end_index) {
          Some(next) => {
            let group_text = String::from_array(
              chars[group.source_start:group.source_end],
            )
            let next_text = String::from_array(
              chars[next.source_range.start:next.source_range.end],
            )
            let gap = String::from_array(
              chars[group.source_end:next.source_range.start],
            )
            let new_start = group.source_start +
              markdown_editor_text_length(next_text) +
              markdown_editor_text_length(gap)
            let next_source = String::from_array(chars[:group.source_start]) +
              next_text +
              gap +
              group_text +
              String::from_array(chars[next.source_range.end:])
            markdown_editor_moved_group_result(
              next_source,
              selection,
              group,
              new_start~,
            )
          }
          None => markdown_editor_noop_result(source, Some(selection))
        }
      }
    }
    None => markdown_editor_noop_result(source, Some(selection))
  }
}

///|
fn markdown_editor_moved_group_result(
  source : String,
  selection : MarkdownEditorSelection,
  group : MarkdownEditorBlockGroup,
  new_start~ : Int,
) -> MarkdownEditorEditResult {
  {
    source,
    caret: markdown_editor_map_offset_to_group(
      selection.caret,
      group,
      new_start,
    ),
    selection: markdown_editor_map_selection_to_group(
      selection.selection,
      group,
      new_start,
    ),
  }
}

///|
fn markdown_editor_active_edit_group(
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorBlockGroup? {
  let blocks = parse_markdown_editor_blocks(source)
  match markdown_editor_active_selection(selection.selection) {
    Some(range) => {
      let normalized = range.normalized()
      let mut start_index = -1
      let mut end_index = -1
      for index in 0.. block.source_range.start {
          if start_index < 0 {
            start_index = index
          }
          end_index = index
        }
      }
      if start_index >= 0 && end_index >= start_index {
        return Some({
          start_index,
          end_index,
          source_start: blocks[start_index].source_range.start,
          source_end: blocks[end_index].source_range.end,
        })
      }
    }
    None => ()
  }
  match
    markdown_editor_active_edit_block_from_blocks(blocks, source, selection) {
    Some(current) =>
      Some({
        start_index: current.index,
        end_index: current.index,
        source_start: current.block.source_range.start,
        source_end: current.block.source_range.end,
      })
    None => None
  }
}

///|
fn markdown_editor_active_edit_block_from_blocks(
  blocks : Array[MarkdownEditorBlock],
  source : String,
  selection : MarkdownEditorSelection,
) -> MarkdownEditorBlockPosition? {
  let chars = source.to_array()
  let anchor = match markdown_editor_active_selection(selection.selection) {
    Some(range) => range.start
    None => selection.caret
  }
  let anchor = markdown_editor_clamp_int(anchor, 0, chars.length())
  for index in 0..= block.source_range.start && anchor <= block.source_range.end {
      return Some({ block, index })
    }
  }
  None
}

///|
fn markdown_editor_previous_edit_block(
  blocks : Array[MarkdownEditorBlock],
  index : Int,
) -> MarkdownEditorBlock? {
  let mut candidate = index - 1
  while candidate >= 0 {
    let block = blocks[candidate]
    if !markdown_editor_empty_line_block_for_edit(block) {
      return Some(block)
    }
    candidate = candidate - 1
  }
  None
}

///|
fn markdown_editor_next_edit_block(
  blocks : Array[MarkdownEditorBlock],
  index : Int,
) -> MarkdownEditorBlock? {
  let mut candidate = index + 1
  while candidate < blocks.length() {
    let block = blocks[candidate]
    if !markdown_editor_empty_line_block_for_edit(block) {
      return Some(block)
    }
    candidate = candidate + 1
  }
  None
}

///|
fn markdown_editor_empty_line_block_for_edit(
  block : MarkdownEditorBlock,
) -> Bool {
  block.source_range.start == block.source_range.end && block.block.text == ""
}

///|
fn markdown_editor_map_offset_to_group(
  offset : Int,
  group : MarkdownEditorBlockGroup,
  group_start : Int,
) -> Int {
  group_start +
  markdown_editor_clamp_int(
    offset - group.source_start,
    0,
    group.source_end - group.source_start,
  )
}

///|
fn markdown_editor_map_selection_to_group(
  selection : @core.TextRange?,
  group : MarkdownEditorBlockGroup,
  group_start : Int,
) -> @core.TextRange? {
  match markdown_editor_active_selection(selection) {
    Some(range) =>
      if range.start >= group.source_start && range.end <= group.source_end {
        Some(
          @core.TextRange::new(
            start=markdown_editor_map_offset_to_group(
              range.start,
              group,
              group_start,
            ),
            end=markdown_editor_map_offset_to_group(
              range.end,
              group,
              group_start,
            ),
          ),
        )
      } else {
        None
      }
    None => None
  }
}

///|
fn markdown_editor_block_prefix(command : MarkdownEditorCommand) -> String {
  match command {
    Paragraph => ""
    Heading(level) => markdown_editor_heading_marker(level) + " "
    BulletList => "- "
    TaskList => "- [ ] "
    OrderedList => "1. "
    Quote => "> "
    _ => ""
  }
}

///|
fn markdown_editor_strip_block_marker(line : String) -> String {
  let offset = markdown_editor_marker_prefix_length(line)
  let chars = line.to_array()
  String::from_array(chars[offset:])
}

///|
fn markdown_editor_marker_prefix_length(line : String) -> Int {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  if chars.length() >= indent + 2 && chars[indent] == '#' {
    let mut index = indent
    while index < chars.length() && chars[index] == '#' {
      index = index + 1
    }
    if index < chars.length() && chars[index] == ' ' {
      index + 1
    } else {
      0
    }
  } else if chars.length() >= indent + 6 &&
    chars[indent] == '-' &&
    chars[indent + 1] == ' ' &&
    chars[indent + 2] == '[' &&
    (
      chars[indent + 3] == ' ' ||
      chars[indent + 3] == 'x' ||
      chars[indent + 3] == 'X'
    ) &&
    chars[indent + 4] == ']' &&
    chars[indent + 5] == ' ' {
    indent + 6
  } else if chars.length() >= indent + 2 &&
    (chars[indent] == '-' || chars[indent] == '*' || chars[indent] == '+') &&
    chars[indent + 1] == ' ' {
    indent + 2
  } else if chars.length() >= indent + 2 &&
    chars[indent] == '>' &&
    chars[indent + 1] == ' ' {
    indent + 2
  } else {
    markdown_editor_ordered_marker_length(chars)
  }
}

///|
fn markdown_editor_ordered_marker_length(chars : Array[Char]) -> Int {
  let indent = markdown_editor_leading_indent_length(chars)
  let mut index = indent
  while index < chars.length() && chars[index].is_ascii_digit() {
    index = index + 1
  }
  if index > indent &&
    index + 1 < chars.length() &&
    (chars[index] == '.' || chars[index] == ')') &&
    chars[index + 1] == ' ' {
    index + 2
  } else {
    0
  }
}

///|
fn markdown_editor_line_indent_prefix_length(line : String) -> Int {
  markdown_editor_leading_indent_length(line.to_array())
}

///|
fn markdown_editor_leading_indent_length(chars : Array[Char]) -> Int {
  let mut index = 0
  while index < chars.length() && (chars[index] == ' ' || chars[index] == '\t') {
    index = index + 1
  }
  index
}

///|
fn markdown_editor_string_ends_with(source : String, suffix : String) -> Bool {
  let source_chars = source.to_array()
  let suffix_chars = suffix.to_array()
  if suffix_chars.length() > source_chars.length() {
    return false
  }
  let start = source_chars.length() - suffix_chars.length()
  for index in 0..