///|
/// An opener-closer pair: `()`, `[]`, `{}`, `'…'`, and the `«»` that an `@`
/// splice uses.
fn Parser::parse_opener(
  self : Parser,
  t : @lexer.Token,
  i : Int,
  s : PState,
) -> GroupResult raise @err.ShrubberyError {
  self.check_block_mode(t, s)
  let at_initial = match s.at_mode {
    Some(am) => am.initial
    None => false
  }
  let (closer_text, kind, paren_immed) : (String, OpenerKind, ParenImmed) = match
    t.text {
    "(" =>
      (
        ")",
        KParens,
        // Immediately inside an `@` argument list, groups may be separated by
        // newlines instead of commas.
        if s.at_mode is Some(_) && !at_initial {
          AtArgs
        } else {
          Normal
        },
      )
    "{" => ("}", KBraces, Normal)
    "[" => ("]", KBrackets, Normal)
    "'" => ("'", KQuotes, NotImmed)
    "\u{AB}" =>
      if at_initial {
        ("\u{BB}", KAt, Normal)
      } else {
        self.fail(t, MisplacedGuillemet)
        ("\u{BB}", KAt, Normal)
      }
    _ => (")", KParens, Normal)
  }
  let pre_raw = s.raw
  let (group_commenting, a0) = self.next_of_commenting(
    i + 1,
    Some(t.line()),
    s.delta,
    RNil,
    s.count,
  )
  // `'«…»'` is a different `«»`: it disambiguates nested quotes and does NOT
  // turn off line sensitivity. The guillemets are not preserved.
  let quote_nested = kind is KQuotes &&
    group_commenting is None &&
    !self.at_end(a0.rest) &&
    self.tok(a0.rest).kind is Opener &&
    self.tok(a0.rest).text == "\u{AB}"
  let (a, use_closer) = if quote_nested {
    let g = self.tok(a0.rest)
    (
      self.next_of(a0.rest + 1, a0.last_line, a0.delta, a0.raw.push(g), s.count),
      "\u{BB}",
    )
  } else {
    (a0, closer_text)
  }
  let sub_column = if self.at_end(a.rest) {
    t.column().next()
  } else {
    self.tok(a.rest).column()
  }
  let inner = self.parse_groups(a.rest, {
    count: s.count,
    closer: Delim(use_closer, t),
    paren_immed,
    column: Some(sub_column),
    bar_column: None,
    check_column: s.count,
    bar_closes: false,
    bar_closes_line: None,
    // A group immediately inside an opener may not start with `|`.
    block_mode: Start,
    can_empty: true,
    comma_time: false,
    sequence_mode: if kind is KAt {
      OneOnly
    } else {
      AnyNumber
    },
    last_line: a.last_line,
    delta: a.delta,
    commenting: group_commenting,
    tail_commenting: None,
    raw: a.raw,
    variant: s.variant,
  })
  // Two closers need something after them.
  let (rest_i, close_line, close_delta, end_t, group_tail_raw) = if quote_nested {
    let b = self.next_of(
      inner.rest,
      inner.end_line,
      inner.end_delta,
      inner.tail_raw,
      s.count,
    )
    if !self.at_end(b.rest) &&
      self.tok(b.rest).kind is Closer &&
      self.tok(b.rest).text == "'" {
      let q = self.tok(b.rest)
      (b.rest + 1, b.last_line, b.delta, Some(q), b.raw.push(q))
    } else {
      match inner.end_token {
        Some(e) => self.fail(e, ExpectedQuoteAfterGuillemet)
        None => ()
      }
      (b.rest, b.last_line, b.delta, inner.end_token, b.raw)
    }
  } else if (match s.at_mode {
      Some(am) => am.splice
      None => false
    }) {
    // `@(«…»)`: the `(` was dropped when the splice was recognised, so the `)`
    // is consumed here.
    let post = self.peek(inner.rest)
    let ok = match post {
      Some(p) => p.kind is Closer && p.text == ")"
      None => false
    }
    if !ok {
      match inner.end_token {
        Some(e) => self.fail(e, ExpectedParenAfterGuillemet)
        None => ()
      }
    }
    match post {
      Some(p) =>
        (
          inner.rest + 1,
          inner.end_line,
          inner.end_delta,
          Some(p),
          inner.tail_raw.push(p),
        )
      None =>
        (
          inner.rest,
          inner.end_line,
          inner.end_delta,
          inner.end_token,
          inner.tail_raw,
        )
    }
  } else {
    (
      inner.rest,
      inner.end_line,
      inner.end_delta,
      inner.end_token,
      inner.tail_raw,
    )
  }
  let (suffix_raw, suffix_i, suffix_line, suffix_delta) = self.suffix_comments(
    rest_i, close_line, close_delta,
  )
  let cont = self.continue_at(
    s.at_mode,
    closer_text == ")",
    suffix_i,
    suffix_line,
    suffix_delta,
    s.count,
    s.variant,
  )
  let rest = self.parse_group(cont.rest, {
    ..s,
    line: cont.line,
    delta: cont.delta,
    block_mode: NoBlock,
    bar_closes_line: next_block_close_line(
      s.bar_closes_line,
      Some(t.line()),
      cont.line,
    ),
    raw: RNil,
    at_mode: cont.at_mode,
  })
  let children = to_group_nodes(inner.groups)
  let node = @ast.Node::new(
    match kind {
      KParens | KAt => Parens(children)
      KBrackets => Brackets(children)
      KBraces => Braces(children)
      KQuotes => Quotes(children)
    },
    span_of(t, end_t),
  )
  attach_prefix(node, pre_raw)
  node.meta.raw = Str(t.raw_text())
  node.meta.tail = group_tail_raw.to_raw()
  node.meta.suffix = suffix_raw.to_raw()
  let items = [node]
  for x in rest.items {
    items.push(x)
  }
  let adjusted = (cont.adjust)(items)
  if kind is KAt {
    let (spliced, tail) = self.splice_at(t, adjusted, rest.tail_raw)
    return { ..rest, items: spliced, tail_raw: tail, }
  }
  { ..rest, items: adjusted, }
}

///|
priv enum OpenerKind {
  KParens
  KBrackets
  KBraces
  KQuotes
  /// The `«»` of an `@` splice, which becomes `parens` and is then spliced
  /// away.
  KAt
}