tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main widgets/text-editor-core/src/find.rs Rust · 267 líneas · 8.8 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! Búsqueda en el buffer. PMV: case-insensitive opcional, sin regex,
2//! sin replace. La UI del prompt vive en el caller (típicamente una
3//! barra arriba del editor); este módulo sólo provee:
4//!
5//! - [`FindState`] con el query actual + dirección + flag case-sensitive.
6//! - [`find_next`] / [`find_prev`] que devuelven la próxima/anterior
7//!   match desde el caret del editor.
8//! - [`all_matches`] para que el render resalte cada ocurrencia.
9
10use alloc::string::String;
11use alloc::vec::Vec;
12
13use crate::buffer::Buffer;
14use crate::cursor::{Cursor, Pos};
15
16/// Configuración de búsqueda del editor.
17#[derive(Debug, Clone, Default)]
18pub struct FindState {
19    pub query: String,
20    pub case_sensitive: bool,
21}
22
23impl FindState {
24    pub fn new() -> Self {
25        Self::default()
26    }
27    pub fn with_query(query: impl Into<String>) -> Self {
28        Self { query: query.into(), case_sensitive: false }
29    }
30    pub fn is_active(&self) -> bool {
31        !self.query.is_empty()
32    }
33}
34
35/// Devuelve todas las ocurrencias del query en el buffer como
36/// `(start_offset, end_offset)` en char offsets. Vacío si query vacío.
37pub fn all_matches(buf: &Buffer, find: &FindState) -> Vec<(usize, usize)> {
38    if find.query.is_empty() {
39        return Vec::new();
40    }
41    let hay = buf.text();
42    let (hay_search, needle_search) = if find.case_sensitive {
43        (hay.clone(), find.query.clone())
44    } else {
45        (hay.to_lowercase(), find.query.to_lowercase())
46    };
47
48    // Buscamos en bytes; convertimos a char_offsets al devolver.
49    let mut out: Vec<(usize, usize)> = Vec::new();
50    let mut byte_start = 0;
51    while let Some(pos) = hay_search[byte_start..].find(&needle_search) {
52        let byte_match = byte_start + pos;
53        let char_start = hay[..byte_match].chars().count();
54        let char_end = char_start + find.query.chars().count();
55        out.push((char_start, char_end));
56        byte_start = byte_match + needle_search.len().max(1);
57    }
58    out
59}
60
61/// Encuentra la próxima ocurrencia con `start >= caret_off` (la match
62/// **en** el caret cuenta, no la saltea). Para avanzar a la siguiente
63/// real, el caller mueve el caret al `end` de la match anterior y
64/// vuelve a llamar. Wrap-around al fin del buffer → primera match.
65pub fn find_next(buf: &Buffer, find: &FindState, cursor: &Cursor) -> Option<(Pos, Pos)> {
66    let matches = all_matches(buf, find);
67    if matches.is_empty() {
68        return None;
69    }
70    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
71    let next = matches
72        .iter()
73        .find(|(s, _)| *s >= caret_off)
74        .copied()
75        .or_else(|| matches.first().copied())?;
76    Some(positions_of(buf, next))
77}
78
79/// Como [`find_next`] pero en reverso.
80pub fn find_prev(buf: &Buffer, find: &FindState, cursor: &Cursor) -> Option<(Pos, Pos)> {
81    let matches = all_matches(buf, find);
82    if matches.is_empty() {
83        return None;
84    }
85    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
86    let prev = matches
87        .iter()
88        .rev()
89        .find(|(_, e)| *e < caret_off)
90        .copied()
91        .or_else(|| matches.last().copied())?;
92    Some(positions_of(buf, prev))
93}
94
95fn positions_of(buf: &Buffer, (start, end): (usize, usize)) -> (Pos, Pos) {
96    let (sl, sc) = buf.offset_to_pos(start);
97    let (el, ec) = buf.offset_to_pos(end);
98    (Pos::new(sl, sc), Pos::new(el, ec))
99}
100
101/// Reemplaza la **próxima** ocurrencia desde el caret por `replacement` y
102/// deja el caret al final del texto insertado. Devuelve el [`EditDelta`]
103/// para la pila de undo, o `None` si no hay ninguna ocurrencia. El
104/// wrap-around es el de [`find_next`].
105pub fn replace_next(
106    buf: &mut Buffer,
107    find: &FindState,
108    cursor: &mut Cursor,
109    replacement: &str,
110) -> Option<crate::ops::EditDelta> {
111    let (start_pos, end_pos) = find_next(buf, find, cursor)?;
112    cursor.anchor = Some(start_pos);
113    cursor.caret = end_pos;
114    Some(crate::ops::replace_selection(buf, cursor, replacement))
115}
116
117/// Reemplaza **todas** las ocurrencias del query por `replacement` en una
118/// sola operación reversible. Devuelve `(delta, n)` con el delta para undo
119/// y la cantidad reemplazada, o `None` si no había ninguna. El reemplazo
120/// es literal (sin grupos/regex).
121pub fn replace_all(
122    buf: &mut Buffer,
123    find: &FindState,
124    cursor: &mut Cursor,
125    replacement: &str,
126) -> Option<(crate::ops::EditDelta, usize)> {
127    let matches = all_matches(buf, find);
128    if matches.is_empty() {
129        return None;
130    }
131    let n = matches.len();
132    // Región mínima que cubre de la primera a la última match: un único
133    // delta atómico (un solo undo deshace todo el reemplazo).
134    let region_start = matches.first().unwrap().0;
135    let region_end = matches.last().unwrap().1;
136    let removed = buf.slice(region_start, region_end);
137
138    // Reconstruimos la región reemplazando cada match, copiando lo de entre
139    // medio tal cual.
140    let mut inserted = String::with_capacity(removed.len());
141    let mut prev_end = region_start;
142    for (s, e) in &matches {
143        inserted.push_str(&buf.slice(prev_end, *s));
144        inserted.push_str(replacement);
145        prev_end = *e;
146    }
147    inserted.push_str(&buf.slice(prev_end, region_end));
148
149    let before = *cursor;
150    buf.delete(region_start, region_end);
151    buf.insert(region_start, &inserted);
152    let new_off = region_start + inserted.chars().count();
153    let (line, col) = buf.offset_to_pos(new_off);
154    cursor.caret = Pos::new(line, col);
155    cursor.desired_col = col;
156    cursor.anchor = None;
157    let delta = crate::ops::EditDelta {
158        start: region_start,
159        removed,
160        inserted,
161        cursor_before: before,
162        cursor_after: *cursor,
163    };
164    Some((delta, n))
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn all_matches_vacio_devuelve_vacio() {
173        let b = Buffer::from_str("hola hola");
174        let f = FindState::new();
175        assert!(all_matches(&b, &f).is_empty());
176    }
177
178    #[test]
179    fn all_matches_encuentra_todas() {
180        let b = Buffer::from_str("ab cd ab ef ab");
181        let f = FindState::with_query("ab");
182        let m = all_matches(&b, &f);
183        assert_eq!(m, vec![(0, 2), (6, 8), (12, 14)]);
184    }
185
186    #[test]
187    fn case_insensitive_por_default() {
188        let b = Buffer::from_str("Hola HOLA hola");
189        let f = FindState::with_query("hola");
190        assert_eq!(all_matches(&b, &f).len(), 3);
191    }
192
193    #[test]
194    fn case_sensitive_filtra() {
195        let b = Buffer::from_str("Hola HOLA hola");
196        let f = FindState { query: "hola".into(), case_sensitive: true };
197        assert_eq!(all_matches(&b, &f).len(), 1);
198    }
199
200    #[test]
201    fn find_next_wrap_al_final() {
202        let b = Buffer::from_str("ab cd ab");
203        let f = FindState::with_query("ab");
204        let c = Cursor::at(0, 8); // al final
205        let (a, _) = find_next(&b, &f, &c).unwrap();
206        assert_eq!(a, Pos::new(0, 0)); // wrap al primero
207    }
208
209    #[test]
210    fn find_prev_wrap_al_principio() {
211        let b = Buffer::from_str("ab cd ab");
212        let f = FindState::with_query("ab");
213        let c = Cursor::at(0, 0);
214        let (a, _) = find_prev(&b, &f, &c).unwrap();
215        assert_eq!(a, Pos::new(0, 6)); // wrap al último
216    }
217
218    #[test]
219    fn find_next_devuelve_match_en_el_caret() {
220        let b = Buffer::from_str("ab ab ab");
221        let f = FindState::with_query("ab");
222        let c = Cursor::at(0, 0);
223        let (a, _) = find_next(&b, &f, &c).unwrap();
224        assert_eq!(a, Pos::new(0, 0));
225    }
226
227    #[test]
228    fn replace_next_reemplaza_una() {
229        let mut b = Buffer::from_str("ab cd ab");
230        let f = FindState::with_query("ab");
231        let mut c = Cursor::at(0, 0);
232        replace_next(&mut b, &f, &mut c, "XY");
233        assert_eq!(b.text(), "XY cd ab");
234    }
235
236    #[test]
237    fn replace_all_reemplaza_todas() {
238        let mut b = Buffer::from_str("ab cd ab ef ab");
239        let f = FindState::with_query("ab");
240        let mut c = Cursor::at(0, 0);
241        let (_, n) = replace_all(&mut b, &f, &mut c, "Z").unwrap();
242        assert_eq!(n, 3);
243        assert_eq!(b.text(), "Z cd Z ef Z");
244    }
245
246    #[test]
247    fn replace_all_distinto_largo_y_undo() {
248        let mut b = Buffer::from_str("foo foo");
249        let f = FindState::with_query("foo");
250        let mut c = Cursor::at(0, 0);
251        let (delta, _) = replace_all(&mut b, &f, &mut c, "barbar").unwrap();
252        assert_eq!(b.text(), "barbar barbar");
253        delta.undo(&mut b, &mut c);
254        assert_eq!(b.text(), "foo foo");
255    }
256
257    #[test]
258    fn find_next_avanza_si_caret_va_al_fin_de_match_anterior() {
259        let b = Buffer::from_str("ab ab ab");
260        let f = FindState::with_query("ab");
261        let mut c = Cursor::at(0, 0);
262        let (_, end1) = find_next(&b, &f, &c).unwrap();
263        c.caret = end1; // (0, 2) — fin de la primera
264        let (a2, _) = find_next(&b, &f, &c).unwrap();
265        assert_eq!(a2, Pos::new(0, 3));
266    }
267}