Find the last unclosed opening bracket in source code. Returns the bracket character and its byte offset, or None if all brackets are balanced.
(source: &str)
| 152 | /// Find the last unclosed opening bracket in source code. |
| 153 | /// Returns the bracket character and its byte offset, or None if all brackets are balanced. |
| 154 | fn find_unclosed_bracket(source: &str) -> Option<(char, usize)> { |
| 155 | let mut stack: Vec<(char, usize)> = Vec::new(); |
| 156 | let mut in_string = false; |
| 157 | let mut string_quote = '\0'; |
| 158 | let mut triple_quote = false; |
| 159 | let mut escape_next = false; |
| 160 | let mut is_raw_string = false; |
| 161 | |
| 162 | let chars: Vec<(usize, char)> = source.char_indices().collect(); |
| 163 | let mut i = 0; |
| 164 | |
| 165 | while i < chars.len() { |
| 166 | let (byte_offset, ch) = chars[i]; |
| 167 | |
| 168 | if escape_next { |
| 169 | escape_next = false; |
| 170 | i += 1; |
| 171 | continue; |
| 172 | } |
| 173 | |
| 174 | if in_string { |
| 175 | if ch == '\\' && !is_raw_string { |
| 176 | escape_next = true; |
| 177 | } else if triple_quote { |
| 178 | if ch == string_quote |
| 179 | && i + 2 < chars.len() |
| 180 | && chars[i + 1].1 == string_quote |
| 181 | && chars[i + 2].1 == string_quote |
| 182 | { |
| 183 | in_string = false; |
| 184 | i += 3; |
| 185 | continue; |
| 186 | } |
| 187 | } else if ch == string_quote { |
| 188 | in_string = false; |
| 189 | } |
| 190 | i += 1; |
| 191 | continue; |
| 192 | } |
| 193 | |
| 194 | // Check for comments |
| 195 | if ch == '#' { |
| 196 | // Skip to end of line |
| 197 | while i < chars.len() && chars[i].1 != '\n' { |
| 198 | i += 1; |
| 199 | } |
| 200 | continue; |
| 201 | } |
| 202 | |
| 203 | // Check for string start (with optional prefix like r, b, f, u, rb, br, etc.) |
| 204 | if ch == '\'' || ch == '"' { |
| 205 | // Check up to 2 characters before the quote for string prefix |
| 206 | is_raw_string = false; |
| 207 | for look_back in 1..=2.min(i) { |
| 208 | let prev = chars[i - look_back].1; |
| 209 | if matches!(prev, 'r' | 'R') { |
| 210 | is_raw_string = true; |
| 211 | break; |