#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PairAction {
Pair,
SkipOver,
Plain,
}
pub fn closing_for(open: char) -> Option<char> {
match open {
'(' => Some(')'),
'[' => Some(']'),
'{' => Some('}'),
'"' => Some('"'),
'\'' => Some('\''),
'`' => Some('`'),
_ => None,
}
}
pub fn opening_for(close: char) -> Option<char> {
match close {
')' => Some('('),
']' => Some('['),
'}' => Some('{'),
'"' => Some('"'),
'\'' => Some('\''),
'`' => Some('`'),
_ => None,
}
}
pub fn is_bracket(ch: char) -> bool {
matches!(ch, '(' | ')' | '[' | ']' | '{' | '}')
}
pub fn is_quote(ch: char) -> bool {
matches!(ch, '"' | '\'' | '`')
}
fn is_word_char(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
pub fn plan_typing(
typed: char,
before: Option<char>,
after: Option<char>,
has_selection: bool,
) -> PairAction {
if has_selection {
return if closing_for(typed).is_some() { PairAction::Pair } else { PairAction::Plain };
}
if is_quote(typed) {
if after == Some(typed) {
return PairAction::SkipOver;
}
return if before.map(is_word_char).unwrap_or(false) {
PairAction::Plain
} else {
PairAction::Pair
};
}
if closing_for(typed).is_some() {
return PairAction::Pair;
}
if let Some(open) = opening_for(typed) {
if after == Some(typed) && before == Some(open) {
return PairAction::SkipOver;
}
if after == Some(typed) {
return PairAction::SkipOver;
}
return PairAction::Plain;
}
PairAction::Plain
}
pub fn should_delete_pair(before: Option<char>, after: Option<char>) -> bool {
match (before, after) {
(Some(open), Some(close)) => closing_for(open) == Some(close),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn brackets_pair_and_close() {
assert_eq!(closing_for('('), Some(')'));
assert_eq!(closing_for('['), Some(']'));
assert_eq!(closing_for('{'), Some('}'));
assert_eq!(closing_for('x'), None);
assert_eq!(opening_for('}'), Some('{'));
}
#[test]
fn openers_insert_their_partner() {
assert_eq!(plan_typing('(', None, None, false), PairAction::Pair);
assert_eq!(plan_typing('{', Some(' '), Some(' '), false), PairAction::Pair);
}
#[test]
fn closer_skips_over_existing_partner() {
assert_eq!(plan_typing(')', Some('('), Some(')'), false), PairAction::SkipOver);
assert_eq!(plan_typing(']', None, Some(']'), false), PairAction::SkipOver);
assert_eq!(plan_typing(')', Some('x'), Some('y'), false), PairAction::Plain);
}
#[test]
fn quotes_pair_but_not_inside_words() {
assert_eq!(plan_typing('"', None, None, false), PairAction::Pair);
assert_eq!(plan_typing('\'', Some('t'), None, false), PairAction::Plain);
assert_eq!(plan_typing('"', Some(' '), Some('"'), false), PairAction::SkipOver);
}
#[test]
fn selection_wraps_for_brackets_and_quotes_but_not_letters() {
assert_eq!(plan_typing('(', None, None, true), PairAction::Pair);
assert_eq!(plan_typing('"', None, None, true), PairAction::Pair);
assert_eq!(plan_typing('a', None, None, true), PairAction::Plain);
}
#[test]
fn empty_pair_is_deleted_together() {
assert!(should_delete_pair(Some('('), Some(')')));
assert!(should_delete_pair(Some('"'), Some('"')));
assert!(!should_delete_pair(Some('a'), Some('b')));
assert!(!should_delete_pair(Some('('), None));
}
}