tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! Matching de paréntesis/corchetes/llaves bajo el cursor.
2//!
3//! Si el carácter inmediatamente *antes* o *en* el caret es un bracket
4//! abridor o cerrador, busca su par contando profundidad y devuelve las
5//! dos posiciones. Útil para el visor (resaltar ambas).
6//!
7//! Restricciones del PMV: no diferencia brackets dentro de strings ni
8//! comentarios — el tokenizer del bloque de highlight (tree-sitter) lo
9//! resolverá mejor en una pasada futura. Para WAT/JSON/Lisp esto basta.
10
11
12use crate::buffer::Buffer;
13use crate::cursor::{Cursor, Pos};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum Direction {
17 Forward,
18 Backward,
19}
20
21/// Pares reconocidos.
22const PAIRS: &[(char, char)] = &[('(', ')'), ('[', ']'), ('{', '}')];
23
24fn pair_of(c: char) -> Option<(char, char, Direction)> {
25 for &(o, cl) in PAIRS {
26 if c == o {
27 return Some((o, cl, Direction::Forward));
28 }
29 if c == cl {
30 return Some((o, cl, Direction::Backward));
31 }
32 }
33 None
34}
35
36/// Si el caret toca un bracket, devuelve `(pos_del_bracket, pos_del_par)`.
37pub fn find_bracket_pair(buf: &Buffer, cursor: &Cursor) -> Option<(Pos, Pos)> {
38 let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
39
40 // Probamos en `caret` y `caret-1` — un caret "entre" dos chars puede
41 // tocar al de la izquierda visualmente.
42 let candidates: [Option<usize>; 2] = [
43 Some(caret_off).filter(|&o| o < buf.len_chars()),
44 caret_off.checked_sub(1),
45 ];
46
47 for opt in candidates {
48 let Some(off) = opt else { continue };
49 let Some(ch) = buf.char_at(off) else { continue };
50 let Some((open, close, dir)) = pair_of(ch) else { continue };
51 let mate = match dir {
52 Direction::Forward => find_forward(buf, off + 1, open, close),
53 Direction::Backward => find_backward(buf, off, open, close),
54 };
55 if let Some(mate_off) = mate {
56 let a = buf.offset_to_pos(off);
57 let b = buf.offset_to_pos(mate_off);
58 return Some((Pos::new(a.0, a.1), Pos::new(b.0, b.1)));
59 }
60 }
61 None
62}
63
64fn find_forward(buf: &Buffer, from: usize, open: char, close: char) -> Option<usize> {
65 let mut depth = 1usize;
66 let mut off = from;
67 let len = buf.len_chars();
68 while off < len {
69 match buf.char_at(off) {
70 Some(c) if c == open => depth += 1,
71 Some(c) if c == close => {
72 depth -= 1;
73 if depth == 0 {
74 return Some(off);
75 }
76 }
77 _ => {}
78 }
79 off += 1;
80 }
81 None
82}
83
84fn find_backward(buf: &Buffer, before: usize, open: char, close: char) -> Option<usize> {
85 if before == 0 {
86 return None;
87 }
88 let mut depth = 1usize;
89 let mut off = before;
90 while off > 0 {
91 off -= 1;
92 match buf.char_at(off) {
93 Some(c) if c == close => depth += 1,
94 Some(c) if c == open => {
95 depth -= 1;
96 if depth == 0 {
97 return Some(off);
98 }
99 }
100 _ => {}
101 }
102 }
103 None
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109
110 #[test]
111 fn empareja_paren_simple() {
112 let b = Buffer::from_str("(a)");
113 let c = Cursor::at(0, 0); // caret antes del '('
114 let (a, m) = find_bracket_pair(&b, &c).unwrap();
115 assert_eq!(a, Pos::new(0, 0));
116 assert_eq!(m, Pos::new(0, 2));
117 }
118
119 #[test]
120 fn empareja_desde_el_lado_derecho() {
121 let b = Buffer::from_str("(a)");
122 let c = Cursor::at(0, 3); // caret después del ')'
123 let (a, m) = find_bracket_pair(&b, &c).unwrap();
124 assert_eq!(a, Pos::new(0, 2)); // ')'
125 assert_eq!(m, Pos::new(0, 0)); // '('
126 }
127
128 #[test]
129 fn anidados_respeta_profundidad() {
130 let b = Buffer::from_str("((a))");
131 let c = Cursor::at(0, 0); // primer '('
132 let (_, m) = find_bracket_pair(&b, &c).unwrap();
133 assert_eq!(m, Pos::new(0, 4)); // último ')'
134 }
135
136 #[test]
137 fn empareja_brackets_y_llaves() {
138 let b = Buffer::from_str("[a]");
139 assert!(find_bracket_pair(&b, &Cursor::at(0, 0)).is_some());
140
141 let b2 = Buffer::from_str("{a}");
142 assert!(find_bracket_pair(&b2, &Cursor::at(0, 0)).is_some());
143 }
144
145 #[test]
146 fn caret_lejos_de_bracket_devuelve_none() {
147 let b = Buffer::from_str("hola");
148 let c = Cursor::at(0, 2);
149 assert!(find_bracket_pair(&b, &c).is_none());
150 }
151
152 #[test]
153 fn bracket_sin_par_devuelve_none() {
154 let b = Buffer::from_str("(a");
155 let c = Cursor::at(0, 0);
156 assert!(find_bracket_pair(&b, &c).is_none());
157 }
158
159 #[test]
160 fn multilinea_pasa_saltos() {
161 let b = Buffer::from_str("(\n a\n)");
162 let c = Cursor::at(0, 0);
163 let (_, m) = find_bracket_pair(&b, &c).unwrap();
164 assert_eq!(m, Pos::new(2, 0));
165 }
166}