Skip to main content

idet_core/
brackets.rs

1//! Bracket-pair matching: find the counterpart of a bracket at or near the cursor.
2
3fn forward_match(chars: &[char], from: usize, open: char, close: char) -> Option<usize> {
4    let mut depth: usize = 1;
5    for (index, &character) in chars.iter().enumerate().skip(from + 1) {
6        if character == open {
7            depth += 1;
8        } else if character == close {
9            depth -= 1;
10            if depth == 0 {
11                return Some(index);
12            }
13        }
14    }
15    None
16}
17
18fn backward_match(chars: &[char], from: usize, open: char, close: char) -> Option<usize> {
19    let mut depth: usize = 1;
20    for (index, &character) in chars.iter().enumerate().take(from).rev() {
21        if character == close {
22            depth += 1;
23        } else if character == open {
24            depth -= 1;
25            if depth == 0 {
26                return Some(index);
27            }
28        }
29    }
30    None
31}
32
33/// Returns the positions of a matched bracket pair if the cursor is on or
34/// adjacent to a bracket. The first element is the opening bracket,
35/// the second is the closing bracket.
36#[must_use]
37pub fn find_matching_bracket(text: &str, cursor: usize) -> Option<(usize, usize)> {
38    let chars: Vec<char> = text.chars().collect();
39
40    if let Some(result) = bracket_at(&chars, cursor) {
41        return Some(result);
42    }
43    bracket_at(&chars, cursor.checked_sub(1)?)
44}
45
46fn bracket_at(chars: &[char], index: usize) -> Option<(usize, usize)> {
47    let character = *chars.get(index)?;
48    if let Some(close) = closing(character) {
49        return forward_match(chars, index, character, close).map(|end| (index, end));
50    }
51    let open = opening(character)?;
52    backward_match(chars, index, open, character).map(|start| (start, index))
53}
54
55const fn closing(open: char) -> Option<char> {
56    match open {
57        '(' => Some(')'),
58        '[' => Some(']'),
59        '{' => Some('}'),
60        _ => None,
61    }
62}
63
64const fn opening(close: char) -> Option<char> {
65    match close {
66        ')' => Some('('),
67        ']' => Some('['),
68        '}' => Some('{'),
69        _ => None,
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    #[test]
78    fn simple_pair() {
79        assert_eq!(find_matching_bracket("(hello)", 1), Some((0, 6)));
80    }
81
82    #[test]
83    fn cursor_before_close() {
84        assert_eq!(find_matching_bracket("(hello)", 6), Some((0, 6)));
85    }
86
87    #[test]
88    fn nested_brackets() {
89        assert_eq!(find_matching_bracket("a(b[c]d)e", 4), Some((3, 5)));
90    }
91
92    #[test]
93    fn unmatched() {
94        assert_eq!(find_matching_bracket("(hello", 1), None);
95    }
96
97    #[test]
98    fn mixed_types() {
99        assert_eq!(find_matching_bracket("([)]", 3), Some((1, 3)));
100    }
101
102    #[test]
103    fn no_bracket() {
104        assert_eq!(find_matching_bracket("hello", 2), None);
105    }
106
107    #[test]
108    fn cursor_at_open() {
109        assert_eq!(find_matching_bracket("(x)", 0), Some((0, 2)));
110    }
111
112    #[test]
113    fn cursor_at_close() {
114        assert_eq!(find_matching_bracket("(x)", 2), Some((0, 2)));
115    }
116
117    #[test]
118    fn empty_text() {
119        assert_eq!(find_matching_bracket("", 0), None);
120    }
121}