Skip to main content

oxicode_textarea/
editor.rs

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    /// External cursor requests use nearest grapheme boundaries; ties go left for determinism.
213    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    /// External cursor requests use nearest grapheme boundaries; ties go left for determinism.
237    #[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    /// Edit-result cursors keep right affinity when adjacent text merges into one grapheme.
255    #[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;