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//! 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}