tawasuyu

tawasuyu/llimphi

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

Rust 614Markdown 274TOML 149RON 310606 Definiciones

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

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

1//! Operaciones de edición. Cada una toma `&mut Buffer + &mut Cursor` y
2//! devuelve un [`EditDelta`] reversible que la pila de undo guarda.
3//!
4//! El delta es minimal: el rango `[start..end)` que se reemplazó + el
5//! texto que estaba antes + el texto nuevo. Aplicado en reversa,
6//! restaura el estado anterior exactamente.
7
8use alloc::format;
9use alloc::string::{String, ToString};
10use alloc::vec::Vec;
11
12use crate::buffer::Buffer;
13use crate::cursor::{Cursor, Pos};
14
15/// Delta atómico de edición — útil para undo/redo y log de cambios.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct EditDelta {
18    pub start: usize,
19    pub removed: String,
20    pub inserted: String,
21    /// Caret antes de la operación (para restaurarlo en undo).
22    pub cursor_before: Cursor,
23    /// Caret después de la operación.
24    pub cursor_after: Cursor,
25}
26
27impl EditDelta {
28    /// Aplica el delta a `(buf, cursor)`.
29    pub fn apply(&self, buf: &mut Buffer, cursor: &mut Cursor) {
30        let end = self.start + self.removed.chars().count();
31        buf.delete(self.start, end);
32        if !self.inserted.is_empty() {
33            buf.insert(self.start, &self.inserted);
34        }
35        *cursor = self.cursor_after;
36    }
37
38    /// Aplica el inverso (undo).
39    pub fn undo(&self, buf: &mut Buffer, cursor: &mut Cursor) {
40        let end = self.start + self.inserted.chars().count();
41        buf.delete(self.start, end);
42        if !self.removed.is_empty() {
43            buf.insert(self.start, &self.removed);
44        }
45        *cursor = self.cursor_before;
46    }
47}
48
49/// Genera la string de indentación según la config.
50pub fn indent_str(tab_to_spaces: bool, indent_size: usize) -> String {
51    if tab_to_spaces {
52        " ".repeat(indent_size)
53    } else {
54        "\t".to_string()
55    }
56}
57
58/// Reemplaza la selección activa por `text`. Si no hay selección,
59/// inserta `text` en el caret. Devuelve el delta resultante.
60pub fn replace_selection(
61    buf: &mut Buffer,
62    cursor: &mut Cursor,
63    text: &str,
64) -> EditDelta {
65    let before = *cursor;
66    let (start, end) = cursor.selection_range(buf);
67    let removed = buf.slice(start, end);
68
69    if start != end {
70        buf.delete(start, end);
71    }
72    if !text.is_empty() {
73        buf.insert(start, text);
74    }
75
76    let new_off = start + text.chars().count();
77    let (line, col) = buf.offset_to_pos(new_off);
78    cursor.caret = Pos::new(line, col);
79    cursor.desired_col = col;
80    cursor.anchor = None;
81
82    EditDelta {
83        start,
84        removed,
85        inserted: text.to_string(),
86        cursor_before: before,
87        cursor_after: *cursor,
88    }
89}
90
91/// Borra hacia atrás (Backspace). Si hay selección, la borra; si no,
92/// borra el char antes del caret. Devuelve `None` si no había nada que
93/// borrar (cursor al inicio + sin selección).
94pub fn delete_backward(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
95    if cursor.has_selection() {
96        return Some(replace_selection(buf, cursor, ""));
97    }
98    let before = *cursor;
99    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
100    if caret_off == 0 {
101        return None;
102    }
103    let removed = buf.slice(caret_off - 1, caret_off);
104    buf.delete(caret_off - 1, caret_off);
105    let (line, col) = buf.offset_to_pos(caret_off - 1);
106    cursor.caret = Pos::new(line, col);
107    cursor.desired_col = col;
108    Some(EditDelta {
109        start: caret_off - 1,
110        removed,
111        inserted: String::new(),
112        cursor_before: before,
113        cursor_after: *cursor,
114    })
115}
116
117/// Borra hacia adelante (Delete).
118pub fn delete_forward(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
119    if cursor.has_selection() {
120        return Some(replace_selection(buf, cursor, ""));
121    }
122    let before = *cursor;
123    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
124    if caret_off >= buf.len_chars() {
125        return None;
126    }
127    let removed = buf.slice(caret_off, caret_off + 1);
128    buf.delete(caret_off, caret_off + 1);
129    Some(EditDelta {
130        start: caret_off,
131        removed,
132        inserted: String::new(),
133        cursor_before: before,
134        cursor_after: *cursor,
135    })
136}
137
138/// Borra la palabra a la izquierda del caret (Ctrl+Backspace). Si hay
139/// selección, la borra como [`delete_backward`]. El límite de palabra es
140/// el mismo que usa `Ctrl+Left` ([`Cursor::move_word_left`]): salta el
141/// whitespace y luego el run de caracteres de palabra. `None` si el caret
142/// ya estaba al inicio del buffer.
143pub fn delete_word_backward(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
144    if cursor.has_selection() {
145        return Some(replace_selection(buf, cursor, ""));
146    }
147    let before = *cursor;
148    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
149    if caret_off == 0 {
150        return None;
151    }
152    let mut probe = *cursor;
153    probe.move_word_left(buf, false);
154    let target = buf.pos_to_offset(probe.caret.line, probe.caret.col);
155    let (start, end) = (target.min(caret_off), target.max(caret_off));
156    if start == end {
157        return None;
158    }
159    let removed = buf.slice(start, end);
160    buf.delete(start, end);
161    let (line, col) = buf.offset_to_pos(start);
162    cursor.caret = Pos::new(line, col);
163    cursor.desired_col = col;
164    cursor.anchor = None;
165    Some(EditDelta {
166        start,
167        removed,
168        inserted: String::new(),
169        cursor_before: before,
170        cursor_after: *cursor,
171    })
172}
173
174/// Borra la palabra a la derecha del caret (Ctrl+Delete). Espejo de
175/// [`delete_word_backward`] usando el límite de `Ctrl+Right`. `None` si el
176/// caret ya estaba al final del buffer.
177pub fn delete_word_forward(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
178    if cursor.has_selection() {
179        return Some(replace_selection(buf, cursor, ""));
180    }
181    let before = *cursor;
182    let caret_off = buf.pos_to_offset(cursor.caret.line, cursor.caret.col);
183    if caret_off >= buf.len_chars() {
184        return None;
185    }
186    let mut probe = *cursor;
187    probe.move_word_right(buf, false);
188    let target = buf.pos_to_offset(probe.caret.line, probe.caret.col);
189    let (start, end) = (caret_off.min(target), caret_off.max(target));
190    if start == end {
191        return None;
192    }
193    let removed = buf.slice(start, end);
194    buf.delete(start, end);
195    // El caret se queda en `start` (= caret original).
196    cursor.anchor = None;
197    Some(EditDelta {
198        start,
199        removed,
200        inserted: String::new(),
201        cursor_before: before,
202        cursor_after: *cursor,
203    })
204}
205
206/// Duplica la línea del caret hacia abajo (Ctrl+D). La copia queda
207/// inmediatamente debajo y el caret baja a ella conservando la columna.
208pub fn duplicate_line(buf: &mut Buffer, cursor: &mut Cursor) -> EditDelta {
209    let before = *cursor;
210    let line = cursor.caret.line;
211    let start = buf.pos_to_offset(line, 0);
212    let text = buf.line(line); // incluye '\n' salvo en la última línea
213    let after_line = start + text.chars().count();
214    // Si la línea ya termina en '\n', la copia es el texto tal cual; si es
215    // la última (sin '\n'), anteponemos uno para abrir renglón nuevo.
216    let inserted = if text.ends_with('\n') {
217        text.clone()
218    } else {
219        format!("\n{text}")
220    };
221    buf.insert(after_line, &inserted);
222    let col = cursor.caret.col.min(buf.line_len_chars(line + 1));
223    cursor.caret = Pos::new(line + 1, col);
224    cursor.desired_col = col;
225    cursor.anchor = None;
226    EditDelta {
227        start: after_line,
228        removed: String::new(),
229        inserted,
230        cursor_before: before,
231        cursor_after: *cursor,
232    }
233}
234
235/// Texto de las dos líneas `l` y `l+1` intercambiadas, listo para
236/// reemplazar la región `[inicio_de_l .. inicio_de_l+2)`. `None` si no hay
237/// línea siguiente. No muta el buffer.
238fn swap_lines_text(buf: &Buffer, l: usize) -> Option<(usize, String, String)> {
239    if l + 1 >= buf.len_lines() {
240        return None;
241    }
242    let start = buf.pos_to_offset(l, 0);
243    let a = buf.line(l); // "A\n"
244    let b = buf.line(l + 1); // "B\n" o "B" (si es la última)
245    let removed = format!("{a}{b}");
246    let inserted = if b.ends_with('\n') {
247        format!("{b}{a}")
248    } else {
249        // `b` es la última línea (sin '\n'); `a` sí termina en '\n'.
250        let a_no_nl = a.strip_suffix('\n').unwrap_or(a.as_str());
251        format!("{b}\n{a_no_nl}")
252    };
253    Some((start, removed, inserted))
254}
255
256/// Mueve la línea del caret una posición hacia abajo (Alt+Down),
257/// intercambiándola con la siguiente. El caret la sigue. `None` si ya está
258/// en la última línea.
259pub fn move_line_down(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
260    let l = cursor.caret.line;
261    let before = *cursor;
262    let (start, removed, inserted) = swap_lines_text(buf, l)?;
263    buf.delete(start, start + removed.chars().count());
264    buf.insert(start, &inserted);
265    let col = cursor.caret.col.min(buf.line_len_chars(l + 1));
266    cursor.caret = Pos::new(l + 1, col);
267    cursor.desired_col = col;
268    cursor.anchor = None;
269    Some(EditDelta {
270        start,
271        removed,
272        inserted,
273        cursor_before: before,
274        cursor_after: *cursor,
275    })
276}
277
278/// Mueve la línea del caret una posición hacia arriba (Alt+Up). `None` si
279/// ya está en la primera línea.
280pub fn move_line_up(buf: &mut Buffer, cursor: &mut Cursor) -> Option<EditDelta> {
281    let l = cursor.caret.line;
282    if l == 0 {
283        return None;
284    }
285    let before = *cursor;
286    let (start, removed, inserted) = swap_lines_text(buf, l - 1)?;
287    buf.delete(start, start + removed.chars().count());
288    buf.insert(start, &inserted);
289    let col = cursor.caret.col.min(buf.line_len_chars(l - 1));
290    cursor.caret = Pos::new(l - 1, col);
291    cursor.desired_col = col;
292    cursor.anchor = None;
293    Some(EditDelta {
294        start,
295        removed,
296        inserted,
297        cursor_before: before,
298        cursor_after: *cursor,
299    })
300}
301
302/// Comenta o descomenta las líneas tocadas por la selección (o la línea
303/// del caret) con el `token` de comentario de línea (`//`, `#`, `--`…).
304/// Si **todas** las líneas no vacías ya están comentadas, las
305/// descomenta; si no, las comenta insertando `token ` tras la
306/// indentación. `None` si no hay cambio que aplicar.
307pub fn toggle_line_comment(buf: &mut Buffer, cursor: &mut Cursor, token: &str) -> Option<EditDelta> {
308    if token.is_empty() {
309        return None;
310    }
311    let before = *cursor;
312    let (sel_start, sel_end) = cursor.selection_range(buf);
313    let l0 = buf.char_to_line(sel_start);
314    let l1 = if sel_end > sel_start {
315        buf.char_to_line(sel_end - 1)
316    } else {
317        l0
318    };
319    let start = buf.pos_to_offset(l0, 0);
320    let end = if l1 + 1 < buf.len_lines() {
321        buf.pos_to_offset(l1 + 1, 0)
322    } else {
323        buf.len_chars()
324    };
325    let removed = buf.slice(start, end);
326
327    let lines: Vec<&str> = removed.split_inclusive('\n').collect();
328    let all_commented = lines
329        .iter()
330        .filter(|l| !l.trim().is_empty())
331        .all(|l| l.trim_start().starts_with(token));
332
333    let mut out = String::with_capacity(removed.len() + lines.len() * (token.len() + 1));
334    for line in &lines {
335        if line.trim().is_empty() {
336            out.push_str(line);
337            continue;
338        }
339        let indent_len = line.len() - line.trim_start().len();
340        let (indent, rest) = line.split_at(indent_len);
341        out.push_str(indent);
342        if all_commented {
343            let rest = rest.strip_prefix(token).unwrap_or(rest);
344            let rest = rest.strip_prefix(' ').unwrap_or(rest);
345            out.push_str(rest);
346        } else {
347            out.push_str(token);
348            out.push(' ');
349            out.push_str(rest);
350        }
351    }
352    if out == removed {
353        return None;
354    }
355
356    buf.delete(start, end);
357    buf.insert(start, &out);
358    let line = before.caret.line.min(buf.len_lines().saturating_sub(1));
359    let col = before.caret.col.min(buf.line_len_chars(line));
360    cursor.caret = Pos::new(line, col);
361    cursor.desired_col = col;
362    cursor.anchor = None;
363    Some(EditDelta {
364        start,
365        removed,
366        inserted: out,
367        cursor_before: before,
368        cursor_after: *cursor,
369    })
370}
371
372/// Inserta un salto de línea con **indentación automática**: copia los
373/// whitespace iniciales del renglón actual al renglón nuevo.
374pub fn insert_newline_auto_indent(buf: &mut Buffer, cursor: &mut Cursor) -> EditDelta {
375    let current_line = buf.line(cursor.caret.line);
376    let indent: String = current_line
377        .chars()
378        .take_while(|c| *c == ' ' || *c == '\t')
379        .collect();
380    let text = format!("\n{indent}");
381    replace_selection(buf, cursor, &text)
382}
383
384/// Inserta un tab (o `indent_size` spaces según config). Si hay
385/// selección **multilínea**, indenta cada línea de la selección.
386pub fn indent_or_insert_tab(
387    buf: &mut Buffer,
388    cursor: &mut Cursor,
389    tab_to_spaces: bool,
390    indent_size: usize,
391) -> EditDelta {
392    let indent = indent_str(tab_to_spaces, indent_size);
393
394    // Sin selección o selección en una sola línea → inserta indent.
395    let multi_line = match cursor.selection() {
396        Some(sel) => sel.anchor.line != sel.caret.line,
397        None => false,
398    };
399    if !multi_line {
400        return replace_selection(buf, cursor, &indent);
401    }
402
403    // Selección multilínea: indenta cada línea afectada por el rango.
404    let before = *cursor;
405    let sel = cursor.selection().expect("multi_line implica selección");
406    let first = sel.anchor.line.min(sel.caret.line);
407    let last = sel.anchor.line.max(sel.caret.line);
408
409    let mut start_global = buf.pos_to_offset(first, 0);
410    let removed = String::new();
411    let mut inserted = String::new();
412    for line in first..=last {
413        let line_start = buf.pos_to_offset(line, 0);
414        buf.insert(line_start, &indent);
415        inserted.push_str(&indent);
416        let _ = start_global; // (sin uso; se mantiene por simetría)
417        start_global = buf.pos_to_offset(first, 0);
418    }
419
420    // Mantenemos la selección extendida sobre las líneas indentadas.
421    let n_added = indent.chars().count();
422    let new_anchor = Pos::new(sel.anchor.line, sel.anchor.col + n_added);
423    let new_caret = Pos::new(sel.caret.line, sel.caret.col + n_added);
424    cursor.anchor = Some(new_anchor);
425    cursor.caret = new_caret;
426    cursor.desired_col = new_caret.col;
427
428    EditDelta {
429        start: start_global,
430        removed,
431        inserted,
432        cursor_before: before,
433        cursor_after: *cursor,
434    }
435}
436
437/// Quita un nivel de indent del renglón actual (o de cada línea si hay
438/// selección multilínea). Devuelve `None` si nada cambió.
439pub fn dedent(
440    buf: &mut Buffer,
441    cursor: &mut Cursor,
442    tab_to_spaces: bool,
443    indent_size: usize,
444) -> Option<EditDelta> {
445    let before = *cursor;
446    let (first, last) = match cursor.selection() {
447        Some(sel) => (
448            sel.anchor.line.min(sel.caret.line),
449            sel.anchor.line.max(sel.caret.line),
450        ),
451        None => (cursor.caret.line, cursor.caret.line),
452    };
453
454    let mut total_removed = 0usize;
455    let mut removed_text = String::new();
456    let start_offset = buf.pos_to_offset(first, 0);
457
458    for line in first..=last {
459        let line_str = buf.line(line);
460        let mut n = 0usize;
461        let mut chars = line_str.chars();
462        if tab_to_spaces {
463            for _ in 0..indent_size {
464                if chars.next() == Some(' ') {
465                    n += 1;
466                } else {
467                    break;
468                }
469            }
470        } else if chars.next() == Some('\t') {
471            n = 1;
472        }
473        if n == 0 {
474            continue;
475        }
476        let line_start = buf.pos_to_offset(line, 0);
477        removed_text.push_str(&buf.slice(line_start, line_start + n));
478        buf.delete(line_start, line_start + n);
479        total_removed += n;
480    }
481
482    if total_removed == 0 {
483        return None;
484    }
485
486    // Cursor: clampea col al nuevo line_len.
487    let caret_line = cursor.caret.line;
488    let caret_col = cursor
489        .caret
490        .col
491        .saturating_sub(if caret_line >= first && caret_line <= last {
492            // Cuánto se removió de esta línea (varía); aproximamos al
493            // common case de mismo n por línea. Si fuera distinto el
494            // visual queda OK porque clampea.
495            removed_text.chars().count() / (last - first + 1).max(1)
496        } else {
497            0
498        });
499    cursor.caret.col = caret_col.min(buf.line_len_chars(caret_line));
500    cursor.desired_col = cursor.caret.col;
501
502    if let Some(anchor) = cursor.anchor.as_mut() {
503        if anchor.line >= first && anchor.line <= last {
504            anchor.col = anchor
505                .col
506                .saturating_sub(removed_text.chars().count() / (last - first + 1).max(1))
507                .min(buf.line_len_chars(anchor.line));
508        }
509    }
510
511    Some(EditDelta {
512        start: start_offset,
513        removed: removed_text,
514        inserted: String::new(),
515        cursor_before: before,
516        cursor_after: *cursor,
517    })
518}
519
520#[cfg(test)]
521mod tests {
522    use super::*;
523
524    #[test]
525    fn replace_selection_sin_seleccion_inserta() {
526        let mut b = Buffer::from_str("ab");
527        let mut c = Cursor::at(0, 1);
528        let d = replace_selection(&mut b, &mut c, "X");
529        assert_eq!(b.text(), "aXb");
530        assert_eq!(c.caret, Pos::new(0, 2));
531        assert_eq!(d.removed, "");
532        assert_eq!(d.inserted, "X");
533    }
534
535    #[test]
536    fn replace_selection_con_seleccion_reemplaza() {
537        let mut b = Buffer::from_str("hola mundo");
538        let mut c = Cursor { caret: Pos::new(0, 9), anchor: Some(Pos::new(0, 5)), desired_col: 9 };
539        replace_selection(&mut b, &mut c, "luna");
540        assert_eq!(b.text(), "hola lunao");
541    }
542
543    #[test]
544    fn backspace_borra_char() {
545        let mut b = Buffer::from_str("hola");
546        let mut c = Cursor::at(0, 4);
547        delete_backward(&mut b, &mut c);
548        assert_eq!(b.text(), "hol");
549        assert_eq!(c.caret, Pos::new(0, 3));
550    }
551
552    #[test]
553    fn backspace_en_inicio_no_hace_nada() {
554        let mut b = Buffer::from_str("a");
555        let mut c = Cursor::at(0, 0);
556        assert!(delete_backward(&mut b, &mut c).is_none());
557    }
558
559    #[test]
560    fn delete_forward_borra_char() {
561        let mut b = Buffer::from_str("ab");
562        let mut c = Cursor::at(0, 0);
563        delete_forward(&mut b, &mut c);
564        assert_eq!(b.text(), "b");
565    }
566
567    #[test]
568    fn ctrl_backspace_borra_palabra() {
569        let mut b = Buffer::from_str("hola mundo");
570        let mut c = Cursor::at(0, 10); // al final
571        delete_word_backward(&mut b, &mut c);
572        assert_eq!(b.text(), "hola ");
573        assert_eq!(c.caret, Pos::new(0, 5));
574    }
575
576    #[test]
577    fn ctrl_backspace_en_inicio_no_hace_nada() {
578        let mut b = Buffer::from_str("hola");
579        let mut c = Cursor::at(0, 0);
580        assert!(delete_word_backward(&mut b, &mut c).is_none());
581    }
582
583    #[test]
584    fn ctrl_delete_borra_palabra_adelante() {
585        let mut b = Buffer::from_str("hola mundo");
586        let mut c = Cursor::at(0, 0);
587        delete_word_forward(&mut b, &mut c);
588        // borra "hola" + el espacio siguiente (límite de Ctrl+Right).
589        assert_eq!(b.text(), "mundo");
590        assert_eq!(c.caret, Pos::new(0, 0));
591    }
592
593    #[test]
594    fn duplicate_line_copia_debajo() {
595        let mut b = Buffer::from_str("uno\ndos");
596        let mut c = Cursor::at(0, 1);
597        duplicate_line(&mut b, &mut c);
598        assert_eq!(b.text(), "uno\nuno\ndos");
599        assert_eq!(c.caret, Pos::new(1, 1));
600    }
601
602    #[test]
603    fn duplicate_line_ultima_sin_newline() {
604        let mut b = Buffer::from_str("uno\ndos");
605        let mut c = Cursor::at(1, 0);
606        duplicate_line(&mut b, &mut c);
607        assert_eq!(b.text(), "uno\ndos\ndos");
608    }
609
610    #[test]
611    fn move_line_down_intercambia() {
612        let mut b = Buffer::from_str("uno\ndos\ntres");
613        let mut c = Cursor::at(0, 2);
614        move_line_down(&mut b, &mut c);
615        assert_eq!(b.text(), "dos\nuno\ntres");
616        assert_eq!(c.caret, Pos::new(1, 2));
617    }
618
619    #[test]
620    fn move_line_down_con_ultima_sin_newline() {
621        let mut b = Buffer::from_str("uno\ndos");
622        let mut c = Cursor::at(0, 0);
623        move_line_down(&mut b, &mut c);
624        assert_eq!(b.text(), "dos\nuno");
625    }
626
627    #[test]
628    fn move_line_up_intercambia() {
629        let mut b = Buffer::from_str("uno\ndos\ntres");
630        let mut c = Cursor::at(2, 1);
631        move_line_up(&mut b, &mut c);
632        assert_eq!(b.text(), "uno\ntres\ndos");
633        assert_eq!(c.caret, Pos::new(1, 1));
634    }
635
636    #[test]
637    fn move_line_up_en_primera_no_hace_nada() {
638        let mut b = Buffer::from_str("uno\ndos");
639        let mut c = Cursor::at(0, 0);
640        assert!(move_line_up(&mut b, &mut c).is_none());
641    }
642
643    #[test]
644    fn toggle_comment_comenta_y_descomenta() {
645        let mut b = Buffer::from_str("uno\ndos");
646        let mut c = Cursor::at(0, 0);
647        // sin selección → línea del caret
648        toggle_line_comment(&mut b, &mut c, "//");
649        assert_eq!(b.text(), "// uno\ndos");
650        toggle_line_comment(&mut b, &mut c, "//");
651        assert_eq!(b.text(), "uno\ndos");
652    }
653
654    #[test]
655    fn toggle_comment_seleccion_multilinea_respeta_indent() {
656        let mut b = Buffer::from_str("  uno\n  dos");
657        let mut c = Cursor { caret: Pos::new(1, 5), anchor: Some(Pos::new(0, 0)), desired_col: 5 };
658        toggle_line_comment(&mut b, &mut c, "#");
659        assert_eq!(b.text(), "  # uno\n  # dos");
660    }
661
662    #[test]
663    fn newline_copia_indent_del_renglon_anterior() {
664        let mut b = Buffer::from_str("    hola");
665        let mut c = Cursor::at(0, 8);
666        insert_newline_auto_indent(&mut b, &mut c);
667        assert_eq!(b.text(), "    hola\n    ");
668        assert_eq!(c.caret, Pos::new(1, 4));
669    }
670
671    #[test]
672    fn tab_inserta_spaces() {
673        let mut b = Buffer::from_str("ab");
674        let mut c = Cursor::at(0, 1);
675        indent_or_insert_tab(&mut b, &mut c, true, 4);
676        assert_eq!(b.text(), "a    b");
677        assert_eq!(c.caret, Pos::new(0, 5));
678    }
679
680    #[test]
681    fn tab_con_seleccion_multilinea_indenta_cada_linea() {
682        let mut b = Buffer::from_str("a\nb\nc");
683        let mut c = Cursor {
684            anchor: Some(Pos::new(0, 0)),
685            caret: Pos::new(2, 1),
686            desired_col: 1,
687        };
688        indent_or_insert_tab(&mut b, &mut c, true, 2);
689        assert_eq!(b.text(), "  a\n  b\n  c");
690    }
691
692    #[test]
693    fn dedent_quita_indent_del_renglon() {
694        let mut b = Buffer::from_str("    hola");
695        let mut c = Cursor::at(0, 8);
696        dedent(&mut b, &mut c, true, 4);
697        assert_eq!(b.text(), "hola");
698    }
699
700    #[test]
701    fn dedent_sin_indent_devuelve_none() {
702        let mut b = Buffer::from_str("hola");
703        let mut c = Cursor::at(0, 0);
704        assert!(dedent(&mut b, &mut c, true, 4).is_none());
705    }
706
707    #[test]
708    fn delta_undo_restaura_estado() {
709        let mut b = Buffer::from_str("hola");
710        let mut c = Cursor::at(0, 4);
711        let d = replace_selection(&mut b, &mut c, "!");
712        assert_eq!(b.text(), "hola!");
713        d.undo(&mut b, &mut c);
714        assert_eq!(b.text(), "hola");
715        assert_eq!(c.caret, Pos::new(0, 4));
716    }
717}