tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 484Markdown 258TOML 131RON 38118 Definitions

⎇ main widgets/text-editor-core/src/plegado.rs Rust · 400 lines · 15.9 KB History Who touched what raw

chore: refresco desde el monorepo (pin tawasuyu.git v0.3.0) b575c63f1 · Sergio ·

1//! Qué se puede plegar en un texto, y hasta dónde llega cada bloque.
2//!
3//! El criterio es **la indentación**, y una sola regla sin excepciones: una
4//! línea encabeza un bloque plegable si la que le sigue está más adentro, y el
5//! bloque llega hasta la última línea consecutiva que siga más adentro que la
6//! cabecera.
7//!
8//! Podría haber sido por llaves, o por el árbol de tree-sitter, o por los
9//! `document symbols` del LSP. La indentación gana por tres razones concretas:
10//!
11//! - **Funciona en todos los lenguajes del editor**, incluidos los de llaves —
12//!   el cuerpo de un `fn { … }` está indentado igual que el de un `def:`— y en
13//!   los que no tienen gramática acá (YAML, Markdown, un `.conf`).
14//! - **No depende de que haya un servidor de lenguaje andando.** Los `document
15//!   symbols` sólo aparecen si `rust-analyzer` levantó, y plegar es de las cosas
16//!   que uno espera que anden en un archivo cualquiera recién abierto.
17//! - **Es una regla sin criterio**: no hay que decidir si un `match` cuenta o si
18//!   un `impl` de una línea vale. Lo que se ve indentado se pliega.
19//!
20//! Lo que la indentación **no** da es el pliegue que no coincide con sangría —
21//! un comentario de bloque `/* … */` a columna cero, o un `<div>` mal formateado.
22//! Cuando eso moleste, el arreglo es sumar una segunda fuente de bloques y
23//! fusionarlas, no cambiar ésta.
24//!
25//! Vive en el núcleo agnóstico porque es texto puro: sirve igual a un TUI o a un
26//! `--fold` de línea de comandos. La capa visual (qué está plegado ahora, la
27//! flecha en el gutter) es del widget.
28
29use alloc::vec::Vec;
30
31/// Un bloque que se puede plegar.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct Plegable {
34    /// Línea que lo encabeza. Queda **visible** al plegar y es la que lleva la
35    /// flecha en el gutter.
36    pub cabecera: usize,
37    /// Última línea del cuerpo, inclusive. El cuerpo es `cabecera+1 ..= ultima`.
38    pub ultima: usize,
39}
40
41impl Plegable {
42    /// Cuántas líneas esconde si se lo pliega.
43    pub const fn cuerpo(&self) -> usize {
44        if self.ultima > self.cabecera {
45            self.ultima - self.cabecera
46        } else {
47            0
48        }
49    }
50}
51
52/// Ancho con el que se expande un tab al medir la sangría. Es sólo para
53/// **comparar** profundidades, así que el número exacto no cambia qué se pliega
54/// mientras el archivo no mezcle tabs y espacios en el mismo nivel.
55pub const ANCHO_TAB: usize = 4;
56
57/// La sangría de una línea en columnas, o `None` si la línea está en blanco.
58///
59/// Las líneas en blanco devuelven `None` a propósito y no cero: una línea vacía
60/// en medio de una función no la corta, y contarla como sangría cero cortaría
61/// todos los bloques del archivo en el primer renglón vacío.
62fn sangria(linea: &str) -> Option<usize> {
63    let mut cols = 0usize;
64    for c in linea.chars() {
65        match c {
66            ' ' => cols += 1,
67            '\t' => cols += ANCHO_TAB - (cols % ANCHO_TAB),
68            '\n' | '\r' => break,
69            _ => return Some(cols),
70        }
71    }
72    None
73}
74
75/// Las cabeceras que **contienen** a `linea`, de la más externa a la más
76/// interna. Es la cadena que un editor pega arriba de todo para que no se
77/// pierda de vista de qué `impl`, de qué `fn` o de qué clave del YAML es lo que
78/// se está mirando.
79///
80/// Misma regla que [`plegables`] y por las mismas tres razones: la sangría. Se
81/// camina **hacia arriba** desde la línea y se anota cada vez que la sangría
82/// baja; eso es exactamente la cadena de ancestros, y cuesta lo que hay entre la
83/// línea y el margen izquierdo —unas decenas de líneas en código real— en vez
84/// del archivo entero, que es lo que costaría pedir `plegables` por cuadro.
85///
86/// `tope` acota ese camino para el caso patológico (un archivo enorme donde
87/// nada llega a la columna cero): se corta y se devuelve lo que se encontró, que
88/// es un encabezado de menos y nunca uno equivocado.
89///
90/// Las líneas en blanco no cortan nada, igual que en [`plegables`]. Si la línea
91/// de arranque está en blanco, la sangría de referencia es la de la primera
92/// línea con texto **hacia abajo**: entre dos funciones, el contexto es el de la
93/// que viene.
94pub fn cadena_de_bloques(
95    linea: usize,
96    tope: usize,
97    leer: impl Fn(usize) -> Option<alloc::string::String>,
98) -> Vec<usize> {
99    // La sangría de referencia: la de la línea, o la de la primera con texto
100    // que le siga.
101    let mut nivel = None;
102    for l in linea..linea.saturating_add(tope) {
103        let Some(texto) = leer(l) else { break };
104        if let Some(s) = sangria(&texto) {
105            nivel = Some(s);
106            break;
107        }
108    }
109    let Some(mut nivel) = nivel else { return Vec::new() };
110
111    let desde = linea.saturating_sub(tope);
112    let mut cadena = Vec::new();
113    for l in (desde..linea).rev() {
114        let Some(texto) = leer(l) else { continue };
115        let Some(s) = sangria(&texto) else { continue };
116        if s < nivel {
117            cadena.push(l);
118            nivel = s;
119            if s == 0 {
120                break;
121            }
122        }
123    }
124    cadena.reverse();
125    cadena
126}
127
128/// Todos los bloques plegables del texto, ordenados por cabecera.
129///
130/// Los bloques **anidan**: una función adentro de un `impl` produce dos, y el de
131/// afuera contiene al de adentro. Es O(n) en líneas, con una pila cuya
132/// profundidad es la del anidamiento.
133///
134/// No se llama por cuadro: el resultado se guarda y se rehace cuando el buffer
135/// cambia. Recorrer cien mil líneas en cada `view()` es exactamente el costo que
136/// la geometría del editor viene a evitar.
137pub fn plegables<S: AsRef<str>>(lineas: impl IntoIterator<Item = S>) -> Vec<Plegable> {
138    let sangrias: Vec<Option<usize>> = lineas.into_iter().map(|l| sangria(l.as_ref())).collect();
139    let mut bloques: Vec<Plegable> = Vec::new();
140    // Pila de bloques abiertos: (línea de la cabecera, su sangría).
141    let mut abiertos: Vec<(usize, usize)> = Vec::new();
142    // Última línea con contenido: es hasta donde llega un bloque que se cierra,
143    // porque las líneas en blanco del final pertenecen a lo que viene después.
144    let mut ultima_con_texto: Option<usize> = None;
145
146    for (n, s) in sangrias.iter().enumerate() {
147        let Some(actual) = *s else { continue };
148        // Todo bloque cuya cabecera está a esta profundidad o más adentro se
149        // termina acá: la línea actual ya no es cuerpo suyo.
150        while let Some(&(cabecera, sang)) = abiertos.last() {
151            if actual > sang {
152                break;
153            }
154            abiertos.pop();
155            if let Some(fin) = ultima_con_texto {
156                if fin > cabecera {
157                    bloques.push(Plegable { cabecera, ultima: fin });
158                }
159            }
160        }
161        abiertos.push((n, actual));
162        ultima_con_texto = Some(n);
163    }
164    // Lo que quedó abierto al llegar al final llega hasta la última línea con
165    // texto.
166    while let Some((cabecera, _)) = abiertos.pop() {
167        if let Some(fin) = ultima_con_texto {
168            if fin > cabecera {
169                bloques.push(Plegable { cabecera, ultima: fin });
170            }
171        }
172    }
173    bloques.sort_by_key(|b| b.cabecera);
174    bloques
175}
176
177/// El bloque que encabeza esa línea exacta, si hay alguno.
178pub fn plegable_en(bloques: &[Plegable], linea: usize) -> Option<Plegable> {
179    bloques.iter().copied().find(|b| b.cabecera == linea)
180}
181
182/// El bloque que encabeza `linea`, resuelto **sin barrer el archivo**.
183///
184/// `leer` entrega el texto de una línea, o `None` pasado el final. La función
185/// mira la sangría de la cabecera y avanza mientras el texto siga más adentro:
186/// cuesta el tamaño del bloque, no el del archivo.
187///
188/// Existe porque [`plegables`] no puede llamarse por cuadro. El gutter necesita
189/// saber, de cada línea **visible**, si lleva flecha; con el barrido completo
190/// eso serían cien mil `String` por pulsación de tecla en un archivo grande,
191/// que es exactamente el costo que la geometría del editor viene evitando. Con
192/// esto son treinta líneas por cuadro.
193pub fn plegable_desde(linea: usize, leer: impl Fn(usize) -> Option<alloc::string::String>) -> Option<Plegable> {
194    let cabecera_sang = sangria(leer(linea)?.as_ref())?;
195    let mut ultima = None;
196    let mut n = linea + 1;
197    loop {
198        let Some(texto) = leer(n) else { break };
199        match sangria(texto.as_ref()) {
200            // Una línea en blanco no corta el bloque, pero tampoco lo estira:
201            // si el bloque termina acá, termina en la última con texto.
202            None => {}
203            Some(s) if s > cabecera_sang => ultima = Some(n),
204            Some(_) => break,
205        }
206        n += 1;
207    }
208    ultima.map(|u| Plegable { cabecera: linea, ultima: u })
209}
210
211/// Si esa línea encabeza un bloque plegable. Más barato que
212/// [`plegable_desde`]: alcanza con mirar hasta la primera línea con texto que
213/// venga después.
214pub fn hay_plegable_en(linea: usize, leer: impl Fn(usize) -> Option<alloc::string::String>) -> bool {
215    let Some(Some(cabecera_sang)) = leer(linea).map(|t| sangria(t.as_ref())) else {
216        return false;
217    };
218    let mut n = linea + 1;
219    loop {
220        let Some(texto) = leer(n) else { return false };
221        match sangria(texto.as_ref()) {
222            None => {}
223            Some(s) => return s > cabecera_sang,
224        }
225        n += 1;
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use alloc::vec;
233
234    fn de(texto: &str) -> Vec<Plegable> {
235        plegables(texto.lines())
236    }
237
238    #[test]
239    fn una_funcion_se_pliega_desde_su_firma() {
240        let b = de("fn a() {\n    uno();\n    dos();\n}\n");
241        // La firma encabeza; el cuerpo llega hasta la llave de cierre, que está
242        // a la misma sangría que la firma… y por eso el bloque termina en la
243        // línea de antes. Es el comportamiento clásico del fold por sangría.
244        assert_eq!(b, vec![Plegable { cabecera: 0, ultima: 2 }]);
245        assert_eq!(b[0].cuerpo(), 2);
246    }
247
248    #[test]
249    fn los_bloques_anidan() {
250        let b = de("impl X {\n    fn a() {\n        uno();\n    }\n}\n");
251        assert_eq!(
252            b,
253            vec![
254                Plegable { cabecera: 0, ultima: 3 },
255                Plegable { cabecera: 1, ultima: 2 },
256            ]
257        );
258    }
259
260    #[test]
261    fn una_linea_en_blanco_no_corta_el_bloque() {
262        let b = de("fn a() {\n    uno();\n\n    dos();\n}\n");
263        assert_eq!(b, vec![Plegable { cabecera: 0, ultima: 3 }]);
264    }
265
266    #[test]
267    fn las_lineas_en_blanco_del_final_no_estiran_el_bloque() {
268        let b = de("fn a() {\n    uno();\n\n\nfn b() {\n    dos();\n");
269        // El primer bloque termina en la última línea CON TEXTO de su cuerpo,
270        // no en los renglones vacíos que lo separan del siguiente.
271        assert_eq!(b[0], Plegable { cabecera: 0, ultima: 1 });
272        assert_eq!(b[1], Plegable { cabecera: 4, ultima: 5 });
273    }
274
275    #[test]
276    fn un_texto_plano_sin_sangria_no_tiene_nada_que_plegar() {
277        assert!(de("uno\ndos\ntres\n").is_empty());
278        assert!(de("").is_empty());
279    }
280
281    #[test]
282    fn python_se_pliega_igual_que_rust_porque_la_regla_es_la_sangria() {
283        let b = de("def a():\n    uno()\n    dos()\n\ndef b():\n    tres()\n");
284        assert_eq!(b[0], Plegable { cabecera: 0, ultima: 2 });
285        assert_eq!(b[1], Plegable { cabecera: 4, ultima: 5 });
286    }
287
288    #[test]
289    fn el_tab_cuenta_como_una_parada_de_tabulacion() {
290        assert_eq!(sangria("\tx"), Some(4));
291        assert_eq!(sangria("  \tx"), Some(4)); // 2 espacios + tab → siguiente parada
292        assert_eq!(sangria("     x"), Some(5));
293        assert_eq!(sangria("   "), None);
294        assert_eq!(sangria(""), None);
295    }
296
297    #[test]
298    fn un_bloque_de_una_sola_linea_no_es_plegable() {
299        // `if x { y }` en una línea no esconde nada: plegarlo sería un botón que
300        // no hace nada.
301        let b = de("fn a() { uno() }\nfn b() { dos() }\n");
302        assert!(b.is_empty());
303    }
304
305    fn lector(texto: &'static str) -> impl Fn(usize) -> Option<alloc::string::String> {
306        let lineas: Vec<alloc::string::String> =
307            texto.lines().map(alloc::string::ToString::to_string).collect();
308        move |n| lineas.get(n).cloned()
309    }
310
311    #[test]
312    fn el_bloque_de_una_linea_se_resuelve_sin_leer_el_archivo_entero() {
313        let texto = "fn a() {\n    uno();\n    dos();\n}\nfn b() {\n    tres();\n}\n";
314        // Lo mismo que da el barrido completo, sin barrer.
315        assert_eq!(
316            plegable_desde(0, lector(texto)),
317            Some(Plegable { cabecera: 0, ultima: 2 })
318        );
319        assert_eq!(
320            plegable_desde(4, lector(texto)),
321            Some(Plegable { cabecera: 4, ultima: 5 })
322        );
323        // Una línea que no encabeza nada no produce bloque.
324        assert_eq!(plegable_desde(1, lector(texto)), None);
325        assert_eq!(plegable_desde(3, lector(texto)), None);
326    }
327
328    #[test]
329    fn el_barato_y_el_completo_dicen_lo_mismo() {
330        let texto = "impl X {\n    fn a() {\n        uno();\n\n        dos();\n    }\n}\nsuelta\n";
331        let completo = de(texto);
332        for n in 0..texto.lines().count() {
333            let barato = hay_plegable_en(n, lector(texto));
334            let esperado = plegable_en(&completo, n).is_some();
335            assert_eq!(barato, esperado, "línea {n}");
336            if esperado {
337                assert_eq!(
338                    plegable_desde(n, lector(texto)).map(|b| b.ultima),
339                    plegable_en(&completo, n).map(|b| b.ultima),
340                    "línea {n}"
341                );
342            }
343        }
344    }
345
346    #[test]
347    fn el_dedent_parcial_cierra_solo_lo_que_corresponde() {
348        let b = de("a:\n  b:\n    c\n  d:\n    e\nf\n");
349        // `a` contiene todo hasta `e`; `b` y `d` son hermanos adentro.
350        assert_eq!(b[0], Plegable { cabecera: 0, ultima: 4 });
351        assert_eq!(b[1], Plegable { cabecera: 1, ultima: 2 });
352        assert_eq!(b[2], Plegable { cabecera: 3, ultima: 4 });
353    }
354    #[test]
355    fn la_cadena_de_bloques_son_los_ancestros_por_sangria() {
356        let texto = [
357            "impl Cosa {",          // 0
358            "    fn una() {",       // 1
359            "        if x {",       // 2
360            "            hacer();", // 3
361            "        }",            // 4
362            "    }",                // 5
363            "}",                    // 6
364        ];
365        let leer = |n: usize| texto.get(n).map(|s| alloc::string::String::from(*s));
366        // Parado en el cuerpo del `if`: los tres de afuera, de afuera hacia
367        // adentro.
368        assert_eq!(cadena_de_bloques(3, 100, leer), alloc::vec![0, 1, 2]);
369        // Parado en la línea del `if`: sus dos ancestros, no él mismo — ya se ve.
370        assert_eq!(cadena_de_bloques(2, 100, leer), alloc::vec![0, 1]);
371        // En la primera línea no hay nada arriba.
372        assert!(cadena_de_bloques(0, 100, leer).is_empty());
373        // El cierre del `impl` está a sangría cero: tampoco tiene ancestros.
374        assert!(cadena_de_bloques(6, 100, leer).is_empty());
375    }
376
377    #[test]
378    fn una_linea_en_blanco_toma_el_contexto_de_la_que_sigue() {
379        let texto = ["fn a() {", "", "    paso();", "}"];
380        let leer = |n: usize| texto.get(n).map(|s| alloc::string::String::from(*s));
381        // La línea 1 está vacía: la referencia es la sangría de `paso()`.
382        assert_eq!(cadena_de_bloques(1, 100, leer), alloc::vec![0]);
383    }
384
385    #[test]
386    fn el_tope_corta_el_camino_y_no_inventa_un_ancestro() {
387        let mut texto = alloc::vec::Vec::new();
388        texto.push(alloc::string::String::from("fn grande() {"));
389        for _ in 0..500 {
390            texto.push(alloc::string::String::from("    paso();"));
391        }
392        let leer = |n: usize| texto.get(n).cloned();
393        // Con tope de sobra, aparece la cabecera.
394        assert_eq!(cadena_de_bloques(400, 1000, leer), alloc::vec![0]);
395        // Con un tope corto se devuelve lo que se encontró: nada. Un encabezado
396        // de menos, nunca uno equivocado.
397        assert!(cadena_de_bloques(400, 10, leer).is_empty());
398    }
399
400}