tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definitions

⎇ main widgets/text-editor-core/src/buffer.rs Rust · 600 lines · 19.2 KB History Who touched what raw

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

1//! Buffer del editor — fachada fina con las conversiones de coordenadas
2//! que el resto del crate usa, sobre UNA de dos espaldas del mismo
3//! contrato:
4//!
5//! - **ropey** (feature `ropey`, default): rope de verdad, O(log n) —
6//!   el escritorio, archivos grandes.
7//! - **la soga** (sin la feature): `String` + índice de líneas, `no_std`
8//!   + alloc, O(n) por edición — la jaula WASM de wawa, donde lo que se
9//!   edita cabe en memoria y no hay `std`.
10//!
11//! La semántica es idéntica por contrato (la de `Rope::lines()`), y los
12//! tests de este módulo corren contra la espalda que esté puesta:
13//! `cargo test -p llimphi-widget-text-editor-core` prueba ropey,
14//! `cargo test -p llimphi-widget-text-editor-core --no-default-features`
15//! prueba la soga.
16//!
17//! Coordenadas:
18//! - `char_offset`: índice de carácter (no byte) en el buffer entero.
19//! - `(line, col)`: línea (0-based) + columna en chars dentro de esa línea.
20//!
21//! Convenciones:
22//! - Las líneas son las que define `Rope::lines()` — un `\n` separa
23//!   líneas; la última línea puede o no terminar en `\n` (en cuyo caso
24//!   hay una línea vacía extra después).
25//! - `col` cuenta chars, no graphemes ni bytes. Para CJK ancho doble
26//!   el render decidirá el ancho visual; el cursor avanza en chars.
27
28use alloc::string::{String, ToString};
29use alloc::vec::Vec;
30
31// ---------------------------------------------------------------------------
32// La espalda ropey (escritorio).
33// ---------------------------------------------------------------------------
34
35#[cfg(feature = "ropey")]
36mod nucleo {
37    use super::{String, ToString};
38    use ropey::Rope;
39
40    #[derive(Debug, Clone)]
41    pub(super) struct Nucleo {
42        rope: Rope,
43    }
44
45    impl Nucleo {
46        pub fn new() -> Self {
47            Self { rope: Rope::new() }
48        }
49
50        pub fn from_str(s: &str) -> Self {
51            Self { rope: Rope::from_str(s) }
52        }
53
54        pub fn text(&self) -> String {
55            self.rope.to_string()
56        }
57
58        pub fn len_chars(&self) -> usize {
59            self.rope.len_chars()
60        }
61
62        pub fn len_lines(&self) -> usize {
63            self.rope.len_lines()
64        }
65
66        /// La línea `n` con su `\n` terminal si no es la última.
67        /// Precondición del caller: `n < len_lines()`.
68        pub fn line_string(&self, n: usize) -> String {
69            self.rope.line(n).to_string()
70        }
71
72        /// Chars de la línea `n` SIN contar el `\n` terminal.
73        /// Precondición del caller: `n < len_lines()`.
74        pub fn line_len_chars(&self, n: usize) -> usize {
75            let line = self.rope.line(n);
76            let mut len = line.len_chars();
77            if len > 0 && line.char(len - 1) == '\n' {
78                len -= 1;
79            }
80            len
81        }
82
83        /// El largo de **todas** las líneas, de un solo recorrido.
84        ///
85        /// No es `(0..n).map(line_len_chars)`: cada `rope.line(n)` es una
86        /// búsqueda `O(log n)` en el árbol, y el iterador secuencial no. Con
87        /// 100.000 líneas la diferencia es de decenas de milisegundos, que es
88        /// justo el orden en el que un rearmado por tecla se siente.
89        pub fn line_lens(&self) -> Vec<usize> {
90            self.rope
91                .lines()
92                .map(|l| {
93                    let n = l.len_chars();
94                    if n > 0 && l.char(n - 1) == '\n' { n - 1 } else { n }
95                })
96                .collect()
97        }
98
99        /// Precondición del caller: `off <= len_chars()`.
100        pub fn char_to_line(&self, off: usize) -> usize {
101            self.rope.char_to_line(off)
102        }
103
104        /// Precondición del caller: `n <= len_lines()`.
105        pub fn line_to_char(&self, n: usize) -> usize {
106            self.rope.line_to_char(n)
107        }
108
109        /// Precondición del caller: `off <= len_chars()`.
110        pub fn char_to_byte(&self, off: usize) -> usize {
111            self.rope.char_to_byte(off)
112        }
113
114        /// Precondición del caller: `n <= len_lines()`.
115        pub fn line_to_byte(&self, n: usize) -> usize {
116            self.rope.line_to_byte(n)
117        }
118
119        /// Precondición del caller: `off < len_chars()`.
120        pub fn char_at(&self, off: usize) -> char {
121            self.rope.char(off)
122        }
123
124        /// Precondición del caller: `s <= e <= len_chars()`.
125        pub fn slice(&self, s: usize, e: usize) -> String {
126            self.rope.slice(s..e).to_string()
127        }
128
129        /// Precondición del caller: `off <= len_chars()`.
130        pub fn insert(&mut self, off: usize, s: &str) {
131            self.rope.insert(off, s);
132        }
133
134        /// Precondición del caller: `s <= e <= len_chars()`.
135        pub fn remove(&mut self, s: usize, e: usize) {
136            self.rope.remove(s..e);
137        }
138    }
139}
140
141// ---------------------------------------------------------------------------
142// La soga (no_std): String + índice de arranques de línea.
143//
144// El índice guarda, por línea, su `(char_start, byte_start)`. Se
145// reconstruye entero en cada edición — O(n), y está bien: la soga existe
146// para editar lo que cabe en una jaula (notas, config, celdas), no para
147// abrir un log de 200 MB. Quien necesite eso está en el escritorio, con
148// ropey.
149// ---------------------------------------------------------------------------
150
151#[cfg(not(feature = "ropey"))]
152mod nucleo {
153    use super::{String, ToString, Vec};
154
155    #[derive(Debug, Clone)]
156    pub(super) struct Nucleo {
157        texto: String,
158        /// `(char_start, byte_start)` de cada línea. Nunca vacío: el
159        /// buffer vacío tiene una línea (la 0, vacía) — igual que ropey.
160        lineas: Vec<(usize, usize)>,
161        n_chars: usize,
162    }
163
164    impl Nucleo {
165        pub fn new() -> Self {
166            Self::from_str("")
167        }
168
169        pub fn from_str(s: &str) -> Self {
170            let mut n = Self {
171                texto: s.to_string(),
172                lineas: Vec::new(),
173                n_chars: 0,
174            };
175            n.reindexar();
176            n
177        }
178
179        /// Reconstruye el índice de líneas y el conteo de chars. La única
180        /// función O(n) sobre el texto entero; TODO lo demás la consulta.
181        fn reindexar(&mut self) {
182            self.lineas.clear();
183            self.lineas.push((0, 0));
184            let mut ci = 0usize;
185            for (bi, c) in self.texto.char_indices() {
186                ci += 1;
187                if c == '\n' {
188                    self.lineas.push((ci, bi + 1));
189                }
190            }
191            self.n_chars = ci;
192        }
193
194        pub fn text(&self) -> String {
195            self.texto.clone()
196        }
197
198        pub fn len_chars(&self) -> usize {
199            self.n_chars
200        }
201
202        pub fn len_lines(&self) -> usize {
203            self.lineas.len()
204        }
205
206        /// Rango de bytes `[inicio..fin)` de la línea `n` (con su `\n`).
207        fn bytes_de_linea(&self, n: usize) -> (usize, usize) {
208            let ini = self.lineas[n].1;
209            let fin = match self.lineas.get(n + 1) {
210                Some(&(_, b)) => b,
211                None => self.texto.len(),
212            };
213            (ini, fin)
214        }
215
216        pub fn line_string(&self, n: usize) -> String {
217            let (ini, fin) = self.bytes_de_linea(n);
218            self.texto[ini..fin].to_string()
219        }
220
221        pub fn line_len_chars(&self, n: usize) -> usize {
222            let fin_chars = match self.lineas.get(n + 1) {
223                // La línea siguiente arranca después del `\n` de ésta.
224                Some(&(c, _)) => c - 1,
225                None => self.n_chars,
226            };
227            fin_chars - self.lineas[n].0
228        }
229
230        /// El largo de todas las líneas. Acá el índice ya está armado, así
231        /// que es una resta por línea.
232        pub fn line_lens(&self) -> Vec<usize> {
233            (0..self.lineas.len()).map(|n| self.line_len_chars(n)).collect()
234        }
235
236        pub fn char_to_line(&self, off: usize) -> usize {
237            // Última línea cuyo char_start <= off. `partition_point`
238            // devuelve el primer índice donde el predicado falla.
239            self.lineas.partition_point(|&(c, _)| c <= off) - 1
240        }
241
242        pub fn line_to_char(&self, n: usize) -> usize {
243            match self.lineas.get(n) {
244                Some(&(c, _)) => c,
245                // ropey permite `line_to_char(len_lines()) == len_chars()`.
246                None => self.n_chars,
247            }
248        }
249
250        pub fn char_to_byte(&self, off: usize) -> usize {
251            let linea = self.char_to_line(off);
252            let (cs, bs) = self.lineas[linea];
253            // Caminar chars desde el arranque de la línea — corto: una línea.
254            let mut b = bs;
255            let mut c = cs;
256            let mut iter = self.texto[bs..].chars();
257            while c < off {
258                // `off <= len_chars` garantiza que el char existe.
259                let ch = iter.next().expect("char_to_byte: offset fuera del texto");
260                b += ch.len_utf8();
261                c += 1;
262            }
263            b
264        }
265
266        pub fn line_to_byte(&self, n: usize) -> usize {
267            match self.lineas.get(n) {
268                Some(&(_, b)) => b,
269                None => self.texto.len(),
270            }
271        }
272
273        pub fn char_at(&self, off: usize) -> char {
274            let b = self.char_to_byte(off);
275            self.texto[b..]
276                .chars()
277                .next()
278                .expect("char_at: offset fuera del texto")
279        }
280
281        pub fn slice(&self, s: usize, e: usize) -> String {
282            let bs = self.char_to_byte(s);
283            let be = self.char_to_byte(e);
284            self.texto[bs..be].to_string()
285        }
286
287        pub fn insert(&mut self, off: usize, s: &str) {
288            let b = self.char_to_byte(off);
289            self.texto.insert_str(b, s);
290            self.reindexar();
291        }
292
293        pub fn remove(&mut self, s: usize, e: usize) {
294            let bs = self.char_to_byte(s);
295            let be = self.char_to_byte(e);
296            self.texto.replace_range(bs..be, "");
297            self.reindexar();
298        }
299    }
300}
301
302use nucleo::Nucleo;
303
304#[derive(Debug, Clone)]
305pub struct Buffer {
306    rope: Nucleo,
307}
308
309impl Default for Buffer {
310    fn default() -> Self {
311        Self::new()
312    }
313}
314
315impl Buffer {
316    pub fn new() -> Self {
317        Self { rope: Nucleo::new() }
318    }
319
320    pub fn from_str(s: &str) -> Self {
321        Self { rope: Nucleo::from_str(s) }
322    }
323
324    pub fn text(&self) -> String {
325        self.rope.text()
326    }
327
328    pub fn len_chars(&self) -> usize {
329        self.rope.len_chars()
330    }
331
332    pub fn len_lines(&self) -> usize {
333        self.rope.len_lines().max(1)
334    }
335
336    pub fn is_empty(&self) -> bool {
337        self.rope.len_chars() == 0
338    }
339
340    /// Devuelve la línea `n` como `String` (incluye su trailing `\n` si
341    /// no es la última). Si `n` está fuera de rango devuelve `""`.
342    pub fn line(&self, n: usize) -> String {
343        if n >= self.rope.len_lines() {
344            return String::new();
345        }
346        self.rope.line_string(n)
347    }
348
349    /// Cantidad de chars en la línea `n` **sin contar** el `\n` terminal.
350    pub fn line_len_chars(&self, n: usize) -> usize {
351        if n >= self.rope.len_lines() {
352            return 0;
353        }
354        self.rope.line_len_chars(n)
355    }
356
357    /// El largo (en chars, sin el `\n`) de **todas** las líneas, de un solo
358    /// recorrido. Para quien necesita el perfil entero del archivo —el mapa de
359    /// ajuste de línea, el minimapa— y no una línea suelta.
360    pub fn line_lens(&self) -> Vec<usize> {
361        self.rope.line_lens()
362    }
363
364    /// Convierte `char_offset` global a `(line, col)`.
365    pub fn offset_to_pos(&self, offset: usize) -> (usize, usize) {
366        let off = offset.min(self.rope.len_chars());
367        let line = self.rope.char_to_line(off);
368        let line_start = self.rope.line_to_char(line);
369        (line, off - line_start)
370    }
371
372    /// Convierte `(line, col)` a `char_offset`. Clampea `line` y `col`
373    /// para no panicear con coordenadas fuera de rango.
374    pub fn pos_to_offset(&self, line: usize, col: usize) -> usize {
375        let line = line.min(self.rope.len_lines().saturating_sub(1));
376        let line_start = self.rope.line_to_char(line);
377        let line_chars = self.line_len_chars(line);
378        let col = col.min(line_chars);
379        line_start + col
380    }
381
382    /// Carácter en `char_offset`. `None` si está fuera de rango.
383    pub fn char_at(&self, offset: usize) -> Option<char> {
384        if offset >= self.rope.len_chars() {
385            return None;
386        }
387        Some(self.rope.char_at(offset))
388    }
389
390    /// Slice `[start..end)` como `String`. Clampea para no panicear.
391    pub fn slice(&self, start: usize, end: usize) -> String {
392        let len = self.rope.len_chars();
393        let s = start.min(len);
394        let e = end.min(len).max(s);
395        self.rope.slice(s, e)
396    }
397
398    /// Inserta `s` en `offset`. Clampea `offset`.
399    pub fn insert(&mut self, offset: usize, s: &str) {
400        let off = offset.min(self.rope.len_chars());
401        self.rope.insert(off, s);
402    }
403
404    /// Borra `[start..end)`. Clampea ambos.
405    pub fn delete(&mut self, start: usize, end: usize) {
406        let len = self.rope.len_chars();
407        let s = start.min(len);
408        let e = end.min(len).max(s);
409        if s == e {
410            return;
411        }
412        self.rope.remove(s, e);
413    }
414
415    pub fn set_text(&mut self, s: &str) {
416        self.rope = Nucleo::from_str(s);
417    }
418
419    pub fn replace_all(&mut self, s: &str) {
420        self.set_text(s);
421    }
422
423    /// Convierte char_offset → byte_offset. tree-sitter trabaja en bytes
424    /// (UTF-8); el editor en chars. Esto las conecta.
425    pub fn char_to_byte(&self, char_offset: usize) -> usize {
426        let off = char_offset.min(self.rope.len_chars());
427        self.rope.char_to_byte(off)
428    }
429
430    /// Línea (0-based) que contiene el char_offset dado.
431    pub fn char_to_line(&self, char_offset: usize) -> usize {
432        let off = char_offset.min(self.rope.len_chars());
433        self.rope.char_to_line(off)
434    }
435
436    /// Byte_offset del primer char de la línea `n`.
437    pub fn line_to_byte(&self, line: usize) -> usize {
438        let line = line.min(self.rope.len_lines());
439        self.rope.line_to_byte(line)
440    }
441
442    /// Devuelve el rango `[start_col..col)` que contiene el "word" actual
443    /// — desde el último carácter no-de-palabra hasta `col`, en la línea
444    /// `line`. Útil para autocompletion (smart-replace del prefijo).
445    pub fn current_word_prefix(&self, line: usize, col: usize) -> (usize, String) {
446        let line_text = self.line(line);
447        let chars: Vec<char> = line_text
448            .chars()
449            .filter(|c| *c != '\n')
450            .collect();
451        let end = col.min(chars.len());
452        let mut start = end;
453        while start > 0 && is_word_char(chars[start - 1]) {
454            start -= 1;
455        }
456        let prefix: String = chars[start..end].iter().collect();
457        (start, prefix)
458    }
459}
460
461fn is_word_char(c: char) -> bool {
462    c.is_alphanumeric() || c == '_'
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468
469    #[test]
470    fn empty_buffer_has_one_line() {
471        let b = Buffer::new();
472        assert_eq!(b.len_lines(), 1);
473        assert_eq!(b.line_len_chars(0), 0);
474    }
475
476    #[test]
477    fn pos_offset_roundtrip() {
478        let b = Buffer::from_str("hola\nmundo\nfin");
479        let cases = [(0usize, 0usize), (0, 4), (1, 0), (1, 5), (2, 3)];
480        for (line, col) in cases {
481            let off = b.pos_to_offset(line, col);
482            assert_eq!(b.offset_to_pos(off), (line, col));
483        }
484    }
485
486    #[test]
487    fn line_len_excludes_trailing_newline() {
488        let b = Buffer::from_str("hola\nfin");
489        assert_eq!(b.line_len_chars(0), 4); // "hola" sin \n
490        assert_eq!(b.line_len_chars(1), 3); // "fin"
491    }
492
493    #[test]
494    fn insert_and_delete_modify_text() {
495        let mut b = Buffer::from_str("ab");
496        b.insert(1, "X");
497        assert_eq!(b.text(), "aXb");
498        b.delete(1, 2);
499        assert_eq!(b.text(), "ab");
500    }
501
502    #[test]
503    fn slice_clampea() {
504        let b = Buffer::from_str("hola");
505        assert_eq!(b.slice(0, 100), "hola");
506        assert_eq!(b.slice(50, 100), "");
507        assert_eq!(b.slice(2, 1), ""); // end < start clampea
508    }
509
510    #[test]
511    fn current_word_prefix_basic() {
512        let b = Buffer::from_str("let hola_mundo = 1;");
513        // Caret en col 14 (después de la 'o' de "hola_mundo").
514        let (start, p) = b.current_word_prefix(0, 14);
515        assert_eq!(start, 4);
516        assert_eq!(p, "hola_mundo");
517    }
518
519    #[test]
520    fn current_word_prefix_en_inicio_es_vacio() {
521        let b = Buffer::from_str("hola");
522        let (start, p) = b.current_word_prefix(0, 0);
523        assert_eq!(start, 0);
524        assert!(p.is_empty());
525    }
526
527    #[test]
528    fn current_word_prefix_caret_despues_de_no_word() {
529        let b = Buffer::from_str("foo.bar");
530        let (start, p) = b.current_word_prefix(0, 4);
531        // El '.' no es word; el prefijo empieza ahí.
532        assert_eq!(start, 4);
533        assert!(p.is_empty());
534    }
535
536    #[test]
537    fn pos_to_offset_clampea_col() {
538        let b = Buffer::from_str("ab\ncd");
539        // col fuera de rango → fin de línea
540        assert_eq!(b.pos_to_offset(0, 99), 2);
541        assert_eq!(b.pos_to_offset(1, 99), 5);
542    }
543
544    // --- Contrato de espaldas: estos casos clavan la semántica que las
545    // --- DOS espaldas (ropey y soga) deben compartir. Corren contra la
546    // --- que esté compilada.
547
548    #[test]
549    fn texto_con_newline_final_tiene_linea_vacia_extra() {
550        // Semántica de `Rope::lines()`: "a\n" son DOS líneas ("a\n", "").
551        let b = Buffer::from_str("hola\n");
552        assert_eq!(b.len_lines(), 2);
553        assert_eq!(b.line(0), "hola\n");
554        assert_eq!(b.line(1), "");
555        assert_eq!(b.line_len_chars(1), 0);
556    }
557
558    #[test]
559    fn char_to_line_en_el_borde() {
560        let b = Buffer::from_str("ab\ncd");
561        assert_eq!(b.char_to_line(0), 0);
562        assert_eq!(b.char_to_line(2), 0); // el '\n' pertenece a la línea 0
563        assert_eq!(b.char_to_line(3), 1); // primer char de "cd"
564        assert_eq!(b.char_to_line(5), 1); // len_chars → última línea
565    }
566
567    #[test]
568    fn char_byte_multibyte() {
569        // "ñ" = 2 bytes, "中" = 3 bytes: chars ≠ bytes.
570        let b = Buffer::from_str("ñ中x\nfin");
571        assert_eq!(b.len_chars(), 7);
572        assert_eq!(b.char_to_byte(0), 0);
573        assert_eq!(b.char_to_byte(1), 2); // después de "ñ"
574        assert_eq!(b.char_to_byte(2), 5); // después de "中"
575        assert_eq!(b.char_to_byte(3), 6); // después de "x"
576        assert_eq!(b.line_to_byte(1), 7); // "fin" arranca tras el '\n'
577        assert_eq!(b.char_at(1), Some('中'));
578        assert_eq!(b.slice(0, 2), "ñ中");
579    }
580
581    #[test]
582    fn editar_multibyte_conserva_indices() {
583        let mut b = Buffer::from_str("aña\nbé");
584        b.insert(2, "中");
585        assert_eq!(b.text(), "añ中a\nbé");
586        assert_eq!(b.line_len_chars(0), 4);
587        b.delete(0, 3);
588        assert_eq!(b.text(), "a\nbé");
589        assert_eq!(b.len_lines(), 2);
590        assert_eq!(b.line_len_chars(1), 2);
591    }
592
593    #[test]
594    fn line_to_byte_permite_len_lines() {
595        // ropey permite pedir la línea después de la última: devuelve el
596        // final del texto. La soga debe hacer lo mismo.
597        let b = Buffer::from_str("ab\ncd");
598        assert_eq!(b.line_to_byte(2), 5);
599    }
600}