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