use crate::editor::cursor::Position;
use crate::editor::document::Document;
const PAIRS: [(char, char); 6] = [
('(', ')'),
('[', ']'),
('{', '}'),
('"', '"'),
('\'', '\''),
('`', '`'),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Insertion {
Plain,
Pair(char),
Skip,
}
#[must_use]
pub fn closing_for(open: char) -> Option<char> {
PAIRS
.iter()
.find(|(candidate, _)| *candidate == open)
.map(|(_, close)| *close)
}
#[must_use]
pub fn resolve(doc: &Document, pos: Position, ch: char) -> Insertion {
let next = char_at(doc, pos);
if is_closing(ch) && next == Some(ch) {
return Insertion::Skip;
}
let Some(close) = closing_for(ch) else {
return Insertion::Plain;
};
if next.is_some_and(is_word) {
return Insertion::Plain;
}
if close == ch {
let previous = char_before(doc, pos);
if previous.is_some_and(|previous| is_word(previous) || previous == ch || previous == '\\')
{
return Insertion::Plain;
}
}
Insertion::Pair(close)
}
#[must_use]
pub fn surrounds(doc: &Document, pos: Position) -> bool {
match (char_before(doc, pos), char_at(doc, pos)) {
(Some(open), Some(close)) => closing_for(open) == Some(close),
_ => false,
}
}
fn is_closing(ch: char) -> bool {
PAIRS.iter().any(|(_, close)| *close == ch)
}
fn is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
fn char_at(doc: &Document, pos: Position) -> Option<char> {
(pos.col < doc.line_len(pos.line)).then(|| doc.line(pos.line).char(pos.col))
}
fn char_before(doc: &Document, pos: Position) -> Option<char> {
(pos.col > 0).then(|| doc.line(pos.line).char(pos.col - 1))
}
#[cfg(test)]
mod tests {
use super::*;
fn typing(text: &str, col: usize, ch: char) -> Insertion {
let doc = Document::from_text(text, None);
resolve(&doc, Position::new(0, col), ch)
}
#[test]
fn a_bracket_typed_at_the_end_of_a_line_is_closed() {
assert_eq!(typing("call", 4, '('), Insertion::Pair(')'));
assert_eq!(typing("", 0, '{'), Insertion::Pair('}'));
assert_eq!(typing("", 0, '['), Insertion::Pair(']'));
}
#[test]
fn a_bracket_typed_in_front_of_a_word_is_left_alone() {
assert_eq!(typing("foo", 0, '('), Insertion::Plain);
assert_eq!(typing(" foo", 0, '('), Insertion::Pair(')'));
assert_eq!(typing(")", 0, '('), Insertion::Pair(')'));
}
#[test]
fn typing_a_closing_bracket_steps_over_the_one_already_there() {
assert_eq!(typing("()", 1, ')'), Insertion::Skip);
assert_eq!(typing("{}", 1, '}'), Insertion::Skip);
assert_eq!(typing("", 0, ')'), Insertion::Plain);
}
#[test]
fn a_quote_opens_a_pair_where_a_string_could_start() {
assert_eq!(typing("let s = ", 8, '"'), Insertion::Pair('"'));
assert_eq!(typing("", 0, '\''), Insertion::Pair('\''));
}
#[test]
fn an_apostrophe_inside_a_word_stays_a_single_character() {
assert_eq!(typing("don", 3, '\''), Insertion::Plain);
assert_eq!(typing("it", 2, '"'), Insertion::Plain);
}
#[test]
fn an_escaped_quote_is_not_a_pair() {
assert_eq!(typing("\"a\\", 3, '"'), Insertion::Plain);
}
#[test]
fn the_closing_quote_of_a_pair_is_stepped_over() {
assert_eq!(typing("\"\"", 1, '"'), Insertion::Skip);
}
#[test]
fn a_third_quote_does_not_open_yet_another_pair() {
assert_eq!(typing("\"\"", 2, '"'), Insertion::Plain);
}
#[test]
fn a_caret_between_the_halves_of_a_pair_is_recognised() {
let doc = Document::from_text("()", None);
assert!(surrounds(&doc, Position::new(0, 1)));
assert!(!surrounds(&doc, Position::new(0, 0)));
assert!(!surrounds(&doc, Position::new(0, 2)));
let mismatched = Document::from_text("(]", None);
assert!(!surrounds(&mismatched, Position::new(0, 1)));
}
}