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