1use crate::ropetext::motion::{self, Goal, Words};
30use crate::ropetext::{Change, Column, EditBuffer as Rope, Position, Span, Text};
31
32const DEFAULT_INDENT_WIDTH: std::num::NonZeroU8 = std::num::NonZeroU8::new(4).unwrap();
43
44#[must_use = "an edit's outcome drives the revision bump and the parse-damage signal"]
51#[derive(Debug, Clone, PartialEq, Eq, Default)]
52pub struct EditOutcome {
53 pub changed: bool,
56 pub bulk: bool,
59 pub damage: Option<std::ops::Range<usize>>,
67 pub line_delta: isize,
73}
74
75pub(super) fn shift_rows(
86 range: std::ops::Range<usize>,
87 at: usize,
88 delta: isize,
89) -> std::ops::Range<usize> {
90 let shift = |row: usize| {
91 if row < at {
92 row
93 } else {
94 row.saturating_add_signed(delta)
95 }
96 };
97 shift(range.start)..shift(range.end)
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102enum Yank {
103 Keep,
104 Discard,
105}
106
107#[derive(Debug, Clone, Copy)]
110enum RowEdit {
111 Insert(Position),
112 Delete(Span),
113}
114
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
122pub enum CursorMove {
123 Forward,
124 Back,
125 Up,
126 Down,
127 Head,
128 End,
129 Top,
130 Bottom,
131 WordForward,
132 WordBack,
133 WordEnd,
134 WordForwardBig,
136 WordBackBig,
138 WordEndBig,
140 WordEndBack {
142 big: bool,
143 },
144 MatchingPair,
147 ParagraphForward,
148 ParagraphBack,
149 Jump(usize, usize),
150}
151
152impl CursorMove {
153 fn is_vertical(self) -> bool {
160 matches!(self, CursorMove::Up | CursorMove::Down)
161 }
162}
163
164#[derive(Debug)]
166pub struct RopeBuffer {
167 inner: Rope,
168 pending: EditOutcome,
171 depth: u32,
174 group_started: bool,
177 continue_group: bool,
181 goal: Option<Column>,
184 yank: String,
185 search: Option<regex::Regex>,
186 indent_width: std::num::NonZeroU8,
187}
188
189impl Default for RopeBuffer {
190 fn default() -> Self {
191 Self::new(Text::new())
192 }
193}
194
195impl RopeBuffer {
196 pub fn new(text: Text) -> Self {
197 Self {
198 inner: Rope::new(text),
199 pending: EditOutcome::default(),
200 depth: 0,
201 group_started: false,
202 continue_group: false,
203 goal: None,
204 yank: String::new(),
205 search: None,
206 indent_width: DEFAULT_INDENT_WIDTH,
207 }
208 }
209
210 pub fn replace(&mut self, text: Text) {
212 self.inner.set_text(text);
213 self.pending = EditOutcome::default();
214 self.goal = None;
215 }
216
217 pub fn text(&self) -> &Text {
218 self.inner.text()
219 }
220
221 #[cfg(test)]
224 pub fn set_indent_width(&mut self, spaces: u8) {
225 self.indent_width =
226 std::num::NonZeroU8::new(spaces).expect("an indent step is at least one space");
227 }
228
229 pub fn snapshot(&self) -> crate::ropetext::Snapshot {
230 self.inner.snapshot()
231 }
232
233 pub fn row(&self, row: usize) -> Option<std::borrow::Cow<'_, str>> {
237 self.inner.text().line(row)
238 }
239
240 pub fn row_count(&self) -> usize {
242 self.inner.text().line_count()
243 }
244
245 pub fn rows(&self) -> Vec<String> {
251 self.inner.text().lines().map(|l| l.to_string()).collect()
252 }
253
254 pub fn joined_rows(&self, first: usize, last: usize) -> String {
256 (first..=last)
257 .filter_map(|row| self.row(row))
258 .collect::<Vec<_>>()
259 .join("\n")
260 }
261
262 pub fn row_len(&self, row: usize) -> usize {
264 self.inner.text().line_len_chars(row).unwrap_or(0)
265 }
266
267 pub fn cursor(&self) -> (usize, usize) {
268 let cursor = self.inner.cursor();
269 (cursor.row(), cursor.column().get())
270 }
271
272 pub fn is_empty(&self) -> bool {
273 self.inner.text().len_bytes() == 0
274 }
275
276 pub fn selection_range(&self) -> Option<((usize, usize), (usize, usize))> {
277 let span = self.inner.selection()?;
278 Some((rc(span.start()), rc(span.end())))
279 }
280
281 pub fn yank_text(&self) -> String {
282 self.yank.clone()
283 }
284
285 pub fn set_yank_text(&mut self, text: impl Into<String>) {
286 self.yank = text.into();
287 }
288
289 pub fn search_pattern(&self) -> Option<®ex::Regex> {
290 self.search.as_ref()
291 }
292
293 pub fn take_outcome(&mut self) -> EditOutcome {
294 std::mem::take(&mut self.pending)
295 }
296
297 pub fn edit<R>(&mut self, f: impl FnOnce(&mut Self) -> R) -> R {
305 if self.depth > 0 {
306 return f(self);
307 }
308 self.depth = 1;
309 self.group_started = false;
310 let out = f(self);
311 self.depth = 0;
312 self.group_started = false;
313 out
314 }
315
316 pub fn continue_group(&mut self) {
323 self.continue_group = true;
324 }
325
326 fn mutate(&mut self, f: impl FnOnce(&mut crate::ropetext::Txn<'_>)) -> bool {
328 let extending =
329 (self.depth > 0 && self.group_started) || std::mem::take(&mut self.continue_group);
330 let mut txn = if extending {
331 self.inner.begin_extending()
332 } else {
333 self.inner.begin()
334 };
335 f(&mut txn);
336 let change = txn.commit();
337 if self.depth > 0 {
338 self.group_started = true;
339 }
340 self.record(change)
341 }
342
343 fn record(&mut self, change: Option<Change>) -> bool {
344 let Some(change) = change else {
345 return false;
346 };
347 self.pending.changed = true;
348 self.pending.bulk |= change.is_bulk();
349 self.pending.line_delta += change.line_delta();
350 self.pending.damage = Some(match self.pending.damage.take() {
351 Some(seen) => {
352 let seen = shift_rows(seen, change.rows().start, change.line_delta());
360 seen.start.min(change.rows().start)..seen.end.max(change.rows().end)
361 }
362 None => change.rows(),
363 });
364 true
365 }
366
367 pub fn insert_str(&mut self, s: impl AsRef<str>) -> bool {
370 let text = s.as_ref().to_string();
371 let span = self.inner.selection().filter(|span| !span.is_empty());
374 self.inner.clear_selection();
375 let cursor = self.inner.cursor();
376 self.goal = None;
377 self.mutate(|txn| match span {
378 Some(span) => {
379 txn.replace(span, &text);
380 }
381 None => {
382 txn.insert(cursor, &text);
383 }
384 })
385 }
386
387 pub fn insert_char(&mut self, c: char) {
388 self.insert_str(c.to_string());
389 }
390
391 pub fn insert_newline(&mut self) {
392 self.insert_str("\n");
393 }
394
395 pub fn delete_str(&mut self, clusters: usize) -> bool {
401 if self.take_selection() {
402 return true;
403 }
404 if clusters == 0 {
405 return false;
406 }
407 let from = self.inner.cursor();
408 let to = self.forward_by(from, clusters);
409 self.delete_between(from, to, Yank::Keep)
410 }
411
412 pub fn delete_char(&mut self) -> bool {
414 if self.take_selection() {
415 return true;
416 }
417 let to = self.inner.cursor();
418 let from = motion::prev_cluster(self.inner.text(), to);
419 self.delete_between(from, to, Yank::Discard)
423 }
424
425 pub fn delete_next_char(&mut self) -> bool {
427 if self.take_selection() {
428 return true;
429 }
430 let from = self.inner.cursor();
431 let to = motion::next_cluster(self.inner.text(), from);
432 self.delete_between(from, to, Yank::Discard)
433 }
434
435 pub fn delete_word(&mut self) -> bool {
436 if self.take_selection() {
437 return true;
438 }
439 let to = self.inner.cursor();
440 let text = self.inner.text();
441 let candidate = motion::word_start_back(text, to, Words::Small);
445 let (from, yank) = if candidate.row() == to.row() && candidate.byte() < to.byte() {
446 (candidate, Yank::Keep)
447 } else if to.column().get() > 0 {
448 (motion::row_start(text, to), Yank::Keep)
449 } else {
450 (motion::prev_cluster(text, to), Yank::Discard)
454 };
455 self.delete_between(from, to, yank)
456 }
457
458 pub fn delete_next_word(&mut self) -> bool {
459 if self.take_selection() {
460 return true;
461 }
462 let from = self.inner.cursor();
463 let text = self.inner.text();
464 let candidate = motion::word_end_at_or_after(text, from, Words::Small);
470 let row_end = motion::row_end(text, from);
471 let (to, yank) = match candidate {
472 Some(end) if end.row() == from.row() && end.byte() > from.byte() => (end, Yank::Keep),
473 _ if from.byte() < row_end.byte() => (row_end, Yank::Keep),
474 _ => (motion::next_cluster(text, from), Yank::Discard),
475 };
476 self.delete_between(from, to, yank)
477 }
478
479 pub fn cut(&mut self) -> bool {
480 let span = self.inner.selection().filter(|span| !span.is_empty());
483 self.inner.clear_selection();
484 let Some(span) = span else {
485 return false;
486 };
487 self.yank = self
488 .inner
489 .text()
490 .slice(span)
491 .map(|text| text.to_string())
492 .unwrap_or_default();
493 self.goal = None;
494 self.mutate(|txn| {
495 txn.delete(span);
496 })
497 }
498
499 pub fn copy(&mut self) {
502 if let Some(span) = self.inner.selection().filter(|span| !span.is_empty())
503 && let Some(text) = self.inner.text().slice(span)
504 {
505 self.yank = text.to_string();
506 }
507 }
508
509 pub fn paste(&mut self) -> bool {
510 if self.yank.is_empty() {
511 return false;
512 }
513 let text = std::mem::take(&mut self.yank);
514 let changed = self.insert_str(&text);
515 self.yank = text;
516 changed
517 }
518
519 fn take_selection(&mut self) -> bool {
530 let span = self.inner.selection().filter(|span| !span.is_empty());
531 self.inner.clear_selection();
532 let Some(span) = span else {
533 return false;
534 };
535 self.delete_between(span.start(), span.end(), Yank::Discard)
536 }
537
538 fn delete_between(&mut self, from: Position, to: Position, yank: Yank) -> bool {
539 let Some(span) = self.inner.text().span(from, to) else {
540 return false;
541 };
542 if span.is_empty() {
543 return false;
544 }
545 if yank == Yank::Keep
546 && let Some(text) = self.inner.text().slice(span)
547 {
548 self.yank = text.to_string();
549 }
550 self.goal = None;
551 self.mutate(|txn| {
552 txn.delete(span);
553 })
554 }
555
556 pub fn undo(&mut self) -> bool {
559 let change = self.inner.undo();
560 self.after_history(change)
561 }
562
563 pub fn redo(&mut self) -> bool {
564 let change = self.inner.redo();
565 self.after_history(change)
566 }
567
568 fn after_history(&mut self, change: Option<Change>) -> bool {
574 if change.is_none() {
575 return false;
578 }
579 self.goal = None;
580 self.inner.clear_selection();
581 self.record(change)
582 }
583
584 pub fn move_cursor(&mut self, movement: CursorMove) {
591 let text = self.inner.text();
592 let from = self.inner.cursor();
593 let goal = self
594 .goal
595 .filter(|_| movement.is_vertical())
596 .unwrap_or_else(|| from.column());
597
598 let to = match movement {
599 CursorMove::Forward => motion::next_cluster(text, from),
600 CursorMove::Back => motion::prev_cluster(text, from),
601 CursorMove::Up => motion::vertical(text, from, -1, Goal::Column(goal)),
602 CursorMove::Down => motion::vertical(text, from, 1, Goal::Column(goal)),
603 CursorMove::Head => motion::row_start(text, from),
604 CursorMove::End => motion::row_end(text, from),
605 CursorMove::Top => {
608 let up = -(from.row() as isize);
609 motion::vertical(text, from, up, Goal::Column(goal))
610 }
611 CursorMove::Bottom => {
612 let down = (text.line_count().saturating_sub(1) as isize) - from.row() as isize;
613 motion::vertical(text, from, down, Goal::Column(goal))
614 }
615 CursorMove::WordForward => motion::word_start_forward(text, from, Words::Small),
616 CursorMove::WordBack => motion::word_start_back(text, from, Words::Small),
617 CursorMove::WordForwardBig => motion::word_start_forward(text, from, Words::Big),
618 CursorMove::WordBackBig => motion::word_start_back(text, from, Words::Big),
619 CursorMove::WordEndBig => match motion::word_end_forward(text, from, Words::Big) {
622 Some(end) => motion::prev_cluster(text, end),
623 None => from,
624 },
625 CursorMove::WordEndBack { big } => {
626 let words = if big { Words::Big } else { Words::Small };
627 match motion::word_end_back(text, from, words) {
628 Some(end) => motion::prev_cluster(text, end),
629 None => from,
630 }
631 }
632 CursorMove::MatchingPair => motion::matching_bracket(text, from).unwrap_or(from),
633 CursorMove::WordEnd => match motion::word_end_forward(text, from, Words::Small) {
640 Some(end) => motion::prev_cluster(text, end),
641 None => motion::text_end(text),
645 },
646 CursorMove::ParagraphForward => motion::paragraph_forward(text, from),
647 CursorMove::ParagraphBack => motion::paragraph_back(text, from),
648 CursorMove::Jump(row, column) => {
649 match text.position(row, Column::new(column)) {
650 Some(position) => position,
651 None => return,
654 }
655 }
656 };
657
658 self.goal = if movement.is_vertical() {
659 Some(goal)
660 } else {
661 None
662 };
663 self.place(to);
664 }
665
666 pub fn jump_to(&mut self, row: usize, col: usize) -> bool {
669 let Some(to) = self.inner.text().position(row, Column::new(col)) else {
670 return false;
671 };
672 self.goal = None;
673 self.place(to);
674 true
675 }
676
677 pub fn move_to(&mut self, to: Position) {
680 if self.inner.text().is_stale(to) {
681 return;
682 }
683 self.goal = None;
684 self.place(to);
685 }
686
687 fn place(&mut self, to: Position) {
688 if self.inner.selection().is_some() {
689 self.inner.extend_to(to);
690 } else {
691 self.inner.set_cursor(to);
692 }
693 }
694
695 pub fn start_selection(&mut self) {
696 let cursor = self.inner.cursor();
699 self.inner.clear_selection();
700 self.inner.extend_to(cursor);
701 }
702
703 pub fn cancel_selection(&mut self) {
704 self.inner.clear_selection();
705 }
706
707 pub fn select_all(&mut self) {
708 let span = self.inner.text().full_span();
709 self.inner.select(span);
710 }
711
712 pub fn set_selection(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
713 let text = self.inner.text();
714 let Some(from) = text.position(start.0, Column::new(start.1)) else {
715 return false;
716 };
717 let Some(to) = text.position(end.0, Column::new(end.1)) else {
718 return false;
719 };
720 let Some(span) = text.span(from, to) else {
721 return false;
722 };
723 self.inner.select(span)
724 }
725
726 pub fn text_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<String> {
729 let span = self
730 .span_between(start, end)
731 .filter(|span| !span.is_empty())?;
732 self.inner.text().slice(span).map(|text| text.into_owned())
733 }
734
735 pub fn selection_text(&self) -> Option<String> {
738 let (start, end) = self.selection_range()?;
739 self.text_between(start, end)
740 }
741
742 pub fn indent_rows(&mut self, rows: std::ops::RangeInclusive<usize>, dedent: bool) -> bool {
761 let step = self.indent_width.get() as usize;
762 let first = *rows.start();
763 let last = (*rows.end()).min(self.row_count() - 1);
764 if first > last {
765 return false;
766 }
767 let text = self.inner.text();
770 let mut edits: Vec<RowEdit> = Vec::with_capacity(last - first + 1);
771 let mut deltas: Vec<isize> = Vec::with_capacity(last - first + 1);
772 for row in first..=last {
773 let at = text
774 .position(row, Column::new(0))
775 .expect("row is within the buffer");
776 if dedent {
777 let count = self.leading_step(row);
778 if count > 0
779 && let Some(span) = text.span(at, self.forward_by(at, count))
780 {
781 edits.push(RowEdit::Delete(span));
782 }
783 deltas.push(-(count as isize));
784 } else {
785 edits.push(RowEdit::Insert(at));
786 deltas.push(step as isize);
787 }
788 }
789 if edits.is_empty() {
790 return false;
791 }
792 let selection = self.selection_range();
793 let shifted = |(row, col): (usize, usize)| -> (usize, usize) {
794 if row < first || row > last {
795 return (row, col);
796 }
797 (row, col.saturating_add_signed(deltas[row - first]))
798 };
799 let spaces = " ".repeat(step);
800 self.goal = None;
801 self.mutate(|txn| {
802 for edit in &edits {
803 match *edit {
804 RowEdit::Insert(at) => {
805 txn.insert(at, &spaces);
806 }
807 RowEdit::Delete(span) => {
808 txn.delete(span);
809 }
810 }
811 }
812 if let Some((start, end)) = selection {
815 let (start, end) = (shifted(start), shifted(end));
816 let text = txn.text();
817 if let Some(from) = text.position(start.0, Column::new(start.1))
818 && let Some(to) = text.position(end.0, Column::new(end.1))
819 && let Some(span) = text.span(from, to)
820 {
821 txn.select(span);
822 }
823 }
824 })
825 }
826
827 fn leading_step(&self, row: usize) -> usize {
832 use unicode_segmentation::UnicodeSegmentation;
833 let step = self.indent_width.get() as usize;
834 let Some(line) = self.row(row) else {
835 return 0;
836 };
837 let mut count = 0;
838 for cluster in line.graphemes(true).take(step) {
839 match cluster {
840 " " => count += 1,
841 "\t" => return count + 1,
842 _ => break,
843 }
844 }
845 count
846 }
847
848 pub fn set_search_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
855 if pattern.is_empty() {
856 self.search = None;
857 return Ok(());
858 }
859 self.search = Some(regex::Regex::new(pattern)?);
860 Ok(())
861 }
862
863 pub fn search_forward(&mut self, match_cursor: bool) -> bool {
865 self.step_search(false, match_cursor)
866 }
867
868 pub fn search_back(&mut self, match_cursor: bool) -> bool {
870 self.step_search(true, match_cursor)
871 }
872
873 pub fn search_repeat(&mut self, backward: bool) -> bool {
875 self.step_search(backward, false)
876 }
877
878 fn step_search(&mut self, backward: bool, match_cursor: bool) -> bool {
879 self.cancel_selection();
883 let Some(found) = self.find_match(backward, match_cursor) else {
884 return false;
885 };
886 self.goal = None;
887 self.inner.set_cursor(found);
888 true
889 }
890
891 fn find_match(&self, backward: bool, match_cursor: bool) -> Option<Position> {
892 let pattern = self.search.as_ref()?;
893 let text = self.inner.text();
894 let cursor = self.inner.cursor();
895 let rows = text.line_count();
896
897 for step in 0..=rows {
903 let wrapped = step == rows;
904 let row = if backward {
905 (cursor.row() + rows - (step % rows.max(1))) % rows
906 } else {
907 (cursor.row() + step) % rows
908 };
909 let line = text.line(row)?;
910 let mut hits: Vec<usize> = pattern
911 .find_iter(&line)
912 .map(|found| line[..found.start()].chars().count())
913 .collect();
914 if backward {
915 hits.reverse();
916 }
917 for column in hits {
918 let same_row = row == cursor.row();
919 let beyond = if backward {
920 column < cursor.column().get()
921 } else if match_cursor {
922 column >= cursor.column().get()
923 } else {
924 column > cursor.column().get()
925 };
926 if wrapped || !same_row || beyond {
927 if let Some(at) = text.position(row, Column::new(column)) {
933 return Some(at);
934 }
935 }
936 }
937 }
938 None
939 }
940
941 pub fn match_at_cursor(&self) -> Option<((usize, usize), (usize, usize))> {
943 let pattern = self.search.as_ref()?;
944 let text = self.inner.text();
945 let cursor = self.inner.cursor();
946 let line = text.line(cursor.row())?;
947 let byte = line
948 .char_indices()
949 .nth(cursor.column().get())
950 .map(|(at, _)| at)
951 .unwrap_or(line.len());
952 let found = pattern.find_at(&line, byte)?;
953 if found.start() != byte {
954 return None;
955 }
956 let chars = line[found.range()].chars().count();
957 Some((rc(cursor), (cursor.row(), cursor.column().get() + chars)))
958 }
959
960 fn forward_by(&self, from: Position, chars: usize) -> Position {
964 let text = self.inner.text();
965 let mut at = from;
966 for _ in 0..chars {
967 let next = motion::next_cluster(text, at);
968 if next.byte() == at.byte() {
969 break;
970 }
971 at = next;
972 }
973 at
974 }
975
976 fn span_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<Span> {
979 let text = self.inner.text();
980 let from = text.position(start.0, Column::new(start.1))?;
981 let to = text.position(end.0, Column::new(end.1))?;
982 text.span(from, to)
983 }
984}
985
986fn rc(position: Position) -> (usize, usize) {
987 (position.row(), position.column().get())
988}
989
990#[cfg(test)]
991mod search_tests {
992 use super::*;
993 use crate::ropetext::Text;
994
995 fn buffer(text: &str, pattern: &str, cursor: (usize, usize)) -> RopeBuffer {
996 let mut buf = RopeBuffer::new(Text::from(text));
997 buf.set_search_pattern(pattern).expect("valid pattern");
998 buf.move_cursor(CursorMove::Jump(cursor.0, cursor.1));
999 buf
1000 }
1001
1002 #[test]
1003 fn a_forward_search_wraps_to_a_match_behind_the_cursor() {
1004 let mut buf = buffer("xx foo", "foo", (0, 5));
1007 assert!(buf.search_forward(false), "the match is behind the cursor");
1008 assert_eq!(buf.cursor(), (0, 3));
1009 }
1010
1011 #[test]
1012 fn a_backward_search_wraps_to_a_match_ahead_of_the_cursor() {
1013 let mut buf = buffer("xx foo", "foo", (0, 1));
1014 assert!(buf.search_back(false));
1015 assert_eq!(buf.cursor(), (0, 3));
1016 }
1017
1018 #[test]
1019 fn wrapping_crosses_rows_back_to_the_cursors_own_row() {
1020 let mut buf = buffer("aaa\nxx foo", "foo", (1, 5));
1021 assert!(buf.search_forward(false));
1022 assert_eq!(buf.cursor(), (1, 3));
1023 }
1024
1025 #[test]
1026 fn the_only_match_is_re_offered_rather_than_reported_missing() {
1027 let mut buf = buffer("xx foo", "foo", (0, 3));
1031 assert!(buf.search_forward(false), "the one match is still a match");
1032 assert_eq!(
1033 buf.cursor(),
1034 (0, 3),
1035 "and the cursor has nowhere else to go"
1036 );
1037 }
1038
1039 #[test]
1040 fn a_match_starting_inside_a_cluster_is_skipped_not_fatal() {
1041 let mut buf = buffer(
1046 "\u{1F469}\u{200D}\u{1F4BB}\nx\u{1F4BB}",
1047 "\u{1F4BB}",
1048 (0, 0),
1049 );
1050 assert!(buf.search_forward(false), "the row 1 match is reachable");
1051 assert_eq!(buf.cursor(), (1, 1));
1052 }
1053}
1054
1055#[cfg(test)]
1056mod cluster_tests {
1057 use super::*;
1058 use crate::ropetext::Text;
1059
1060 #[test]
1061 fn delete_str_spends_its_count_on_clusters() {
1062 let mut buf = RopeBuffer::new(Text::from("[[\u{1F1EA}\u{1F1F8} rest"));
1066 buf.move_cursor(CursorMove::Jump(0, 0));
1067 buf.delete_str(3);
1068 assert_eq!(buf.rows(), &[" rest"]);
1069 }
1070
1071 #[test]
1072 fn inserting_before_a_combining_mark_keeps_the_cursor_addressable() {
1073 let mut buf = RopeBuffer::new(Text::from("\u{301}f"));
1077 buf.move_cursor(CursorMove::Jump(0, 0));
1078 buf.insert_char('a');
1079 assert_eq!(buf.rows(), &["a\u{301}f"]);
1080 }
1081}
1082
1083#[cfg(test)]
1084mod damage_tests {
1085 use super::*;
1086 use crate::ropetext::Text;
1087
1088 #[test]
1089 fn damage_from_several_edits_is_in_one_numbering() {
1090 let mut buf = RopeBuffer::new(Text::from("r0\nr1\nr2\nr3\nr4"));
1093 buf.edit(|b| {
1094 b.move_cursor(CursorMove::Jump(4, 0));
1095 b.insert_str("X");
1096 b.move_cursor(CursorMove::Jump(0, 0));
1097 b.insert_newline();
1098 });
1099 assert_eq!(buf.rows(), ["", "r0", "r1", "r2", "r3", "Xr4"]);
1100
1101 let damage = buf.take_outcome().damage.expect("the edits were reported");
1102 assert!(
1103 damage.contains(&5),
1104 "the row edited first is row 5 once the group ends, but the damage \
1105 reported was {damage:?} — a range in the older numbering"
1106 );
1107 }
1108}
1109
1110#[cfg(test)]
1111mod read_tests {
1112 use super::*;
1113 use crate::ropetext::Text;
1114
1115 #[test]
1116 fn text_between_slices_by_char_columns_across_rows_in_either_order() {
1117 let buf = RopeBuffer::new(Text::from("héllo🦀\nworld"));
1118 assert_eq!(
1119 buf.text_between((0, 1), (1, 2)).as_deref(),
1120 Some("éllo🦀\nwo")
1121 );
1122 assert_eq!(
1123 buf.text_between((1, 2), (0, 1)).as_deref(),
1124 Some("éllo🦀\nwo")
1125 );
1126 }
1127
1128 #[test]
1129 fn an_empty_or_unaddressable_range_is_none() {
1130 let buf = RopeBuffer::new(Text::from("abc"));
1131 assert_eq!(buf.text_between((0, 1), (0, 1)), None);
1132 assert_eq!(buf.text_between((0, 0), (7, 0)), None);
1133 }
1134
1135 #[test]
1136 fn selection_text_is_the_live_selection_or_none() {
1137 let mut buf = RopeBuffer::new(Text::from("hello world"));
1138 assert_eq!(buf.selection_text(), None);
1139 assert!(buf.set_selection((0, 0), (0, 5)));
1140 assert_eq!(buf.selection_text().as_deref(), Some("hello"));
1141 buf.start_selection();
1142 assert_eq!(
1143 buf.selection_text(),
1144 None,
1145 "a zero-width selection is not text"
1146 );
1147 }
1148}
1149
1150#[cfg(test)]
1151mod indent_tests {
1152 use super::*;
1153 use crate::ropetext::Text;
1154
1155 fn buffer(text: &str) -> RopeBuffer {
1156 RopeBuffer::new(Text::from(text))
1157 }
1158
1159 #[test]
1160 fn indent_inserts_one_step_of_spaces_never_a_tab() {
1161 let mut buf = buffer("foo\nbar");
1162 assert!(buf.indent_rows(0..=1, false));
1163 assert_eq!(buf.rows(), &[" foo", " bar"]);
1164 }
1165
1166 #[test]
1167 fn indent_follows_indent_width() {
1168 let mut buf = buffer("x");
1169 buf.set_indent_width(2);
1170 assert!(buf.indent_rows(0..=0, false));
1171 assert_eq!(buf.rows(), &[" x"]);
1172 }
1173
1174 #[test]
1175 fn dedent_removes_up_to_one_step_of_spaces() {
1176 let mut buf = buffer(" x\n y\nz");
1177 assert!(buf.indent_rows(0..=2, true));
1178 assert_eq!(buf.rows(), &[" x", "y", "z"]);
1179 }
1180
1181 #[test]
1182 fn dedent_counts_a_leading_tab_as_a_whole_step() {
1183 let mut buf = buffer("\t\tx\n \ty");
1184 assert!(buf.indent_rows(0..=1, true));
1185 assert_eq!(buf.rows(), &["\tx", "y"]);
1188 }
1189
1190 #[test]
1191 fn dedent_stops_at_a_space_that_carries_a_combining_mark() {
1192 let mut buf = buffer(" \u{301}foo");
1194 assert!(buf.set_selection((0, 3), (0, 6)));
1195 assert!(buf.indent_rows(0..=0, true));
1196 assert_eq!(buf.rows(), &[" \u{301}foo"]);
1197 assert_eq!(buf.selection_range(), Some(((0, 2), (0, 5))));
1198 }
1199
1200 #[test]
1201 fn dedent_with_nothing_to_remove_reports_no_change() {
1202 let mut buf = buffer("foo");
1203 assert!(!buf.indent_rows(0..=0, true));
1204 assert_eq!(buf.rows(), &["foo"]);
1205 assert!(!buf.take_outcome().changed);
1206 }
1207
1208 #[test]
1209 fn rows_past_the_end_are_ignored() {
1210 let mut buf = buffer("a\nb");
1211 assert!(buf.indent_rows(1..=9, false));
1212 assert_eq!(buf.rows(), &["a", " b"]);
1213 assert!(!buf.indent_rows(5..=9, false));
1214 assert_eq!(buf.rows(), &["a", " b"]);
1215 }
1216
1217 #[test]
1218 fn the_cursor_keeps_its_character() {
1219 let mut buf = buffer("one\ntwo");
1220 assert!(buf.jump_to(0, 1)); buf.indent_rows(0..=1, false);
1222 assert_eq!(buf.cursor(), (0, 1 + 4));
1223 buf.indent_rows(0..=1, true);
1224 assert_eq!(buf.cursor(), (0, 1));
1225 }
1226
1227 #[test]
1228 fn a_cursor_outside_the_rows_does_not_move() {
1229 let mut buf = buffer("a\nb\nc");
1230 assert!(buf.jump_to(2, 1));
1231 buf.indent_rows(0..=1, false);
1232 assert_eq!(buf.cursor(), (2, 1));
1233 }
1234
1235 #[test]
1236 fn a_dedent_never_pushes_the_cursor_below_column_zero() {
1237 let mut buf = buffer(" x");
1238 assert!(buf.jump_to(0, 2));
1239 buf.indent_rows(0..=0, true);
1240 assert_eq!(buf.cursor(), (0, 0));
1241 }
1242
1243 #[test]
1244 fn the_selection_is_put_back_shifted_with_its_rows() {
1245 let mut buf = buffer("hello world\nnext");
1246 assert!(buf.set_selection((0, 6), (1, 2)));
1247 buf.indent_rows(0..=1, false);
1248 assert_eq!(buf.selection_range(), Some(((0, 10), (1, 6))));
1249 assert_eq!(buf.rows()[0].trim_start(), "hello world");
1250 }
1251
1252 #[test]
1253 fn a_dedent_under_a_selection_shifts_each_endpoint_by_its_own_row() {
1254 let mut buf = buffer(" foo\n bar\nbaz");
1255 assert!(buf.set_selection((0, 4), (2, 3)));
1256 assert!(buf.indent_rows(0..=2, true));
1257 assert_eq!(buf.rows(), &["foo", "bar", "baz"]);
1258 assert_eq!(buf.selection_range(), Some(((0, 0), (2, 3))));
1259 }
1260
1261 #[test]
1262 fn a_block_indent_is_one_undo_group() {
1263 let mut buf = buffer("a\nb\nc");
1264 buf.indent_rows(0..=2, false);
1265 assert!(buf.undo(), "the block is one entry");
1266 assert_eq!(buf.rows(), &["a", "b", "c"]);
1267 assert!(!buf.undo(), "and has nothing left to take back");
1268 }
1269
1270 #[test]
1271 fn undo_and_redo_of_an_indent_land_the_cursor_on_its_own_character() {
1272 let mut buf = buffer("one\ntwo");
1273 assert!(buf.jump_to(0, 1)); buf.indent_rows(0..=1, false);
1275 assert_eq!(buf.cursor(), (0, 5));
1276 assert!(buf.undo());
1277 assert_eq!(buf.cursor(), (0, 1), "undo returns to where the user was");
1278 assert!(buf.redo());
1279 assert_eq!(
1280 buf.cursor(),
1281 (0, 5),
1282 "redo lands where the indent left them"
1283 );
1284 }
1285
1286 #[test]
1287 fn a_block_indent_is_one_transaction() {
1288 let mut buf = buffer("a\nb\nc");
1289 buf.indent_rows(0..=2, false);
1290 let outcome = buf.take_outcome();
1291 assert!(outcome.changed);
1292 assert!(outcome.bulk, "three rows in one change");
1293 assert!(buf.undo());
1294 assert_eq!(buf.rows(), &["a", "b", "c"]);
1295 assert!(!buf.undo());
1296 }
1297
1298 #[test]
1299 fn a_dedent_does_not_fill_the_yank_transport() {
1300 let mut buf = buffer(" x");
1301 buf.set_yank_text("kept");
1302 buf.indent_rows(0..=0, true);
1303 assert_eq!(buf.yank_text(), "kept");
1304 }
1305
1306 #[test]
1307 fn damage_covers_every_touched_row() {
1308 let mut buf = buffer("a\nb\nc\nd");
1309 buf.indent_rows(1..=2, false);
1310 let outcome = buf.take_outcome();
1311 assert!(outcome.changed);
1312 let damage = outcome.damage.expect("the edits were reported");
1313 assert!(damage.contains(&1) && damage.contains(&2), "{damage:?}");
1314 assert_eq!(outcome.line_delta, 0);
1315 }
1316}