Skip to main content

heatsync_core/
edit.rs

1//! a vi line editor for the composer. insert and normal sub-modes, motions,
2//! operators, registers and history — the same muscle memory as everywhere else,
3//! rather than a handful of emacs control chords.
4//!
5//! text is held as `Vec<char>` and the cursor is a CHAR index. chat is full of
6//! emoji and CJK, and byte offsets would put motions and deletes half-way
7//! through a glyph; at composer length the extra allocation is irrelevant.
8//!
9//! no rendering and no app state here — main.rs asks what a key did and draws
10//! the result, which is what lets every motion below be tested directly.
11
12use crate::key::{KeyCode, KeyEvent, KeyModifiers};
13
14#[derive(Clone, Copy, PartialEq, Debug)]
15pub enum Mode {
16    Insert,
17    Normal,
18}
19
20/// what the caller should do after a key.
21#[derive(PartialEq, Debug)]
22pub enum Act {
23    Continue,
24    /// enter — send what's in the line.
25    Send,
26    /// escape from normal mode — put the composer away, keeping the draft.
27    Leave,
28}
29
30/// what a half-typed multi-key command is still waiting for.
31#[derive(Clone, Copy, PartialEq)]
32enum Await {
33    /// f F t T — jump to a character
34    Find { back: bool, till: bool },
35    /// r — replace the character under the cursor
36    Replace,
37}
38
39/// character classes, vi's rules: word characters, punctuation, blanks. `w` and
40/// `b` stop at every class change; `W` and `B` only at blanks.
41fn class(c: char) -> u8 {
42    if c.is_whitespace() {
43        0
44    } else if c.is_alphanumeric() || c == '_' {
45        1
46    } else {
47        2
48    }
49}
50
51pub struct Line {
52    text: Vec<char>,
53    cursor: usize,
54    mode: Mode,
55    /// pending operator: d, c or y.
56    op: Option<char>,
57    count: usize,
58    awaiting: Option<Await>,
59    /// unnamed register, for p / P.
60    reg: Vec<char>,
61    /// last f/F/t/T, for ; and ,
62    last_find: Option<(char, bool, bool)>,
63    undo: Vec<(Vec<char>, usize)>,
64    history: Vec<String>,
65    /// where we are walking history; None means "on the live draft".
66    hist: Option<usize>,
67    /// the draft we set aside when history walking started.
68    stash: Vec<char>,
69}
70
71impl Default for Line {
72    fn default() -> Self {
73        Line {
74            text: Vec::new(),
75            cursor: 0,
76            mode: Mode::Insert,
77            op: None,
78            count: 0,
79            awaiting: None,
80            reg: Vec::new(),
81            last_find: None,
82            undo: Vec::new(),
83            history: Vec::new(),
84            hist: None,
85            stash: Vec::new(),
86        }
87    }
88}
89
90impl Line {
91    pub fn text(&self) -> String {
92        self.text.iter().collect()
93    }
94
95    pub fn is_empty(&self) -> bool {
96        self.text.is_empty()
97    }
98
99    pub fn mode(&self) -> Mode {
100        self.mode
101    }
102
103    /// cursor position as a char index, for drawing the block.
104    pub fn cursor(&self) -> usize {
105        self.cursor
106    }
107
108    /// the pending command, echoed in the footer the way vi shows one.
109    pub fn pending(&self) -> String {
110        let mut s = String::new();
111        if self.count > 0 {
112            s.push_str(&self.count.to_string());
113        }
114        if let Some(op) = self.op {
115            s.push(op);
116        }
117        match self.awaiting {
118            Some(Await::Find { back, till }) => s.push(match (back, till) {
119                (false, false) => 'f',
120                (true, false) => 'F',
121                (false, true) => 't',
122                (true, true) => 'T',
123            }),
124            Some(Await::Replace) => s.push('r'),
125            None => {}
126        }
127        s
128    }
129
130    /// accept the line: record it in history and start a fresh one in insert
131    /// mode, the way hitting enter in any chat client does.
132    pub fn accept(&mut self) -> String {
133        let out: String = self.text.iter().collect();
134        if !out.trim().is_empty() && self.history.last().map(String::as_str) != Some(out.as_str()) {
135            self.history.push(out.clone());
136        }
137        self.reset();
138        out
139    }
140
141    /// throw the draft away and return to insert mode.
142    pub fn reset(&mut self) {
143        self.text.clear();
144        self.cursor = 0;
145        self.mode = Mode::Insert;
146        self.op = None;
147        self.count = 0;
148        self.awaiting = None;
149        self.hist = None;
150        self.stash.clear();
151        self.undo.clear();
152    }
153
154    /// entering the composer always starts in insert mode — you came here to
155    /// type, and the draft you left behind is still there.
156    pub fn focus(&mut self) {
157        self.mode = Mode::Insert;
158        self.op = None;
159        self.count = 0;
160        self.awaiting = None;
161    }
162
163    fn snapshot(&mut self) {
164        self.undo.push((self.text.clone(), self.cursor));
165        if self.undo.len() > 100 {
166            self.undo.remove(0);
167        }
168    }
169
170    /// in normal mode the cursor sits ON a character, so it stops one short of
171    /// the end; in insert mode it may sit past the last one.
172    fn clamp(&mut self) {
173        let max = match self.mode {
174            Mode::Insert => self.text.len(),
175            Mode::Normal => self.text.len().saturating_sub(1),
176        };
177        self.cursor = self.cursor.min(max);
178    }
179
180    fn take_count(&mut self) -> usize {
181        let n = self.count.max(1);
182        self.count = 0;
183        n
184    }
185
186    pub fn key(&mut self, k: KeyEvent) -> Act {
187        match self.mode {
188            Mode::Insert => self.insert_key(k),
189            Mode::Normal => self.normal_key(k),
190        }
191    }
192
193    fn insert_key(&mut self, k: KeyEvent) -> Act {
194        match k.code {
195            KeyCode::Esc => {
196                self.mode = Mode::Normal;
197                // vi steps left when leaving insert
198                self.cursor = self.cursor.saturating_sub(1);
199                self.clamp();
200            }
201            KeyCode::Enter => return Act::Send,
202            KeyCode::Backspace => {
203                if self.cursor > 0 {
204                    self.snapshot();
205                    self.cursor -= 1;
206                    self.text.remove(self.cursor);
207                }
208            }
209            KeyCode::Delete => {
210                if self.cursor < self.text.len() {
211                    self.snapshot();
212                    self.text.remove(self.cursor);
213                }
214            }
215            KeyCode::Left => self.cursor = self.cursor.saturating_sub(1),
216            KeyCode::Right => self.cursor = (self.cursor + 1).min(self.text.len()),
217            KeyCode::Home => self.cursor = 0,
218            KeyCode::End => self.cursor = self.text.len(),
219            KeyCode::Char(c) if !k.modifiers.contains(KeyModifiers::CONTROL) => {
220                self.snapshot();
221                self.text.insert(self.cursor, c);
222                self.cursor += 1;
223            }
224            _ => {}
225        }
226        Act::Continue
227    }
228
229    fn normal_key(&mut self, k: KeyEvent) -> Act {
230        let KeyCode::Char(c) = k.code else {
231            return self.normal_special(k);
232        };
233        if k.modifiers.contains(KeyModifiers::CONTROL) {
234            return Act::Continue;
235        }
236
237        // a pending f/F/t/T or r consumes the very next character, whatever it is
238        if let Some(a) = self.awaiting.take() {
239            match a {
240                Await::Find { back, till } => {
241                    self.last_find = Some((c, back, till));
242                    let n = self.take_count();
243                    if let Some(t) = self.find_target(c, back, till, n) {
244                        self.apply_motion(t, !back);
245                    } else {
246                        self.op = None;
247                    }
248                }
249                Await::Replace => {
250                    if self.cursor < self.text.len() {
251                        self.snapshot();
252                        self.text[self.cursor] = c;
253                    }
254                }
255            }
256            return Act::Continue;
257        }
258
259        // counts
260        if c.is_ascii_digit() && !(c == '0' && self.count == 0) {
261            self.count = (self.count.saturating_mul(10))
262                .saturating_add(c.to_digit(10).unwrap_or(0) as usize)
263                .min(100_000);
264            return Act::Continue;
265        }
266
267        // an operator repeated is the linewise form: dd, cc, yy
268        if let Some(op) = self.op {
269            if c == op {
270                self.op = None;
271                self.count = 0;
272                return self.whole_line(op);
273            }
274        }
275
276        match c {
277            // ---- motions ------------------------------------------------
278            'h' => {
279                let n = self.take_count();
280                self.apply_motion(self.cursor.saturating_sub(n), false)
281            }
282            'l' | ' ' => {
283                let n = self.take_count();
284                self.apply_motion((self.cursor + n).min(self.text.len()), false)
285            }
286            '0' => self.apply_motion(0, false),
287            '^' => {
288                let t = self.first_non_blank();
289                self.apply_motion(t, false)
290            }
291            '$' => self.apply_motion(self.text.len(), false),
292            'w' | 'W' => {
293                let n = self.take_count();
294                let t = self.word_fwd(n, c == 'W');
295                self.apply_motion(t, false)
296            }
297            'b' | 'B' => {
298                let n = self.take_count();
299                let t = self.word_back(n, c == 'B');
300                self.apply_motion(t, false)
301            }
302            'e' | 'E' => {
303                let n = self.take_count();
304                let t = self.word_end(n, c == 'E');
305                self.apply_motion(t, true)
306            }
307            'f' | 'F' | 't' | 'T' => {
308                self.awaiting = Some(Await::Find {
309                    back: c == 'F' || c == 'T',
310                    till: c == 't' || c == 'T',
311                });
312            }
313            ';' | ',' => {
314                if let Some((ch, back, till)) = self.last_find {
315                    let back = if c == ',' { !back } else { back };
316                    let n = self.take_count();
317                    if let Some(t) = self.find_target(ch, back, till, n) {
318                        self.apply_motion(t, !back);
319                    }
320                }
321            }
322            // ---- operators ----------------------------------------------
323            'd' | 'c' | 'y' => self.op = Some(c),
324            'D' => return self.op_to_end('d'),
325            'C' => return self.op_to_end('c'),
326            'Y' => return self.whole_line('y'),
327            // ---- edits ----------------------------------------------------
328            'x' => {
329                let n = self.take_count();
330                let end = (self.cursor + n).min(self.text.len());
331                self.cut(self.cursor, end, false);
332            }
333            'X' => {
334                let n = self.take_count();
335                let start = self.cursor.saturating_sub(n);
336                self.cut(start, self.cursor, false);
337            }
338            'r' => self.awaiting = Some(Await::Replace),
339            'i' => self.mode = Mode::Insert,
340            'a' => {
341                self.cursor = (self.cursor + 1).min(self.text.len());
342                self.mode = Mode::Insert;
343            }
344            'I' => {
345                self.cursor = self.first_non_blank();
346                self.mode = Mode::Insert;
347            }
348            'A' => {
349                self.cursor = self.text.len();
350                self.mode = Mode::Insert;
351            }
352            'p' | 'P' => {
353                if !self.reg.is_empty() {
354                    self.snapshot();
355                    let at = if c == 'p' {
356                        (self.cursor + 1).min(self.text.len())
357                    } else {
358                        self.cursor
359                    };
360                    let reg = self.reg.clone();
361                    let n = reg.len();
362                    self.text.splice(at..at, reg);
363                    self.cursor = at + n - 1;
364                    self.clamp();
365                }
366            }
367            'u' => {
368                if let Some((t, cur)) = self.undo.pop() {
369                    self.text = t;
370                    self.cursor = cur;
371                    self.clamp();
372                }
373            }
374            // ---- history --------------------------------------------------
375            'k' => self.history_step(-1),
376            'j' => self.history_step(1),
377            _ => {}
378        }
379        Act::Continue
380    }
381
382    fn normal_special(&mut self, k: KeyEvent) -> Act {
383        match k.code {
384            KeyCode::Enter => Act::Send,
385            KeyCode::Esc => {
386                if self.op.is_some() || self.count > 0 || self.awaiting.is_some() {
387                    // first escape cancels a half-typed command, as in vi
388                    self.op = None;
389                    self.count = 0;
390                    self.awaiting = None;
391                    Act::Continue
392                } else {
393                    Act::Leave
394                }
395            }
396            KeyCode::Left => {
397                self.cursor = self.cursor.saturating_sub(1);
398                Act::Continue
399            }
400            KeyCode::Right => {
401                self.cursor += 1;
402                self.clamp();
403                Act::Continue
404            }
405            KeyCode::Up => {
406                self.history_step(-1);
407                Act::Continue
408            }
409            KeyCode::Down => {
410                self.history_step(1);
411                Act::Continue
412            }
413            KeyCode::Home => {
414                self.cursor = 0;
415                Act::Continue
416            }
417            KeyCode::End => {
418                self.cursor = self.text.len();
419                self.clamp();
420                Act::Continue
421            }
422            KeyCode::Backspace => {
423                self.cursor = self.cursor.saturating_sub(1);
424                Act::Continue
425            }
426            _ => Act::Continue,
427        }
428    }
429
430    /// move, or apply the pending operator across the span just travelled.
431    /// `inclusive` motions (e, f, t) cover the character they land on.
432    fn apply_motion(&mut self, target: usize, inclusive: bool) {
433        let target = target.min(self.text.len());
434        match self.op.take() {
435            None => {
436                self.cursor = target;
437                self.clamp();
438            }
439            Some(op) => {
440                let (lo, hi) = if target >= self.cursor {
441                    (
442                        self.cursor,
443                        if inclusive {
444                            (target + 1).min(self.text.len())
445                        } else {
446                            target
447                        },
448                    )
449                } else {
450                    (target, self.cursor)
451                };
452                match op {
453                    'y' => self.reg = self.text[lo..hi].to_vec(),
454                    'd' => self.cut(lo, hi, false),
455                    'c' => self.cut(lo, hi, true),
456                    _ => {}
457                }
458            }
459        }
460    }
461
462    fn op_to_end(&mut self, op: char) -> Act {
463        self.op = Some(op);
464        self.apply_motion(self.text.len(), false);
465        Act::Continue
466    }
467
468    fn whole_line(&mut self, op: char) -> Act {
469        match op {
470            'y' => self.reg = self.text.clone(),
471            'd' => self.cut(0, self.text.len(), false),
472            'c' => self.cut(0, self.text.len(), true),
473            _ => {}
474        }
475        Act::Continue
476    }
477
478    /// remove `lo..hi` into the register. `into_insert` is the `c` behaviour.
479    fn cut(&mut self, lo: usize, hi: usize, into_insert: bool) {
480        let hi = hi.min(self.text.len());
481        if lo < hi {
482            self.snapshot();
483            self.reg = self.text[lo..hi].to_vec();
484            self.text.drain(lo..hi);
485        }
486        self.cursor = lo;
487        if into_insert {
488            self.mode = Mode::Insert;
489        }
490        self.clamp();
491    }
492
493    fn first_non_blank(&self) -> usize {
494        self.text
495            .iter()
496            .position(|c| !c.is_whitespace())
497            .unwrap_or(0)
498    }
499
500    fn find_target(&self, ch: char, back: bool, till: bool, n: usize) -> Option<usize> {
501        let mut at = self.cursor;
502        for _ in 0..n {
503            at = if back {
504                self.text[..at.min(self.text.len())]
505                    .iter()
506                    .rposition(|&c| c == ch)?
507            } else {
508                let from = at + 1;
509                from + self.text.get(from..)?.iter().position(|&c| c == ch)?
510            };
511        }
512        Some(match (back, till) {
513            (false, true) => at.saturating_sub(1),
514            (true, true) => at + 1,
515            _ => at,
516        })
517    }
518
519    fn word_fwd(&self, n: usize, big: bool) -> usize {
520        let mut i = self.cursor;
521        let len = self.text.len();
522        for _ in 0..n {
523            if i >= len {
524                break;
525            }
526            let start = if big {
527                1.min(class(self.text[i]))
528            } else {
529                class(self.text[i])
530            };
531            // step off the current run…
532            while i < len && cls(self.text[i], big) == start && start != 0 {
533                i += 1;
534            }
535            // …then over any blanks
536            while i < len && self.text[i].is_whitespace() {
537                i += 1;
538            }
539        }
540        i
541    }
542
543    fn word_back(&self, n: usize, big: bool) -> usize {
544        let mut i = self.cursor;
545        for _ in 0..n {
546            if i == 0 {
547                break;
548            }
549            i -= 1;
550            while i > 0 && self.text[i].is_whitespace() {
551                i -= 1;
552            }
553            let c = cls(self.text[i], big);
554            while i > 0 && cls(self.text[i - 1], big) == c && c != 0 {
555                i -= 1;
556            }
557        }
558        i
559    }
560
561    fn word_end(&self, n: usize, big: bool) -> usize {
562        let mut i = self.cursor;
563        let len = self.text.len();
564        for _ in 0..n {
565            if i + 1 >= len {
566                break;
567            }
568            i += 1;
569            while i < len && self.text[i].is_whitespace() {
570                i += 1;
571            }
572            if i >= len {
573                break;
574            }
575            let c = cls(self.text[i], big);
576            while i + 1 < len && cls(self.text[i + 1], big) == c {
577                i += 1;
578            }
579        }
580        i.min(len.saturating_sub(1))
581    }
582
583    /// walk sent history. -1 is older (vi's `k`), 1 is newer.
584    fn history_step(&mut self, dir: i32) {
585        if self.history.is_empty() {
586            return;
587        }
588        let last = self.history.len() - 1;
589        let next = match (self.hist, dir) {
590            (None, -1) => {
591                self.stash = self.text.clone();
592                Some(last)
593            }
594            (Some(i), -1) => Some(i.saturating_sub(1)),
595            (Some(i), 1) if i >= last => None,
596            (Some(i), 1) => Some(i + 1),
597            _ => return,
598        };
599        self.text = match next {
600            Some(i) => self.history[i].chars().collect(),
601            None => std::mem::take(&mut self.stash),
602        };
603        self.hist = next;
604        self.cursor = self.text.len();
605        self.clamp();
606    }
607}
608
609/// class, collapsing word and punctuation for the WORD motions.
610fn cls(c: char, big: bool) -> u8 {
611    let k = class(c);
612    if big && k == 2 {
613        1
614    } else {
615        k
616    }
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622
623    fn key(c: char) -> KeyEvent {
624        KeyEvent::new(KeyCode::Char(c), KeyModifiers::NONE)
625    }
626    fn special(k: KeyCode) -> KeyEvent {
627        KeyEvent::new(k, KeyModifiers::NONE)
628    }
629
630    /// drive a sequence; `<esc>` and `<cr>` are spelled out.
631    fn run(l: &mut Line, keys: &str) -> Act {
632        let mut act = Act::Continue;
633        let mut it = keys.chars().peekable();
634        while let Some(c) = it.next() {
635            if c == '<' {
636                let tag: String = it.by_ref().take_while(|&c| c != '>').collect();
637                act = l.key(special(match tag.as_str() {
638                    "esc" => KeyCode::Esc,
639                    "cr" => KeyCode::Enter,
640                    "bs" => KeyCode::Backspace,
641                    "up" => KeyCode::Up,
642                    "down" => KeyCode::Down,
643                    other => panic!("unknown key <{other}>"),
644                }));
645            } else {
646                act = l.key(key(c));
647            }
648        }
649        act
650    }
651
652    fn line(s: &str) -> Line {
653        let mut l = Line::default();
654        run(&mut l, s);
655        l
656    }
657
658    #[test]
659    fn typing_inserts_and_starts_in_insert_mode() {
660        let l = line("hello");
661        assert_eq!(l.text(), "hello");
662        assert_eq!(l.mode(), Mode::Insert);
663        assert_eq!(l.cursor(), 5);
664    }
665
666    #[test]
667    fn escape_enters_normal_mode_and_steps_left() {
668        let l = line("hello<esc>");
669        assert_eq!(l.mode(), Mode::Normal);
670        assert_eq!(
671            l.cursor(),
672            4,
673            "vi puts the cursor on the last char, not past it"
674        );
675    }
676
677    #[test]
678    fn escape_from_normal_leaves_the_composer_keeping_the_draft() {
679        let mut l = line("wip<esc>");
680        assert_eq!(l.key(special(KeyCode::Esc)), Act::Leave);
681        assert_eq!(l.text(), "wip", "the draft survives");
682    }
683
684    #[test]
685    fn escape_first_cancels_a_half_typed_command() {
686        let mut l = line("hello world<esc>0");
687        run(&mut l, "2d");
688        assert_eq!(
689            l.key(special(KeyCode::Esc)),
690            Act::Continue,
691            "cancels the pending d"
692        );
693        assert_eq!(l.text(), "hello world");
694        assert_eq!(
695            l.key(special(KeyCode::Esc)),
696            Act::Leave,
697            "second one leaves"
698        );
699    }
700
701    #[test]
702    fn enter_sends_from_either_mode() {
703        let mut l = line("hi");
704        assert_eq!(l.key(special(KeyCode::Enter)), Act::Send);
705        let mut l = line("hi<esc>");
706        assert_eq!(l.key(special(KeyCode::Enter)), Act::Send);
707    }
708
709    // ---- motions ---------------------------------------------------------
710
711    #[test]
712    fn hl_and_line_ends() {
713        let mut l = line("hello world<esc>");
714        run(&mut l, "0");
715        assert_eq!(l.cursor(), 0);
716        run(&mut l, "$");
717        assert_eq!(l.cursor(), 10);
718        run(&mut l, "5h");
719        assert_eq!(l.cursor(), 5);
720        run(&mut l, "2l");
721        assert_eq!(l.cursor(), 7);
722    }
723
724    #[test]
725    fn caret_goes_to_the_first_non_blank() {
726        let mut l = line("   hi<esc>");
727        run(&mut l, "^");
728        assert_eq!(l.cursor(), 3);
729    }
730
731    #[test]
732    fn word_motions_stop_at_class_changes() {
733        let mut l = line("foo.bar baz<esc>0");
734        run(&mut l, "w");
735        assert_eq!(l.cursor(), 3, "w stops at the punctuation");
736        run(&mut l, "w");
737        assert_eq!(l.cursor(), 4);
738        run(&mut l, "w");
739        assert_eq!(l.cursor(), 8);
740    }
741
742    #[test]
743    fn big_word_motions_only_stop_at_blanks() {
744        let mut l = line("foo.bar baz<esc>0");
745        run(&mut l, "W");
746        assert_eq!(l.cursor(), 8, "W treats foo.bar as one WORD");
747        run(&mut l, "B");
748        assert_eq!(l.cursor(), 0);
749    }
750
751    #[test]
752    fn e_lands_on_the_last_char_of_a_word() {
753        let mut l = line("hello world<esc>0");
754        run(&mut l, "e");
755        assert_eq!(l.cursor(), 4);
756        run(&mut l, "e");
757        assert_eq!(l.cursor(), 10);
758    }
759
760    #[test]
761    fn find_and_till_with_repeat() {
762        let mut l = line("a,b,c<esc>0");
763        run(&mut l, "f,");
764        assert_eq!(l.cursor(), 1);
765        run(&mut l, ";");
766        assert_eq!(l.cursor(), 3);
767        run(&mut l, "0t,");
768        assert_eq!(l.cursor(), 0, "t stops before the target");
769        run(&mut l, "$F,");
770        assert_eq!(l.cursor(), 3);
771    }
772
773    // ---- operators -------------------------------------------------------
774
775    #[test]
776    fn dw_deletes_a_word() {
777        let mut l = line("hello world<esc>0");
778        run(&mut l, "dw");
779        assert_eq!(l.text(), "world");
780    }
781
782    #[test]
783    fn counts_apply_to_operators() {
784        let mut l = line("one two three four<esc>0");
785        run(&mut l, "3dw");
786        assert_eq!(l.text(), "four");
787    }
788
789    #[test]
790    fn dd_and_cc_take_the_whole_line() {
791        let mut l = line("throw this away<esc>");
792        run(&mut l, "dd");
793        assert_eq!(l.text(), "");
794        let mut l = line("replace me<esc>");
795        run(&mut l, "cc");
796        assert_eq!(l.text(), "");
797        assert_eq!(l.mode(), Mode::Insert, "c leaves you typing");
798    }
799
800    #[test]
801    fn shift_d_and_c_run_to_end_of_line() {
802        let mut l = line("keep this drop that<esc>0");
803        run(&mut l, "wwD");
804        assert_eq!(l.text(), "keep this ");
805        let mut l = line("keep drop<esc>0");
806        run(&mut l, "wC");
807        assert_eq!(l.text(), "keep ");
808        assert_eq!(l.mode(), Mode::Insert);
809    }
810
811    #[test]
812    fn ci_style_change_then_type() {
813        let mut l = line("hello world<esc>0");
814        run(&mut l, "cwbye ");
815        assert_eq!(l.text(), "bye world");
816    }
817
818    #[test]
819    fn df_deletes_through_the_target() {
820        let mut l = line("a,b,c<esc>0");
821        run(&mut l, "df,");
822        assert_eq!(l.text(), "b,c", "f is inclusive");
823    }
824
825    #[test]
826    fn backwards_operator_deletes_the_span_behind() {
827        let mut l = line("hello world<esc>$");
828        run(&mut l, "db");
829        assert_eq!(l.text(), "hello d");
830    }
831
832    // ---- edits -----------------------------------------------------------
833
834    #[test]
835    fn x_deletes_forward_and_yanks_to_the_register() {
836        let mut l = line("abcdef<esc>0");
837        run(&mut l, "3x");
838        assert_eq!(l.text(), "def");
839        run(&mut l, "$p");
840        assert_eq!(l.text(), "defabc", "p pastes after the cursor");
841    }
842
843    #[test]
844    fn shift_p_pastes_before() {
845        let mut l = line("abcdef<esc>0");
846        run(&mut l, "3x0P");
847        assert_eq!(l.text(), "abcdef");
848    }
849
850    #[test]
851    fn r_replaces_one_char() {
852        let mut l = line("cat<esc>0");
853        run(&mut l, "rb");
854        assert_eq!(l.text(), "bat");
855    }
856
857    #[test]
858    fn insert_entry_points_land_in_the_right_place() {
859        let mut l = line("bc<esc>0");
860        run(&mut l, "ia");
861        assert_eq!(l.text(), "abc");
862        let mut l = line("ab<esc>");
863        run(&mut l, "Ac");
864        assert_eq!(l.text(), "abc");
865        let mut l = line("ac<esc>0");
866        run(&mut l, "ab");
867        assert_eq!(l.text(), "abc", "a appends after the cursor");
868        let mut l = line("  bc<esc>");
869        run(&mut l, "Ia");
870        assert_eq!(l.text(), "  abc", "I goes to the first non-blank");
871    }
872
873    #[test]
874    fn u_undoes_the_last_change() {
875        let mut l = line("hello world<esc>0");
876        run(&mut l, "dw");
877        assert_eq!(l.text(), "world");
878        run(&mut l, "u");
879        assert_eq!(l.text(), "hello world");
880    }
881
882    #[test]
883    fn yank_then_paste_leaves_the_text_intact() {
884        let mut l = line("word<esc>0");
885        run(&mut l, "yw$p");
886        assert_eq!(l.text(), "wordword");
887    }
888
889    // ---- utf-8 -----------------------------------------------------------
890
891    /// chat is full of CJK and emoji; a byte-indexed editor would slice glyphs.
892    #[test]
893    fn motions_and_deletes_are_char_safe() {
894        let mut l = line("キツネス pog<esc>0");
895        run(&mut l, "x");
896        assert_eq!(l.text(), "ツネス pog");
897        run(&mut l, "dw");
898        assert_eq!(l.text(), "pog");
899    }
900
901    #[test]
902    fn emoji_are_single_positions() {
903        let mut l = line("a🔥b<esc>0");
904        run(&mut l, "l");
905        assert_eq!(l.cursor(), 1);
906        run(&mut l, "x");
907        assert_eq!(l.text(), "ab");
908    }
909
910    // ---- history ---------------------------------------------------------
911
912    #[test]
913    fn accept_records_history_and_clears_the_line() {
914        let mut l = line("first");
915        assert_eq!(l.accept(), "first");
916        assert_eq!(l.text(), "");
917        assert_eq!(l.mode(), Mode::Insert, "ready to type the next one");
918    }
919
920    #[test]
921    fn k_and_j_walk_sent_history() {
922        let mut l = Line::default();
923        run(&mut l, "one");
924        l.accept();
925        run(&mut l, "two");
926        l.accept();
927        run(&mut l, "draft<esc>");
928        run(&mut l, "k");
929        assert_eq!(l.text(), "two");
930        run(&mut l, "k");
931        assert_eq!(l.text(), "one");
932        run(&mut l, "j");
933        assert_eq!(l.text(), "two");
934        run(&mut l, "j");
935        assert_eq!(l.text(), "draft", "coming back restores the draft");
936    }
937
938    #[test]
939    fn history_ignores_blanks_and_immediate_repeats() {
940        let mut l = Line::default();
941        run(&mut l, "   ");
942        l.accept();
943        run(&mut l, "same");
944        l.accept();
945        run(&mut l, "same");
946        l.accept();
947        run(&mut l, "<esc>k");
948        assert_eq!(l.text(), "same");
949        run(&mut l, "k");
950        assert_eq!(l.text(), "same", "only one entry, nothing older");
951    }
952
953    // ---- guards ----------------------------------------------------------
954
955    #[test]
956    fn every_motion_survives_an_empty_line() {
957        let mut l = Line::default();
958        l.key(special(KeyCode::Esc));
959        for keys in [
960            "h", "l", "w", "b", "e", "$", "0", "^", "x", "X", "dw", "dd", "p", "u", "D",
961        ] {
962            run(&mut l, keys);
963            assert_eq!(l.text(), "");
964            assert_eq!(l.cursor(), 0, "{keys} moved the cursor off an empty line");
965        }
966    }
967
968    #[test]
969    fn counts_cannot_run_off_the_end() {
970        let mut l = line("ab<esc>0");
971        run(&mut l, "999l");
972        assert_eq!(l.cursor(), 1, "normal mode stops on the last char");
973        run(&mut l, "999x");
974        assert_eq!(l.text(), "a");
975    }
976}