///|
pub(all) struct GrepLineMatch {
  line_number : Int
  column : Int
  line : String
} derive(Eq, Debug)

///|
pub(all) struct GrepFileMatch {
  path : String
  matches : Array[GrepLineMatch]
} derive(Eq, Debug)

///|
fn grep_search_show_string(value : String) -> String {
  let buf = StringBuilder::new()
  buf.write_char('"')
  for c in value {
    if c == '"' {
      buf.write_string("\\\"")
    } else if c == '\\' {
      buf.write_string("\\\\")
    } else if c == '\n' {
      buf.write_string("\\n")
    } else if c == '\r' {
      buf.write_string("\\r")
    } else if c == '\t' {
      buf.write_string("\\t")
    } else {
      buf.write_char(c)
    }
  }
  buf.write_char('"')
  buf.to_string()
}

///|
pub impl Show for GrepLineMatch with fn output(self, logger) {
  logger.write_string(
    "{line_number: " +
    self.line_number.to_string() +
    ", column: " +
    self.column.to_string() +
    ", line: " +
    grep_search_show_string(self.line) +
    "}",
  )
}

///|
pub impl Show for GrepFileMatch with fn output(self, logger) {
  logger.write_string(
    "{path: " +
    grep_search_show_string(self.path) +
    ", matches: " +
    Repr(self.matches).to_string() +
    "}",
  )
}

///|
priv struct GrepBlobSource {
  display_path : String
  blob_id : @bit.ObjectId
}

///|
pub fn search_tracked_worktree(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  root : String,
  patterns : Array[String],
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let expr = match grep_or_expr_from_patterns(patterns) {
    Some(value) => value
    None => return []
  }
  search_tracked_worktree_expr(
    fs,
    git_dir,
    root,
    expr,
    paths,
    pattern_type~,
    ignore_case~,
    invert_match~,
    word_regexp~,
    max_depth~,
  )
}

///|
pub fn search_tracked_worktree_expr(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  root : String,
  expr : GrepExpr,
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let index_entries = @bitlib.read_index_entries(fs, git_dir)
  let assume_unchanged_paths = @bitlib.read_assume_unchanged_paths(fs, git_dir)
  let skip_worktree_paths = @bitlib.read_skip_worktree_paths(fs, git_dir)
  let db = @bitlib.ObjectDb::load(fs, git_dir)
  let matches : Array[GrepFileMatch] = []
  for entry in index_entries {
    if !grep_path_matches_filters(entry.path, paths, max_depth) {
      continue
    }
    let abs_path = root + "/" + entry.path
    let has_worktree_file = fs.is_file(abs_path)
    let content = if assume_unchanged_paths.contains(entry.path) {
      if skip_worktree_paths.contains(entry.path) {
        if !has_worktree_file {
          continue
        }
        @utf8.decode_lossy(fs.read_file(abs_path)[:])
      } else {
        if entry.intent_to_add {
          continue
        }
        let obj = db.get(fs, entry.id)
        guard obj is Some(blob) else { continue }
        @utf8.decode_lossy(blob.data[:])
      }
    } else if skip_worktree_paths.contains(entry.path) {
      if !has_worktree_file {
        continue
      }
      @utf8.decode_lossy(fs.read_file(abs_path)[:])
    } else {
      if entry.intent_to_add {
        continue
      }
      if !has_worktree_file {
        continue
      }
      @utf8.decode_lossy(fs.read_file(abs_path)[:])
    }
    let file_matches = collect_line_matches(
      content, expr, pattern_type, ignore_case, invert_match, word_regexp,
    )
    if file_matches.length() > 0 {
      matches.push({ path: entry.path, matches: file_matches })
    }
  }
  matches
}

///|
pub fn list_tracked_worktree_candidate_paths(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  paths : Array[String],
  max_depth? : Int = -1,
) -> Array[String] raise @bit.GitError {
  let index_entries = @bitlib.read_index_entries(fs, git_dir)
  let assume_unchanged_paths = @bitlib.read_assume_unchanged_paths(fs, git_dir)
  let out : Array[String] = []
  for entry in index_entries {
    if !grep_path_matches_filters(entry.path, paths, max_depth) {
      continue
    }
    if entry.intent_to_add && assume_unchanged_paths.contains(entry.path) {
      continue
    }
    out.push(entry.path)
  }
  out
}

///|
pub fn search_index(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  patterns : Array[String],
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let expr = match grep_or_expr_from_patterns(patterns) {
    Some(value) => value
    None => return []
  }
  search_index_expr(
    fs,
    git_dir,
    expr,
    paths,
    pattern_type~,
    ignore_case~,
    invert_match~,
    word_regexp~,
    max_depth~,
  )
}

///|
pub fn search_index_expr(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  expr : GrepExpr,
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let index_entries = @bitlib.read_index_entries(fs, git_dir)
  let sources = collect_index_blob_sources(index_entries, paths, max_depth)
  let db = @bitlib.ObjectDb::load(fs, git_dir)
  search_blob_sources(
    db, fs, sources, expr, pattern_type, ignore_case, invert_match, word_regexp,
  )
}

///|
pub fn list_index_candidate_paths(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  paths : Array[String],
  max_depth? : Int = -1,
) -> Array[String] raise @bit.GitError {
  let index_entries = @bitlib.read_index_entries(fs, git_dir)
  grep_collect_candidate_paths(index_entries, paths, max_depth)
}

///|
pub fn search_treeish(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  spec : String,
  patterns : Array[String],
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let expr = match grep_or_expr_from_patterns(patterns) {
    Some(value) => value
    None => return []
  }
  search_treeish_expr(
    fs,
    git_dir,
    spec,
    expr,
    paths,
    pattern_type~,
    ignore_case~,
    invert_match~,
    word_regexp~,
    max_depth~,
  )
}

///|
pub fn search_treeish_expr(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  spec : String,
  expr : GrepExpr,
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let db = @bitlib.ObjectDb::load(fs, git_dir)
  let tree_id = match @bitrepo.rev_parse(fs, git_dir, spec + "^{tree}") {
    Some(id) => id
    None =>
      raise @bit.GitError::InvalidObject(
        "fatal: ambiguous argument '" + spec + "': unknown revision or path",
      )
  }
  let tree_paths = collect_tree_blob_sources(db, fs, tree_id, "", spec)
  let filtered_sources = filter_blob_sources_by_paths(
    tree_paths, paths, max_depth,
  )
  search_blob_sources(
    db, fs, filtered_sources, expr, pattern_type, ignore_case, invert_match, word_regexp,
  )
}

///|
pub fn list_treeish_candidate_paths(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  spec : String,
  paths : Array[String],
  max_depth? : Int = -1,
) -> Array[String] raise @bit.GitError {
  let db = @bitlib.ObjectDb::load(fs, git_dir)
  let tree_id = match @bitrepo.rev_parse(fs, git_dir, spec + "^{tree}") {
    Some(id) => id
    None =>
      raise @bit.GitError::InvalidObject(
        "fatal: ambiguous argument '" + spec + "': unknown revision or path",
      )
  }
  let sources = filter_blob_sources_by_paths(
    collect_tree_blob_sources(db, fs, tree_id, "", spec),
    paths,
    max_depth,
  )
  let out : Array[String] = []
  for source in sources {
    out.push(source.display_path)
  }
  out
}

///|
pub fn search_plain_worktree_expr(
  fs : &@bit.RepoFileSystem,
  root : String,
  expr : GrepExpr,
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
  exclude_standard? : Bool = false,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let candidate_paths = list_plain_worktree_candidate_paths(
    fs,
    root,
    paths,
    max_depth~,
    exclude_standard~,
  )
  search_worktree_paths(
    fs, root, candidate_paths, expr, pattern_type, ignore_case, invert_match, word_regexp,
  )
}

///|
pub fn list_plain_worktree_candidate_paths(
  fs : &@bit.RepoFileSystem,
  root : String,
  paths : Array[String],
  max_depth? : Int = -1,
  exclude_standard? : Bool = false,
) -> Array[String] raise @bit.GitError {
  let out : Array[String] = []
  let matcher = @ignore.Matcher::new()
  grep_walk_plain_dir(fs, root, "", exclude_standard, matcher, out)
  out.sort_by((a, b) => String::lexical_compare(a, b))
  let filtered : Array[String] = []
  for path in out {
    if grep_path_matches_filters(path, paths, max_depth) {
      filtered.push(path)
    }
  }
  filtered
}

///|
pub fn search_untracked_worktree_expr(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  root : String,
  expr : GrepExpr,
  paths : Array[String],
  pattern_type? : GrepPatternType = GrepPatternType::Fixed,
  ignore_case? : Bool = false,
  invert_match? : Bool = false,
  word_regexp? : Bool = false,
  max_depth? : Int = -1,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let candidate_paths = list_untracked_worktree_candidate_paths(
    fs,
    git_dir,
    root,
    paths,
    max_depth~,
  )
  search_worktree_paths(
    fs, root, candidate_paths, expr, pattern_type, ignore_case, invert_match, word_regexp,
  )
}

///|
pub fn list_untracked_worktree_candidate_paths(
  fs : &@bit.RepoFileSystem,
  git_dir : String,
  root : String,
  paths : Array[String],
  max_depth? : Int = -1,
) -> Array[String] raise @bit.GitError {
  let tracked_entries = @bitlib.read_index_entries(fs, git_dir)
  let tracked : Map[String, Bool] = Map([])
  for entry in tracked_entries {
    tracked[entry.path] = true
  }
  let visible_paths = @bitlib.list_working_files(fs, root)
  let out : Array[String] = []
  for path in visible_paths {
    if tracked.contains(path) {
      continue
    }
    if grep_path_matches_filters(path, paths, max_depth) {
      out.push(path)
    }
  }
  out.sort_by((a, b) => String::lexical_compare(a, b))
  out
}

///|
fn search_worktree_paths(
  fs : &@bit.RepoFileSystem,
  root : String,
  candidate_paths : Array[String],
  expr : GrepExpr,
  pattern_type : GrepPatternType,
  ignore_case : Bool,
  invert_match : Bool,
  word_regexp : Bool,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let matches : Array[GrepFileMatch] = []
  for path in candidate_paths {
    let abs_path = root + "/" + path
    if !fs.is_file(abs_path) {
      continue
    }
    let content = @utf8.decode_lossy(fs.read_file(abs_path)[:])
    let file_matches = collect_line_matches(
      content, expr, pattern_type, ignore_case, invert_match, word_regexp,
    )
    if file_matches.length() > 0 {
      matches.push({ path, matches: file_matches })
    }
  }
  matches
}

///|
fn grep_walk_plain_dir(
  fs : &@bit.RepoFileSystem,
  root : String,
  rel : String,
  exclude_standard : Bool,
  matcher : @ignore.Matcher,
  out : Array[String],
) -> Unit raise @bit.GitError {
  let dir = if rel == "" { root } else { @bit.join_path(root, rel) }
  match @bitio.readdir_typed(dir) {
    Some(typed_entries) =>
      grep_walk_plain_dir_typed(
        fs, root, rel, dir, exclude_standard, matcher, out, typed_entries,
      )
    None =>
      grep_walk_plain_dir_fallback(
        fs, root, rel, dir, exclude_standard, matcher, out,
      )
  }
}

///|
fn grep_walk_plain_dir_typed(
  fs : &@bit.RepoFileSystem,
  root : String,
  rel : String,
  dir : String,
  exclude_standard : Bool,
  matcher : @ignore.Matcher,
  out : Array[String],
  typed_entries : Array[(String, Int)],
) -> Unit raise @bit.GitError {
  let mut has_gitignore = false
  for entry in typed_entries {
    if entry.0 == ".gitignore" {
      has_gitignore = true
      break
    }
  }
  let prev_len = matcher.len()
  if exclude_standard && has_gitignore {
    let ignore_path = @bit.join_path(dir, ".gitignore")
    let content = @utf8.decode_lossy(fs.read_file(ignore_path)[:])
    matcher.add_rules(rel, content)
  }
  for entry in typed_entries {
    let name = entry.0
    let d_type = entry.1
    if name == ".git" || name == ".bit" || name == ".jj" {
      continue
    }
    let child_rel = if rel == "" { name } else { rel + "/" + name }
    let is_dir = if d_type == 4 {
      true
    } else if d_type == 10 || d_type == 0 {
      fs.is_dir(@bit.join_path(root, child_rel))
    } else {
      false
    }
    if exclude_standard && matcher.is_ignored(child_rel, is_dir) {
      if is_dir && matcher.has_negation() {
        grep_walk_plain_dir(fs, root, child_rel, exclude_standard, matcher, out)
      }
      continue
    }
    if is_dir {
      grep_walk_plain_dir(fs, root, child_rel, exclude_standard, matcher, out)
    } else {
      out.push(child_rel)
    }
  }
  if exclude_standard {
    matcher.truncate(prev_len)
  }
}

///|
fn grep_walk_plain_dir_fallback(
  fs : &@bit.RepoFileSystem,
  root : String,
  rel : String,
  dir : String,
  exclude_standard : Bool,
  matcher : @ignore.Matcher,
  out : Array[String],
) -> Unit raise @bit.GitError {
  let entries = fs.readdir(dir)
  let mut has_gitignore = false
  for name in entries {
    if name == ".gitignore" {
      has_gitignore = true
      break
    }
  }
  let prev_len = matcher.len()
  if exclude_standard && has_gitignore {
    let ignore_path = @bit.join_path(dir, ".gitignore")
    let content = @utf8.decode_lossy(fs.read_file(ignore_path)[:])
    matcher.add_rules(rel, content)
  }
  for name in entries {
    if name == "." ||
      name == ".." ||
      name == ".git" ||
      name == ".bit" ||
      name == ".jj" {
      continue
    }
    let child_rel = if rel == "" { name } else { rel + "/" + name }
    let child_path = @bit.join_path(root, child_rel)
    let is_dir = fs.is_dir(child_path)
    if exclude_standard && matcher.is_ignored(child_rel, is_dir) {
      if is_dir && matcher.has_negation() {
        grep_walk_plain_dir(fs, root, child_rel, exclude_standard, matcher, out)
      }
      continue
    }
    if is_dir {
      grep_walk_plain_dir(fs, root, child_rel, exclude_standard, matcher, out)
    } else {
      out.push(child_rel)
    }
  }
  if exclude_standard {
    matcher.truncate(prev_len)
  }
}

///|
fn collect_tracked_paths(
  index_entries : Array[@bitlib.IndexEntry],
  paths : Array[String],
  max_depth : Int,
) -> Array[String] {
  let files_to_search : Array[String] = []
  for entry in index_entries {
    if grep_path_matches_filters(entry.path, paths, max_depth) {
      files_to_search.push(entry.path)
    }
  }
  files_to_search
}

///|
fn grep_collect_candidate_paths(
  index_entries : Array[@bitlib.IndexEntry],
  paths : Array[String],
  max_depth : Int,
) -> Array[String] {
  let files_to_search : Array[String] = []
  for entry in index_entries {
    if entry.intent_to_add {
      continue
    }
    if grep_path_matches_filters(entry.path, paths, max_depth) {
      files_to_search.push(entry.path)
    }
  }
  files_to_search
}

///|
fn collect_index_paths(
  index_entries : Array[@bitlib.IndexEntry],
  paths : Array[String],
  max_depth : Int,
) -> Array[String] {
  collect_tracked_paths(index_entries, paths, max_depth)
}

///|
fn collect_index_blob_sources(
  index_entries : Array[@bitlib.IndexEntry],
  paths : Array[String],
  max_depth : Int,
) -> Array[GrepBlobSource] {
  let selected_paths = collect_index_paths(index_entries, paths, max_depth)
  let selected : Map[String, Bool] = Map([])
  for path in selected_paths {
    selected[path] = true
  }
  let sources : Array[GrepBlobSource] = []
  for entry in index_entries {
    if selected.get(entry.path).unwrap_or(false) {
      sources.push({ display_path: entry.path, blob_id: entry.id })
    }
  }
  sources
}

///|
fn collect_tree_blob_sources(
  db : @bitlib.ObjectDb,
  fs : &@bit.RepoFileSystem,
  tree_id : @bit.ObjectId,
  prefix : String,
  spec : String,
) -> Array[GrepBlobSource] raise @bit.GitError {
  let sources : Array[GrepBlobSource] = []
  let tree_obj = db.get(fs, tree_id)
  guard tree_obj is Some(obj) else { return sources }
  let entries = @bit.parse_tree(obj.data)
  for entry in entries {
    let path = if prefix == "" { entry.name } else { prefix + "/" + entry.name }
    if entry.mode.has_prefix("100") {
      sources.push({ display_path: spec + ":" + path, blob_id: entry.id })
    } else if entry.mode == "40000" || entry.mode == "040000" {
      let sub_sources = collect_tree_blob_sources(db, fs, entry.id, path, spec)
      for source in sub_sources {
        sources.push(source)
      }
    }
  }
  sources
}

///|
fn filter_blob_sources_by_paths(
  sources : Array[GrepBlobSource],
  paths : Array[String],
  max_depth : Int,
) -> Array[GrepBlobSource] {
  if paths.length() == 0 && max_depth < 0 {
    return sources
  }
  let filtered : Array[GrepBlobSource] = []
  for source in sources {
    let path = strip_treeish_display_prefix(source.display_path)
    if grep_path_matches_filters(path, paths, max_depth) {
      filtered.push(source)
    }
  }
  filtered
}

///|
fn strip_treeish_display_prefix(display_path : String) -> String {
  match display_path.find(":") {
    Some(idx) =>
      String::unsafe_substring(
        display_path,
        start=idx + 1,
        end=display_path.length(),
      )
    None => display_path
  }
}

///|
fn grep_path_matches_filters(
  path : String,
  filters : Array[String],
  max_depth : Int,
) -> Bool {
  if filters.length() == 0 {
    return max_depth < 0 || grep_path_depth(path) <= max_depth
  }
  for filter in filters {
    let normalized = grep_normalize_path_filter(filter)
    if grep_filter_has_glob(normalized) {
      if grep_glob_matches(path, normalized) {
        return true
      }
      continue
    }
    if normalized == "" {
      if max_depth < 0 || grep_path_depth(path) <= max_depth {
        return true
      }
      continue
    }
    if path == normalized {
      return true
    }
    if path.has_prefix(normalized + "/") {
      if max_depth < 0 {
        return true
      }
      let rel = String::unsafe_substring(
        path,
        start=normalized.length() + 1,
        end=path.length(),
      )
      if grep_path_depth(rel) <= max_depth {
        return true
      }
    }
  }
  false
}

///|
fn grep_filter_has_glob(path : String) -> Bool {
  path.contains("*") || path.contains("?") || path.contains("[")
}

///|
fn grep_glob_matches(path : String, pattern : String) -> Bool {
  grep_glob_match_chars(path.to_array(), 0, pattern.to_array(), 0)
}

///|
fn grep_glob_match_chars(
  text : Array[Char],
  ti : Int,
  pattern : Array[Char],
  pi : Int,
) -> Bool {
  if pi >= pattern.length() {
    return ti >= text.length()
  }
  let pc = pattern[pi]
  if pc == '*' {
    let mut next_ti = ti
    while next_ti <= text.length() {
      if grep_glob_match_chars(text, next_ti, pattern, pi + 1) {
        return true
      }
      next_ti += 1
    }
    return false
  }
  if ti >= text.length() {
    return false
  }
  if pc == '?' {
    return grep_glob_match_chars(text, ti + 1, pattern, pi + 1)
  }
  if pc == '[' {
    let class_end = grep_glob_class_end(pattern, pi)
    if class_end <= pi + 1 {
      return text[ti] == '[' &&
        grep_glob_match_chars(text, ti + 1, pattern, pi + 1)
    }
    if grep_glob_class_matches(pattern, pi, class_end, text[ti]) {
      return grep_glob_match_chars(text, ti + 1, pattern, class_end + 1)
    }
    return false
  }
  if text[ti] == pc {
    return grep_glob_match_chars(text, ti + 1, pattern, pi + 1)
  }
  false
}

///|
fn grep_glob_class_end(pattern : Array[Char], start : Int) -> Int {
  let mut i = start + 1
  while i < pattern.length() {
    if pattern[i] == ']' {
      return i
    }
    i += 1
  }
  start
}

///|
fn grep_glob_class_matches(
  pattern : Array[Char],
  start : Int,
  class_end : Int,
  c : Char,
) -> Bool {
  let mut i = start + 1
  while i < class_end {
    if pattern[i] == c {
      return true
    }
    i += 1
  }
  false
}

///|
fn grep_normalize_path_filter(path : String) -> String {
  if path == "." {
    ""
  } else if path.has_suffix("/") {
    String::unsafe_substring(path, start=0, end=path.length() - 1)
  } else {
    path
  }
}

///|
fn grep_path_depth(path : String) -> Int {
  let mut depth = 0
  for c in path.to_array() {
    if c == '/' {
      depth += 1
    }
  }
  depth
}

///|
fn search_blob_sources(
  db : @bitlib.ObjectDb,
  fs : &@bit.RepoFileSystem,
  sources : Array[GrepBlobSource],
  expr : GrepExpr,
  pattern_type : GrepPatternType,
  ignore_case : Bool,
  invert_match : Bool,
  word_regexp : Bool,
) -> Array[GrepFileMatch] raise @bit.GitError {
  let matches : Array[GrepFileMatch] = []
  for source in sources {
    let obj = db.get(fs, source.blob_id)
    guard obj is Some(blob) else { continue }
    let file_matches = collect_line_matches(
      @utf8.decode_lossy(blob.data[:]),
      expr,
      pattern_type,
      ignore_case,
      invert_match,
      word_regexp,
    )
    if file_matches.length() > 0 {
      matches.push({ path: source.display_path, matches: file_matches })
    }
  }
  matches
}

///|
fn collect_line_matches(
  content : String,
  expr : GrepExpr,
  pattern_type : GrepPatternType,
  ignore_case : Bool,
  invert_match : Bool,
  word_regexp : Bool,
) -> Array[GrepLineMatch] {
  let matches : Array[GrepLineMatch] = []
  let lines : Array[String] = []
  for line_view in content.split("\n") {
    lines.push(line_view.to_owned())
  }
  let last = lines.length() - 1
  for i in 0.. value.start_column
        None => 1
      }
      matches.push({ line_number, column, line })
    }
  }
  matches
}