1use unicode_segmentation::UnicodeSegmentation;
21
22use crate::buffer::TextBuffer;
23use crate::position::Position;
24
25const PAIRS: [(char, char); 3] = [('(', ')'), ('[', ']'), ('{', '}')];
29
30fn close_for(open: char) -> Option<char> {
31 PAIRS.iter().find(|(o, _)| *o == open).map(|(_, c)| *c)
32}
33
34fn open_for(close: char) -> Option<char> {
35 PAIRS.iter().find(|(_, c)| *c == close).map(|(o, _)| *o)
36}
37
38fn char_at(buffer: &TextBuffer, at: Position) -> Option<char> {
40 buffer.with_line_str(at.line, |line| {
41 line.graphemes(true)
42 .nth(at.col)
43 .and_then(|g| g.chars().next())
44 })
45}
46
47pub fn match_at(
56 buffer: &TextBuffer,
57 at: Position,
58 max_lines: usize,
59) -> Option<(Position, Position)> {
60 let mut probes = Vec::with_capacity(2);
61 probes.push(at);
62 if at.col > 0 {
63 probes.push(Position {
64 line: at.line,
65 col: at.col - 1,
66 });
67 }
68
69 for probe in probes {
70 let Some(ch) = char_at(buffer, probe) else {
71 continue;
72 };
73 if let Some(close) = close_for(ch) {
74 if let Some(found) = scan_forward(buffer, probe, ch, close, max_lines) {
75 return Some((probe, found));
76 }
77 } else if let Some(open) = open_for(ch)
78 && let Some(found) = scan_backward(buffer, probe, open, ch, max_lines)
79 {
80 return Some((found, probe));
81 }
82 }
83 None
84}
85
86fn scan_line(
91 buffer: &TextBuffer,
92 line: usize,
93 range: impl Iterator<Item = usize>,
94 open: char,
95 close: char,
96 entering: char,
97 depth: &mut usize,
98) -> Option<usize> {
99 buffer.with_line_str(line, |text| {
103 let graphemes: Vec<&str> = text.graphemes(true).collect();
104 for i in range {
105 let Some(c) = graphemes.get(i).and_then(|g| g.chars().next()) else {
106 continue;
107 };
108 if c == entering {
109 *depth += 1;
110 } else if (c == open || c == close) && c != entering {
111 *depth -= 1;
112 if *depth == 0 {
113 return Some(i);
114 }
115 }
116 }
117 None
118 })
119}
120
121fn scan_forward(
122 buffer: &TextBuffer,
123 from: Position,
124 open: char,
125 close: char,
126 max_lines: usize,
127) -> Option<Position> {
128 let last = (from.line + max_lines).min(buffer.line_count().saturating_sub(1));
129 let mut depth = 1usize;
130
131 for line in from.line..=last {
132 let count = buffer.line_grapheme_count(line);
133 let start = if line == from.line { from.col + 1 } else { 0 };
134 if start >= count {
135 continue;
136 }
137 if let Some(col) = scan_line(buffer, line, start..count, open, close, open, &mut depth) {
138 return Some(Position { line, col });
139 }
140 }
141 None
142}
143
144fn scan_backward(
145 buffer: &TextBuffer,
146 from: Position,
147 open: char,
148 close: char,
149 max_lines: usize,
150) -> Option<Position> {
151 let first = from.line.saturating_sub(max_lines);
152 let mut depth = 1usize;
153
154 for line in (first..=from.line).rev() {
155 let count = buffer.line_grapheme_count(line);
156 let end = if line == from.line { from.col } else { count };
157 if end == 0 {
158 continue;
159 }
160 if let Some(col) = scan_line(buffer, line, (0..end).rev(), open, close, close, &mut depth) {
163 return Some(Position { line, col });
164 }
165 }
166 None
167}