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 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 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 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 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 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 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 #[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 #[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 #[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 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 let mut cursor = CursorView::try_from(("@\u{1a}\u{36b}", 2)).unwrap();
622 cursor.delete_before();
623 cursor.assert_invariants().unwrap();
624 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 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 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 cursor.move_right(1);
698 assert_eq!(cursor.byte_index(), 2);
699
700 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 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 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 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 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 assert!(cursor.move_up(GoalColumn::Column(7), 1));
756 assert_eq!(cursor.byte_index(), 7);
757
758 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 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 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 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 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}