1use azul_core::selection::{
8 CursorAffinity, GraphemeClusterId, Selection, SelectionRange, TextCursor,
9};
10
11use crate::text3::cache::{InlineContent, StyledRun};
12
13#[derive(Debug, Clone)]
15pub enum TextEdit {
16 Insert(String),
18 DeleteBackward,
20 DeleteForward,
22}
23
24const fn selection_start_run(selection: &Selection) -> u32 {
25 match selection {
26 Selection::Cursor(c) => c.cluster_id.source_run,
27 Selection::Range(r) => r.start.cluster_id.source_run,
28 }
29}
30
31const fn selection_start_byte(selection: &Selection) -> u32 {
32 match selection {
33 Selection::Cursor(c) => c.cluster_id.start_byte_in_run,
34 Selection::Range(r) => r.start.cluster_id.start_byte_in_run,
35 }
36}
37
38fn sort_selections_back_to_front(selections: &[Selection]) -> Vec<Selection> {
42 let mut sorted = selections.to_vec();
43 sorted.sort_by(|a, b| {
44 let cursor_a = match a {
45 Selection::Cursor(c) => c,
46 Selection::Range(r) => &r.start,
47 };
48 let cursor_b = match b {
49 Selection::Cursor(c) => c,
50 Selection::Range(r) => &r.start,
51 };
52 cursor_b.cluster_id.cmp(&cursor_a.cluster_id) });
54 sorted
55}
56
57#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn adjust_cursors(
61 selections: &mut [Selection],
62 edit_run: u32,
63 edit_byte: u32,
64 byte_offset_change: i32,
65) {
66 for sel in selections.iter_mut() {
67 if let Selection::Cursor(cursor) = sel {
68 if cursor.cluster_id.source_run == edit_run
69 && cursor.cluster_id.start_byte_in_run >= edit_byte
70 {
71 cursor.cluster_id.start_byte_in_run =
72 (cursor.cluster_id.start_byte_in_run as i32 + byte_offset_change).max(0) as u32;
73 }
74 }
75 }
76}
77
78#[allow(clippy::cast_possible_wrap, clippy::cast_sign_loss)] fn adjust_cursor_runs(selections: &mut [Selection], boundary_run: u32, run_count_change: i32) {
90 if run_count_change == 0 {
91 return;
92 }
93 for sel in selections.iter_mut() {
94 if let Selection::Cursor(cursor) = sel {
95 if cursor.cluster_id.source_run > boundary_run {
96 let shifted = (cursor.cluster_id.source_run as i32 + run_count_change)
97 .max(boundary_run as i32);
98 cursor.cluster_id.source_run = shifted as u32;
99 }
100 }
101 }
102}
103
104fn run_text_len(content: &[InlineContent], run_idx: u32) -> usize {
106 match content.get(run_idx as usize) {
107 Some(InlineContent::Text(run)) => run.text.len(),
108 _ => 0,
109 }
110}
111
112#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[must_use] pub fn edit_text(
116 content: &[InlineContent],
117 selections: &[Selection],
118 edit: &TextEdit,
119) -> (Vec<InlineContent>, Vec<Selection>) {
120 if selections.is_empty() {
121 return (content.to_vec(), Vec::new());
122 }
123
124 let mut new_content = content.to_vec();
125 let mut new_selections = Vec::new();
126
127 let sorted_selections = sort_selections_back_to_front(selections);
131
132 for selection in sorted_selections {
133 let edit_run = selection_start_run(&selection);
134 let edit_byte = selection_start_byte(&selection);
135
136 let old_run_len = run_text_len(&new_content, edit_run);
140 let old_run_count = new_content.len();
141 let (temp_content, new_cursor) =
142 apply_edit_to_selection(&new_content, &selection, edit);
143 let new_run_len = run_text_len(&temp_content, edit_run);
144 let byte_offset_change = new_run_len as i32 - old_run_len as i32;
145 let run_count_change = temp_content.len() as i32 - old_run_count as i32;
146
147 adjust_cursors(&mut new_selections, edit_run, edit_byte, byte_offset_change);
149 adjust_cursor_runs(&mut new_selections, edit_run, run_count_change);
152
153 new_content = temp_content;
154 new_selections.push(Selection::Cursor(new_cursor));
155 }
156
157 new_selections.reverse();
159
160 (new_content, new_selections)
161}
162
163#[must_use] pub fn apply_edit_to_selection(
171 content: &[InlineContent],
172 selection: &Selection,
173 edit: &TextEdit,
174) -> (Vec<InlineContent>, TextCursor) {
175 let mut new_content = content.to_vec();
176
177 match selection {
178 Selection::Range(range) => {
179 let (content_after_delete, cursor_pos) = delete_range(&new_content, range);
181 match edit {
182 TextEdit::Insert(text_to_insert) => {
184 let mut c = content_after_delete;
185 insert_text(&c, &cursor_pos, text_to_insert)
186 }
187 TextEdit::DeleteBackward | TextEdit::DeleteForward => {
189 (content_after_delete, cursor_pos)
190 }
191 }
192 }
193 Selection::Cursor(cursor) => {
194 match edit {
195 TextEdit::Insert(text_to_insert) => {
196 insert_text(&new_content, cursor, text_to_insert)
197 }
198 TextEdit::DeleteBackward => delete_backward(&new_content, cursor),
199 TextEdit::DeleteForward => delete_forward(&new_content, cursor),
200 }
201 }
202 }
203}
204
205pub(crate) fn cursor_byte_offset_in_run(text: &str, cursor: &TextCursor) -> usize {
212 use unicode_segmentation::UnicodeSegmentation;
213 let csb = cursor.cluster_id.start_byte_in_run as usize;
214 match cursor.affinity {
215 CursorAffinity::Leading => csb.min(text.len()),
216 CursorAffinity::Trailing => {
217 if csb >= text.len() {
218 text.len()
219 } else {
220 text[csb..]
221 .grapheme_indices(true)
222 .next()
223 .map_or(text.len(), |(_, g)| csb + g.len())
224 }
225 }
226 }
227}
228
229#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn delete_range(
242 content: &[InlineContent],
243 range: &SelectionRange,
244) -> (Vec<InlineContent>, TextCursor) {
245 let mut new_content = content.to_vec();
246 let start_run_idx = range.start.cluster_id.source_run as usize;
247 let end_run_idx = range.end.cluster_id.source_run as usize;
248
249 let mut cursor_after = range.start;
255 if start_run_idx == end_run_idx {
256 if let Some(InlineContent::Text(run)) = new_content.get_mut(start_run_idx) {
257 let a = cursor_byte_offset_in_run(&run.text, &range.start);
258 let b = cursor_byte_offset_in_run(&run.text, &range.end);
259 let lo = a.min(b);
260 let hi = a.max(b);
261 if hi <= run.text.len() && lo < hi {
262 run.text.drain(lo..hi);
263 cursor_after = TextCursor {
266 cluster_id: GraphemeClusterId {
267 source_run: start_run_idx as u32,
268 start_byte_in_run: lo as u32,
269 },
270 affinity: CursorAffinity::Leading,
271 };
272 }
273 } else if start_run_idx < new_content.len() && range.start != range.end {
274 new_content.remove(start_run_idx);
279 cursor_after = TextCursor {
280 cluster_id: GraphemeClusterId {
281 source_run: start_run_idx as u32,
282 start_byte_in_run: 0,
283 },
284 affinity: CursorAffinity::Leading,
285 };
286 }
287 } else {
288 let (lo_run, lo_cursor, hi_run, hi_cursor) = if start_run_idx <= end_run_idx {
297 (start_run_idx, range.start, end_run_idx, range.end)
298 } else {
299 (end_run_idx, range.end, start_run_idx, range.start)
300 };
301
302 let lo_byte = match new_content.get(lo_run) {
305 Some(InlineContent::Text(run)) => cursor_byte_offset_in_run(&run.text, &lo_cursor),
306 _ => 0,
307 };
308 let hi_byte = match new_content.get(hi_run) {
309 Some(InlineContent::Text(run)) => cursor_byte_offset_in_run(&run.text, &hi_cursor),
310 _ => 0,
311 };
312
313 let head_len = if let Some(InlineContent::Text(run)) = new_content.get_mut(lo_run) {
316 let cut = lo_byte.min(run.text.len());
317 run.text.truncate(cut);
318 cut
319 } else {
320 0
321 };
322
323 if let Some(InlineContent::Text(run)) = new_content.get_mut(hi_run) {
325 let cut = hi_byte.min(run.text.len());
326 run.text.drain(..cut);
327 }
328
329 let drain_end = hi_run.min(new_content.len());
333 if drain_end > lo_run + 1 {
334 new_content.drain((lo_run + 1)..drain_end);
335 }
336 let tail_idx = lo_run + 1;
337
338 let mergeable = matches!(
343 (new_content.get(lo_run), new_content.get(tail_idx)),
344 (Some(InlineContent::Text(a)), Some(InlineContent::Text(b)))
345 if a.style == b.style
346 );
347 if mergeable {
348 if let InlineContent::Text(tail) = new_content.remove(tail_idx) {
349 if let Some(InlineContent::Text(head)) = new_content.get_mut(lo_run) {
350 head.text.push_str(&tail.text);
351 }
352 }
353 }
354
355 cursor_after = TextCursor {
357 cluster_id: GraphemeClusterId {
358 source_run: lo_run as u32,
359 start_byte_in_run: head_len as u32,
360 },
361 affinity: CursorAffinity::Leading,
362 };
363 }
364
365 (new_content, cursor_after) }
367
368#[allow(clippy::cast_possible_truncation)] #[must_use]
375pub fn insert_text(
376 content: &[InlineContent],
377 cursor: &TextCursor,
378 text_to_insert: &str,
379) -> (Vec<InlineContent>, TextCursor) {
380 use unicode_segmentation::UnicodeSegmentation;
381
382 let mut new_content = content.to_vec();
383 let run_idx = cursor.cluster_id.source_run as usize;
384 let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
385
386 if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
387 let byte_offset = match cursor.affinity {
389 CursorAffinity::Leading => {
390 cluster_start_byte
392 },
393 CursorAffinity::Trailing => {
394 if cluster_start_byte >= run.text.len() {
397 run.text.len()
399 } else {
400 run.text[cluster_start_byte..]
402 .grapheme_indices(true)
403 .next()
404 .map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
405 }
406 },
407 };
408
409 if byte_offset <= run.text.len() {
410 run.text.insert_str(byte_offset, text_to_insert);
411
412 let new_cursor = TextCursor {
413 cluster_id: GraphemeClusterId {
414 source_run: run_idx as u32,
415 start_byte_in_run: (byte_offset + text_to_insert.len()) as u32,
416 },
417 affinity: CursorAffinity::Leading,
418 };
419 return (new_content, new_cursor);
420 }
421 }
422
423 (content.to_vec(), *cursor)
425}
426
427#[allow(clippy::cast_possible_truncation)] #[allow(clippy::too_many_lines)] #[must_use]
435pub fn delete_backward(
436 content: &[InlineContent],
437 cursor: &TextCursor,
438) -> (Vec<InlineContent>, TextCursor) {
439 use unicode_segmentation::UnicodeSegmentation;
440 let mut new_content = content.to_vec();
441 let run_idx = cursor.cluster_id.source_run as usize;
442 let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
443
444 if new_content.get(run_idx).is_some()
447 && !matches!(new_content.get(run_idx), Some(InlineContent::Text(_)))
448 {
449 return match cursor.affinity {
450 CursorAffinity::Trailing => {
452 new_content.remove(run_idx);
453 (
454 new_content,
455 TextCursor {
456 cluster_id: GraphemeClusterId {
457 source_run: run_idx as u32,
458 start_byte_in_run: 0,
459 },
460 affinity: CursorAffinity::Leading,
461 },
462 )
463 }
464 CursorAffinity::Leading if run_idx > 0 => {
466 let prev_byte = match content.get(run_idx - 1) {
467 Some(InlineContent::Text(r)) => r.text.len() as u32,
468 _ => 0,
469 };
470 delete_backward(
471 content,
472 &TextCursor {
473 cluster_id: GraphemeClusterId {
474 source_run: (run_idx - 1) as u32,
475 start_byte_in_run: prev_byte,
476 },
477 affinity: CursorAffinity::Trailing,
478 },
479 )
480 }
481 CursorAffinity::Leading => (content.to_vec(), *cursor),
482 };
483 }
484
485 if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
486 let byte_offset = match cursor.affinity {
488 CursorAffinity::Leading => cluster_start_byte,
489 CursorAffinity::Trailing => {
490 if cluster_start_byte >= run.text.len() {
492 run.text.len()
493 } else {
494 run.text[cluster_start_byte..]
495 .grapheme_indices(true)
496 .next()
497 .map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
498 }
499 },
500 };
501
502 if byte_offset > 0 {
503 let prev_grapheme_start = run.text[..byte_offset]
504 .grapheme_indices(true)
505 .next_back()
506 .map_or(0, |(i, _)| i);
507 run.text.drain(prev_grapheme_start..byte_offset);
508
509 let new_cursor = TextCursor {
510 cluster_id: GraphemeClusterId {
511 source_run: run_idx as u32,
512 start_byte_in_run: prev_grapheme_start as u32,
513 },
514 affinity: CursorAffinity::Leading,
515 };
516 return (new_content, new_cursor);
517 } else if run_idx > 0 {
518 match content.get(run_idx - 1).cloned() {
520 Some(InlineContent::Text(prev_run)) => {
522 let mut merged_text = prev_run.text;
523 let new_cursor_byte_offset = merged_text.len();
524 merged_text.push_str(&run.text);
525
526 new_content[run_idx - 1] = InlineContent::Text(StyledRun {
527 text: merged_text,
528 style: prev_run.style,
529 logical_start_byte: prev_run.logical_start_byte,
530 source_node_id: prev_run.source_node_id,
531 });
532 new_content.remove(run_idx);
533
534 let new_cursor = TextCursor {
535 cluster_id: GraphemeClusterId {
536 source_run: (run_idx - 1) as u32,
537 start_byte_in_run: new_cursor_byte_offset as u32,
538 },
539 affinity: CursorAffinity::Leading,
540 };
541 return (new_content, new_cursor);
542 }
543 Some(_) => {
545 new_content.remove(run_idx - 1);
546 let new_cursor = TextCursor {
547 cluster_id: GraphemeClusterId {
548 source_run: (run_idx - 1) as u32,
549 start_byte_in_run: 0,
550 },
551 affinity: CursorAffinity::Leading,
552 };
553 return (new_content, new_cursor);
554 }
555 None => {}
556 }
557 }
558 }
559
560 (content.to_vec(), *cursor)
561}
562
563#[allow(clippy::cast_possible_truncation)] #[must_use]
570pub fn delete_forward(
571 content: &[InlineContent],
572 cursor: &TextCursor,
573) -> (Vec<InlineContent>, TextCursor) {
574 use unicode_segmentation::UnicodeSegmentation;
575 let mut new_content = content.to_vec();
576 let run_idx = cursor.cluster_id.source_run as usize;
577 let cluster_start_byte = cursor.cluster_id.start_byte_in_run as usize;
578
579 if new_content.get(run_idx).is_some()
581 && !matches!(new_content.get(run_idx), Some(InlineContent::Text(_)))
582 {
583 return match cursor.affinity {
584 CursorAffinity::Leading => {
586 new_content.remove(run_idx);
587 (
588 new_content,
589 TextCursor {
590 cluster_id: GraphemeClusterId {
591 source_run: run_idx as u32,
592 start_byte_in_run: 0,
593 },
594 affinity: CursorAffinity::Leading,
595 },
596 )
597 }
598 CursorAffinity::Trailing if run_idx + 1 < content.len() => delete_forward(
600 content,
601 &TextCursor {
602 cluster_id: GraphemeClusterId {
603 source_run: (run_idx + 1) as u32,
604 start_byte_in_run: 0,
605 },
606 affinity: CursorAffinity::Leading,
607 },
608 ),
609 CursorAffinity::Trailing => (content.to_vec(), *cursor),
610 };
611 }
612
613 if let Some(InlineContent::Text(run)) = new_content.get_mut(run_idx) {
614 let byte_offset = match cursor.affinity {
616 CursorAffinity::Leading => cluster_start_byte,
617 CursorAffinity::Trailing => {
618 if cluster_start_byte >= run.text.len() {
620 run.text.len()
621 } else {
622 run.text[cluster_start_byte..]
623 .grapheme_indices(true)
624 .next()
625 .map_or(run.text.len(), |(_, grapheme)| cluster_start_byte + grapheme.len())
626 }
627 },
628 };
629
630 if byte_offset < run.text.len() {
631 let next_grapheme_end = run.text[byte_offset..]
632 .grapheme_indices(true)
633 .nth(1)
634 .map_or(run.text.len(), |(i, _)| byte_offset + i);
635 run.text.drain(byte_offset..next_grapheme_end);
636
637 let new_cursor = TextCursor {
639 cluster_id: GraphemeClusterId {
640 source_run: run_idx as u32,
641 start_byte_in_run: byte_offset as u32,
642 },
643 affinity: CursorAffinity::Leading,
644 };
645 return (new_content, new_cursor);
646 } else if run_idx < content.len() - 1 {
647 match content.get(run_idx + 1).cloned() {
649 Some(InlineContent::Text(next_run)) => {
651 let mut merged_text = run.text.clone();
652 merged_text.push_str(&next_run.text);
653
654 new_content[run_idx] = InlineContent::Text(StyledRun {
655 text: merged_text,
656 style: run.style.clone(),
657 logical_start_byte: run.logical_start_byte,
658 source_node_id: run.source_node_id,
659 });
660 new_content.remove(run_idx + 1);
661
662 return (new_content, *cursor);
663 }
664 Some(_) => {
666 new_content.remove(run_idx + 1);
667 return (new_content, *cursor);
668 }
669 None => {}
670 }
671 }
672 }
673
674 (content.to_vec(), *cursor)
675}
676
677#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)] #[must_use] pub fn edit_text_multi(
687 content: &[InlineContent],
688 selections: &[Selection],
689 texts: &[&str],
690) -> (Vec<InlineContent>, Vec<Selection>) {
691 assert_eq!(
692 selections.len(),
693 texts.len(),
694 "edit_text_multi: selections and texts must have the same length"
695 );
696
697 if selections.is_empty() {
698 return (content.to_vec(), Vec::new());
699 }
700
701 let mut new_content = content.to_vec();
702 let mut new_selections = Vec::new();
703
704 let mut pairs: Vec<(Selection, &str)> = selections
706 .iter()
707 .copied()
708 .zip(texts.iter().copied())
709 .collect();
710 pairs.sort_by(|a, b| {
711 let cursor_a = match &a.0 {
712 Selection::Cursor(c) => c,
713 Selection::Range(r) => &r.start,
714 };
715 let cursor_b = match &b.0 {
716 Selection::Cursor(c) => c,
717 Selection::Range(r) => &r.start,
718 };
719 cursor_b.cluster_id.cmp(&cursor_a.cluster_id) });
721
722 for (selection, text) in &pairs {
723 let edit = TextEdit::Insert((*text).to_string());
724
725 let edit_run = selection_start_run(selection);
726 let edit_byte = selection_start_byte(selection);
727
728 let old_run_len = run_text_len(&new_content, edit_run);
729 let old_run_count = new_content.len();
730 let (temp_content, new_cursor) =
731 apply_edit_to_selection(&new_content, selection, &edit);
732 let new_run_len = run_text_len(&temp_content, edit_run);
733 let byte_offset_change = new_run_len as i32 - old_run_len as i32;
734 let run_count_change = temp_content.len() as i32 - old_run_count as i32;
735
736 adjust_cursors(&mut new_selections, edit_run, edit_byte, byte_offset_change);
737 adjust_cursor_runs(&mut new_selections, edit_run, run_count_change);
738
739 new_content = temp_content;
740 new_selections.push(Selection::Cursor(new_cursor));
741 }
742
743 new_selections.reverse();
744 (new_content, new_selections)
745}
746
747#[must_use] pub fn inspect_delete(
753 content: &[InlineContent],
754 selection: &Selection,
755 forward: bool,
756) -> Option<(SelectionRange, String)> {
757 match selection {
758 Selection::Range(range) => {
759 let deleted_text = extract_text_in_range(content, range);
761 Some((*range, deleted_text))
762 }
763 Selection::Cursor(cursor) => {
764 if forward {
766 inspect_delete_forward(content, cursor)
767 } else {
768 inspect_delete_backward(content, cursor)
769 }
770 }
771 }
772}
773
774#[allow(clippy::cast_possible_truncation)] fn inspect_delete_forward(
777 content: &[InlineContent],
778 cursor: &TextCursor,
779) -> Option<(SelectionRange, String)> {
780 use unicode_segmentation::UnicodeSegmentation;
781
782 let run_idx = cursor.cluster_id.source_run as usize;
783
784 if let Some(InlineContent::Text(run)) = content.get(run_idx) {
785 let byte_offset = cursor_byte_offset_in_run(&run.text, cursor);
788 if byte_offset < run.text.len() {
789 let next_grapheme_end = run.text[byte_offset..]
791 .grapheme_indices(true)
792 .nth(1)
793 .map_or(run.text.len(), |(i, _)| byte_offset + i);
794
795 let deleted_text = run.text[byte_offset..next_grapheme_end].to_string();
796
797 let range = SelectionRange {
798 start: *cursor,
799 end: TextCursor {
800 cluster_id: GraphemeClusterId {
801 source_run: run_idx as u32,
802 start_byte_in_run: next_grapheme_end as u32,
803 },
804 affinity: CursorAffinity::Leading,
805 },
806 };
807
808 return Some((range, deleted_text));
809 } else if run_idx < content.len() - 1 {
810 if let Some(InlineContent::Text(next_run)) = content.get(run_idx + 1) {
812 let deleted_text = next_run.text.graphemes(true).next()?.to_string();
813
814 let next_grapheme_end = next_run
815 .text
816 .grapheme_indices(true)
817 .nth(1)
818 .map_or(next_run.text.len(), |(i, _)| i);
819
820 let range = SelectionRange {
821 start: *cursor,
822 end: TextCursor {
823 cluster_id: GraphemeClusterId {
824 source_run: (run_idx + 1) as u32,
825 start_byte_in_run: next_grapheme_end as u32,
826 },
827 affinity: CursorAffinity::Leading,
828 },
829 };
830
831 return Some((range, deleted_text));
832 }
833 }
834 }
835
836 None }
838
839#[allow(clippy::cast_possible_truncation)] fn inspect_delete_backward(
842 content: &[InlineContent],
843 cursor: &TextCursor,
844) -> Option<(SelectionRange, String)> {
845 use unicode_segmentation::UnicodeSegmentation;
846
847 let run_idx = cursor.cluster_id.source_run as usize;
848
849 if let Some(InlineContent::Text(run)) = content.get(run_idx) {
850 let byte_offset = cursor_byte_offset_in_run(&run.text, cursor);
853 if byte_offset > 0 {
854 let prev_grapheme_start = run.text[..byte_offset]
856 .grapheme_indices(true)
857 .next_back()
858 .map_or(0, |(i, _)| i);
859
860 let deleted_text = run.text[prev_grapheme_start..byte_offset].to_string();
861
862 let range = SelectionRange {
863 start: TextCursor {
864 cluster_id: GraphemeClusterId {
865 source_run: run_idx as u32,
866 start_byte_in_run: prev_grapheme_start as u32,
867 },
868 affinity: CursorAffinity::Leading,
869 },
870 end: *cursor,
871 };
872
873 return Some((range, deleted_text));
874 } else if run_idx > 0 {
875 if let Some(InlineContent::Text(prev_run)) = content.get(run_idx - 1) {
877 let deleted_text = prev_run.text.graphemes(true).next_back()?.to_string();
878
879 let prev_grapheme_start = prev_run.text[..]
880 .grapheme_indices(true)
881 .next_back()
882 .map_or(0, |(i, _)| i);
883
884 let range = SelectionRange {
885 start: TextCursor {
886 cluster_id: GraphemeClusterId {
887 source_run: (run_idx - 1) as u32,
888 start_byte_in_run: prev_grapheme_start as u32,
889 },
890 affinity: CursorAffinity::Leading,
891 },
892 end: *cursor,
893 };
894
895 return Some((range, deleted_text));
896 }
897 }
898 }
899
900 None }
902
903fn extract_text_in_range(content: &[InlineContent], range: &SelectionRange) -> String {
905 let start_run = range.start.cluster_id.source_run as usize;
906 let end_run = range.end.cluster_id.source_run as usize;
907 let start_byte = range.start.cluster_id.start_byte_in_run as usize;
908 let end_byte = range.end.cluster_id.start_byte_in_run as usize;
909
910 if start_run == end_run {
911 if let Some(InlineContent::Text(run)) = content.get(start_run) {
913 if start_byte <= end_byte && end_byte <= run.text.len() {
914 return run.text[start_byte..end_byte].to_string();
915 }
916 }
917 } else {
918 let mut result = String::new();
920
921 for (idx, item) in content.iter().enumerate() {
922 if let InlineContent::Text(run) = item {
923 if idx == start_run {
924 if start_byte < run.text.len() {
926 result.push_str(&run.text[start_byte..]);
927 }
928 } else if idx > start_run && idx < end_run {
929 result.push_str(&run.text);
931 } else if idx == end_run {
932 if end_byte <= run.text.len() {
934 result.push_str(&run.text[..end_byte]);
935 }
936 break;
937 }
938 }
939 }
940
941 return result;
942 }
943
944 String::new()
945}
946
947#[cfg(test)]
948#[allow(clippy::float_cmp, clippy::too_many_lines)]
949mod autotest_generated {
950 use std::sync::Arc;
951
952 use unicode_segmentation::UnicodeSegmentation;
953
954 use super::*;
955 use crate::text3::cache::StyleProperties;
956
957 fn style_a() -> Arc<StyleProperties> {
960 Arc::new(StyleProperties::default())
961 }
962
963 fn style_b() -> Arc<StyleProperties> {
966 Arc::new(StyleProperties {
967 font_size_px: 99.0,
968 ..StyleProperties::default()
969 })
970 }
971
972 fn text(s: &str) -> InlineContent {
973 InlineContent::Text(StyledRun {
974 text: s.to_string(),
975 style: style_a(),
976 logical_start_byte: 0,
977 source_node_id: None,
978 })
979 }
980
981 fn text_styled(s: &str, style: Arc<StyleProperties>) -> InlineContent {
982 InlineContent::Text(StyledRun {
983 text: s.to_string(),
984 style,
985 logical_start_byte: 0,
986 source_node_id: None,
987 })
988 }
989
990 fn obj() -> InlineContent {
993 InlineContent::Tab { style: style_a() }
994 }
995
996 fn dump(content: &[InlineContent]) -> Vec<String> {
999 content
1000 .iter()
1001 .map(|c| match c {
1002 InlineContent::Text(r) => r.text.clone(),
1003 _ => "<obj>".to_string(),
1004 })
1005 .collect()
1006 }
1007
1008 fn lead(run: u32, byte: u32) -> TextCursor {
1009 TextCursor {
1010 cluster_id: GraphemeClusterId {
1011 source_run: run,
1012 start_byte_in_run: byte,
1013 },
1014 affinity: CursorAffinity::Leading,
1015 }
1016 }
1017
1018 fn trail(run: u32, byte: u32) -> TextCursor {
1019 TextCursor {
1020 cluster_id: GraphemeClusterId {
1021 source_run: run,
1022 start_byte_in_run: byte,
1023 },
1024 affinity: CursorAffinity::Trailing,
1025 }
1026 }
1027
1028 fn range_sel(start: TextCursor, end: TextCursor) -> Selection {
1029 Selection::Range(SelectionRange { start, end })
1030 }
1031
1032 fn cursor_of(sel: &Selection) -> TextCursor {
1033 match sel {
1034 Selection::Cursor(c) => *c,
1035 Selection::Range(r) => r.start,
1036 }
1037 }
1038
1039 const FAMILY: &str = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}";
1042
1043 #[test]
1044 fn family_constant_is_one_grapheme_of_18_bytes() {
1045 assert_eq!(FAMILY.len(), 18);
1047 assert_eq!(FAMILY.graphemes(true).count(), 1);
1048 }
1049
1050 #[test]
1053 fn selection_start_run_and_byte_read_the_cursor() {
1054 let sel = Selection::Cursor(lead(3, 7));
1055 assert_eq!(selection_start_run(&sel), 3);
1056 assert_eq!(selection_start_byte(&sel), 7);
1057 }
1058
1059 #[test]
1060 fn selection_start_run_and_byte_read_range_start_not_end() {
1061 let sel = range_sel(lead(9, 40), lead(1, 2));
1063 assert_eq!(selection_start_run(&sel), 9);
1064 assert_eq!(selection_start_byte(&sel), 40);
1065 }
1066
1067 #[test]
1068 fn selection_start_accessors_survive_u32_max() {
1069 let sel = Selection::Cursor(trail(u32::MAX, u32::MAX));
1070 assert_eq!(selection_start_run(&sel), u32::MAX);
1071 assert_eq!(selection_start_byte(&sel), u32::MAX);
1072
1073 let sel = range_sel(lead(u32::MAX, u32::MAX), lead(0, 0));
1074 assert_eq!(selection_start_run(&sel), u32::MAX);
1075 assert_eq!(selection_start_byte(&sel), u32::MAX);
1076 }
1077
1078 #[test]
1081 fn sort_back_to_front_empty_and_single() {
1082 assert!(sort_selections_back_to_front(&[]).is_empty());
1083 let one = [Selection::Cursor(lead(0, 0))];
1084 assert_eq!(sort_selections_back_to_front(&one).len(), 1);
1085 }
1086
1087 #[test]
1088 fn sort_back_to_front_is_descending_by_cluster_id() {
1089 let sels = [
1090 Selection::Cursor(lead(0, 0)),
1091 Selection::Cursor(lead(2, 5)),
1092 Selection::Cursor(lead(1, 3)),
1093 Selection::Cursor(lead(2, 1)),
1094 ];
1095 let sorted = sort_selections_back_to_front(&sels);
1096 let keys: Vec<(u32, u32)> = sorted
1097 .iter()
1098 .map(|s| {
1099 let c = cursor_of(s).cluster_id;
1100 (c.source_run, c.start_byte_in_run)
1101 })
1102 .collect();
1103 assert_eq!(keys, vec![(2, 5), (2, 1), (1, 3), (0, 0)]);
1104 assert!(keys.windows(2).all(|w| w[0] >= w[1]));
1107 }
1108
1109 #[test]
1110 fn sort_back_to_front_is_a_permutation_with_duplicates() {
1111 let sels = [
1112 Selection::Cursor(lead(1, 1)),
1113 Selection::Cursor(lead(1, 1)),
1114 Selection::Cursor(lead(0, 0)),
1115 ];
1116 let sorted = sort_selections_back_to_front(&sels);
1117 assert_eq!(sorted.len(), 3);
1118 let mut got: Vec<Selection> = sorted;
1119 let mut want: Vec<Selection> = sels.to_vec();
1120 got.sort();
1121 want.sort();
1122 assert_eq!(got, want);
1123 }
1124
1125 #[test]
1126 fn sort_back_to_front_keys_ranges_on_their_start() {
1127 let sels = [
1129 Selection::Cursor(lead(0, 0)),
1130 range_sel(lead(5, 0), lead(0, 0)),
1131 ];
1132 let sorted = sort_selections_back_to_front(&sels);
1133 assert!(matches!(sorted[0], Selection::Range(_)));
1134 assert!(matches!(sorted[1], Selection::Cursor(_)));
1135 }
1136
1137 #[test]
1138 fn sort_back_to_front_handles_u32_max_keys() {
1139 let sels = [
1140 Selection::Cursor(lead(u32::MAX, u32::MAX)),
1141 Selection::Cursor(lead(0, 0)),
1142 ];
1143 let sorted = sort_selections_back_to_front(&sels);
1144 assert_eq!(cursor_of(&sorted[0]).cluster_id.source_run, u32::MAX);
1145 }
1146
1147 fn byte_at(sels: &[Selection], i: usize) -> u32 {
1150 cursor_of(&sels[i]).cluster_id.start_byte_in_run
1151 }
1152
1153 #[test]
1154 fn adjust_cursors_empty_slice_is_a_noop() {
1155 let mut sels: Vec<Selection> = Vec::new();
1156 adjust_cursors(&mut sels, 0, 0, i32::MIN);
1157 assert!(sels.is_empty());
1158 }
1159
1160 #[test]
1161 fn adjust_cursors_zero_change_leaves_everything_alone() {
1162 let mut sels = vec![
1163 Selection::Cursor(lead(0, 0)),
1164 Selection::Cursor(lead(0, 10)),
1165 ];
1166 adjust_cursors(&mut sels, 0, 0, 0);
1167 assert_eq!(byte_at(&sels, 0), 0);
1168 assert_eq!(byte_at(&sels, 1), 10);
1169 }
1170
1171 #[test]
1172 fn adjust_cursors_only_shifts_at_or_after_edit_byte_in_the_edit_run() {
1173 let mut sels = vec![
1174 Selection::Cursor(lead(0, 2)), Selection::Cursor(lead(0, 5)), Selection::Cursor(lead(0, 9)), Selection::Cursor(lead(1, 0)), ];
1179 adjust_cursors(&mut sels, 0, 5, 3);
1180 assert_eq!(byte_at(&sels, 0), 2);
1181 assert_eq!(byte_at(&sels, 1), 8);
1182 assert_eq!(byte_at(&sels, 2), 12);
1183 assert_eq!(byte_at(&sels, 3), 0);
1184 }
1185
1186 #[test]
1187 fn adjust_cursors_negative_change_clamps_at_zero() {
1188 let mut sels = vec![Selection::Cursor(lead(0, 3))];
1189 adjust_cursors(&mut sels, 0, 0, -100);
1190 assert_eq!(byte_at(&sels, 0), 0, "documented clamp-to-zero");
1191 }
1192
1193 #[test]
1194 fn adjust_cursors_i32_extremes_do_not_panic() {
1195 let mut sels = vec![Selection::Cursor(lead(0, 0))];
1197 adjust_cursors(&mut sels, 0, 0, i32::MAX);
1198 assert_eq!(byte_at(&sels, 0), i32::MAX as u32);
1199
1200 let mut sels = vec![Selection::Cursor(lead(0, 0))];
1202 adjust_cursors(&mut sels, 0, 0, i32::MIN);
1203 assert_eq!(byte_at(&sels, 0), 0);
1204 }
1205
1206 #[test]
1207 fn adjust_cursors_u32_max_byte_collapses_to_zero() {
1208 let mut sels = vec![Selection::Cursor(lead(0, u32::MAX))];
1212 adjust_cursors(&mut sels, 0, 0, 0);
1213 assert_eq!(byte_at(&sels, 0), 0);
1214 }
1215
1216 #[test]
1217 fn adjust_cursors_never_touches_range_selections() {
1218 let mut sels = vec![range_sel(lead(0, 4), lead(0, 8))];
1219 adjust_cursors(&mut sels, 0, 0, 100);
1220 match &sels[0] {
1221 Selection::Range(r) => {
1222 assert_eq!(r.start.cluster_id.start_byte_in_run, 4);
1223 assert_eq!(r.end.cluster_id.start_byte_in_run, 8);
1224 }
1225 Selection::Cursor(_) => panic!("range must stay a range"),
1226 }
1227 }
1228
1229 fn run_at(sels: &[Selection], i: usize) -> u32 {
1232 cursor_of(&sels[i]).cluster_id.source_run
1233 }
1234
1235 #[test]
1236 fn adjust_cursor_runs_zero_change_returns_early() {
1237 let mut sels = vec![Selection::Cursor(lead(u32::MAX, 0))];
1238 adjust_cursor_runs(&mut sels, 0, 0);
1239 assert_eq!(run_at(&sels, 0), u32::MAX, "zero change must not touch runs");
1240 }
1241
1242 #[test]
1243 fn adjust_cursor_runs_shifts_only_runs_strictly_after_the_boundary() {
1244 let mut sels = vec![
1245 Selection::Cursor(lead(0, 0)), Selection::Cursor(lead(1, 0)), Selection::Cursor(lead(2, 0)), Selection::Cursor(lead(3, 0)), ];
1250 adjust_cursor_runs(&mut sels, 1, -1);
1251 assert_eq!(run_at(&sels, 0), 0);
1252 assert_eq!(run_at(&sels, 1), 1);
1253 assert_eq!(run_at(&sels, 2), 1);
1254 assert_eq!(run_at(&sels, 3), 2);
1255 }
1256
1257 #[test]
1258 fn adjust_cursor_runs_positive_change_shifts_up() {
1259 let mut sels = vec![Selection::Cursor(lead(2, 0))];
1260 adjust_cursor_runs(&mut sels, 0, 3);
1261 assert_eq!(run_at(&sels, 0), 5);
1262 }
1263
1264 #[test]
1265 fn adjust_cursor_runs_negative_overshoot_clamps_to_the_boundary_run() {
1266 let mut sels = vec![Selection::Cursor(lead(3, 0))];
1267 adjust_cursor_runs(&mut sels, 1, -100);
1268 assert_eq!(run_at(&sels, 0), 1, "never drops below the surviving run");
1269 }
1270
1271 #[test]
1272 fn adjust_cursor_runs_i32_min_clamps_instead_of_wrapping() {
1273 let mut sels = vec![Selection::Cursor(lead(3, 0))];
1275 adjust_cursor_runs(&mut sels, 2, i32::MIN);
1276 assert_eq!(run_at(&sels, 0), 2);
1277
1278 let mut sels = vec![Selection::Cursor(lead(1, 0))];
1279 adjust_cursor_runs(&mut sels, 0, i32::MIN);
1280 assert_eq!(run_at(&sels, 0), 0);
1281 }
1282
1283 #[test]
1284 fn adjust_cursor_runs_i32_max_is_inert_when_no_cursor_qualifies() {
1285 let mut sels = vec![
1287 Selection::Cursor(lead(0, 0)),
1288 Selection::Cursor(lead(5, 0)),
1289 ];
1290 adjust_cursor_runs(&mut sels, 5, i32::MAX);
1291 assert_eq!(run_at(&sels, 0), 0);
1292 assert_eq!(run_at(&sels, 1), 5);
1293 }
1294
1295 #[test]
1296 fn adjust_cursor_runs_never_touches_range_selections() {
1297 let mut sels = vec![range_sel(lead(9, 0), lead(9, 1))];
1298 adjust_cursor_runs(&mut sels, 0, -5);
1299 match &sels[0] {
1300 Selection::Range(r) => assert_eq!(r.start.cluster_id.source_run, 9),
1301 Selection::Cursor(_) => panic!("range must stay a range"),
1302 }
1303 }
1304
1305 #[test]
1308 fn run_text_len_counts_bytes_not_chars() {
1309 let content = vec![text("héllo")]; assert_eq!(run_text_len(&content, 0), 6);
1311 assert_eq!(content_text_chars(&content), 5);
1312 }
1313
1314 fn content_text_chars(content: &[InlineContent]) -> usize {
1315 content
1316 .iter()
1317 .map(|c| match c {
1318 InlineContent::Text(r) => r.text.chars().count(),
1319 _ => 0,
1320 })
1321 .sum()
1322 }
1323
1324 #[test]
1325 fn run_text_len_zero_for_empty_missing_and_non_text_runs() {
1326 let content = vec![text(""), obj()];
1327 assert_eq!(run_text_len(&content, 0), 0, "empty text run");
1328 assert_eq!(run_text_len(&content, 1), 0, "non-text run");
1329 assert_eq!(run_text_len(&content, 2), 0, "one past the end");
1330 assert_eq!(run_text_len(&content, u32::MAX), 0, "u32::MAX index");
1331 assert_eq!(run_text_len(&[], 0), 0, "empty content");
1332 }
1333
1334 #[test]
1337 fn edit_text_empty_selections_returns_content_unchanged() {
1338 let content = vec![text("hello")];
1339 let (new_content, sels) = edit_text(&content, &[], &TextEdit::Insert("x".into()));
1340 assert_eq!(dump(&new_content), vec!["hello"]);
1341 assert!(sels.is_empty());
1342 }
1343
1344 #[test]
1345 fn edit_text_on_empty_content_does_not_panic() {
1346 let (new_content, sels) = edit_text(
1347 &[],
1348 &[Selection::Cursor(lead(0, 0))],
1349 &TextEdit::Insert("x".into()),
1350 );
1351 assert!(new_content.is_empty());
1352 assert_eq!(sels.len(), 1, "the cursor survives, unmoved");
1353 assert_eq!(cursor_of(&sels[0]), lead(0, 0));
1354 }
1355
1356 #[test]
1357 fn edit_text_out_of_range_cursor_is_a_noop_not_a_panic() {
1358 let content = vec![text("hi")];
1359 let (new_content, sels) = edit_text(
1360 &content,
1361 &[Selection::Cursor(lead(u32::MAX, 0))],
1362 &TextEdit::DeleteBackward,
1363 );
1364 assert_eq!(dump(&new_content), vec!["hi"]);
1365 assert_eq!(sels.len(), 1);
1366 }
1367
1368 #[test]
1369 fn edit_text_multi_cursor_insert_keeps_both_cursors_correct() {
1370 let content = vec![text("hello")];
1373 let sels = [
1374 Selection::Cursor(lead(0, 0)),
1375 Selection::Cursor(lead(0, 3)),
1376 ];
1377 let (new_content, new_sels) = edit_text(&content, &sels, &TextEdit::Insert("X".into()));
1378 assert_eq!(dump(&new_content), vec!["XhelXlo"]);
1379 assert_eq!(cursor_of(&new_sels[0]), lead(0, 1));
1380 assert_eq!(cursor_of(&new_sels[1]), lead(0, 5));
1381 }
1382
1383 #[test]
1384 fn edit_text_multi_cursor_insert_shifts_by_multibyte_length_not_one() {
1385 let content = vec![text("ab")];
1387 let sels = [
1388 Selection::Cursor(lead(0, 0)),
1389 Selection::Cursor(lead(0, 2)),
1390 ];
1391 let (new_content, new_sels) = edit_text(&content, &sels, &TextEdit::Insert("👍".into()));
1392 assert_eq!(dump(&new_content), vec!["👍ab👍"]);
1393 assert_eq!(cursor_of(&new_sels[0]), lead(0, 4));
1394 assert_eq!(cursor_of(&new_sels[1]), lead(0, 10)); }
1396
1397 #[test]
1398 fn edit_text_backspace_with_a_range_deletes_the_range_only() {
1399 let content = vec![text("hello")];
1402 let sel = [range_sel(lead(0, 1), lead(0, 3))];
1403 let (new_content, _) = edit_text(&content, &sel, &TextEdit::DeleteBackward);
1404 assert_eq!(dump(&new_content), vec!["hlo"]);
1405 }
1406
1407 #[test]
1410 fn apply_edit_range_insert_replaces_the_range() {
1411 let content = vec![text("hello")];
1412 let sel = range_sel(lead(0, 1), lead(0, 4));
1413 let (new_content, cursor) =
1414 apply_edit_to_selection(&content, &sel, &TextEdit::Insert("EY".into()));
1415 assert_eq!(dump(&new_content), vec!["hEYo"]);
1416 assert_eq!(cursor, lead(0, 3));
1417 }
1418
1419 #[test]
1420 fn apply_edit_range_delete_forward_deletes_range_only() {
1421 let content = vec![text("hello")];
1422 let sel = range_sel(lead(0, 1), lead(0, 3));
1423 let (new_content, cursor) =
1424 apply_edit_to_selection(&content, &sel, &TextEdit::DeleteForward);
1425 assert_eq!(dump(&new_content), vec!["hlo"]);
1426 assert_eq!(cursor, lead(0, 1));
1427 }
1428
1429 #[test]
1430 fn apply_edit_cursor_insert_of_empty_string_only_moves_the_caret() {
1431 let content = vec![text("hello")];
1432 let sel = Selection::Cursor(lead(0, 2));
1433 let (new_content, cursor) =
1434 apply_edit_to_selection(&content, &sel, &TextEdit::Insert(String::new()));
1435 assert_eq!(dump(&new_content), vec!["hello"]);
1436 assert_eq!(cursor, lead(0, 2));
1437 }
1438
1439 #[test]
1442 fn cursor_byte_offset_leading_clamps_past_the_end() {
1443 assert_eq!(cursor_byte_offset_in_run("hi", &lead(0, 999)), 2);
1444 assert_eq!(cursor_byte_offset_in_run("hi", &lead(0, u32::MAX)), 2);
1445 assert_eq!(cursor_byte_offset_in_run("", &lead(0, 5)), 0);
1446 }
1447
1448 #[test]
1449 fn cursor_byte_offset_trailing_clamps_past_the_end() {
1450 assert_eq!(cursor_byte_offset_in_run("hi", &trail(0, 2)), 2);
1451 assert_eq!(cursor_byte_offset_in_run("hi", &trail(0, u32::MAX)), 2);
1452 assert_eq!(cursor_byte_offset_in_run("", &trail(0, 0)), 0);
1453 }
1454
1455 #[test]
1456 fn cursor_byte_offset_trailing_lands_after_the_whole_grapheme() {
1457 assert_eq!(cursor_byte_offset_in_run("e\u{0301}x", &trail(0, 0)), 3);
1459 assert_eq!(cursor_byte_offset_in_run("👍x", &trail(0, 0)), 4);
1461 assert_eq!(cursor_byte_offset_in_run(FAMILY, &trail(0, 0)), 18);
1464 }
1465
1466 #[test]
1467 fn cursor_byte_offset_leading_is_the_raw_offset() {
1468 assert_eq!(cursor_byte_offset_in_run(FAMILY, &lead(0, 0)), 0);
1469 assert_eq!(cursor_byte_offset_in_run("abc", &lead(0, 1)), 1);
1470 }
1471
1472 #[test]
1475 fn delete_range_within_one_run() {
1476 let content = vec![text("hello")];
1477 let r = SelectionRange {
1478 start: lead(0, 1),
1479 end: lead(0, 3),
1480 };
1481 let (new_content, cursor) = delete_range(&content, &r);
1482 assert_eq!(dump(&new_content), vec!["hlo"]);
1483 assert_eq!(cursor, lead(0, 1));
1484 }
1485
1486 #[test]
1487 fn delete_range_backward_range_is_normalized() {
1488 let content = vec![text("hello")];
1491 let backward = SelectionRange {
1492 start: lead(0, 3),
1493 end: lead(0, 1),
1494 };
1495 let (new_content, cursor) = delete_range(&content, &backward);
1496 assert_eq!(dump(&new_content), vec!["hlo"]);
1497 assert_eq!(cursor, lead(0, 1), "caret collapses to the LOW end");
1498 }
1499
1500 #[test]
1501 fn delete_range_collapsed_range_deletes_nothing() {
1502 let content = vec![text("hello")];
1503 let r = SelectionRange {
1504 start: lead(0, 2),
1505 end: lead(0, 2),
1506 };
1507 let (new_content, cursor) = delete_range(&content, &r);
1508 assert_eq!(dump(&new_content), vec!["hello"]);
1509 assert_eq!(cursor, lead(0, 2));
1510 }
1511
1512 #[test]
1513 fn delete_range_select_all_with_trailing_end_covers_the_last_cluster() {
1514 let content = vec![text("hello")];
1517 let r = SelectionRange {
1518 start: lead(0, 0),
1519 end: trail(0, 4),
1520 };
1521 let (new_content, cursor) = delete_range(&content, &r);
1522 assert_eq!(dump(&new_content), vec![""]);
1523 assert_eq!(cursor, lead(0, 0));
1524 }
1525
1526 #[test]
1527 fn delete_range_spanning_runs_merges_matching_styles() {
1528 let content = vec![text("abc"), text("def")];
1529 let r = SelectionRange {
1530 start: lead(0, 1),
1531 end: lead(1, 2),
1532 };
1533 let (new_content, cursor) = delete_range(&content, &r);
1534 assert_eq!(dump(&new_content), vec!["af"], "same style -> one run");
1535 assert_eq!(cursor, lead(0, 1));
1536 }
1537
1538 #[test]
1539 fn delete_range_spanning_runs_keeps_differing_styles_apart() {
1540 let content = vec![text_styled("abc", style_a()), text_styled("def", style_b())];
1541 let r = SelectionRange {
1542 start: lead(0, 1),
1543 end: lead(1, 2),
1544 };
1545 let (new_content, cursor) = delete_range(&content, &r);
1546 assert_eq!(dump(&new_content), vec!["a", "f"], "styles differ -> no merge");
1547 assert_eq!(cursor, lead(0, 1));
1548 }
1549
1550 #[test]
1551 fn delete_range_drops_the_runs_strictly_between_the_boundaries() {
1552 let content = vec![text("abc"), text("XYZ"), obj(), text("def")];
1553 let r = SelectionRange {
1554 start: lead(0, 1),
1555 end: lead(3, 2),
1556 };
1557 let (new_content, _) = delete_range(&content, &r);
1558 assert_eq!(dump(&new_content), vec!["af"], "middle text AND obj dropped");
1559 }
1560
1561 #[test]
1562 fn delete_range_over_a_single_non_text_item_removes_it() {
1563 let content = vec![text("ab"), obj(), text("cd")];
1564 let r = SelectionRange {
1566 start: lead(1, 0),
1567 end: trail(1, 0),
1568 };
1569 let (new_content, cursor) = delete_range(&content, &r);
1570 assert_eq!(dump(&new_content), vec!["ab", "cd"]);
1571 assert_eq!(cursor, lead(1, 0));
1572 }
1573
1574 #[test]
1575 fn delete_range_collapsed_on_a_non_text_item_keeps_it() {
1576 let content = vec![text("ab"), obj()];
1577 let r = SelectionRange {
1578 start: lead(1, 0),
1579 end: lead(1, 0),
1580 };
1581 let (new_content, _) = delete_range(&content, &r);
1582 assert_eq!(dump(&new_content), vec!["ab", "<obj>"]);
1583 }
1584
1585 #[test]
1586 fn delete_range_out_of_bounds_runs_do_not_panic() {
1587 let content = vec![text("ab")];
1588
1589 let r = SelectionRange {
1591 start: lead(99, 0),
1592 end: lead(99, 5),
1593 };
1594 let (new_content, _) = delete_range(&content, &r);
1595 assert_eq!(dump(&new_content), vec!["ab"]);
1596
1597 let r = SelectionRange {
1599 start: lead(0, 0),
1600 end: lead(u32::MAX, 0),
1601 };
1602 let (new_content, cursor) = delete_range(&content, &r);
1603 assert_eq!(dump(&new_content), vec![""]);
1604 assert_eq!(cursor, lead(0, 0));
1605 }
1606
1607 #[test]
1608 fn delete_range_backward_across_runs_is_normalized() {
1609 let content = vec![text("abc"), text("def")];
1610 let backward = SelectionRange {
1611 start: lead(1, 2),
1612 end: lead(0, 1),
1613 };
1614 let (new_content, cursor) = delete_range(&content, &backward);
1615 assert_eq!(dump(&new_content), vec!["af"]);
1616 assert_eq!(cursor, lead(0, 1));
1617 }
1618
1619 #[test]
1622 fn insert_text_leading_inserts_before_the_cluster() {
1623 let content = vec![text("hello")];
1624 let (new_content, cursor) = insert_text(&content, &lead(0, 2), "XY");
1625 assert_eq!(dump(&new_content), vec!["heXYllo"]);
1626 assert_eq!(cursor, lead(0, 4));
1627 }
1628
1629 #[test]
1630 fn insert_text_trailing_inserts_after_the_whole_grapheme() {
1631 let content = vec![text("👍z")];
1633 let (new_content, cursor) = insert_text(&content, &trail(0, 0), "X");
1634 assert_eq!(dump(&new_content), vec!["👍Xz"]);
1635 assert_eq!(cursor, lead(0, 5));
1636 }
1637
1638 #[test]
1639 fn insert_text_trailing_past_the_end_appends() {
1640 let content = vec![text("hi")];
1641 let (new_content, cursor) = insert_text(&content, &trail(0, 999), "!");
1642 assert_eq!(dump(&new_content), vec!["hi!"]);
1643 assert_eq!(cursor, lead(0, 3));
1644 }
1645
1646 #[test]
1647 fn insert_text_leading_past_the_end_is_a_noop() {
1648 let content = vec![text("hi")];
1651 let (new_content, cursor) = insert_text(&content, &lead(0, 999), "!");
1652 assert_eq!(dump(&new_content), vec!["hi"]);
1653 assert_eq!(cursor, lead(0, 999));
1654 }
1655
1656 #[test]
1657 fn insert_text_into_missing_or_non_text_run_is_a_noop() {
1658 let content = vec![obj()];
1659 let (new_content, cursor) = insert_text(&content, &lead(0, 0), "x");
1660 assert_eq!(dump(&new_content), vec!["<obj>"]);
1661 assert_eq!(cursor, lead(0, 0));
1662
1663 let (new_content, cursor) = insert_text(&content, &lead(u32::MAX, 0), "x");
1664 assert_eq!(dump(&new_content), vec!["<obj>"]);
1665 assert_eq!(cursor, lead(u32::MAX, 0));
1666
1667 let (new_content, _) = insert_text(&[], &lead(0, 0), "x");
1668 assert!(new_content.is_empty());
1669 }
1670
1671 #[test]
1672 fn insert_text_empty_string_leaves_the_text_alone() {
1673 let content = vec![text("hi")];
1674 let (new_content, cursor) = insert_text(&content, &lead(0, 1), "");
1675 assert_eq!(dump(&new_content), vec!["hi"]);
1676 assert_eq!(cursor, lead(0, 1));
1677 }
1678
1679 #[test]
1680 fn insert_text_cursor_advances_by_bytes_not_chars() {
1681 let content = vec![text("")];
1682 let (new_content, cursor) = insert_text(&content, &lead(0, 0), FAMILY);
1683 assert_eq!(dump(&new_content), vec![FAMILY]);
1684 assert_eq!(cursor, lead(0, 18));
1685 }
1686
1687 #[test]
1688 fn insert_text_of_a_huge_string_does_not_panic() {
1689 let big = "a".repeat(200_000);
1690 let content = vec![text("hi")];
1691 let (new_content, cursor) = insert_text(&content, &lead(0, 1), &big);
1692 assert_eq!(run_text_len(&new_content, 0), 200_002);
1693 assert_eq!(cursor, lead(0, 200_001));
1694 }
1695
1696 #[test]
1699 fn delete_backward_on_empty_content_is_a_noop() {
1700 let (new_content, cursor) = delete_backward(&[], &lead(0, 0));
1701 assert!(new_content.is_empty());
1702 assert_eq!(cursor, lead(0, 0));
1703 }
1704
1705 #[test]
1706 fn delete_backward_at_the_start_of_the_document_is_a_noop() {
1707 let content = vec![text("hi")];
1708 let (new_content, cursor) = delete_backward(&content, &lead(0, 0));
1709 assert_eq!(dump(&new_content), vec!["hi"]);
1710 assert_eq!(cursor, lead(0, 0));
1711 }
1712
1713 #[test]
1714 fn delete_backward_removes_a_whole_grapheme_cluster() {
1715 let content = vec![text(&format!("a{FAMILY}"))];
1716 let (new_content, cursor) = delete_backward(&content, &lead(0, 19));
1717 assert_eq!(dump(&new_content), vec!["a"], "all 18 bytes go at once");
1718 assert_eq!(cursor, lead(0, 1));
1719 }
1720
1721 #[test]
1722 fn delete_backward_trailing_affinity_removes_the_current_cluster() {
1723 let content = vec![text("ab")];
1724 let (new_content, cursor) = delete_backward(&content, &trail(0, 0));
1725 assert_eq!(dump(&new_content), vec!["b"]);
1726 assert_eq!(cursor, lead(0, 0));
1727 }
1728
1729 #[test]
1730 fn delete_backward_merges_across_a_run_boundary() {
1731 let content = vec![text("ab"), text("cd")];
1732 let (new_content, cursor) = delete_backward(&content, &lead(1, 0));
1733 assert_eq!(dump(&new_content), vec!["abcd"]);
1734 assert_eq!(cursor, lead(0, 2), "caret sits at the join point");
1735 }
1736
1737 #[test]
1738 fn delete_backward_removes_a_non_text_item_sitting_before_the_caret() {
1739 let content = vec![text("ab"), obj(), text("cd")];
1740 let (new_content, cursor) = delete_backward(&content, &lead(2, 0));
1741 assert_eq!(dump(&new_content), vec!["ab", "cd"]);
1742 assert_eq!(cursor, lead(1, 0));
1743 }
1744
1745 #[test]
1746 fn delete_backward_with_the_caret_after_a_non_text_item_removes_the_item() {
1747 let content = vec![text("ab"), obj()];
1748 let (new_content, _) = delete_backward(&content, &trail(1, 0));
1749 assert_eq!(dump(&new_content), vec!["ab"]);
1750 }
1751
1752 #[test]
1753 fn delete_backward_with_the_caret_before_a_non_text_item_acts_on_the_previous_run() {
1754 let content = vec![text("ab"), obj()];
1755 let (new_content, cursor) = delete_backward(&content, &lead(1, 0));
1756 assert_eq!(dump(&new_content), vec!["a", "<obj>"], "the item survives");
1757 assert_eq!(cursor, lead(0, 1));
1758 }
1759
1760 #[test]
1761 fn delete_backward_before_a_leading_non_text_item_at_run_zero_is_a_noop() {
1762 let content = vec![obj()];
1763 let (new_content, cursor) = delete_backward(&content, &lead(0, 0));
1764 assert_eq!(dump(&new_content), vec!["<obj>"]);
1765 assert_eq!(cursor, lead(0, 0));
1766 }
1767
1768 #[test]
1769 fn delete_backward_out_of_range_run_is_a_noop() {
1770 let content = vec![text("hi")];
1771 let (new_content, cursor) = delete_backward(&content, &lead(u32::MAX, u32::MAX));
1772 assert_eq!(dump(&new_content), vec!["hi"]);
1773 assert_eq!(cursor, lead(u32::MAX, u32::MAX));
1774 }
1775
1776 #[test]
1779 fn delete_forward_on_empty_content_is_a_noop() {
1780 let (new_content, cursor) = delete_forward(&[], &lead(0, 0));
1781 assert!(new_content.is_empty());
1782 assert_eq!(cursor, lead(0, 0));
1783 }
1784
1785 #[test]
1786 fn delete_forward_at_the_end_of_the_document_is_a_noop() {
1787 let content = vec![text("hi")];
1788 let (new_content, cursor) = delete_forward(&content, &lead(0, 2));
1789 assert_eq!(dump(&new_content), vec!["hi"]);
1790 assert_eq!(cursor, lead(0, 2));
1791 }
1792
1793 #[test]
1794 fn delete_forward_removes_a_whole_grapheme_cluster() {
1795 let content = vec![text(&format!("{FAMILY}z"))];
1796 let (new_content, cursor) = delete_forward(&content, &lead(0, 0));
1797 assert_eq!(dump(&new_content), vec!["z"]);
1798 assert_eq!(cursor, lead(0, 0));
1799 }
1800
1801 #[test]
1802 fn delete_forward_merges_across_a_run_boundary() {
1803 let content = vec![text("ab"), text("cd")];
1804 let (new_content, cursor) = delete_forward(&content, &lead(0, 2));
1805 assert_eq!(dump(&new_content), vec!["abcd"]);
1806 assert_eq!(cursor, lead(0, 2));
1807 }
1808
1809 #[test]
1810 fn delete_forward_removes_a_non_text_item_sitting_after_the_caret() {
1811 let content = vec![text("ab"), obj()];
1812 let (new_content, _) = delete_forward(&content, &lead(0, 2));
1813 assert_eq!(dump(&new_content), vec!["ab"]);
1814 }
1815
1816 #[test]
1817 fn delete_forward_with_the_caret_before_a_non_text_item_removes_the_item() {
1818 let content = vec![obj(), text("ab")];
1819 let (new_content, cursor) = delete_forward(&content, &lead(0, 0));
1820 assert_eq!(dump(&new_content), vec!["ab"]);
1821 assert_eq!(cursor, lead(0, 0));
1822 }
1823
1824 #[test]
1825 fn delete_forward_with_the_caret_after_a_non_text_item_acts_on_the_next_run() {
1826 let content = vec![obj(), text("ab")];
1827 let (new_content, cursor) = delete_forward(&content, &trail(0, 0));
1828 assert_eq!(dump(&new_content), vec!["<obj>", "b"], "the item survives");
1829 assert_eq!(cursor, lead(1, 0));
1830 }
1831
1832 #[test]
1833 fn delete_forward_after_a_trailing_non_text_item_at_the_last_run_is_a_noop() {
1834 let content = vec![text("ab"), obj()];
1835 let (new_content, cursor) = delete_forward(&content, &trail(1, 0));
1836 assert_eq!(dump(&new_content), vec!["ab", "<obj>"]);
1837 assert_eq!(cursor, trail(1, 0));
1838 }
1839
1840 #[test]
1841 fn delete_forward_out_of_range_run_is_a_noop() {
1842 let content = vec![text("hi")];
1843 let (new_content, cursor) = delete_forward(&content, &lead(u32::MAX, u32::MAX));
1844 assert_eq!(dump(&new_content), vec!["hi"]);
1845 assert_eq!(cursor, lead(u32::MAX, u32::MAX));
1846 }
1847
1848 #[test]
1851 #[should_panic(expected = "same length")]
1852 fn edit_text_multi_panics_on_a_length_mismatch() {
1853 let content = vec![text("hi")];
1855 let sels = [Selection::Cursor(lead(0, 0))];
1856 let _ = edit_text_multi(&content, &sels, &["a", "b"]);
1857 }
1858
1859 #[test]
1860 fn edit_text_multi_with_no_selections_returns_content_unchanged() {
1861 let content = vec![text("hi")];
1862 let (new_content, sels) = edit_text_multi(&content, &[], &[]);
1863 assert_eq!(dump(&new_content), vec!["hi"]);
1864 assert!(sels.is_empty());
1865 }
1866
1867 #[test]
1868 fn edit_text_multi_gives_each_cursor_its_own_text() {
1869 let content = vec![text("ab")];
1870 let sels = [
1871 Selection::Cursor(lead(0, 0)),
1872 Selection::Cursor(lead(0, 2)),
1873 ];
1874 let (new_content, new_sels) = edit_text_multi(&content, &sels, &["X", "Y"]);
1875 assert_eq!(dump(&new_content), vec!["XabY"]);
1876 assert_eq!(cursor_of(&new_sels[0]), lead(0, 1));
1877 assert_eq!(cursor_of(&new_sels[1]), lead(0, 4));
1878 }
1879
1880 #[test]
1881 fn edit_text_multi_with_empty_texts_only_moves_the_carets() {
1882 let content = vec![text("ab")];
1883 let sels = [
1884 Selection::Cursor(lead(0, 0)),
1885 Selection::Cursor(lead(0, 1)),
1886 ];
1887 let (new_content, new_sels) = edit_text_multi(&content, &sels, &["", ""]);
1888 assert_eq!(dump(&new_content), vec!["ab"]);
1889 assert_eq!(new_sels.len(), 2);
1890 }
1891
1892 #[test]
1895 fn inspect_delete_forward_at_the_end_of_the_document_is_none() {
1896 let content = vec![text("hi")];
1897 assert!(inspect_delete(&content, &Selection::Cursor(lead(0, 2)), true).is_none());
1898 assert!(inspect_delete(&[], &Selection::Cursor(lead(0, 0)), true).is_none());
1899 }
1900
1901 #[test]
1902 fn inspect_delete_backward_at_the_start_of_the_document_is_none() {
1903 let content = vec![text("hi")];
1904 assert!(inspect_delete(&content, &Selection::Cursor(lead(0, 0)), false).is_none());
1905 assert!(inspect_delete(&[], &Selection::Cursor(lead(0, 0)), false).is_none());
1906 }
1907
1908 #[test]
1909 fn inspect_delete_forward_reports_exactly_what_delete_forward_removes() {
1910 let content = vec![text("héllo")]; let cursor = lead(0, 1);
1912 let (_, reported) = inspect_delete(&content, &Selection::Cursor(cursor), true).unwrap();
1913 let (after, _) = delete_forward(&content, &cursor);
1914 assert_eq!(reported, "é");
1915 assert_eq!(dump(&after), vec!["hllo"], "inspect and delete agree");
1916 }
1917
1918 #[test]
1919 fn inspect_delete_backward_reports_exactly_what_delete_backward_removes() {
1920 let content = vec![text(&format!("a{FAMILY}"))];
1921 let cursor = lead(0, 19);
1922 let (range, reported) =
1923 inspect_delete(&content, &Selection::Cursor(cursor), false).unwrap();
1924 let (after, _) = delete_backward(&content, &cursor);
1925 assert_eq!(reported, FAMILY, "the whole ZWJ cluster, not one codepoint");
1926 assert_eq!(range.start, lead(0, 1));
1927 assert_eq!(dump(&after), vec!["a"]);
1928 }
1929
1930 #[test]
1931 fn inspect_delete_forward_honors_trailing_affinity() {
1932 let content = vec![text("abc")];
1934 let (_, reported) =
1935 inspect_delete(&content, &Selection::Cursor(trail(0, 0)), true).unwrap();
1936 assert_eq!(reported, "b");
1937 }
1938
1939 #[test]
1940 fn inspect_delete_across_a_run_boundary_reports_the_neighbouring_grapheme() {
1941 let content = vec![text("ab"), text("cd")];
1942 let (_, fwd) = inspect_delete(&content, &Selection::Cursor(lead(0, 2)), true).unwrap();
1943 assert_eq!(fwd, "c");
1944 let (_, back) = inspect_delete(&content, &Selection::Cursor(lead(1, 0)), false).unwrap();
1945 assert_eq!(back, "b");
1946 }
1947
1948 #[test]
1949 fn inspect_delete_reports_none_for_a_non_text_neighbour_that_delete_would_remove() {
1950 let content = vec![text("ab"), obj()];
1955 assert!(inspect_delete(&content, &Selection::Cursor(lead(0, 2)), true).is_none());
1956 let (after, _) = delete_forward(&content, &lead(0, 2));
1957 assert_eq!(dump(&after), vec!["ab"], "...but the item IS removed");
1958
1959 let content = vec![obj(), text("ab")];
1960 assert!(inspect_delete(&content, &Selection::Cursor(lead(1, 0)), false).is_none());
1961 let (after, _) = delete_backward(&content, &lead(1, 0));
1962 assert_eq!(dump(&after), vec!["ab"], "...but the item IS removed");
1963 }
1964
1965 #[test]
1966 fn inspect_delete_on_a_range_returns_the_range_and_its_text() {
1967 let content = vec![text("hello")];
1968 let sel = range_sel(lead(0, 1), lead(0, 3));
1969 let (range, reported) = inspect_delete(&content, &sel, false).unwrap();
1970 assert_eq!(range.start, lead(0, 1));
1971 assert_eq!(range.end, lead(0, 3));
1972 assert_eq!(reported, "el");
1973 let (after, _) = apply_edit_to_selection(&content, &sel, &TextEdit::DeleteBackward);
1975 assert_eq!(dump(&after), vec!["hlo"]);
1976 }
1977
1978 #[test]
1979 fn inspect_delete_out_of_range_cursor_is_none_not_a_panic() {
1980 let content = vec![text("hi")];
1981 assert!(inspect_delete(&content, &Selection::Cursor(lead(u32::MAX, 0)), true).is_none());
1982 assert!(inspect_delete(&content, &Selection::Cursor(lead(u32::MAX, 0)), false).is_none());
1983 }
1984
1985 #[test]
1988 fn extract_text_in_range_single_run() {
1989 let content = vec![text("hello")];
1990 let r = SelectionRange {
1991 start: lead(0, 1),
1992 end: lead(0, 4),
1993 };
1994 assert_eq!(extract_text_in_range(&content, &r), "ell");
1995 }
1996
1997 #[test]
1998 fn extract_text_in_range_multi_run_concatenates_the_span() {
1999 let content = vec![text("abc"), text("MID"), text("def")];
2000 let r = SelectionRange {
2001 start: lead(0, 1),
2002 end: lead(2, 2),
2003 };
2004 assert_eq!(extract_text_in_range(&content, &r), "bcMIDde");
2005 }
2006
2007 #[test]
2008 fn extract_text_in_range_skips_non_text_items_in_the_span() {
2009 let content = vec![text("abc"), obj(), text("def")];
2010 let r = SelectionRange {
2011 start: lead(0, 1),
2012 end: lead(2, 2),
2013 };
2014 assert_eq!(extract_text_in_range(&content, &r), "bcde");
2015 }
2016
2017 #[test]
2018 fn extract_text_in_range_out_of_bounds_yields_empty_string() {
2019 let content = vec![text("hi")];
2020
2021 let r = SelectionRange {
2023 start: lead(0, 0),
2024 end: lead(0, 99),
2025 };
2026 assert_eq!(extract_text_in_range(&content, &r), "");
2027
2028 let r = SelectionRange {
2030 start: lead(9, 0),
2031 end: lead(9, 1),
2032 };
2033 assert_eq!(extract_text_in_range(&content, &r), "");
2034
2035 let r = SelectionRange {
2037 start: lead(0, 0),
2038 end: lead(0, 1),
2039 };
2040 assert_eq!(extract_text_in_range(&[], &r), "");
2041 }
2042
2043 #[test]
2044 fn extract_text_in_range_backward_single_run_yields_empty_string() {
2045 let content = vec![text("hello")];
2048 let r = SelectionRange {
2049 start: lead(0, 3),
2050 end: lead(0, 1),
2051 };
2052 assert_eq!(extract_text_in_range(&content, &r), "");
2053 }
2054
2055 #[test]
2056 fn extract_text_in_range_ignores_affinity_and_drops_the_last_cluster() {
2057 let content = vec![text("hello")];
2062 let r = SelectionRange {
2063 start: lead(0, 0),
2064 end: trail(0, 4),
2065 };
2066 assert_eq!(extract_text_in_range(&content, &r), "hell", "the 'o' is missing");
2067
2068 let (after, _) = delete_range(&content, &r);
2069 assert_eq!(dump(&after), vec![""], "...yet delete_range removes all of it");
2070 }
2071}