idet-core 0.1.0

Shared text-editing library for a Micro-like terminal editor and a gedit-like egui GUI
Documentation
//! Bracket-pair matching: find the counterpart of a bracket at or near the cursor.

fn forward_match(chars: &[char], from: usize, close: char) -> Option<usize> {
    let open = chars[from];
    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) -> Option<usize> {
    let close = chars[from];
    let mut depth: usize = 1;
    for index in (0..from).rev() {
        match chars[index] {
            c if c == close => depth += 1,
            c if c == 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();
    let len = chars.len();

    if cursor < len
        && let Some(result) = bracket_at(&chars, cursor)
    {
        return Some(result);
    }
    if cursor > 0
        && let Some(result) = bracket_at(&chars, cursor - 1)
    {
        return Some(result);
    }
    None
}

fn bracket_at(chars: &[char], index: usize) -> Option<(usize, usize)> {
    match chars[index] {
        '(' | '[' | '{' => forward_match(chars, index, closing(chars[index])).map(|m| (index, m)),
        ')' | ']' | '}' => backward_match(chars, index, opening(chars[index])).map(|m| (m, index)),
        _ => None,
    }
}

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

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

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

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

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

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

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

    #[test]
    fn mixed_types() {
        assert_eq!(
            find_matching_bracket("([)]", 3), // cursor before ')'
            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), // cursor on '('
            Some((0, 2))
        );
    }

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

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