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}