idet-core 0.3.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, 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)> {
    match *chars.get(index)? {
        open @ ('(' | '[' | '{') => {
            forward_match(chars, index, open, closing(open)).map(|m| (index, m))
        }
        close @ (')' | ']' | '}') => {
            backward_match(chars, index, opening(close), close).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), 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);
    }
}