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 cluster_at(&self, row: usize, col: usize) -> (usize, usize) {
595 let text = self.inner.text();
596 let Some(len) = text.line_len_chars(row) else {
597 return (col, col);
598 };
599 if col >= len {
600 return (len, len);
601 }
602 let Some(start) = (0..=col)
605 .rev()
606 .find_map(|c| text.position(row, Column::new(c)))
607 else {
608 return (col, col);
609 };
610 (start.column().get(), Self::next_edge(text, start, len))
611 }
612
613 pub fn clusters_through(&self, row: usize, from: usize, to: usize) -> usize {
622 use unicode_segmentation::UnicodeSegmentation;
623 let Some(line) = self.row(row) else {
624 return 1;
625 };
626 let mut start = 0;
627 let mut count = 0;
628 for cluster in line.graphemes(true) {
629 let end = start + cluster.chars().count();
630 if start > to {
631 break;
632 }
633 if end > from {
634 count += 1;
635 }
636 start = end;
637 }
638 count.max(1)
639 }
640
641 pub fn col_after_clusters(&self, row: usize, col: usize, n: usize) -> usize {
645 use unicode_segmentation::UnicodeSegmentation;
646 let Some(line) = self.row(row) else {
647 return col;
648 };
649 let mut starts = line
651 .graphemes(true)
652 .scan(0, |at, cluster| {
653 let start = *at;
654 *at += cluster.chars().count();
655 Some((start, *at))
656 })
657 .skip_while(|&(_, end)| end <= col)
658 .map(|(start, _)| start);
659 let Some(mut at) = starts.next() else {
660 return line.chars().count();
662 };
663 for next in starts.take(n) {
664 at = next;
665 }
666 at
667 }
668
669 fn next_edge(text: &Text, at: Position, len: usize) -> usize {
672 let next = motion::next_cluster(text, at);
673 if next.row() == at.row() && next.byte() > at.byte() {
674 next.column().get()
675 } else {
676 len
677 }
678 }
679
680 pub fn set_mark(&mut self, name: char, (row, col): (usize, usize)) -> bool {
692 let Some(at) = self.inner.text().position(row, Column::new(col)) else {
693 return false;
694 };
695 self.inner.set_mark(name, at)
696 }
697
698 pub fn mark(&self, name: char) -> Option<(usize, usize)> {
700 self.inner.mark(name).map(|m| (m.row(), m.column().get()))
701 }
702
703 pub fn move_cursor(&mut self, movement: CursorMove) {
710 let text = self.inner.text();
711 let from = self.inner.cursor();
712 let goal = self
713 .goal
714 .filter(|_| movement.is_vertical())
715 .unwrap_or_else(|| from.column());
716
717 let to = match movement {
718 CursorMove::Forward => motion::next_cluster(text, from),
719 CursorMove::Back => motion::prev_cluster(text, from),
720 CursorMove::Up => motion::vertical(text, from, -1, Goal::Column(goal)),
721 CursorMove::Down => motion::vertical(text, from, 1, Goal::Column(goal)),
722 CursorMove::Head => motion::row_start(text, from),
723 CursorMove::End => motion::row_end(text, from),
724 CursorMove::Top => {
727 let up = -(from.row() as isize);
728 motion::vertical(text, from, up, Goal::Column(goal))
729 }
730 CursorMove::Bottom => {
731 let down = (text.line_count().saturating_sub(1) as isize) - from.row() as isize;
732 motion::vertical(text, from, down, Goal::Column(goal))
733 }
734 CursorMove::WordForward => motion::word_start_forward(text, from, Words::Small),
735 CursorMove::WordBack => motion::word_start_back(text, from, Words::Small),
736 CursorMove::WordForwardBig => motion::word_start_forward(text, from, Words::Big),
737 CursorMove::WordBackBig => motion::word_start_back(text, from, Words::Big),
738 CursorMove::WordEndBig => match motion::word_end_forward(text, from, Words::Big) {
741 Some(end) => motion::prev_cluster(text, end),
742 None => from,
743 },
744 CursorMove::WordEndBack { big } => {
745 let words = if big { Words::Big } else { Words::Small };
746 match motion::word_end_back(text, from, words) {
747 Some(end) => motion::prev_cluster(text, end),
748 None => from,
749 }
750 }
751 CursorMove::MatchingPair => motion::matching_bracket(text, from).unwrap_or(from),
752 CursorMove::WordEnd => match motion::word_end_forward(text, from, Words::Small) {
759 Some(end) => motion::prev_cluster(text, end),
760 None => motion::text_end(text),
764 },
765 CursorMove::ParagraphForward => motion::paragraph_forward(text, from),
766 CursorMove::ParagraphBack => motion::paragraph_back(text, from),
767 CursorMove::Jump(row, column) => {
768 match text.position(row, Column::new(column)) {
769 Some(position) => position,
770 None => return,
773 }
774 }
775 };
776
777 self.goal = if movement.is_vertical() {
778 Some(goal)
779 } else {
780 None
781 };
782 self.place(to);
783 }
784
785 pub fn jump_to(&mut self, row: usize, col: usize) -> bool {
788 let Some(to) = self.inner.text().position(row, Column::new(col)) else {
789 return false;
790 };
791 self.goal = None;
792 self.place(to);
793 true
794 }
795
796 pub fn move_to(&mut self, to: Position) {
799 if self.inner.text().is_stale(to) {
800 return;
801 }
802 self.goal = None;
803 self.place(to);
804 }
805
806 fn place(&mut self, to: Position) {
807 if self.inner.selection().is_some() {
808 self.inner.extend_to(to);
809 } else {
810 self.inner.set_cursor(to);
811 }
812 }
813
814 pub fn start_selection(&mut self) {
815 let cursor = self.inner.cursor();
818 self.inner.clear_selection();
819 self.inner.extend_to(cursor);
820 }
821
822 pub fn cancel_selection(&mut self) {
823 self.inner.clear_selection();
824 }
825
826 pub fn select_all(&mut self) {
827 let span = self.inner.text().full_span();
828 self.inner.select(span);
829 }
830
831 pub fn set_selection(&mut self, start: (usize, usize), end: (usize, usize)) -> bool {
832 let text = self.inner.text();
833 let Some(from) = text.position(start.0, Column::new(start.1)) else {
834 return false;
835 };
836 let Some(to) = text.position(end.0, Column::new(end.1)) else {
837 return false;
838 };
839 let Some(span) = text.span(from, to) else {
840 return false;
841 };
842 self.inner.select(span)
843 }
844
845 pub fn text_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<String> {
848 let span = self
849 .span_between(start, end)
850 .filter(|span| !span.is_empty())?;
851 self.inner.text().slice(span).map(|text| text.into_owned())
852 }
853
854 pub fn selection_text(&self) -> Option<String> {
857 let (start, end) = self.selection_range()?;
858 self.text_between(start, end)
859 }
860
861 pub fn indent_rows(&mut self, rows: std::ops::RangeInclusive<usize>, dedent: bool) -> bool {
880 let step = self.indent_width.get() as usize;
881 let first = *rows.start();
882 let last = (*rows.end()).min(self.row_count() - 1);
883 if first > last {
884 return false;
885 }
886 let text = self.inner.text();
889 let mut edits: Vec<RowEdit> = Vec::with_capacity(last - first + 1);
890 let mut deltas: Vec<isize> = Vec::with_capacity(last - first + 1);
891 for row in first..=last {
892 let at = text
893 .position(row, Column::new(0))
894 .expect("row is within the buffer");
895 if dedent {
896 let count = self.leading_step(row);
897 if count > 0
898 && let Some(span) = text.span(at, self.forward_by(at, count))
899 {
900 edits.push(RowEdit::Delete(span));
901 }
902 deltas.push(-(count as isize));
903 } else {
904 edits.push(RowEdit::Insert(at));
905 deltas.push(step as isize);
906 }
907 }
908 if edits.is_empty() {
909 return false;
910 }
911 let selection = self.selection_range();
912 let shifted = |(row, col): (usize, usize)| -> (usize, usize) {
913 if row < first || row > last {
914 return (row, col);
915 }
916 (row, col.saturating_add_signed(deltas[row - first]))
917 };
918 let spaces = " ".repeat(step);
919 self.goal = None;
920 self.mutate(|txn| {
921 for edit in &edits {
922 match *edit {
923 RowEdit::Insert(at) => {
924 txn.insert(at, &spaces);
925 }
926 RowEdit::Delete(span) => {
927 txn.delete(span);
928 }
929 }
930 }
931 if let Some((start, end)) = selection {
934 let (start, end) = (shifted(start), shifted(end));
935 let text = txn.text();
936 if let Some(from) = text.position(start.0, Column::new(start.1))
937 && let Some(to) = text.position(end.0, Column::new(end.1))
938 && let Some(span) = text.span(from, to)
939 {
940 txn.select(span);
941 }
942 }
943 })
944 }
945
946 fn leading_step(&self, row: usize) -> usize {
951 use unicode_segmentation::UnicodeSegmentation;
952 let step = self.indent_width.get() as usize;
953 let Some(line) = self.row(row) else {
954 return 0;
955 };
956 let mut count = 0;
957 for cluster in line.graphemes(true).take(step) {
958 match cluster {
959 " " => count += 1,
960 "\t" => return count + 1,
961 _ => break,
962 }
963 }
964 count
965 }
966
967 pub fn set_search_pattern(&mut self, pattern: &str) -> Result<(), regex::Error> {
974 if pattern.is_empty() {
975 self.search = None;
976 return Ok(());
977 }
978 self.search = Some(regex::Regex::new(pattern)?);
979 Ok(())
980 }
981
982 pub fn search_forward(&mut self, match_cursor: bool) -> bool {
984 self.step_search(false, match_cursor)
985 }
986
987 pub fn search_back(&mut self, match_cursor: bool) -> bool {
989 self.step_search(true, match_cursor)
990 }
991
992 pub fn search_repeat(&mut self, backward: bool) -> bool {
994 self.step_search(backward, false)
995 }
996
997 fn step_search(&mut self, backward: bool, match_cursor: bool) -> bool {
998 self.cancel_selection();
1002 let Some(found) = self.find_match(backward, match_cursor) else {
1003 return false;
1004 };
1005 self.goal = None;
1006 self.inner.set_cursor(found);
1007 true
1008 }
1009
1010 fn find_match(&self, backward: bool, match_cursor: bool) -> Option<Position> {
1011 let pattern = self.search.as_ref()?;
1012 let text = self.inner.text();
1013 let cursor = self.inner.cursor();
1014 let rows = text.line_count();
1015
1016 for step in 0..=rows {
1022 let wrapped = step == rows;
1023 let row = if backward {
1024 (cursor.row() + rows - (step % rows.max(1))) % rows
1025 } else {
1026 (cursor.row() + step) % rows
1027 };
1028 let line = text.line(row)?;
1029 let mut hits: Vec<usize> = pattern
1030 .find_iter(&line)
1031 .map(|found| line[..found.start()].chars().count())
1032 .collect();
1033 if backward {
1034 hits.reverse();
1035 }
1036 for column in hits {
1037 let same_row = row == cursor.row();
1038 let beyond = if backward {
1039 column < cursor.column().get()
1040 } else if match_cursor {
1041 column >= cursor.column().get()
1042 } else {
1043 column > cursor.column().get()
1044 };
1045 if wrapped || !same_row || beyond {
1046 if let Some(at) = text.position(row, Column::new(column)) {
1052 return Some(at);
1053 }
1054 }
1055 }
1056 }
1057 None
1058 }
1059
1060 pub fn match_at_cursor(&self) -> Option<((usize, usize), (usize, usize))> {
1062 let pattern = self.search.as_ref()?;
1063 let text = self.inner.text();
1064 let cursor = self.inner.cursor();
1065 let line = text.line(cursor.row())?;
1066 let byte = line
1067 .char_indices()
1068 .nth(cursor.column().get())
1069 .map(|(at, _)| at)
1070 .unwrap_or(line.len());
1071 let found = pattern.find_at(&line, byte)?;
1072 if found.start() != byte {
1073 return None;
1074 }
1075 let chars = line[found.range()].chars().count();
1076 Some((rc(cursor), (cursor.row(), cursor.column().get() + chars)))
1077 }
1078
1079 fn forward_by(&self, from: Position, chars: usize) -> Position {
1083 let text = self.inner.text();
1084 let mut at = from;
1085 for _ in 0..chars {
1086 let next = motion::next_cluster(text, at);
1087 if next.byte() == at.byte() {
1088 break;
1089 }
1090 at = next;
1091 }
1092 at
1093 }
1094
1095 fn span_between(&self, start: (usize, usize), end: (usize, usize)) -> Option<Span> {
1098 let text = self.inner.text();
1099 let from = text.position(start.0, Column::new(start.1))?;
1100 let to = text.position(end.0, Column::new(end.1))?;
1101 text.span(from, to)
1102 }
1103}
1104
1105fn rc(position: Position) -> (usize, usize) {
1106 (position.row(), position.column().get())
1107}
1108
1109#[cfg(test)]
1110mod search_tests {
1111 use super::*;
1112 use crate::ropetext::Text;
1113
1114 fn buffer(text: &str, pattern: &str, cursor: (usize, usize)) -> RopeBuffer {
1115 let mut buf = RopeBuffer::new(Text::from(text));
1116 buf.set_search_pattern(pattern).expect("valid pattern");
1117 buf.move_cursor(CursorMove::Jump(cursor.0, cursor.1));
1118 buf
1119 }
1120
1121 #[test]
1122 fn a_forward_search_wraps_to_a_match_behind_the_cursor() {
1123 let mut buf = buffer("xx foo", "foo", (0, 5));
1126 assert!(buf.search_forward(false), "the match is behind the cursor");
1127 assert_eq!(buf.cursor(), (0, 3));
1128 }
1129
1130 #[test]
1131 fn a_backward_search_wraps_to_a_match_ahead_of_the_cursor() {
1132 let mut buf = buffer("xx foo", "foo", (0, 1));
1133 assert!(buf.search_back(false));
1134 assert_eq!(buf.cursor(), (0, 3));
1135 }
1136
1137 #[test]
1138 fn wrapping_crosses_rows_back_to_the_cursors_own_row() {
1139 let mut buf = buffer("aaa\nxx foo", "foo", (1, 5));
1140 assert!(buf.search_forward(false));
1141 assert_eq!(buf.cursor(), (1, 3));
1142 }
1143
1144 #[test]
1145 fn the_only_match_is_re_offered_rather_than_reported_missing() {
1146 let mut buf = buffer("xx foo", "foo", (0, 3));
1150 assert!(buf.search_forward(false), "the one match is still a match");
1151 assert_eq!(
1152 buf.cursor(),
1153 (0, 3),
1154 "and the cursor has nowhere else to go"
1155 );
1156 }
1157
1158 #[test]
1159 fn a_match_starting_inside_a_cluster_is_skipped_not_fatal() {
1160 let mut buf = buffer(
1165 "\u{1F469}\u{200D}\u{1F4BB}\nx\u{1F4BB}",
1166 "\u{1F4BB}",
1167 (0, 0),
1168 );
1169 assert!(buf.search_forward(false), "the row 1 match is reachable");
1170 assert_eq!(buf.cursor(), (1, 1));
1171 }
1172}
1173
1174#[cfg(test)]
1175mod cluster_tests {
1176 use super::*;
1177 use crate::ropetext::Text;
1178
1179 #[test]
1180 fn delete_str_spends_its_count_on_clusters() {
1181 let mut buf = RopeBuffer::new(Text::from("[[\u{1F1EA}\u{1F1F8} rest"));
1185 buf.move_cursor(CursorMove::Jump(0, 0));
1186 buf.delete_str(3);
1187 assert_eq!(buf.rows(), &[" rest"]);
1188 }
1189
1190 #[test]
1191 fn inserting_before_a_combining_mark_keeps_the_cursor_addressable() {
1192 let mut buf = RopeBuffer::new(Text::from("\u{301}f"));
1196 buf.move_cursor(CursorMove::Jump(0, 0));
1197 buf.insert_char('a');
1198 assert_eq!(buf.rows(), &["a\u{301}f"]);
1199 }
1200}
1201
1202#[cfg(test)]
1203mod damage_tests {
1204 use super::*;
1205 use crate::ropetext::Text;
1206
1207 #[test]
1208 fn damage_from_several_edits_is_in_one_numbering() {
1209 let mut buf = RopeBuffer::new(Text::from("r0\nr1\nr2\nr3\nr4"));
1212 buf.edit(|b| {
1213 b.move_cursor(CursorMove::Jump(4, 0));
1214 b.insert_str("X");
1215 b.move_cursor(CursorMove::Jump(0, 0));
1216 b.insert_newline();
1217 });
1218 assert_eq!(buf.rows(), ["", "r0", "r1", "r2", "r3", "Xr4"]);
1219
1220 let damage = buf.take_outcome().damage.expect("the edits were reported");
1221 assert!(
1222 damage.contains(&5),
1223 "the row edited first is row 5 once the group ends, but the damage \
1224 reported was {damage:?} — a range in the older numbering"
1225 );
1226 }
1227}
1228
1229#[cfg(test)]
1230mod read_tests {
1231 use super::*;
1232 use crate::ropetext::Text;
1233
1234 #[test]
1235 fn text_between_slices_by_char_columns_across_rows_in_either_order() {
1236 let buf = RopeBuffer::new(Text::from("héllo🦀\nworld"));
1237 assert_eq!(
1238 buf.text_between((0, 1), (1, 2)).as_deref(),
1239 Some("éllo🦀\nwo")
1240 );
1241 assert_eq!(
1242 buf.text_between((1, 2), (0, 1)).as_deref(),
1243 Some("éllo🦀\nwo")
1244 );
1245 }
1246
1247 #[test]
1248 fn an_empty_or_unaddressable_range_is_none() {
1249 let buf = RopeBuffer::new(Text::from("abc"));
1250 assert_eq!(buf.text_between((0, 1), (0, 1)), None);
1251 assert_eq!(buf.text_between((0, 0), (7, 0)), None);
1252 }
1253
1254 #[test]
1255 fn selection_text_is_the_live_selection_or_none() {
1256 let mut buf = RopeBuffer::new(Text::from("hello world"));
1257 assert_eq!(buf.selection_text(), None);
1258 assert!(buf.set_selection((0, 0), (0, 5)));
1259 assert_eq!(buf.selection_text().as_deref(), Some("hello"));
1260 buf.start_selection();
1261 assert_eq!(
1262 buf.selection_text(),
1263 None,
1264 "a zero-width selection is not text"
1265 );
1266 }
1267}
1268
1269#[cfg(test)]
1270mod indent_tests {
1271 use super::*;
1272 use crate::ropetext::Text;
1273
1274 fn buffer(text: &str) -> RopeBuffer {
1275 RopeBuffer::new(Text::from(text))
1276 }
1277
1278 #[test]
1279 fn indent_inserts_one_step_of_spaces_never_a_tab() {
1280 let mut buf = buffer("foo\nbar");
1281 assert!(buf.indent_rows(0..=1, false));
1282 assert_eq!(buf.rows(), &[" foo", " bar"]);
1283 }
1284
1285 #[test]
1286 fn indent_follows_indent_width() {
1287 let mut buf = buffer("x");
1288 buf.set_indent_width(2);
1289 assert!(buf.indent_rows(0..=0, false));
1290 assert_eq!(buf.rows(), &[" x"]);
1291 }
1292
1293 #[test]
1294 fn dedent_removes_up_to_one_step_of_spaces() {
1295 let mut buf = buffer(" x\n y\nz");
1296 assert!(buf.indent_rows(0..=2, true));
1297 assert_eq!(buf.rows(), &[" x", "y", "z"]);
1298 }
1299
1300 #[test]
1301 fn dedent_counts_a_leading_tab_as_a_whole_step() {
1302 let mut buf = buffer("\t\tx\n \ty");
1303 assert!(buf.indent_rows(0..=1, true));
1304 assert_eq!(buf.rows(), &["\tx", "y"]);
1307 }
1308
1309 #[test]
1310 fn dedent_stops_at_a_space_that_carries_a_combining_mark() {
1311 let mut buf = buffer(" \u{301}foo");
1313 assert!(buf.set_selection((0, 3), (0, 6)));
1314 assert!(buf.indent_rows(0..=0, true));
1315 assert_eq!(buf.rows(), &[" \u{301}foo"]);
1316 assert_eq!(buf.selection_range(), Some(((0, 2), (0, 5))));
1317 }
1318
1319 #[test]
1320 fn dedent_with_nothing_to_remove_reports_no_change() {
1321 let mut buf = buffer("foo");
1322 assert!(!buf.indent_rows(0..=0, true));
1323 assert_eq!(buf.rows(), &["foo"]);
1324 assert!(!buf.take_outcome().changed);
1325 }
1326
1327 #[test]
1328 fn rows_past_the_end_are_ignored() {
1329 let mut buf = buffer("a\nb");
1330 assert!(buf.indent_rows(1..=9, false));
1331 assert_eq!(buf.rows(), &["a", " b"]);
1332 assert!(!buf.indent_rows(5..=9, false));
1333 assert_eq!(buf.rows(), &["a", " b"]);
1334 }
1335
1336 #[test]
1337 fn the_cursor_keeps_its_character() {
1338 let mut buf = buffer("one\ntwo");
1339 assert!(buf.jump_to(0, 1)); buf.indent_rows(0..=1, false);
1341 assert_eq!(buf.cursor(), (0, 1 + 4));
1342 buf.indent_rows(0..=1, true);
1343 assert_eq!(buf.cursor(), (0, 1));
1344 }
1345
1346 #[test]
1347 fn a_cursor_outside_the_rows_does_not_move() {
1348 let mut buf = buffer("a\nb\nc");
1349 assert!(buf.jump_to(2, 1));
1350 buf.indent_rows(0..=1, false);
1351 assert_eq!(buf.cursor(), (2, 1));
1352 }
1353
1354 #[test]
1355 fn a_dedent_never_pushes_the_cursor_below_column_zero() {
1356 let mut buf = buffer(" x");
1357 assert!(buf.jump_to(0, 2));
1358 buf.indent_rows(0..=0, true);
1359 assert_eq!(buf.cursor(), (0, 0));
1360 }
1361
1362 #[test]
1363 fn the_selection_is_put_back_shifted_with_its_rows() {
1364 let mut buf = buffer("hello world\nnext");
1365 assert!(buf.set_selection((0, 6), (1, 2)));
1366 buf.indent_rows(0..=1, false);
1367 assert_eq!(buf.selection_range(), Some(((0, 10), (1, 6))));
1368 assert_eq!(buf.rows()[0].trim_start(), "hello world");
1369 }
1370
1371 #[test]
1372 fn a_dedent_under_a_selection_shifts_each_endpoint_by_its_own_row() {
1373 let mut buf = buffer(" foo\n bar\nbaz");
1374 assert!(buf.set_selection((0, 4), (2, 3)));
1375 assert!(buf.indent_rows(0..=2, true));
1376 assert_eq!(buf.rows(), &["foo", "bar", "baz"]);
1377 assert_eq!(buf.selection_range(), Some(((0, 0), (2, 3))));
1378 }
1379
1380 #[test]
1381 fn a_block_indent_is_one_undo_group() {
1382 let mut buf = buffer("a\nb\nc");
1383 buf.indent_rows(0..=2, false);
1384 assert!(buf.undo(), "the block is one entry");
1385 assert_eq!(buf.rows(), &["a", "b", "c"]);
1386 assert!(!buf.undo(), "and has nothing left to take back");
1387 }
1388
1389 #[test]
1390 fn undo_and_redo_of_an_indent_land_the_cursor_on_its_own_character() {
1391 let mut buf = buffer("one\ntwo");
1392 assert!(buf.jump_to(0, 1)); buf.indent_rows(0..=1, false);
1394 assert_eq!(buf.cursor(), (0, 5));
1395 assert!(buf.undo());
1396 assert_eq!(buf.cursor(), (0, 1), "undo returns to where the user was");
1397 assert!(buf.redo());
1398 assert_eq!(
1399 buf.cursor(),
1400 (0, 5),
1401 "redo lands where the indent left them"
1402 );
1403 }
1404
1405 #[test]
1406 fn a_block_indent_is_one_transaction() {
1407 let mut buf = buffer("a\nb\nc");
1408 buf.indent_rows(0..=2, false);
1409 let outcome = buf.take_outcome();
1410 assert!(outcome.changed);
1411 assert!(outcome.bulk, "three rows in one change");
1412 assert!(buf.undo());
1413 assert_eq!(buf.rows(), &["a", "b", "c"]);
1414 assert!(!buf.undo());
1415 }
1416
1417 #[test]
1418 fn a_dedent_does_not_fill_the_yank_transport() {
1419 let mut buf = buffer(" x");
1420 buf.set_yank_text("kept");
1421 buf.indent_rows(0..=0, true);
1422 assert_eq!(buf.yank_text(), "kept");
1423 }
1424
1425 #[test]
1426 fn damage_covers_every_touched_row() {
1427 let mut buf = buffer("a\nb\nc\nd");
1428 buf.indent_rows(1..=2, false);
1429 let outcome = buf.take_outcome();
1430 assert!(outcome.changed);
1431 let damage = outcome.damage.expect("the edits were reported");
1432 assert!(damage.contains(&1) && damage.contains(&2), "{damage:?}");
1433 assert_eq!(outcome.line_delta, 0);
1434 }
1435}
1436
1437#[cfg(test)]
1438mod mark_tests {
1439 use super::*;
1440 use crate::ropetext::Text;
1441
1442 #[test]
1446 fn marks_follow_the_text() {
1447 let mut t = RopeBuffer::new(Text::from("one\ntwo"));
1448 assert!(t.set_mark('<', (1, 1)));
1449 t.jump_to(0, 0);
1450 t.insert_str("zero\n");
1451 assert_eq!(t.mark('<'), Some((2, 1)));
1452 t.jump_to(2, 0);
1453 t.insert_str("XX");
1454 assert_eq!(t.mark('<'), Some((2, 3)));
1455 t.jump_to(0, 0);
1456 t.start_selection();
1457 t.jump_to(1, 0);
1458 t.cut();
1459 assert_eq!(t.mark('<'), Some((1, 3)));
1460 }
1461
1462 #[test]
1464 fn marks_follow_undo_and_a_new_text_drops_them() {
1465 let mut t = RopeBuffer::new(Text::from("a\nb\nc"));
1466 t.set_mark('>', (2, 0));
1467 t.jump_to(0, 0);
1468 t.insert_str("x\n");
1469 assert_eq!(t.mark('>'), Some((3, 0)));
1470 t.undo();
1471 assert_eq!(t.mark('>'), Some((2, 0)));
1472 t.replace(Text::from("other"));
1473 assert_eq!(t.mark('>'), None);
1474 }
1475
1476 #[test]
1480 fn a_mark_undo_leaves_inside_a_character_moves_to_the_next() {
1481 let mut t = RopeBuffer::new(Text::from("ab❤️x"));
1482 t.jump_to(0, 2);
1483 t.start_selection();
1484 t.jump_to(0, 4);
1485 t.cut();
1486 assert_eq!(t.rows(), &["abx"]);
1487 assert!(t.set_mark('<', (0, 3)));
1488 t.undo();
1489 assert_eq!(t.rows(), &["ab❤️x"]);
1490 assert_eq!(t.mark('<'), Some((0, 4)));
1491 }
1492}