Skip to main content

indigo_core/
cursor.rs

1use crate::{
2    rope::{LINE_TYPE, RopeExt as _},
3    text::{Anchor, Text},
4};
5use indigo_kernel::{
6    display_width::DisplayWidth as _,
7    edit::{self, Edit},
8};
9use indigo_wrap::{WBox, WMut, WRef, Wrap, WrapMut, WrapRef};
10use ropey::{Rope, RopeSlice};
11use std::{cmp::min, thread};
12use thiserror::Error;
13
14#[cfg(feature = "arbitrary")]
15use arbitrary::Arbitrary;
16
17#[derive(Debug, Error)]
18pub enum Error {
19    #[error("Byte index {byte_index} is not within text of length {length}")]
20    OutOfRange { byte_index: usize, length: usize },
21
22    #[error("Byte index {byte_index} is not the start of a grapheme")]
23    NotOnGrapheme { byte_index: usize },
24
25    #[error("Error from text")]
26    Text(#[source] anyhow::Error),
27}
28
29#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
30#[derive(Debug)]
31pub enum Action {
32    MoveTo { byte_index: usize },
33    MoveLeft { count: u8 },
34    MoveRight { count: u8 },
35    MoveUp { goal_column: GoalColumn, count: u8 },
36    MoveDown { goal_column: GoalColumn, count: u8 },
37    MoveToPrevByte { byte: u8, count: u8 },
38    MoveToNextByte { byte: u8, count: u8 },
39    MoveToPrevBlank { count: u8 },
40    MoveToNextBlank { count: u8 },
41    MoveToStart,
42    MoveToEnd,
43    MoveToBottom,
44    MoveToLineStart,
45    MoveToLineNonBlankStart,
46    MoveToLineEnd,
47    MoveUntilLineEnd,
48    InsertChar { char: char },
49    Insert { text: String },
50    DeleteBefore,
51    DeleteAfter,
52}
53
54#[derive(Clone, Copy)]
55pub enum Direction {
56    Backward,
57    Forward,
58}
59
60#[cfg_attr(feature = "arbitrary", derive(Arbitrary))]
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub enum GoalColumn {
63    Column(usize),
64    UntilLineEnd,
65    OntoLineEnd,
66}
67
68impl From<usize> for GoalColumn {
69    fn from(n: usize) -> Self {
70        if n == usize::MAX {
71            Self::OntoLineEnd
72        } else if n == usize::MAX - 1 {
73            Self::UntilLineEnd
74        } else {
75            Self::Column(n)
76        }
77    }
78}
79
80impl From<GoalColumn> for usize {
81    fn from(goal_column: GoalColumn) -> Self {
82        match goal_column {
83            GoalColumn::Column(n) => {
84                assert!(
85                    n < Self::MAX - 1,
86                    "ambiguous conversion from GoalColumn to usize"
87                );
88                n
89            }
90            GoalColumn::UntilLineEnd => Self::MAX - 1,
91            GoalColumn::OntoLineEnd => Self::MAX,
92        }
93    }
94}
95
96#[derive(Clone, Debug, Default)]
97pub struct CursorState {
98    pub byte_index: usize,
99}
100
101impl CursorState {
102    pub fn transform(&mut self, ops: &Edit, text: &Rope) {
103        let byte_index = ops.map_position(self.byte_index, edit::Bias::Forward);
104        self.byte_index = text
105            .snap_to_grapheme_start(byte_index)
106            .expect("Text is never empty");
107    }
108
109    #[must_use]
110    pub fn save(&self, text: &Text) -> CursorSnapshot {
111        let byte_index = text.create_anchor(self.byte_index);
112        CursorSnapshot { byte_index }
113    }
114}
115
116pub struct CursorSnapshot {
117    pub byte_index: Anchor,
118}
119
120impl CursorSnapshot {
121    #[must_use]
122    pub fn restore(&self, text: &Text) -> Option<CursorState> {
123        let byte_index = text
124            .snap_to_grapheme_start(text.resolve_anchor(&self.byte_index)?)
125            .expect("Text is never empty");
126        Some(CursorState { byte_index })
127    }
128}
129
130#[must_use]
131pub struct CursorView<'a, S: Wrap, T: Wrap> {
132    state: S::Wrap<'a, CursorState>,
133    text: T::Wrap<'a, Text>,
134    #[expect(clippy::type_complexity)]
135    on_drop: Option<Box<dyn FnOnce(&mut Self) + 'a>>,
136}
137
138pub type Cursor<'a> = CursorView<'a, WRef, WRef>;
139
140pub type CursorMove<'a> = CursorView<'a, WMut, WRef>;
141
142pub type CursorEdit<'a> = CursorView<'a, WMut, WMut>;
143
144impl<'a, S: Wrap, T: Wrap> CursorView<'a, S, T> {
145    pub fn on_drop(mut self, f: impl FnOnce(&mut Self) + 'a) -> Self {
146        self.on_drop = Some(Box::new(f));
147        self
148    }
149}
150
151impl<'a, S: WrapRef, T: WrapRef> CursorView<'a, S, T> {
152    pub fn new(
153        text: T::WrapRef<'a, Text>,
154        state: S::WrapRef<'a, CursorState>,
155    ) -> anyhow::Result<Self> {
156        let cursor_view = Self {
157            state,
158            text,
159            on_drop: None,
160        };
161        cursor_view.assert_invariants()?;
162        Ok(cursor_view)
163    }
164
165    pub fn text(&self) -> &Text {
166        &self.text
167    }
168
169    pub fn state(&self) -> &CursorState {
170        &self.state
171    }
172
173    #[must_use]
174    pub fn byte_index(&self) -> usize {
175        self.state.byte_index
176    }
177
178    #[must_use]
179    pub fn line_index(&self) -> usize {
180        self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE)
181    }
182
183    #[must_use]
184    pub fn line_number(&self) -> usize {
185        self.line_index() + 1
186    }
187
188    #[must_use]
189    pub fn column_number(&self) -> usize {
190        let line_byte_index = self.text.line_to_byte_idx(self.line_index(), LINE_TYPE);
191        (self.state.byte_index - line_byte_index) + 1
192    }
193
194    #[must_use]
195    pub fn display_column(&self) -> usize {
196        self.text.display_column(self.state.byte_index)
197    }
198
199    #[must_use]
200    pub fn grapheme(&self) -> RopeSlice<'_> {
201        self.text.grapheme(self.state.byte_index)
202    }
203
204    #[must_use]
205    pub fn line(&self) -> RopeSlice<'_> {
206        self.text
207            .get_line(self.line_index(), LINE_TYPE)
208            .expect("Cursor's line always exists")
209    }
210
211    #[must_use]
212    pub fn is_at_start(&self) -> bool {
213        self.state.byte_index == 0
214    }
215
216    #[must_use]
217    pub fn is_at_end(&self) -> bool {
218        let end_offset = self
219            .text
220            .next_grapheme_boundary(self.state.byte_index)
221            .expect("Cursor is always on a grapheme");
222        end_offset == self.text.len()
223    }
224
225    #[must_use]
226    pub fn save(&self) -> CursorSnapshot {
227        self.state.save(&self.text)
228    }
229
230    pub fn assert_invariants(&self) -> anyhow::Result<()> {
231        self.text.assert_invariants().map_err(Error::Text)?;
232        if self.state.byte_index >= self.text.len() {
233            anyhow::bail!(Error::OutOfRange {
234                byte_index: self.state.byte_index,
235                length: self.text.len(),
236            });
237        }
238        if !self.text.is_grapheme_start(self.state.byte_index) {
239            anyhow::bail!(Error::NotOnGrapheme {
240                byte_index: self.state.byte_index,
241            });
242        }
243        Ok(())
244    }
245}
246
247impl<S: WrapMut, T: WrapRef> CursorView<'_, S, T> {
248    pub fn move_to(&mut self, byte_index: usize) {
249        let byte_index = self
250            .text
251            .snap_to_grapheme_start(byte_index)
252            .expect("Text is never empty");
253        self.state.byte_index = byte_index;
254    }
255
256    pub fn move_left(&mut self, count: usize) -> bool {
257        if count == 0 {
258            return false;
259        }
260        for _ in 0..count {
261            if let Some(prev) = self.text.prev_grapheme_boundary(self.state.byte_index) {
262                self.state.byte_index = prev;
263            } else {
264                return false;
265            }
266        }
267        true
268    }
269
270    pub fn move_right(&mut self, count: usize) -> bool {
271        if count == 0 {
272            return false;
273        }
274        for _ in 0..count {
275            match self.text.next_grapheme_boundary(self.state.byte_index) {
276                Some(next) if next < self.text.len() => self.state.byte_index = next,
277                _ => return false,
278            }
279        }
280        true
281    }
282
283    pub fn move_up(&mut self, goal_column: GoalColumn, count: usize) -> bool {
284        self.move_vertical(Direction::Backward, goal_column, count)
285    }
286
287    pub fn move_down(&mut self, goal_column: GoalColumn, count: usize) -> bool {
288        self.move_vertical(Direction::Forward, goal_column, count)
289    }
290
291    fn move_vertical(
292        &mut self,
293        direction: Direction,
294        goal_column: GoalColumn,
295        count: usize,
296    ) -> bool {
297        if count == 0 {
298            return false;
299        }
300        let old_byte_index = self.state.byte_index;
301        let current_line_index = self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE);
302        // Like Kakoune, clamp to the first or last line, and still apply the goal column. A cursor
303        // on the newline of the first or last line then moves onto that line's last grapheme.
304        let target_line_index = match direction {
305            Direction::Backward => current_line_index.saturating_sub(count),
306            Direction::Forward => min(
307                current_line_index.saturating_add(count),
308                self.text.len_lines_unix() - 1,
309            ),
310        };
311        let target_line_byte_index = self.text.line_to_byte_idx(target_line_index, LINE_TYPE);
312        let target_line_slice = self.text.line(target_line_index, LINE_TYPE);
313        let mut target_line_prefix = 0;
314        let mut target_byte_index = target_line_byte_index;
315        let mut reached_line_end = true;
316        for grapheme in target_line_slice.graphemes() {
317            if grapheme.chars().any(|c| c == '\n' || c == '\r') {
318                break;
319            }
320            let grapheme_width = grapheme.display_width();
321            if target_line_prefix + grapheme_width > usize::from(goal_column) {
322                reached_line_end = false;
323                break;
324            }
325            target_line_prefix += grapheme_width;
326            target_byte_index += grapheme.len();
327        }
328        // Only an empty line or `GoalColumn::OntoLineEnd` puts the cursor on the newline;
329        // otherwise stop on the last grapheme before it, like Kakoune.
330        if reached_line_end
331            && goal_column != GoalColumn::OntoLineEnd
332            && target_byte_index > target_line_byte_index
333        {
334            target_byte_index = self
335                .text
336                .prev_grapheme_boundary(target_byte_index)
337                .expect("Line is not empty, so a grapheme precedes its end");
338        }
339        self.state.byte_index = target_byte_index;
340        self.state.byte_index != old_byte_index
341    }
342
343    pub fn move_to_prev_byte(&mut self, byte: u8, count: usize) -> bool {
344        self.seek_prev(&[byte], count)
345    }
346
347    pub fn move_to_next_byte(&mut self, byte: u8, count: usize) -> bool {
348        self.seek_next(&[byte], count)
349    }
350
351    pub fn move_to_prev_blank(&mut self, count: usize) -> bool {
352        const BYTES: &[u8] = b" \t\n\r";
353        self.seek_prev(BYTES, count)
354    }
355
356    pub fn move_to_next_blank(&mut self, count: usize) -> bool {
357        const BYTES: &[u8] = b" \t\n\r";
358        self.seek_next(BYTES, count)
359    }
360
361    /// Land on the grapheme containing the `count`th previous matching byte. The cursor stays put
362    /// unless every step succeeds.
363    fn seek_prev(&mut self, bytes: &[u8], count: usize) -> bool {
364        if count == 0 {
365            return false;
366        }
367        let mut byte_index = self.state.byte_index;
368        for _ in 0..count {
369            let Some(found) = self.text.find_prev_byte(..byte_index, bytes) else {
370                return false;
371            };
372            byte_index = self.text.floor_grapheme_boundary(found);
373        }
374        self.state.byte_index = byte_index;
375        true
376    }
377
378    /// Land on the grapheme containing the `count`th next matching byte, searching strictly after
379    /// the grapheme the cursor occupies. The cursor stays put unless every step succeeds.
380    fn seek_next(&mut self, bytes: &[u8], count: usize) -> bool {
381        if count == 0 {
382            return false;
383        }
384        let mut byte_index = self.state.byte_index;
385        for _ in 0..count {
386            let start = self
387                .text
388                .next_grapheme_boundary(byte_index)
389                .expect("Cursor is always on a grapheme");
390            let Some(found) = self.text.find_next_byte(start.., bytes) else {
391                return false;
392            };
393            byte_index = self.text.floor_grapheme_boundary(found);
394        }
395        self.state.byte_index = byte_index;
396        true
397    }
398
399    pub fn move_to_start(&mut self) {
400        self.state.byte_index = 0;
401    }
402
403    pub fn move_to_end(&mut self) {
404        self.state.byte_index = self
405            .text
406            .last_grapheme_start()
407            .expect("Text is never empty");
408    }
409
410    pub fn move_to_bottom(&mut self) {
411        self.move_to_end();
412        self.move_to_line_start();
413    }
414
415    pub fn move_to_line_start(&mut self) {
416        let line_index = self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE);
417        self.state.byte_index = self.text.line_to_byte_idx(line_index, LINE_TYPE);
418    }
419
420    pub fn move_to_line_non_blank_start(&mut self) {
421        let line_index = self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE);
422        let line_start_byte_index = self.text.line_to_byte_idx(line_index, LINE_TYPE);
423        let line_slice = self.text.line(line_index, LINE_TYPE);
424        let mut byte_index = line_start_byte_index;
425        for grapheme in line_slice.graphemes() {
426            if grapheme.chars().any(|c| c == '\n' || c == '\r') {
427                break;
428            }
429            if !grapheme.chars().all(char::is_whitespace) {
430                break;
431            }
432            byte_index += grapheme.len();
433        }
434        self.state.byte_index = byte_index;
435    }
436
437    /// Move onto the line's end: its `'\n'` (or `"\r\n"`) grapheme. Always succeeds because
438    /// every line is terminated.
439    pub fn move_to_line_end(&mut self) {
440        let line_index = self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE);
441        let line_start_byte_index = self.text.line_to_byte_idx(line_index, LINE_TYPE);
442        let line_slice = self.text.line(line_index, LINE_TYPE);
443        let mut byte_index = line_start_byte_index;
444        for grapheme in line_slice.graphemes() {
445            if grapheme.chars().any(|c| c == '\n' || c == '\r') {
446                break;
447            }
448            byte_index += grapheme.len();
449        }
450        self.state.byte_index = byte_index;
451    }
452
453    /// Move onto the last grapheme before the line's end. Stays on the line end grapheme when
454    /// the line is empty.
455    pub fn move_until_line_end(&mut self) {
456        self.move_to_line_end();
457        let line_index = self.text.byte_to_line_idx(self.state.byte_index, LINE_TYPE);
458        let line_start_byte_index = self.text.line_to_byte_idx(line_index, LINE_TYPE);
459        if self.state.byte_index > line_start_byte_index {
460            self.state.byte_index = self
461                .text
462                .prev_grapheme_boundary(self.state.byte_index)
463                .expect("Not at start of text");
464        }
465    }
466
467    pub fn restore(&mut self, snapshot: &CursorSnapshot) -> bool {
468        if let Some(state) = snapshot.restore(&self.text) {
469            *self.state = state;
470            true
471        } else {
472            false
473        }
474    }
475}
476
477impl<S: WrapMut, T: WrapMut> CursorView<'_, S, T> {
478    pub fn insert_char(&mut self, char: char) -> Edit {
479        self.insert(&char.to_string())
480    }
481
482    /// Insert before the grapheme the cursor occupies. The cursor stays on its grapheme (i.e.
483    /// ends up after the inserted text).
484    #[tracing::instrument(skip_all)]
485    pub fn insert(&mut self, text: &str) -> Edit {
486        let pre_text_len = self.text.len();
487        let mut ops = Edit::new();
488        ops.retain(self.state.byte_index);
489        ops.insert(text);
490        ops.retain_rest(&self.text)
491            .expect("Operations fit within text");
492        self.text.apply(&ops).expect("Operations are well formed");
493        self.state.transform(&ops, &self.text);
494        debug_assert_eq!(self.text.len(), pre_text_len + text.len());
495        ops
496    }
497
498    /// Delete the grapheme before the cursor. Behavior traditionally associated with the
499    /// Backspace key.
500    #[tracing::instrument(skip_all)]
501    pub fn delete_before(&mut self) -> Option<Edit> {
502        let start = self.text.prev_grapheme_boundary(self.state.byte_index)?;
503        let mut ops = Edit::new();
504        ops.retain(start);
505        ops.delete(&self.text.slice(start..self.state.byte_index).to_string());
506        ops.retain_rest(&self.text)
507            .expect("Operations fit within text");
508        self.text.apply(&ops).expect("Operations are well formed");
509        self.state.transform(&ops, &self.text);
510        Some(ops)
511    }
512
513    /// Delete the grapheme the cursor occupies, unless it is the text's final newline. Behavior
514    /// traditionally associated with the Delete key.
515    #[tracing::instrument(skip_all)]
516    pub fn delete_after(&mut self) -> Option<Edit> {
517        let end = self
518            .text
519            .next_grapheme_boundary(self.state.byte_index)
520            .expect("Cursor is always on a grapheme");
521        if end == self.text.len() {
522            // Deleting the final newline would break the `Text` invariant.
523            return None;
524        }
525        let mut ops = Edit::new();
526        ops.retain(self.state.byte_index);
527        ops.delete(&self.text.slice(self.state.byte_index..end).to_string());
528        ops.retain_rest(&self.text)
529            .expect("Operations fit within text");
530        self.text.apply(&ops).expect("Operations are well formed");
531        self.state.transform(&ops, &self.text);
532        Some(ops)
533    }
534}
535
536pub fn handle_action<S: WrapMut, T: WrapMut>(cursor: &mut CursorView<'_, S, T>, action: &Action) {
537    match action {
538        Action::MoveTo { byte_index } => {
539            cursor.move_to(*byte_index);
540        }
541        Action::MoveLeft { count } => {
542            cursor.move_left(usize::from(*count));
543        }
544        Action::MoveRight { count } => {
545            cursor.move_right(usize::from(*count));
546        }
547        Action::MoveUp { goal_column, count } => {
548            cursor.move_up(*goal_column, usize::from(*count));
549        }
550        Action::MoveDown { goal_column, count } => {
551            cursor.move_down(*goal_column, usize::from(*count));
552        }
553        Action::MoveToPrevByte { byte, count } => {
554            cursor.move_to_prev_byte(*byte, usize::from(*count));
555        }
556        Action::MoveToNextByte { byte, count } => {
557            cursor.move_to_next_byte(*byte, usize::from(*count));
558        }
559        Action::MoveToPrevBlank { count } => {
560            cursor.move_to_prev_blank(usize::from(*count));
561        }
562        Action::MoveToNextBlank { count } => {
563            cursor.move_to_next_blank(usize::from(*count));
564        }
565        Action::MoveToStart => cursor.move_to_start(),
566        Action::MoveToEnd => cursor.move_to_end(),
567        Action::MoveToBottom => cursor.move_to_bottom(),
568        Action::MoveToLineStart => cursor.move_to_line_start(),
569        Action::MoveToLineNonBlankStart => cursor.move_to_line_non_blank_start(),
570        Action::MoveToLineEnd => cursor.move_to_line_end(),
571        Action::MoveUntilLineEnd => cursor.move_until_line_end(),
572        Action::InsertChar { char } => {
573            cursor.insert_char(*char);
574        }
575        Action::Insert { text } => {
576            cursor.insert(text);
577        }
578        Action::DeleteBefore => {
579            cursor.delete_before();
580        }
581        Action::DeleteAfter => {
582            cursor.delete_after();
583        }
584    }
585}
586
587impl<R> TryFrom<(R, usize)> for CursorView<'_, WBox, WBox>
588where
589    R: Into<Text>,
590{
591    type Error = anyhow::Error;
592    fn try_from((text, byte_index): (R, usize)) -> anyhow::Result<Self> {
593        let text = Box::new(text.into());
594        let state = Box::new(CursorState { byte_index });
595        Self::new(text, state)
596    }
597}
598
599impl<S: Wrap, T: Wrap> Drop for CursorView<'_, S, T> {
600    fn drop(&mut self) {
601        if !thread::panicking()
602            && let Some(f) = self.on_drop.take()
603        {
604            f(self);
605        }
606    }
607}
608
609#[cfg(test)]
610mod tests {
611    use super::*;
612    use hegel::{TestCase, generators as gs};
613    use ropey::Rope;
614
615    #[test]
616    fn delete_before_can_merge_graphemes_past_cursor() {
617        // Cursor sits on a combining mark (U+036B) preceded by a Control (SUB, U+001A).
618        // Deleting the Control causes '@' and the combining mark to merge into a single
619        // grapheme cluster, so the cursor's post-transform position is no longer a grapheme
620        // start, and the snap floors it onto the merged grapheme.
621        let mut cursor = CursorView::try_from(("@\u{1a}\u{36b}", 2)).unwrap();
622        cursor.delete_before();
623        cursor.assert_invariants().unwrap();
624        // '@' merged with the combining mark into "@\u{36b}" (one grapheme starting at 0).
625        assert_eq!(cursor.byte_index(), 0);
626        assert_eq!(**cursor.text, "@\u{36b}\n");
627    }
628
629    #[test]
630    fn insert_changes_grapheme_boundary() {
631        // combining acute accent (´)
632        let mut cursor = CursorView::try_from(("\u{0301}", 0)).unwrap();
633        cursor.insert("e");
634        cursor.assert_invariants().unwrap();
635    }
636
637    #[test]
638    fn move_to_next_byte_repeated_after_multibyte_match() {
639        let mut cursor = CursorView::try_from(("\nۉ", 3)).unwrap();
640        cursor.move_to_line_non_blank_start();
641        cursor.move_to_next_byte(219, 2);
642        cursor.assert_invariants().unwrap();
643    }
644
645    #[test]
646    fn move_to_next_blank_repeated_after_crlf_match() {
647        // `Text::from("\r")` normalizes to "\r\n", a single CRLF grapheme.
648        let mut cursor = CursorView::try_from(("\r", 0)).unwrap();
649        cursor.insert("\n");
650        cursor.move_left(27);
651        cursor.move_to_next_blank(10);
652        cursor.delete_after();
653        cursor.assert_invariants().unwrap();
654    }
655
656    #[test]
657    fn restore_snaps_to_grapheme_start_after_insert() {
658        let mut cursor = CursorView::try_from(("\u{0301}", 0)).unwrap();
659        let snapshot = cursor.save();
660        cursor.insert("e");
661        assert!(cursor.restore(&snapshot));
662        cursor.assert_invariants().unwrap();
663    }
664
665    #[test]
666    fn delete_after_preserves_final_newline() {
667        let mut cursor = CursorView::try_from(("x", 1)).unwrap();
668        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
669        assert_eq!(cursor.delete_after(), None);
670        assert_eq!(**cursor.text, "x\n");
671    }
672
673    #[test]
674    fn expected_behavior_left_right() {
675        let mut cursor = CursorView::try_from(("", 0)).unwrap();
676        cursor.assert_invariants().unwrap();
677
678        cursor.insert("hello");
679        assert_eq!(**cursor.text, "hello\n");
680        assert_eq!(cursor.byte_index(), 5);
681
682        cursor.delete_before();
683        cursor.delete_before();
684        assert_eq!(**cursor.text, "hel\n");
685        assert_eq!(cursor.byte_index(), 3);
686
687        cursor.move_left(2);
688        assert_eq!(cursor.byte_index(), 1);
689
690        cursor.delete_after();
691        assert_eq!(cursor.byte_index(), 1);
692        assert_eq!(**cursor.text, "hl\n");
693
694        cursor.move_right(1);
695        assert_eq!(cursor.byte_index(), 2);
696        // The trailing newline is the last grapheme; can't move past it.
697        cursor.move_right(1);
698        assert_eq!(cursor.byte_index(), 2);
699
700        // Deleting the grapheme under the cursor would delete the final newline; refused.
701        cursor.delete_after();
702        assert_eq!(**cursor.text, "hl\n");
703        assert_eq!(cursor.byte_index(), 2);
704
705        cursor.delete_before();
706        cursor.delete_before();
707        assert_eq!(**cursor.text, "\n");
708        assert_eq!(cursor.byte_index(), 0);
709
710        cursor.delete_before();
711        assert_eq!(**cursor.text, "\n");
712        assert_eq!(cursor.byte_index(), 0);
713    }
714
715    #[test]
716    fn expected_behavior_up_down() {
717        let mut cursor = CursorView::try_from(("", 0)).unwrap();
718        cursor.assert_invariants().unwrap();
719
720        let text = "0\n234\n6789AB\n";
721        cursor.insert(text);
722        // On the invariant trailing newline (the empty final line the insert pushed down).
723        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
724        assert_eq!(cursor.line().to_string(), "\n");
725        assert_eq!(cursor.byte_index(), 13);
726        assert_eq!(cursor.display_column(), 0);
727
728        cursor.move_left(1);
729        // On the newline terminating line 2.
730        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
731        assert_eq!(cursor.byte_index(), 12);
732        assert_eq!(cursor.display_column(), 6);
733
734        cursor.move_up(GoalColumn::Column(cursor.display_column()), 1);
735        // On "4", the last grapheme of line 1, which is shorter than the goal column.
736        assert_eq!(cursor.grapheme(), Rope::from("4").slice(..));
737        assert_eq!(cursor.line().to_string(), "234\n");
738        assert_eq!(cursor.byte_index(), 4);
739        assert_eq!(cursor.display_column(), 2);
740
741        cursor.move_left(1);
742        // On "3" in line 1.
743        assert_eq!(cursor.grapheme(), Rope::from("3").slice(..));
744        assert_eq!(cursor.byte_index(), 3);
745        assert_eq!(cursor.display_column(), 1);
746    }
747
748    #[test]
749    fn move_vertical_stops_before_newline() {
750        let mut cursor = CursorView::try_from(("a long line\nab\ncd\n", 7)).unwrap();
751        assert!(cursor.move_down(GoalColumn::Column(7), 1));
752        assert_eq!(cursor.grapheme(), Rope::from("b").slice(..));
753        assert_eq!(cursor.byte_index(), 13);
754        // The goal column is sticky, so moving back up returns to the original grapheme.
755        assert!(cursor.move_up(GoalColumn::Column(7), 1));
756        assert_eq!(cursor.byte_index(), 7);
757
758        // An empty line has nothing but its newline to land on.
759        let mut cursor = CursorView::try_from(("abc\n\nx\n", 2)).unwrap();
760        assert!(cursor.move_down(GoalColumn::Column(2), 1));
761        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
762        assert_eq!(cursor.byte_index(), 4);
763
764        // Landing on a CRLF terminator steps back onto the last grapheme too.
765        let mut cursor = CursorView::try_from(("abc\r\nab\r\n", 2)).unwrap();
766        assert!(cursor.move_down(GoalColumn::Column(2), 1));
767        assert_eq!(cursor.grapheme(), Rope::from("b").slice(..));
768        assert_eq!(cursor.byte_index(), 6);
769    }
770
771    #[test]
772    fn move_vertical_line_end_goal_columns() {
773        let mut cursor = CursorView::try_from(("ab\nabcdef\n", 1)).unwrap();
774        assert!(cursor.move_down(GoalColumn::UntilLineEnd, 1));
775        assert_eq!(cursor.grapheme(), Rope::from("f").slice(..));
776        assert_eq!(cursor.byte_index(), 8);
777
778        let mut cursor = CursorView::try_from(("ab\nabcdef\n", 2)).unwrap();
779        assert!(cursor.move_down(GoalColumn::OntoLineEnd, 1));
780        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
781        assert_eq!(cursor.byte_index(), 9);
782    }
783
784    #[test]
785    fn move_to_line_end() {
786        let mut cursor = CursorView::try_from(("x\ny\n", 2)).unwrap();
787        assert_eq!(cursor.grapheme(), Rope::from("y").slice(..));
788
789        cursor.move_to_line_end();
790        assert_eq!(cursor.byte_index(), 3);
791        assert_eq!(cursor.grapheme(), Rope::from("\n").slice(..));
792
793        // Idempotent: already on the line end.
794        cursor.move_to_line_end();
795        assert_eq!(cursor.byte_index(), 3);
796
797        cursor.move_until_line_end();
798        assert_eq!(cursor.byte_index(), 2);
799        assert_eq!(cursor.grapheme(), Rope::from("y").slice(..));
800
801        // On an empty line, `move_until_line_end` stays on the line end.
802        let mut cursor = CursorView::try_from(("\nx\n", 0)).unwrap();
803        cursor.move_until_line_end();
804        assert_eq!(cursor.byte_index(), 0);
805    }
806
807    #[test]
808    fn move_to_next_byte_count_skips_previous_matches() {
809        let mut cursor = CursorView::try_from(("hello world\n", 0)).unwrap();
810
811        assert!(cursor.move_to_next_byte(b'l', 1));
812        assert_eq!(cursor.byte_index(), 2);
813
814        let mut cursor = CursorView::try_from(("hello world\n", 0)).unwrap();
815        assert!(cursor.move_to_next_byte(b'l', 2));
816        assert_eq!(cursor.byte_index(), 3);
817
818        let mut cursor = CursorView::try_from(("hello world\n", 0)).unwrap();
819        assert!(cursor.move_to_next_byte(b'l', 3));
820        assert_eq!(cursor.byte_index(), 9);
821
822        let mut cursor = CursorView::try_from(("hello world\n", 0)).unwrap();
823        assert!(!cursor.move_to_next_byte(b'l', 4));
824        assert_eq!(cursor.byte_index(), 0);
825
826        let mut cursor = CursorView::try_from(("hello world\n", 11)).unwrap();
827        assert!(!cursor.move_to_prev_byte(b'l', 4));
828        assert_eq!(cursor.byte_index(), 11);
829    }
830
831    #[test]
832    fn move_to_next_byte_skips_match_under_cursor() {
833        // Like Kakoune's `f`, the search starts after the grapheme the cursor occupies.
834        let mut cursor = CursorView::try_from(("ll\n", 0)).unwrap();
835        assert!(cursor.move_to_next_byte(b'l', 1));
836        assert_eq!(cursor.byte_index(), 1);
837    }
838
839    #[test]
840    fn move_to_next_blank_count_skips_previous_matches() {
841        let mut cursor = CursorView::try_from(("a b\tc\n", 0)).unwrap();
842
843        assert!(cursor.move_to_next_blank(1));
844        assert_eq!(cursor.byte_index(), 1);
845
846        let mut cursor = CursorView::try_from(("a b\tc\n", 0)).unwrap();
847        assert!(cursor.move_to_next_blank(2));
848        assert_eq!(cursor.byte_index(), 3);
849
850        let mut cursor = CursorView::try_from(("a b\tc\n", 0)).unwrap();
851        assert!(cursor.move_to_next_blank(3));
852        assert_eq!(cursor.byte_index(), 5);
853
854        let mut cursor = CursorView::try_from(("a b\tc\n", 0)).unwrap();
855        assert!(!cursor.move_to_next_blank(4));
856        assert_eq!(cursor.byte_index(), 0);
857
858        let mut cursor = CursorView::try_from(("a b\tc\n", 4)).unwrap();
859        assert!(!cursor.move_to_prev_blank(3));
860        assert_eq!(cursor.byte_index(), 4);
861    }
862
863    #[hegel::test(test_cases = 1_000)]
864    fn fuzz(tc: TestCase) {
865        #[derive(Default)]
866        struct StateMachine {
867            text: Text,
868            state: CursorState,
869        }
870        #[hegel::state_machine]
871        #[expect(clippy::needless_pass_by_value)]
872        impl StateMachine {
873            fn cursor(&mut self) -> CursorEdit<'_> {
874                CursorEdit::new(&mut self.text, &mut self.state).expect("Cursor state kept valid")
875            }
876            #[rule]
877            fn move_to(&mut self, tc: TestCase) {
878                let byte_index = tc.draw(gs::integers::<usize>().max_value(self.text.len()));
879                self.cursor().move_to(byte_index);
880            }
881            #[rule]
882            fn move_left(&mut self, tc: TestCase) {
883                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
884                self.cursor().move_left(count);
885            }
886            #[rule]
887            fn move_right(&mut self, tc: TestCase) {
888                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
889                self.cursor().move_right(count);
890            }
891            fn goal_column(cursor: &CursorEdit<'_>, tc: TestCase) -> GoalColumn {
892                match tc.draw(gs::integers::<u8>().max_value(2)) {
893                    0 => GoalColumn::Column(cursor.display_column()),
894                    1 => GoalColumn::UntilLineEnd,
895                    2 => GoalColumn::OntoLineEnd,
896                    _ => unreachable!(),
897                }
898            }
899            #[rule]
900            fn move_up(&mut self, tc: TestCase) {
901                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
902                let mut cursor = self.cursor();
903                let goal_column = Self::goal_column(&cursor, tc);
904                cursor.move_up(goal_column, count);
905            }
906            #[rule]
907            fn move_down(&mut self, tc: TestCase) {
908                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
909                let mut cursor = self.cursor();
910                let goal_column = Self::goal_column(&cursor, tc);
911                cursor.move_down(goal_column, count);
912            }
913            #[rule]
914            fn move_to_prev_byte(&mut self, tc: TestCase) {
915                let byte = tc.draw(gs::integers::<u8>());
916                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
917                self.cursor().move_to_prev_byte(byte, count);
918            }
919            #[rule]
920            fn move_to_next_byte(&mut self, tc: TestCase) {
921                let byte = tc.draw(gs::integers::<u8>());
922                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
923                self.cursor().move_to_next_byte(byte, count);
924            }
925            #[rule]
926            fn move_to_prev_blank(&mut self, tc: TestCase) {
927                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
928                self.cursor().move_to_prev_blank(count);
929            }
930            #[rule]
931            fn move_to_next_blank(&mut self, tc: TestCase) {
932                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
933                self.cursor().move_to_next_blank(count);
934            }
935            #[rule]
936            fn move_to_start(&mut self, _: TestCase) {
937                self.cursor().move_to_start();
938            }
939            #[rule]
940            fn move_to_end(&mut self, _: TestCase) {
941                self.cursor().move_to_end();
942            }
943            #[rule]
944            fn move_to_bottom(&mut self, _: TestCase) {
945                self.cursor().move_to_bottom();
946            }
947            #[rule]
948            fn move_to_line_start(&mut self, _: TestCase) {
949                self.cursor().move_to_line_start();
950            }
951            #[rule]
952            fn move_to_line_non_blank_start(&mut self, _: TestCase) {
953                self.cursor().move_to_line_non_blank_start();
954            }
955            #[rule]
956            fn move_to_line_end(&mut self, _: TestCase) {
957                self.cursor().move_to_line_end();
958            }
959            #[rule]
960            fn move_until_line_end(&mut self, _: TestCase) {
961                self.cursor().move_until_line_end();
962            }
963            #[rule]
964            fn insert_char(&mut self, tc: TestCase) {
965                let text = tc.draw(gs::text().min_size(1));
966                let c = text.chars().next().expect("min_size(1) yields a char");
967                self.cursor().insert_char(c);
968            }
969            #[rule]
970            fn insert(&mut self, tc: TestCase) {
971                let text = tc.draw(gs::text());
972                self.cursor().insert(&text);
973            }
974            #[rule]
975            fn delete_before(&mut self, tc: TestCase) {
976                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
977                let mut cursor = self.cursor();
978                for _ in 0..count {
979                    cursor.delete_before();
980                }
981            }
982            #[rule]
983            fn delete_after(&mut self, tc: TestCase) {
984                let count = tc.draw(gs::integers::<usize>().min_value(1).max_value(100));
985                let mut cursor = self.cursor();
986                for _ in 0..count {
987                    cursor.delete_after();
988                }
989            }
990            #[invariant]
991            fn invariants(&self, _: TestCase) {
992                let _ = Cursor::new(&self.text, &self.state)
993                    .expect("Cursor remains valid after every operation");
994                let rope = self.text.rope();
995                assert!(
996                    rope.len() > 0 && rope.byte(rope.len() - 1) == b'\n',
997                    "Text keeps its trailing newline"
998                );
999            }
1000        }
1001        hegel::stateful::machine(StateMachine::default()).run(tc);
1002    }
1003}