1use std::ops::{Deref, Range};
2use std::sync::Arc;
3
4use unicode_segmentation::{GraphemeCursor, UnicodeSegmentation as _};
5use unicode_width::UnicodeWidthStr as _;
6
7#[path = "editor_keys.rs"]
8mod keys;
9
10pub use keys::classify_key_event;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum WordStyle {
14 Small,
15 WhitespaceDelimited,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum EditCommand {
20 Insert(char),
21 MoveGraphemeLeft,
22 MoveGraphemeRight,
23 MoveWordLeft(WordStyle),
24 MoveWordRight(WordStyle),
25 MoveLogicalLineStart,
26 MoveLogicalLineEnd,
27 DeleteGraphemeBackward,
28 DeleteGraphemeForward,
29 DeleteWordBackward(WordStyle),
30 DeleteWordForward(WordStyle),
31 DeleteToLineStart,
32 DeleteToLineEnd,
33}
34
35#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36
37pub(crate) enum EditCommandCategory {
38 Insert,
39 Navigation,
40 Delete,
41 Kill,
42}
43
44impl EditCommand {
45 pub(crate) fn category(self) -> EditCommandCategory {
46 match self {
47 Self::Insert(_) => EditCommandCategory::Insert,
48 Self::MoveGraphemeLeft
49 | Self::MoveGraphemeRight
50 | Self::MoveWordLeft(_)
51 | Self::MoveWordRight(_)
52 | Self::MoveLogicalLineStart
53 | Self::MoveLogicalLineEnd => EditCommandCategory::Navigation,
54 Self::DeleteGraphemeBackward | Self::DeleteGraphemeForward => {
55 EditCommandCategory::Delete
56 }
57 Self::DeleteWordBackward(_)
58 | Self::DeleteWordForward(_)
59 | Self::DeleteToLineStart
60 | Self::DeleteToLineEnd => EditCommandCategory::Kill,
61 }
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Eq)]
66pub struct EditDelta {
67 pub replaced_byte_range: Range<usize>,
68 pub inserted_byte_range: Range<usize>,
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub enum EditOutcome {
73 Unchanged,
74 CursorOnly,
75 TextOnly(EditDelta),
76 TextAndCursor(EditDelta),
77}
78
79impl EditOutcome {
80 fn from_changes(delta: Option<EditDelta>, cursor_changed: bool) -> Self {
81 match (delta, cursor_changed) {
82 (None, false) => Self::Unchanged,
83 (None, true) => Self::CursorOnly,
84 (Some(delta), false) => Self::TextOnly(delta),
85 (Some(delta), true) => Self::TextAndCursor(delta),
86 }
87 }
88}
89
90#[derive(Debug, Clone, Copy, PartialEq, Eq)]
91pub enum PostEditCursorAffinity {
92 Exact,
93 Right,
94}
95
96#[derive(Debug, Clone)]
97pub struct EditPlan {
98 replaced_byte_range: Range<usize>,
99 replacement: String,
100 removed_text: String,
101 cursor_byte: usize,
102 cursor_affinity: PostEditCursorAffinity,
103 source_identity: Arc<BufferIdentity>,
104 source_generation: u64,
105}
106
107impl EditPlan {
108 pub fn replaced_byte_range(&self) -> Range<usize> {
109 self.replaced_byte_range.clone()
110 }
111
112 pub fn replacement(&self) -> &str {
113 &self.replacement
114 }
115
116 pub fn removed_text(&self) -> &str {
117 &self.removed_text
118 }
119
120 pub fn cursor_byte(&self) -> usize {
121 self.cursor_byte
122 }
123
124 pub fn cursor_affinity(&self) -> PostEditCursorAffinity {
125 self.cursor_affinity
126 }
127
128 pub fn into_removed_text(self) -> String {
129 self.removed_text
130 }
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub enum ApplyEditPlanError {
135 StalePlan,
136 InvalidRange,
137 RemovedTextMismatch,
138 InvalidCursor,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub struct SingleLineViewport {
143 pub visible_byte_range: Range<usize>,
144 pub cursor_display_column: usize,
145}
146
147#[derive(Debug)]
148struct BufferIdentity;
149
150#[derive(Debug)]
151pub struct EditBuffer {
152 text: String,
153 cursor_byte: usize,
154 identity: Arc<BufferIdentity>,
155 generation: u64,
156}
157
158impl Default for EditBuffer {
159 fn default() -> Self {
160 Self {
161 text: String::new(),
162 cursor_byte: 0,
163 identity: Arc::new(BufferIdentity),
164 generation: 0,
165 }
166 }
167}
168
169impl Clone for EditBuffer {
170 fn clone(&self) -> Self {
171 Self {
172 text: self.text.clone(),
173 cursor_byte: self.cursor_byte,
174 identity: Arc::new(BufferIdentity),
175 generation: 0,
176 }
177 }
178}
179
180impl PartialEq for EditBuffer {
181 fn eq(&self, other: &Self) -> bool {
182 self.text == other.text && self.cursor_byte == other.cursor_byte
183 }
184}
185
186impl Eq for EditBuffer {}
187
188impl Deref for EditBuffer {
189 type Target = str;
190
191 fn deref(&self) -> &Self::Target {
192 self.text()
193 }
194}
195
196impl EditBuffer {
197 pub fn new() -> Self {
198 Self::default()
199 }
200
201 pub fn from_text(text: impl Into<String>) -> Self {
202 let text = text.into();
203 let cursor_byte = text.len();
204 Self {
205 text,
206 cursor_byte,
207 identity: Arc::new(BufferIdentity),
208 generation: 0,
209 }
210 }
211
212 pub fn from_parts(text: impl Into<String>, cursor_byte: usize) -> Self {
214 let text = text.into();
215 let cursor_byte = normalize_external_cursor(&text, cursor_byte);
216 Self {
217 text,
218 cursor_byte,
219 identity: Arc::new(BufferIdentity),
220 generation: 0,
221 }
222 }
223
224 pub fn text(&self) -> &str {
225 &self.text
226 }
227
228 pub fn into_text(self) -> String {
229 self.text
230 }
231
232 pub fn cursor_byte(&self) -> usize {
233 self.cursor_byte
234 }
235
236 #[must_use]
238 pub fn set_cursor_byte(&mut self, cursor_byte: usize) -> EditOutcome {
239 let old_cursor = self.cursor_byte;
240 self.cursor_byte = normalize_external_cursor(&self.text, cursor_byte);
241 let cursor_changed = self.cursor_byte != old_cursor;
242 if cursor_changed {
243 self.advance_generation();
244 }
245 EditOutcome::from_changes(None, cursor_changed)
246 }
247
248 #[must_use]
249 pub fn insert_str(&mut self, text: &str) -> EditOutcome {
250 let plan = self.plan_replace_byte_range(self.cursor_byte..self.cursor_byte, text, &[]);
251 self.apply_validated_plan(&plan)
252 }
253
254 #[must_use]
256 pub fn replace_byte_range(&mut self, range: Range<usize>, replacement: &str) -> EditOutcome {
257 let plan = self.plan_replace_byte_range(range, replacement, &[]);
258 self.apply_validated_plan(&plan)
259 }
260
261 pub fn plan_replace_byte_range(
262 &self,
263 range: Range<usize>,
264 replacement: &str,
265 atomic_byte_ranges: &[Range<usize>],
266 ) -> EditPlan {
267 let atomic_byte_ranges = normalize_atomic_ranges(&self.text, atomic_byte_ranges);
268 let range = normalize_replacement_range(&self.text, range, &atomic_byte_ranges);
269 let cursor_byte = normalize_cursor_for_atomic_ranges(self.cursor_byte, &atomic_byte_ranges);
270 let next_cursor = if cursor_byte < range.start {
271 cursor_byte
272 } else if cursor_byte <= range.end {
273 range.start + replacement.len()
274 } else {
275 cursor_byte - (range.end - range.start) + replacement.len()
276 };
277 self.make_plan(
278 range,
279 replacement.to_owned(),
280 next_cursor,
281 PostEditCursorAffinity::Right,
282 )
283 }
284
285 pub fn plan_command(
286 &self,
287 command: EditCommand,
288 atomic_byte_ranges: &[Range<usize>],
289 ) -> EditPlan {
290 let atomic_byte_ranges = normalize_atomic_ranges(&self.text, atomic_byte_ranges);
291 let cursor_byte = normalize_cursor_for_atomic_ranges(self.cursor_byte, &atomic_byte_ranges);
292 match command {
293 EditCommand::Insert(character) => {
294 let replacement = character.to_string();
295 self.make_plan(
296 cursor_byte..cursor_byte,
297 replacement,
298 cursor_byte + character.len_utf8(),
299 PostEditCursorAffinity::Right,
300 )
301 }
302 EditCommand::MoveGraphemeLeft => self.make_plan(
303 cursor_byte..cursor_byte,
304 String::new(),
305 previous_atomic_boundary(&self.text, cursor_byte, &atomic_byte_ranges),
306 PostEditCursorAffinity::Exact,
307 ),
308 EditCommand::MoveGraphemeRight => self.make_plan(
309 cursor_byte..cursor_byte,
310 String::new(),
311 next_atomic_boundary(&self.text, cursor_byte, &atomic_byte_ranges),
312 PostEditCursorAffinity::Exact,
313 ),
314 EditCommand::MoveWordLeft(style) => {
315 let target = self.previous_word_boundary(style, cursor_byte, &atomic_byte_ranges);
316 self.make_plan(
317 cursor_byte..cursor_byte,
318 String::new(),
319 target,
320 PostEditCursorAffinity::Exact,
321 )
322 }
323 EditCommand::MoveWordRight(style) => {
324 let target = self.next_word_boundary(style, cursor_byte, &atomic_byte_ranges);
325 self.make_plan(
326 cursor_byte..cursor_byte,
327 String::new(),
328 target,
329 PostEditCursorAffinity::Exact,
330 )
331 }
332 EditCommand::MoveLogicalLineStart => {
333 let target = self.logical_line_start_target(cursor_byte, &atomic_byte_ranges);
334 self.make_plan(
335 cursor_byte..cursor_byte,
336 String::new(),
337 target,
338 PostEditCursorAffinity::Exact,
339 )
340 }
341 EditCommand::MoveLogicalLineEnd => {
342 let target = self.logical_line_end_target(cursor_byte, &atomic_byte_ranges);
343 self.make_plan(
344 cursor_byte..cursor_byte,
345 String::new(),
346 target,
347 PostEditCursorAffinity::Exact,
348 )
349 }
350 EditCommand::DeleteGraphemeBackward => {
351 let start = previous_atomic_boundary(&self.text, cursor_byte, &atomic_byte_ranges);
352 self.make_plan(
353 start..cursor_byte,
354 String::new(),
355 start,
356 PostEditCursorAffinity::Right,
357 )
358 }
359 EditCommand::DeleteGraphemeForward => {
360 let end = next_atomic_boundary(&self.text, cursor_byte, &atomic_byte_ranges);
361 self.make_plan(
362 cursor_byte..end,
363 String::new(),
364 cursor_byte,
365 PostEditCursorAffinity::Right,
366 )
367 }
368 EditCommand::DeleteWordBackward(style) => {
369 let start = self.previous_word_boundary(style, cursor_byte, &atomic_byte_ranges);
370 self.make_plan(
371 start..cursor_byte,
372 String::new(),
373 start,
374 PostEditCursorAffinity::Right,
375 )
376 }
377 EditCommand::DeleteWordForward(style) => {
378 let end = self.next_word_boundary(style, cursor_byte, &atomic_byte_ranges);
379 self.make_plan(
380 cursor_byte..end,
381 String::new(),
382 cursor_byte,
383 PostEditCursorAffinity::Right,
384 )
385 }
386 EditCommand::DeleteToLineStart => {
387 let line_start = self.line_start_at(cursor_byte, &atomic_byte_ranges);
388 let start = if cursor_byte == line_start {
389 previous_atomic_boundary(&self.text, line_start, &atomic_byte_ranges)
390 } else {
391 line_start
392 };
393 self.make_plan(
394 start..cursor_byte,
395 String::new(),
396 start,
397 PostEditCursorAffinity::Right,
398 )
399 }
400 EditCommand::DeleteToLineEnd => {
401 let line_end = self.line_end_from(cursor_byte, &atomic_byte_ranges);
402 let start = cursor_byte.min(line_end);
403 let end = if cursor_byte >= line_end {
404 self.line_ending_at(line_end)
405 .map_or(line_end, |range| range.end)
406 } else {
407 line_end
408 };
409 self.make_plan(
410 start..end,
411 String::new(),
412 start,
413 PostEditCursorAffinity::Right,
414 )
415 }
416 }
417 }
418
419 pub fn apply_plan(&mut self, plan: &EditPlan) -> Result<EditOutcome, ApplyEditPlanError> {
420 self.validate_plan(plan)?;
421 Ok(self.apply_validated_plan(plan))
422 }
423
424 #[must_use]
425 pub fn apply(&mut self, command: EditCommand) -> EditOutcome {
426 let plan = self.plan_command(command, &[]);
427 self.apply_validated_plan(&plan)
428 }
429
430 fn make_plan(
431 &self,
432 replaced_byte_range: Range<usize>,
433 replacement: String,
434 cursor_byte: usize,
435 cursor_affinity: PostEditCursorAffinity,
436 ) -> EditPlan {
437 let removed_text = self.text[replaced_byte_range.clone()].to_owned();
438 EditPlan {
439 replaced_byte_range,
440 replacement,
441 removed_text,
442 cursor_byte,
443 cursor_affinity,
444 source_identity: Arc::clone(&self.identity),
445 source_generation: self.generation,
446 }
447 }
448
449 pub(crate) fn validate_plan(&self, plan: &EditPlan) -> Result<(), ApplyEditPlanError> {
450 if !Arc::ptr_eq(&plan.source_identity, &self.identity)
451 || plan.source_generation != self.generation
452 {
453 return Err(ApplyEditPlanError::StalePlan);
454 }
455 let range = &plan.replaced_byte_range;
456 if range.start > range.end
457 || range.end > self.text.len()
458 || !self.text.is_char_boundary(range.start)
459 || !self.text.is_char_boundary(range.end)
460 || !is_grapheme_boundary(&self.text, range.start)
461 || !is_grapheme_boundary(&self.text, range.end)
462 {
463 return Err(ApplyEditPlanError::InvalidRange);
464 }
465 if self.text.get(range.clone()) != Some(plan.removed_text.as_str()) {
466 return Err(ApplyEditPlanError::RemovedTextMismatch);
467 }
468 let Some(resulting_len) = self
469 .text
470 .len()
471 .checked_sub(range.end - range.start)
472 .and_then(|len| len.checked_add(plan.replacement.len()))
473 else {
474 return Err(ApplyEditPlanError::InvalidCursor);
475 };
476 if plan.cursor_byte > resulting_len {
477 return Err(ApplyEditPlanError::InvalidCursor);
478 }
479 if plan.cursor_affinity == PostEditCursorAffinity::Exact
480 && (plan.replacement != plan.removed_text
481 || !is_grapheme_boundary(&self.text, plan.cursor_byte))
482 {
483 return Err(ApplyEditPlanError::InvalidCursor);
484 }
485 Ok(())
486 }
487
488 pub(crate) fn apply_validated_plan(&mut self, plan: &EditPlan) -> EditOutcome {
489 let old_cursor = self.cursor_byte;
490 let text_changed = plan.removed_text != plan.replacement;
491 let inserted_len = plan.replacement.len();
492 if text_changed {
493 self.text
494 .replace_range(plan.replaced_byte_range.clone(), &plan.replacement);
495 }
496 self.cursor_byte = match plan.cursor_affinity {
497 PostEditCursorAffinity::Exact => plan.cursor_byte,
498 PostEditCursorAffinity::Right => ceil_grapheme_boundary(&self.text, plan.cursor_byte),
499 };
500 let cursor_changed = self.cursor_byte != old_cursor;
501 if text_changed || cursor_changed {
502 self.advance_generation();
503 }
504 let delta = text_changed.then_some(EditDelta {
505 inserted_byte_range: plan.replaced_byte_range.start
506 ..(plan.replaced_byte_range.start + inserted_len),
507 replaced_byte_range: plan.replaced_byte_range.clone(),
508 });
509 EditOutcome::from_changes(delta, cursor_changed)
510 }
511
512 fn advance_generation(&mut self) {
513 if let Some(generation) = self.generation.checked_add(1) {
514 self.generation = generation;
515 } else {
516 self.identity = Arc::new(BufferIdentity);
517 self.generation = 0;
518 }
519 }
520
521 pub fn single_line_viewport(&self, display_width: usize) -> SingleLineViewport {
522 self.single_line_viewport_with_atomic_ranges(display_width, &[])
523 }
524
525 pub fn single_line_viewport_with_atomic_ranges(
526 &self,
527 display_width: usize,
528 atomic_byte_ranges: &[Range<usize>],
529 ) -> SingleLineViewport {
530 let atomic_byte_ranges = normalize_atomic_ranges(&self.text, atomic_byte_ranges);
531 let cursor_byte = self.cursor_byte;
532 if display_width == 0 {
533 return SingleLineViewport {
534 visible_byte_range: cursor_byte..cursor_byte,
535 cursor_display_column: 0,
536 };
537 }
538
539 let line_start = self.line_start_at(cursor_byte, &atomic_byte_ranges);
540 let line_end = self.line_end_from(cursor_byte, &atomic_byte_ranges);
541 let left_budget = display_width - 1;
542 let mut start = cursor_byte;
543 let mut left_width = 0usize;
544 while start > line_start {
545 let previous = previous_atomic_boundary(&self.text, start, &atomic_byte_ranges);
546 let grapheme_width = self.text[previous..start].width();
547 let next_width = left_width.saturating_add(grapheme_width);
548 if next_width > left_budget {
549 break;
550 }
551 start = previous;
552 left_width = next_width;
553 }
554
555 let mut end = start;
556 let mut visible_width = 0usize;
557 while end < line_end {
558 let next = next_atomic_boundary(&self.text, end, &atomic_byte_ranges);
559 let grapheme_width = self.text[end..next].width();
560 let next_width = visible_width.saturating_add(grapheme_width);
561 if next_width > display_width {
562 if end < cursor_byte {
563 end = next;
564 }
565 break;
566 }
567 end = next;
568 visible_width = next_width;
569 }
570
571 SingleLineViewport {
572 visible_byte_range: start..end,
573 cursor_display_column: self.text[start..cursor_byte].width(),
574 }
575 }
576
577 fn previous_word_boundary(
578 &self,
579 style: WordStyle,
580 cursor_byte: usize,
581 atomic_byte_ranges: &[Range<usize>],
582 ) -> usize {
583 let mut position = cursor_byte;
584 while position > 0 {
585 let previous = previous_atomic_boundary(&self.text, position, atomic_byte_ranges);
586 if atomic_word_class(&self.text, previous, position, style, atomic_byte_ranges)
587 == Some(WordClass::Whitespace)
588 {
589 position = previous;
590 } else {
591 break;
592 }
593 }
594
595 if position == 0 {
596 return 0;
597 }
598
599 let previous = previous_atomic_boundary(&self.text, position, atomic_byte_ranges);
600 let target_class =
601 atomic_word_class(&self.text, previous, position, style, atomic_byte_ranges);
602 while position > 0 {
603 let previous = previous_atomic_boundary(&self.text, position, atomic_byte_ranges);
604 if atomic_word_class(&self.text, previous, position, style, atomic_byte_ranges)
605 != target_class
606 {
607 break;
608 }
609 position = previous;
610 }
611 position
612 }
613
614 fn next_word_boundary(
615 &self,
616 style: WordStyle,
617 cursor_byte: usize,
618 atomic_byte_ranges: &[Range<usize>],
619 ) -> usize {
620 let mut position = cursor_byte;
621 while position < self.text.len() {
622 let next = next_atomic_boundary(&self.text, position, atomic_byte_ranges);
623 if atomic_word_class(&self.text, position, next, style, atomic_byte_ranges)
624 == Some(WordClass::Whitespace)
625 {
626 position = next;
627 } else {
628 break;
629 }
630 }
631
632 if position == self.text.len() {
633 return position;
634 }
635
636 let next = next_atomic_boundary(&self.text, position, atomic_byte_ranges);
637 let target_class = atomic_word_class(&self.text, position, next, style, atomic_byte_ranges);
638 while position < self.text.len() {
639 let next = next_atomic_boundary(&self.text, position, atomic_byte_ranges);
640 if atomic_word_class(&self.text, position, next, style, atomic_byte_ranges)
641 != target_class
642 {
643 break;
644 }
645 position = next;
646 }
647 position
648 }
649
650 fn logical_line_start_target(
651 &self,
652 cursor_byte: usize,
653 atomic_byte_ranges: &[Range<usize>],
654 ) -> usize {
655 let line_start = self.line_start_at(cursor_byte, atomic_byte_ranges);
656 if cursor_byte == line_start && line_start > 0 {
657 let previous_line_end =
658 previous_atomic_boundary(&self.text, line_start, atomic_byte_ranges);
659 self.line_start_at(previous_line_end, atomic_byte_ranges)
660 } else {
661 line_start
662 }
663 }
664
665 fn logical_line_end_target(
666 &self,
667 cursor_byte: usize,
668 atomic_byte_ranges: &[Range<usize>],
669 ) -> usize {
670 let line_end = self.line_end_from(cursor_byte, atomic_byte_ranges);
671 if cursor_byte == line_end {
672 self.line_ending_at(line_end).map_or(line_end, |range| {
673 self.line_end_from(range.end, atomic_byte_ranges)
674 })
675 } else {
676 line_end
677 }
678 }
679
680 fn line_start_at(&self, cursor_byte: usize, atomic_byte_ranges: &[Range<usize>]) -> usize {
681 let cursor_byte = cursor_byte.min(self.text.len());
682 (0..cursor_byte)
683 .rev()
684 .find(|position| {
685 self.text.as_bytes()[*position] == b'\n'
686 && !byte_is_inside_atomic_range(*position, atomic_byte_ranges)
687 })
688 .map_or(0, |position| position + 1)
689 }
690
691 fn line_end_from(&self, cursor_byte: usize, atomic_byte_ranges: &[Range<usize>]) -> usize {
692 let cursor_byte = cursor_byte.min(self.text.len());
693 (cursor_byte..self.text.len())
694 .find(|position| {
695 self.text.as_bytes()[*position] == b'\n'
696 && !byte_is_inside_atomic_range(*position, atomic_byte_ranges)
697 })
698 .map_or(self.text.len(), |line_feed| {
699 if line_feed > 0 && self.text.as_bytes()[line_feed - 1] == b'\r' {
700 line_feed - 1
701 } else {
702 line_feed
703 }
704 })
705 }
706
707 fn line_ending_at(&self, line_end: usize) -> Option<Range<usize>> {
708 let remaining = self.text.get(line_end..)?;
709 if remaining.starts_with("\r\n") {
710 Some(line_end..line_end + 2)
711 } else if remaining.starts_with('\n') {
712 Some(line_end..line_end + 1)
713 } else {
714 None
715 }
716 }
717}
718
719#[derive(Debug, Clone, Copy, PartialEq, Eq)]
720enum WordClass {
721 Whitespace,
722 Word,
723 Punctuation,
724 Atomic(usize),
725}
726
727fn word_class(grapheme: &str, style: WordStyle) -> Option<WordClass> {
728 let character = grapheme.chars().next()?;
729 if character.is_whitespace() {
730 Some(WordClass::Whitespace)
731 } else if style == WordStyle::WhitespaceDelimited
732 || character.is_alphanumeric()
733 || character == '_'
734 {
735 Some(WordClass::Word)
736 } else {
737 Some(WordClass::Punctuation)
738 }
739}
740
741fn atomic_word_class(
742 text: &str,
743 start: usize,
744 end: usize,
745 style: WordStyle,
746 atomic_byte_ranges: &[Range<usize>],
747) -> Option<WordClass> {
748 if let Some(index) = atomic_byte_ranges
749 .iter()
750 .position(|range| range.start == start && range.end == end)
751 {
752 match style {
753 WordStyle::Small => Some(WordClass::Atomic(index)),
754 WordStyle::WhitespaceDelimited => Some(WordClass::Word),
755 }
756 } else {
757 word_class(&text[start..end], style)
758 }
759}
760
761fn normalize_atomic_ranges(text: &str, ranges: &[Range<usize>]) -> Vec<Range<usize>> {
762 let mut normalized = ranges
763 .iter()
764 .filter_map(|range| {
765 let raw_start = range.start.min(range.end).min(text.len());
766 let raw_end = range.start.max(range.end).min(text.len());
767 if raw_start == raw_end {
768 return None;
769 }
770 let start = floor_grapheme_boundary(text, raw_start);
771 let end = ceil_grapheme_boundary(text, raw_end);
772 (start < end).then_some(start..end)
773 })
774 .collect::<Vec<_>>();
775 normalized.sort_by_key(|range| (range.start, range.end));
776
777 let mut merged: Vec<Range<usize>> = Vec::with_capacity(normalized.len());
778 for range in normalized {
779 if let Some(previous) = merged.last_mut()
780 && range.start < previous.end
781 {
782 previous.end = previous.end.max(range.end);
783 } else {
784 merged.push(range);
785 }
786 }
787 merged
788}
789
790fn normalize_replacement_range(
791 text: &str,
792 range: Range<usize>,
793 atomic_byte_ranges: &[Range<usize>],
794) -> Range<usize> {
795 let raw_start = range.start.min(range.end).min(text.len());
796 let raw_end = range.start.max(range.end).min(text.len());
797 if raw_start == raw_end {
798 let cursor = normalize_external_cursor(text, raw_start);
799 let cursor = normalize_cursor_for_atomic_ranges(cursor, atomic_byte_ranges);
800 return cursor..cursor;
801 }
802
803 let mut normalized =
804 floor_grapheme_boundary(text, raw_start)..ceil_grapheme_boundary(text, raw_end);
805 loop {
806 let mut changed = false;
807 for atomic in atomic_byte_ranges {
808 if atomic.start < normalized.end && atomic.end > normalized.start {
809 let start = normalized.start.min(atomic.start);
810 let end = normalized.end.max(atomic.end);
811 changed |= start != normalized.start || end != normalized.end;
812 normalized = start..end;
813 }
814 }
815 if !changed {
816 return normalized;
817 }
818 }
819}
820
821fn normalize_cursor_for_atomic_ranges(
822 cursor_byte: usize,
823 atomic_byte_ranges: &[Range<usize>],
824) -> usize {
825 let Some(range) = atomic_byte_ranges
826 .iter()
827 .find(|range| cursor_byte > range.start && cursor_byte < range.end)
828 else {
829 return cursor_byte;
830 };
831 if cursor_byte - range.start <= range.end - cursor_byte {
832 range.start
833 } else {
834 range.end
835 }
836}
837
838fn previous_atomic_boundary(text: &str, byte: usize, atomic_byte_ranges: &[Range<usize>]) -> usize {
839 if let Some(range) = atomic_byte_ranges
840 .iter()
841 .find(|range| byte > range.start && byte <= range.end)
842 {
843 return range.start;
844 }
845 let boundary = previous_grapheme_boundary(text, byte);
846 atomic_byte_ranges
847 .iter()
848 .find(|range| boundary > range.start && boundary < range.end)
849 .map_or(boundary, |range| range.start)
850}
851
852fn next_atomic_boundary(text: &str, byte: usize, atomic_byte_ranges: &[Range<usize>]) -> usize {
853 if let Some(range) = atomic_byte_ranges
854 .iter()
855 .find(|range| byte >= range.start && byte < range.end)
856 {
857 return range.end;
858 }
859 let boundary = next_grapheme_boundary(text, byte);
860 atomic_byte_ranges
861 .iter()
862 .find(|range| boundary > range.start && boundary < range.end)
863 .map_or(boundary, |range| range.end)
864}
865
866fn byte_is_inside_atomic_range(byte: usize, atomic_byte_ranges: &[Range<usize>]) -> bool {
867 atomic_byte_ranges
868 .iter()
869 .any(|range| byte >= range.start && byte < range.end)
870}
871
872fn is_grapheme_boundary(text: &str, byte: usize) -> bool {
873 byte == text.len()
874 || text
875 .grapheme_indices(true)
876 .any(|(boundary, _)| boundary == byte)
877}
878
879fn floor_grapheme_boundary(text: &str, byte: usize) -> usize {
880 let byte = byte.min(text.len());
881 if byte == text.len() {
882 return byte;
883 }
884 text.grapheme_indices(true)
885 .map(|(index, _)| index)
886 .take_while(|index| *index <= byte)
887 .last()
888 .unwrap_or(0)
889}
890
891fn ceil_grapheme_boundary(text: &str, byte: usize) -> usize {
892 let byte = byte.min(text.len());
893 if byte == text.len() {
894 return byte;
895 }
896 text.grapheme_indices(true)
897 .map(|(index, _)| index)
898 .find(|index| *index >= byte)
899 .unwrap_or(text.len())
900}
901
902fn normalize_external_cursor(text: &str, byte: usize) -> usize {
903 let byte = byte.min(text.len());
904 let before = floor_grapheme_boundary(text, byte);
905 let after = ceil_grapheme_boundary(text, byte);
906 if byte - before <= after - byte {
907 before
908 } else {
909 after
910 }
911}
912
913fn previous_grapheme_boundary(text: &str, byte: usize) -> usize {
914 let byte = byte.min(text.len());
915 if byte == 0 {
916 return 0;
917 }
918 let mut cursor = GraphemeCursor::new(byte, text.len(), true);
919 match cursor.prev_boundary(text, 0) {
920 Ok(Some(boundary)) => boundary,
921 Ok(None) => 0,
922 Err(_) => floor_grapheme_boundary(text, byte.saturating_sub(1)),
923 }
924}
925
926fn next_grapheme_boundary(text: &str, byte: usize) -> usize {
927 let byte = byte.min(text.len());
928 if byte == text.len() {
929 return byte;
930 }
931 let mut cursor = GraphemeCursor::new(byte, text.len(), true);
932 match cursor.next_boundary(text, 0) {
933 Ok(Some(boundary)) => boundary,
934 Ok(None) => text.len(),
935 Err(_) => ceil_grapheme_boundary(text, byte.saturating_add(1)),
936 }
937}
938
939#[cfg(test)]
940#[path = "editor_tests/mod.rs"]
941mod tests;