idet-core 0.4.0

Editing logic for text editors, without a frontend
Documentation
//! Bracket-pair matching: find the counterpart of a bracket at or near the cursor.

fn forward_match(chars: &[char], from: usize, open: char, close: char) -> Option<usize> {
    let mut depth: usize = 1;
    for (index, &character) in chars.iter().enumerate().skip(from + 1) {
        if character == open {
            depth += 1;
        } else if character == close {
            depth -= 1;
            if depth == 0 {
                return Some(index);
            }
        }
    }
    None
}

fn backward_match(chars: &[char], from: usize, open: char, close: char) -> Option<usize> {
    let mut depth: usize = 1;
    for (index, &character) in chars.iter().enumerate().take(from).rev() {
        if character == close {
            depth += 1;
        } else if character == open {
            depth -= 1;
            if depth == 0 {
                return Some(index);
            }
        }
    }
    None
}

/// Returns the positions of a matched bracket pair if the cursor is on or
/// adjacent to a bracket. The first element is the opening bracket,
/// the second is the closing bracket.
#[must_use]
pub fn find_matching_bracket(text: &str, cursor: usize) -> Option<(usize, usize)> {
    let chars: Vec<char> = text.chars().collect();

    if let Some(result) = bracket_at(&chars, cursor) {
        return Some(result);
    }
    bracket_at(&chars, cursor.checked_sub(1)?)
}

fn bracket_at(chars: &[char], index: usize) -> Option<(usize, usize)> {
    let character = *chars.get(index)?;
    if let Some(close) = closing(character) {
        return forward_match(chars, index, character, close).map(|end| (index, end));
    }
    let open = opening(character)?;
    backward_match(chars, index, open, character).map(|start| (start, index))
}

const fn closing(open: char) -> Option<char> {
    match open {
        '(' => Some(')'),
        '[' => Some(']'),
        '{' => Some('}'),
        _ => None,
    }
}

const fn opening(close: char) -> Option<char> {
    match close {
        ')' => Some('('),
        ']' => Some('['),
        '}' => Some('{'),
        _ => None,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn simple_pair() {
        assert_eq!(find_matching_bracket("(hello)", 1), Some((0, 6)));
    }

    #[test]
    fn cursor_before_close() {
        assert_eq!(find_matching_bracket("(hello)", 6), Some((0, 6)));
    }

    #[test]
    fn nested_brackets() {
        assert_eq!(find_matching_bracket("a(b[c]d)e", 4), Some((3, 5)));
    }

    #[test]
    fn unmatched() {
        assert_eq!(find_matching_bracket("(hello", 1), None);
    }

    #[test]
    fn mixed_types() {
        assert_eq!(find_matching_bracket("([)]", 3), Some((1, 3)));
    }

    #[test]
    fn no_bracket() {
        assert_eq!(find_matching_bracket("hello", 2), None);
    }

    #[test]
    fn cursor_at_open() {
        assert_eq!(find_matching_bracket("(x)", 0), Some((0, 2)));
    }

    #[test]
    fn cursor_at_close() {
        assert_eq!(find_matching_bracket("(x)", 2), Some((0, 2)));
    }

    #[test]
    fn empty_text() {
        assert_eq!(find_matching_bracket("", 0), None);
    }
}