Skip to main content

azul_core/
selection.rs

1//! Text selection and cursor positioning for inline content.
2//!
3//! This module provides data structures for managing text cursors and selection ranges
4//! in a bidirectional (Bidi) and line-breaking aware manner. It handles:
5//!
6//! - **Grapheme cluster identification**: Unicode-aware character boundaries
7//! - **Bidi support**: Cursor movement in mixed LTR/RTL text
8//! - **Stable positions**: Selection anchors survive layout changes
9//! - **Affinity tracking**: Cursor position at leading/trailing edges
10//! - **Multi-node selection**: Browser-style selection spanning multiple DOM nodes
11//!
12//! # Architecture
13//!
14//! Text positions are represented as:
15//! - `ContentIndex`: Logical position in the original inline content array
16//! - `GraphemeClusterId`: Stable identifier for a grapheme cluster (survives reordering)
17//! - `TextCursor`: Precise cursor location with leading/trailing affinity
18//! - `SelectionRange`: Start and end cursors defining a selection
19//!
20//! Multi-node selection uses an Anchor/Focus model (W3C Selection API):
21//! - `SelectionAnchor`: Fixed point where user started selection (mousedown)
22//! - `SelectionFocus`: Movable point where selection currently ends (drag position)
23//! - `TextSelection`: Complete selection state spanning potentially multiple IFC roots
24//!
25//! # Use Cases
26//!
27//! - Text editing: Insert/delete at cursor position
28//! - Selection rendering: Highlight selected text across multiple nodes
29//! - Keyboard navigation: Move cursor by grapheme/word/line
30//! - Mouse selection: Convert pixel coordinates to text positions
31//! - Drag selection: Extend selection across multiple DOM nodes
32//!
33//! # Examples
34//!
35//! ```rust,no_run
36//! use azul_core::selection::{CursorAffinity, GraphemeClusterId, TextCursor};
37//!
38//! let cursor = TextCursor {
39//!     cluster_id: GraphemeClusterId {
40//!         source_run: 0,
41//!         start_byte_in_run: 0,
42//!     },
43//!     affinity: CursorAffinity::Leading,
44//! };
45//! ```
46
47use alloc::collections::BTreeMap;
48use alloc::vec::Vec;
49use core::sync::atomic::{AtomicU64, Ordering};
50
51use crate::dom::{DomId, DomNodeId, NodeId};
52use crate::geom::{LogicalPosition, LogicalRect};
53
54/// A stable, logical pointer to an item within the original `InlineContent` array.
55///
56/// This structure eliminates the need for string concatenation and byte-offset math
57/// by tracking both the run index and the item index within that run.
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
59pub struct ContentIndex {
60    /// The index of the `InlineContent` run in the original input array.
61    pub run_index: u32,
62    /// The byte index of the character or item *within* that run's string.
63    pub item_index: u32,
64}
65
66/// A stable, logical identifier for a grapheme cluster.
67///
68/// This survives Bidi reordering and line breaking, making it ideal for tracking
69/// text positions for selection and cursor logic.
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
71#[repr(C)]
72pub struct GraphemeClusterId {
73    /// The `run_index` from the source `ContentIndex`.
74    pub source_run: u32,
75    /// The byte index of the start of the cluster in its original `StyledRun`.
76    pub start_byte_in_run: u32,
77}
78
79/// Represents the logical position of the cursor *between* two grapheme clusters
80/// or at the start/end of the text.
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
82#[repr(C)]
83pub enum CursorAffinity {
84    /// The cursor is at the leading edge of the character (left in LTR, right in RTL).
85    Leading,
86    /// The cursor is at the trailing edge of the character (right in LTR, left in RTL).
87    Trailing,
88}
89
90/// Represents a precise cursor location in the logical text.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
92#[repr(C)]
93pub struct TextCursor {
94    /// The grapheme cluster the cursor is associated with.
95    pub cluster_id: GraphemeClusterId,
96    /// The edge of the cluster the cursor is on.
97    pub affinity: CursorAffinity,
98}
99
100impl_option!(
101    TextCursor,
102    OptionTextCursor,
103    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
104);
105
106/// Represents a range of selected text. The direction is implicit (start can be
107/// logically after end if selecting backwards).
108#[derive(Debug, PartialOrd, Ord, Clone, Copy, PartialEq, Eq, Hash)]
109#[repr(C)]
110pub struct SelectionRange {
111    pub start: TextCursor,
112    pub end: TextCursor,
113}
114
115impl_option!(
116    SelectionRange,
117    OptionSelectionRange,
118    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
119);
120
121impl_vec!(SelectionRange, SelectionRangeVec, SelectionRangeVecDestructor, SelectionRangeVecDestructorType, SelectionRangeVecSlice, OptionSelectionRange);
122impl_vec_debug!(SelectionRange, SelectionRangeVec);
123impl_vec_clone!(
124    SelectionRange,
125    SelectionRangeVec,
126    SelectionRangeVecDestructor
127);
128impl_vec_partialeq!(SelectionRange, SelectionRangeVec);
129impl_vec_partialord!(SelectionRange, SelectionRangeVec);
130
131/// A single selection, which can be either a blinking cursor or a highlighted range.
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
133#[repr(C, u8)]
134pub enum Selection {
135    Cursor(TextCursor),
136    Range(SelectionRange),
137}
138
139impl_option!(
140    Selection,
141    OptionSelection,
142    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
143);
144
145impl_vec!(Selection, SelectionVec, SelectionVecDestructor, SelectionVecDestructorType, SelectionVecSlice, OptionSelection);
146impl_vec_debug!(Selection, SelectionVec);
147impl_vec_clone!(Selection, SelectionVec, SelectionVecDestructor);
148impl_vec_partialeq!(Selection, SelectionVec);
149impl_vec_partialord!(Selection, SelectionVec);
150
151/// The complete selection state for a single text block, supporting multiple cursors/ranges.
152#[derive(Debug, Clone, PartialEq)]
153#[repr(C)]
154pub struct SelectionState {
155    /// A list of all active selections. This list is kept sorted and non-overlapping.
156    pub selections: SelectionVec,
157    /// The DOM node this selection state applies to.
158    pub node_id: DomNodeId,
159}
160
161impl SelectionState {
162    /// Adds a new selection, merging it with any existing selections it overlaps with.
163    pub fn add(&mut self, new_selection: Selection) {
164        // A full implementation would handle merging overlapping ranges.
165        // For now, we simply add and sort for simplicity.
166        let mut selections: Vec<Selection> = self.selections.as_ref().to_vec();
167        selections.push(new_selection);
168        selections.sort_unstable();
169        selections.dedup(); // Removes duplicate cursors
170        self.selections = selections.into();
171    }
172
173}
174
175impl_option!(
176    SelectionState,
177    OptionSelectionState,
178    copy = false,
179    clone = false,
180    [Debug, Clone, PartialEq]
181);
182
183// ============================================================================
184// MULTI-CURSOR SUPPORT (Sublime Text style)
185// ============================================================================
186
187/// Stable identifier for a cursor/selection within a `MultiCursorState`.
188///
189/// Uses a monotonic u64 counter (not UUID) so it is `Copy` and C-API friendly.
190/// Each `SelectionId` is unique within the lifetime of the process.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
192#[repr(C)]
193pub struct SelectionId {
194    pub inner: u64,
195}
196
197impl SelectionId {
198    /// Generate a new unique `SelectionId`.
199    pub fn new() -> Self {
200        static COUNTER: AtomicU64 = AtomicU64::new(1);
201        Self { inner: COUNTER.fetch_add(1, Ordering::Relaxed) }
202    }
203}
204
205/// Note: `Default` generates a new unique ID (increments global counter),
206/// rather than returning a zero/sentinel value.
207impl Default for SelectionId {
208    fn default() -> Self {
209        Self::new()
210    }
211}
212
213impl_option!(
214    SelectionId,
215    OptionSelectionId,
216    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
217);
218
219impl_vec!(SelectionId, SelectionIdVec, SelectionIdVecDestructor, SelectionIdVecDestructorType, SelectionIdVecSlice, OptionSelectionId);
220impl_vec_debug!(SelectionId, SelectionIdVec);
221impl_vec_clone!(SelectionId, SelectionIdVec, SelectionIdVecDestructor);
222impl_vec_partialeq!(SelectionId, SelectionIdVec);
223impl_vec_partialord!(SelectionId, SelectionIdVec);
224
225/// A selection (cursor or range) paired with a stable identity.
226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
227#[repr(C)]
228pub struct IdentifiedSelection {
229    pub id: SelectionId,
230    pub selection: Selection,
231}
232
233impl_option!(
234    IdentifiedSelection,
235    OptionIdentifiedSelection,
236    [Debug, Clone, Copy, PartialEq, Eq, Hash]
237);
238
239impl_vec!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor, IdentifiedSelectionVecDestructorType, IdentifiedSelectionVecSlice, OptionIdentifiedSelection);
240impl_vec_debug!(IdentifiedSelection, IdentifiedSelectionVec);
241impl_vec_clone!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor);
242impl_vec_partialeq!(IdentifiedSelection, IdentifiedSelectionVec);
243
244/// Multi-cursor state for a contenteditable element (Sublime Text style).
245///
246/// Replaces the split `CursorManager` + `SelectionManager` pattern for text editing.
247/// Supports multiple simultaneous cursors/selections, each with a stable ID.
248///
249/// ## Invariants
250///
251/// - `selections` is sorted by position and non-overlapping.
252/// - The **primary** selection is identified by the stable `primary_id`, NOT by
253///   vector position: `merge_overlapping()` re-sorts `selections` by position,
254///   so "last index" is not the most-recently-added cursor.
255/// - After any mutation, `merge_overlapping()` is called to maintain invariants.
256#[derive(Debug, Clone, PartialEq, Eq)]
257pub struct MultiCursorState {
258    /// Sorted by position, non-overlapping. Primary is tracked via `primary_id`.
259    pub selections: Vec<IdentifiedSelection>,
260    /// Stable ID of the primary selection (most recently added/set). Survives the
261    /// position sort in `merge_overlapping`, which would otherwise make the
262    /// vector's last element (position-last) masquerade as the primary.
263    pub primary_id: SelectionId,
264    /// The DOM node this multi-cursor state applies to.
265    pub node_id: DomNodeId,
266    /// Stable key that survives DOM rebuilds (from `calculate_contenteditable_key`).
267    pub contenteditable_key: u64,
268}
269
270impl MultiCursorState {
271    /// Create a new `MultiCursorState` with a single cursor.
272    #[must_use] pub fn new_with_cursor(cursor: TextCursor, node_id: DomNodeId, contenteditable_key: u64) -> Self {
273        let id = SelectionId::new();
274        Self {
275            selections: vec![IdentifiedSelection {
276                id,
277                selection: Selection::Cursor(cursor),
278            }],
279            primary_id: id,
280            node_id,
281            contenteditable_key,
282        }
283    }
284
285    /// Add a cursor, merging if it overlaps with existing selections.
286    /// Returns the `SelectionId` of the new (or merged) cursor.
287    #[must_use]
288    pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId {
289        let id = SelectionId::new();
290        self.selections.push(IdentifiedSelection {
291            id,
292            selection: Selection::Cursor(cursor),
293        });
294        self.primary_id = id;
295        self.merge_overlapping();
296        id
297    }
298
299    /// Add a selection range, merging if it overlaps.
300    /// Returns the `SelectionId` of the new (or merged) selection.
301    #[must_use]
302    pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId {
303        let id = SelectionId::new();
304        self.selections.push(IdentifiedSelection {
305            id,
306            selection: Selection::Range(range),
307        });
308        self.primary_id = id;
309        self.merge_overlapping();
310        id
311    }
312
313    /// Remove a selection by its stable ID. Returns true if found and removed.
314    #[must_use]
315    pub fn remove_selection(&mut self, id: SelectionId) -> bool {
316        let len_before = self.selections.len();
317        self.selections.retain(|s| s.id != id);
318        let removed = self.selections.len() < len_before;
319        if removed {
320            // If we just removed the primary, re-point it at a surviving one.
321            self.ensure_primary_valid();
322        }
323        removed
324    }
325
326    /// Get the primary selection (the most recently added/set, tracked by
327    /// `primary_id` — NOT the vector's last element, which position-sorting
328    /// reorders). Falls back to the last element if `primary_id` was somehow
329    /// lost.
330    #[must_use] pub fn get_primary(&self) -> Option<&IdentifiedSelection> {
331        let pid = self.primary_id;
332        self.selections
333            .iter()
334            .find(|s| s.id == pid)
335            .or_else(|| self.selections.last())
336    }
337
338    /// Get a mutable reference to the primary selection (see `get_primary`).
339    pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection> {
340        let pid = self.primary_id;
341        if let Some(pos) = self.selections.iter().position(|s| s.id == pid) {
342            return self.selections.get_mut(pos);
343        }
344        self.selections.last_mut()
345    }
346
347    /// Ensure `primary_id` names a selection that still exists; if not, adopt the
348    /// last selection's id (best effort) so `get_primary` stays meaningful.
349    fn ensure_primary_valid(&mut self) {
350        let pid = self.primary_id;
351        if !self.selections.iter().any(|s| s.id == pid) {
352            if let Some(last) = self.selections.last() {
353                self.primary_id = last.id;
354            }
355        }
356    }
357
358    /// Get the primary cursor position (for scroll-into-view, IME, etc.)
359    #[must_use] pub fn get_primary_cursor(&self) -> Option<TextCursor> {
360        self.get_primary().map(|s| match &s.selection {
361            Selection::Cursor(c) => *c,
362            Selection::Range(r) => r.end,
363        })
364    }
365
366    /// Convert to a Vec<Selection> for passing to `edit_text()`.
367    #[must_use] pub fn to_selections(&self) -> Vec<Selection> {
368        self.selections.iter().map(|s| s.selection).collect()
369    }
370
371    /// Update selections from the result of `edit_text()`.
372    ///
373    /// Preserves existing IDs where possible (by index), assigns new IDs for extras.
374    pub fn update_from_edit_result(&mut self, new_selections: &[Selection]) {
375        let old_ids: Vec<SelectionId> = self.selections.iter().map(|s| s.id).collect();
376        self.selections.clear();
377        for (i, sel) in new_selections.iter().enumerate() {
378            let id = old_ids.get(i).copied().unwrap_or_else(SelectionId::new);
379            self.selections.push(IdentifiedSelection {
380                id,
381                selection: *sel,
382            });
383        }
384        // IDs are reassigned by index; make sure primary_id still resolves.
385        self.ensure_primary_valid();
386        // Don't merge here — edit_text already returns correct positions
387    }
388
389    /// Set all selections to a single cursor (e.g., on plain click without Ctrl).
390    pub fn set_single_cursor(&mut self, cursor: TextCursor) {
391        let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
392        self.selections.clear();
393        self.selections.push(IdentifiedSelection {
394            id,
395            selection: Selection::Cursor(cursor),
396        });
397        self.primary_id = id;
398    }
399
400    /// Set all selections to a single range.
401    pub fn set_single_range(&mut self, range: SelectionRange) {
402        let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
403        self.selections.clear();
404        self.selections.push(IdentifiedSelection {
405            id,
406            selection: Selection::Range(range),
407        });
408        self.primary_id = id;
409    }
410
411    /// Number of active cursors/selections.
412    #[must_use] pub const fn len(&self) -> usize {
413        self.selections.len()
414    }
415
416    /// Whether there are no selections (should not normally happen).
417    #[must_use] pub const fn is_empty(&self) -> bool {
418        self.selections.is_empty()
419    }
420
421    /// Sort selections by position and merge any that overlap.
422    pub fn merge_overlapping(&mut self) {
423        if self.selections.len() <= 1 {
424            return;
425        }
426
427        // Capture the primary before sorting/merging reorders and rewrites IDs.
428        let primary = self.primary_id;
429        let mut new_primary = primary;
430
431        // Sort by the start position of each selection
432        self.selections.sort_by(|a, b| {
433            let pos_a = selection_start_pos(&a.selection);
434            let pos_b = selection_start_pos(&b.selection);
435            pos_a.cmp(&pos_b)
436        });
437
438        // Merge overlapping: if selection[i+1] starts at or before selection[i] ends,
439        // merge them into one range (keeping the later ID as it's more recent).
440        let mut merged: Vec<IdentifiedSelection> = Vec::with_capacity(self.selections.len());
441        for sel in self.selections.drain(..) {
442            if let Some(last) = merged.last_mut() {
443                let last_end = selection_end_pos(&last.selection);
444                let cur_start = selection_start_pos(&sel.selection);
445                if cur_start <= last_end {
446                    // Overlap — merge into one range covering both
447                    let new_start = selection_start_pos(&last.selection);
448                    let cur_end = selection_end_pos(&sel.selection);
449                    let new_end = if cur_end > last_end { cur_end } else { last_end };
450                    if new_start == new_end {
451                        last.selection = Selection::Cursor(new_start);
452                    } else {
453                        last.selection = Selection::Range(SelectionRange {
454                            start: new_start,
455                            end: new_end,
456                        });
457                    }
458                    // If either side of the merge — or the accumulator that has
459                    // already absorbed the primary earlier in the chain — was the
460                    // primary, the merged selection inherits primary status.
461                    // `last.id == new_primary` carries the primary across a 3+-link
462                    // chain: without it, `new_primary` would keep pointing at an
463                    // intermediate id that the next merge overwrites, and
464                    // `ensure_primary_valid` would then adopt an unrelated tail.
465                    let inherits_primary =
466                        last.id == primary || sel.id == primary || last.id == new_primary;
467                    // Keep the newer ID (the one being merged in)
468                    last.id = sel.id;
469                    if inherits_primary {
470                        new_primary = sel.id;
471                    }
472                    continue;
473                }
474            }
475            merged.push(sel);
476        }
477        self.selections = merged;
478
479        // Point primary at a surviving selection (fallback: last element).
480        self.primary_id = new_primary;
481        self.ensure_primary_valid();
482    }
483
484    /// Move all cursors using a movement function. Merges collisions afterward.
485    ///
486    /// `move_fn` takes a `TextCursor` and returns the new `TextCursor` after movement.
487    /// If `extend_selection` is true, the anchor stays and only the focus moves,
488    /// creating or extending a range.
489    pub fn move_all_cursors(
490        &mut self,
491        extend_selection: bool,
492        move_fn: impl Fn(&TextCursor) -> TextCursor,
493    ) {
494        for sel in &mut self.selections {
495            match &sel.selection {
496                Selection::Cursor(c) => {
497                    let new_cursor = move_fn(c);
498                    if extend_selection {
499                        if *c != new_cursor {
500                            sel.selection = Selection::Range(SelectionRange {
501                                start: *c,
502                                end: new_cursor,
503                            });
504                        }
505                    } else {
506                        sel.selection = Selection::Cursor(new_cursor);
507                    }
508                }
509                Selection::Range(r) => {
510                    if extend_selection {
511                        let new_end = move_fn(&r.end);
512                        if r.start == new_end {
513                            sel.selection = Selection::Cursor(r.start);
514                        } else {
515                            sel.selection = Selection::Range(SelectionRange {
516                                start: r.start,
517                                end: new_end,
518                            });
519                        }
520                    } else {
521                        // Bare arrow with an active selection collapses the caret
522                        // to the selection boundary in the arrow's direction WITHOUT
523                        // advancing a character (standard editor behavior). Running
524                        // move_fn on the focus and using that as the caret would step
525                        // one unit past the edge. We don't get the arrow direction
526                        // here, so probe it: apply move_fn to the focus and compare —
527                        // a forward move collapses to the max boundary, a backward
528                        // move to the min boundary.
529                        let (lo, hi) = if r.start <= r.end {
530                            (r.start, r.end)
531                        } else {
532                            (r.end, r.start)
533                        };
534                        let probe = move_fn(&r.end);
535                        let collapsed = if probe >= r.end { hi } else { lo };
536                        sel.selection = Selection::Cursor(collapsed);
537                    }
538                }
539            }
540        }
541        self.merge_overlapping();
542    }
543
544    /// Remap the `NodeId` in `node_id` after DOM reconciliation.
545    ///
546    /// If the node was removed (not in the map), the multi-cursor state is cleared.
547    pub fn remap_node_ids(
548        &mut self,
549        dom_id: DomId,
550        node_id_map: &BTreeMap<NodeId, NodeId>,
551    ) {
552        if self.node_id.dom != dom_id {
553            return;
554        }
555        if let Some(old_node_id) = self.node_id.node.into_crate_internal() {
556            if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
557                self.node_id.node = crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
558            } else {
559                // Node removed — clear selections
560                self.selections.clear();
561            }
562        }
563    }
564}
565
566/// Helper: get the start position of a Selection for sorting.
567fn selection_start_pos(sel: &Selection) -> TextCursor {
568    match sel {
569        Selection::Cursor(c) => *c,
570        Selection::Range(r) => {
571            if r.start <= r.end { r.start } else { r.end }
572        }
573    }
574}
575
576/// Helper: get the end position of a Selection for merging.
577fn selection_end_pos(sel: &Selection) -> TextCursor {
578    match sel {
579        Selection::Cursor(c) => *c,
580        Selection::Range(r) => {
581            if r.end >= r.start { r.end } else { r.start }
582        }
583    }
584}
585
586// ============================================================================
587// MULTI-NODE SELECTION (Browser-style Anchor/Focus model)
588// ============================================================================
589
590/// The anchor point of a text selection - where the user started selecting.
591///
592/// This is the fixed point during a drag operation. It records:
593/// - The IFC root node (where the `UnifiedLayout` lives)
594/// - The exact cursor position within that layout
595/// - The visual bounds of the anchor character (for logical rectangle calculations)
596///
597/// The anchor remains constant during a drag; only the focus moves.
598#[derive(Debug, Clone, Copy, PartialEq, Eq)]
599pub struct SelectionAnchor {
600    /// The IFC root node ID where selection started.
601    /// This is the node that has `inline_layout_result` (e.g., `<p>`, `<div>`).
602    pub ifc_root_node_id: NodeId,
603    
604    /// The exact cursor position within the IFC's `UnifiedLayout`.
605    pub cursor: TextCursor,
606    
607    /// Visual bounds of the anchor character in viewport coordinates.
608    /// Used for computing the logical selection rectangle during multi-line/multi-node selection.
609    pub char_bounds: LogicalRect,
610    
611    /// The mouse position when the selection started (viewport coordinates).
612    pub mouse_position: LogicalPosition,
613}
614
615/// The focus point of a text selection - where the selection currently ends.
616///
617/// This is the movable point during a drag operation. It updates on every mouse move.
618#[derive(Debug, Clone, Copy, PartialEq, Eq)]
619pub struct SelectionFocus {
620    /// The IFC root node ID where selection currently ends.
621    /// May differ from anchor's IFC root during cross-node selection.
622    pub ifc_root_node_id: NodeId,
623    
624    /// The exact cursor position within the IFC's `UnifiedLayout`.
625    pub cursor: TextCursor,
626    
627    /// Current mouse position in viewport coordinates.
628    pub mouse_position: LogicalPosition,
629}
630
631/// Complete selection state spanning potentially multiple DOM nodes.
632///
633/// This implements the W3C Selection API model with anchor/focus endpoints.
634/// The selection can span multiple IFC roots (e.g., multiple `<p>` elements).
635///
636/// ## Storage Model
637///
638/// Uses `BTreeMap<NodeId, SelectionRange>` for O(log N) lookup during rendering.
639/// The key is the **IFC root `NodeId`**, and the value is the `SelectionRange` for that IFC.
640///
641/// ## Example
642///
643/// ```text
644/// <p id="1">Hello [World</p>     <- Anchor in IFC 1, partial selection
645/// <p id="2">Complete line</p>    <- InBetween, fully selected
646/// <p id="3">Partial] end</p>     <- Focus in IFC 3, partial selection
647/// ```
648#[derive(Debug, Clone, PartialEq, Eq)]
649pub struct TextSelection {
650    /// The DOM this selection belongs to.
651    pub dom_id: DomId,
652    
653    /// The anchor point - where the selection started (fixed during drag).
654    pub anchor: SelectionAnchor,
655    
656    /// The focus point - where the selection currently ends (moves during drag).
657    pub focus: SelectionFocus,
658    
659    /// Map from IFC root `NodeId` to the `SelectionRange` for that IFC.
660    /// This allows O(log N) lookup during rendering.
661    ///
662    /// The `SelectionRange` contains the actual `TextCursor` positions for that IFC,
663    /// ready to be passed to `UnifiedLayout::get_selection_rects()`.
664    pub affected_nodes: BTreeMap<NodeId, SelectionRange>,
665    
666    /// Indicates whether anchor comes before focus in document order.
667    /// True = forward selection (left-to-right), False = backward selection.
668    pub is_forward: bool,
669}
670
671impl TextSelection {
672    /// Create a new collapsed selection (cursor) at the given position.
673    #[must_use] pub fn new_collapsed(
674        dom_id: DomId,
675        ifc_root_node_id: NodeId,
676        cursor: TextCursor,
677        char_bounds: LogicalRect,
678        mouse_position: LogicalPosition,
679    ) -> Self {
680        let anchor = SelectionAnchor {
681            ifc_root_node_id,
682            cursor,
683            char_bounds,
684            mouse_position,
685        };
686        
687        let focus = SelectionFocus {
688            ifc_root_node_id,
689            cursor,
690            mouse_position,
691        };
692        
693        // For a collapsed selection, the anchor node has a zero-width range
694        let mut affected_nodes = BTreeMap::new();
695        affected_nodes.insert(ifc_root_node_id, SelectionRange {
696            start: cursor,
697            end: cursor,
698        });
699        
700        Self {
701            dom_id,
702            anchor,
703            focus,
704            affected_nodes,
705            is_forward: true, // Direction doesn't matter for collapsed selection
706        }
707    }
708    
709    /// Check if this is a collapsed selection (cursor with no range).
710    #[must_use] pub fn is_collapsed(&self) -> bool {
711        self.anchor.ifc_root_node_id == self.focus.ifc_root_node_id
712            && self.anchor.cursor == self.focus.cursor
713    }
714    
715    /// Get the selection range for a specific IFC root node.
716    /// Returns `None` if this node is not part of the selection.
717    #[must_use] pub fn get_range_for_node(&self, ifc_root_node_id: &NodeId) -> Option<&SelectionRange> {
718        self.affected_nodes.get(ifc_root_node_id)
719    }
720
721}
722
723impl_option!(
724    TextSelection,
725    OptionTextSelection,
726    copy = false,
727    clone = false,
728    [Debug, Clone, PartialEq, Eq]
729);
730
731#[cfg(test)]
732mod audit_tests {
733    use super::*;
734
735    fn cursor(byte: u32) -> TextCursor {
736        TextCursor {
737            cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: byte },
738            affinity: CursorAffinity::Leading,
739        }
740    }
741
742    fn state(byte: u32) -> MultiCursorState {
743        MultiCursorState::new_with_cursor(cursor(byte), DomNodeId::ROOT, 0)
744    }
745
746    #[test]
747    fn primary_tracked_by_id_not_vec_position() {
748        let mut mc = state(100);
749        // Add a cursor at an EARLIER position; after merge_overlapping's sort it
750        // becomes the vector's FIRST element, but it is the primary (last added).
751        let b = mc.add_cursor(cursor(0));
752        assert_eq!(mc.len(), 2);
753        // The primary must be the just-added cursor at byte 0, not the
754        // position-last cursor at byte 100.
755        assert_eq!(mc.get_primary().unwrap().id, b);
756        assert_eq!(mc.get_primary_cursor().unwrap(), cursor(0));
757    }
758
759    #[test]
760    fn merge_preserves_primary() {
761        let mut mc = state(5);
762        let _b = mc.add_cursor(cursor(5)); // same position -> merges to one
763        assert_eq!(mc.len(), 1);
764        // primary_id must resolve to the surviving selection.
765        let primary = mc.get_primary().unwrap();
766        assert_eq!(primary.id, mc.selections[0].id);
767    }
768
769    #[test]
770    fn removing_primary_repoints_it() {
771        let mut mc = state(0);
772        let b = mc.add_cursor(cursor(10)); // primary = b
773        assert_eq!(mc.get_primary().unwrap().id, b);
774        assert!(mc.remove_selection(b));
775        // primary must now be a still-existing selection, not a dangling id.
776        let p = mc.get_primary().unwrap();
777        assert!(mc.selections.iter().any(|s| s.id == p.id));
778    }
779}
780
781#[cfg(test)]
782mod autotest_generated {
783    use super::*;
784    use crate::geom::LogicalSize;
785    use crate::styled_dom::NodeHierarchyItemId;
786
787    // ---------------------------------------------------------------------
788    // Fixtures
789    // ---------------------------------------------------------------------
790
791    /// Cursor in run 0 at `byte`, Leading affinity.
792    fn c(byte: u32) -> TextCursor {
793        TextCursor {
794            cluster_id: GraphemeClusterId {
795                source_run: 0,
796                start_byte_in_run: byte,
797            },
798            affinity: CursorAffinity::Leading,
799        }
800    }
801
802    /// Cursor with explicit run + affinity (for ordering / boundary probes).
803    fn c_full(run: u32, byte: u32, affinity: CursorAffinity) -> TextCursor {
804        TextCursor {
805            cluster_id: GraphemeClusterId {
806                source_run: run,
807                start_byte_in_run: byte,
808            },
809            affinity,
810        }
811    }
812
813    fn rng(a: u32, b: u32) -> SelectionRange {
814        SelectionRange {
815            start: c(a),
816            end: c(b),
817        }
818    }
819
820    fn dom_node(index: usize) -> DomNodeId {
821        DomNodeId {
822            dom: DomId::ROOT_ID,
823            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(index))),
824        }
825    }
826
827    fn state(byte: u32) -> MultiCursorState {
828        MultiCursorState::new_with_cursor(c(byte), DomNodeId::ROOT, 0)
829    }
830
831    /// A `MultiCursorState` with zero selections — "should not normally happen",
832    /// so every getter must survive it.
833    fn empty_state() -> MultiCursorState {
834        MultiCursorState {
835            selections: Vec::new(),
836            primary_id: SelectionId::new(),
837            node_id: DomNodeId::ROOT,
838            contenteditable_key: 0,
839        }
840    }
841
842    fn ident(id: SelectionId, sel: Selection) -> IdentifiedSelection {
843        IdentifiedSelection { id, selection: sel }
844    }
845
846    // ---------------------------------------------------------------------
847    // Invariant checkers (documented in `MultiCursorState`'s `## Invariants`)
848    // ---------------------------------------------------------------------
849
850    /// `selections` is sorted by position and non-overlapping.
851    fn assert_sorted_nonoverlapping(mc: &MultiCursorState) {
852        for w in mc.selections.windows(2) {
853            let prev_end = selection_end_pos(&w[0].selection);
854            let next_start = selection_start_pos(&w[1].selection);
855            assert!(
856                next_start > prev_end,
857                "selections must be sorted and non-overlapping after merge: {:?} then {:?}",
858                w[0],
859                w[1]
860            );
861        }
862    }
863
864    /// `primary_id` must always name a selection that actually exists (or the
865    /// state must be empty). A dangling `primary_id` makes `get_primary()` lie.
866    fn assert_primary_resolves(mc: &MultiCursorState) {
867        if mc.is_empty() {
868            assert!(mc.get_primary().is_none());
869            assert!(mc.get_primary_cursor().is_none());
870        } else {
871            let p = mc.get_primary().expect("non-empty state must have a primary");
872            assert!(
873                mc.selections.iter().any(|s| s.id == p.id),
874                "get_primary() returned a selection not in the vec"
875            );
876            assert_eq!(
877                mc.primary_id, p.id,
878                "primary_id must name an existing selection (not fall back to last)"
879            );
880        }
881    }
882
883    /// All selection IDs must be distinct.
884    fn assert_ids_unique(mc: &MultiCursorState) {
885        for (i, a) in mc.selections.iter().enumerate() {
886            for b in mc.selections.iter().skip(i + 1) {
887                assert_ne!(a.id, b.id, "duplicate SelectionId in state");
888            }
889        }
890    }
891
892    // =====================================================================
893    // SelectionId::new  (constructor)
894    // =====================================================================
895
896    #[test]
897    fn selection_id_new_is_unique_and_strictly_increasing() {
898        let mut prev = SelectionId::new();
899        assert!(prev.inner > 0, "counter starts at 1, never the 0 sentinel");
900        for _ in 0..1000 {
901            let next = SelectionId::new();
902            // Other tests share the global atomic, so ids may skip — but within
903            // one thread they must be strictly increasing and never repeat.
904            assert!(
905                next.inner > prev.inner,
906                "SelectionId counter must be strictly monotonic"
907            );
908            assert_ne!(next, prev);
909            prev = next;
910        }
911    }
912
913    #[test]
914    fn selection_id_default_mints_a_fresh_id() {
915        // Documented: Default does NOT return a zero/sentinel value.
916        let a = SelectionId::default();
917        let b = SelectionId::default();
918        let d = SelectionId::new();
919        assert_ne!(a, b);
920        assert_ne!(b, d);
921        assert!(a.inner > 0 && b.inner > 0);
922    }
923
924    // =====================================================================
925    // SelectionState::add
926    // =====================================================================
927
928    #[test]
929    fn selection_state_add_dedups_identical_cursors() {
930        let mut st = SelectionState {
931            selections: Vec::<Selection>::new().into(),
932            node_id: DomNodeId::ROOT,
933        };
934        for _ in 0..100 {
935            st.add(Selection::Cursor(c(42)));
936        }
937        assert_eq!(st.selections.as_ref().len(), 1);
938        assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(42)));
939    }
940
941    #[test]
942    fn selection_state_add_sorts_descending_input_ascending() {
943        let mut st = SelectionState {
944            selections: Vec::<Selection>::new().into(),
945            node_id: DomNodeId::ROOT,
946        };
947        for byte in [90u32, 10, 50, 0, 70] {
948            st.add(Selection::Cursor(c(byte)));
949        }
950        let got: Vec<Selection> = st.selections.as_ref().to_vec();
951        assert_eq!(got.len(), 5);
952        let want: Vec<Selection> = [0u32, 10, 50, 70, 90]
953            .iter()
954            .map(|b| Selection::Cursor(c(*b)))
955            .collect();
956        assert_eq!(got, want);
957    }
958
959    #[test]
960    fn selection_state_add_boundary_and_reversed_ranges_do_not_panic() {
961        let mut st = SelectionState {
962            selections: Vec::<Selection>::new().into(),
963            node_id: DomNodeId::ROOT,
964        };
965        // u32::MAX bytes, max run index, both affinities, and a *reversed* range
966        // (start logically after end — explicitly allowed by SelectionRange docs).
967        st.add(Selection::Cursor(c_full(
968            u32::MAX,
969            u32::MAX,
970            CursorAffinity::Trailing,
971        )));
972        st.add(Selection::Cursor(c_full(0, 0, CursorAffinity::Leading)));
973        st.add(Selection::Range(SelectionRange {
974            start: c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
975            end: c_full(0, 0, CursorAffinity::Leading),
976        }));
977        st.add(Selection::Range(rng(0, u32::MAX)));
978        // add() only sorts + dedups; it does not normalize or merge, so all 4 stay.
979        assert_eq!(st.selections.as_ref().len(), 4);
980        // ... and the result is sorted.
981        let got: Vec<Selection> = st.selections.as_ref().to_vec();
982        let mut sorted = got.clone();
983        sorted.sort_unstable();
984        assert_eq!(got, sorted);
985    }
986
987    #[test]
988    fn selection_state_add_cursor_and_range_at_same_pos_are_distinct() {
989        let mut st = SelectionState {
990            selections: Vec::<Selection>::new().into(),
991            node_id: DomNodeId::ROOT,
992        };
993        st.add(Selection::Range(rng(5, 5)));
994        st.add(Selection::Cursor(c(5)));
995        // A zero-width Range and a Cursor are different `Selection` variants,
996        // so dedup() cannot collapse them.
997        assert_eq!(st.selections.as_ref().len(), 2);
998        // Cursor variant sorts before Range variant.
999        assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(5)));
1000    }
1001
1002    // =====================================================================
1003    // MultiCursorState::new_with_cursor  (constructor)
1004    // =====================================================================
1005
1006    #[test]
1007    fn new_with_cursor_invariants_hold() {
1008        let node = dom_node(7);
1009        let mc = MultiCursorState::new_with_cursor(c(3), node, 0xDEAD_BEEF);
1010        assert_eq!(mc.len(), 1);
1011        assert!(!mc.is_empty());
1012        assert_eq!(mc.selections.len(), mc.len());
1013        assert_eq!(mc.primary_id, mc.selections[0].id);
1014        assert_eq!(mc.node_id, node);
1015        assert_eq!(mc.contenteditable_key, 0xDEAD_BEEF);
1016        assert_eq!(mc.get_primary_cursor(), Some(c(3)));
1017        assert_eq!(mc.to_selections(), vec![Selection::Cursor(c(3))]);
1018        assert_primary_resolves(&mc);
1019    }
1020
1021    #[test]
1022    fn new_with_cursor_extreme_args_do_not_panic() {
1023        let mc = MultiCursorState::new_with_cursor(
1024            c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
1025            dom_node(usize::MAX / 4),
1026            u64::MAX,
1027        );
1028        assert_eq!(mc.len(), 1);
1029        assert_eq!(mc.contenteditable_key, u64::MAX);
1030        assert_eq!(
1031            mc.get_primary_cursor(),
1032            Some(c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing))
1033        );
1034        assert_primary_resolves(&mc);
1035    }
1036
1037    #[test]
1038    fn two_states_get_distinct_ids() {
1039        let a = state(0);
1040        let b = state(0);
1041        assert_ne!(a.primary_id, b.primary_id);
1042    }
1043
1044    // =====================================================================
1045    // add_cursor / add_selection
1046    // =====================================================================
1047
1048    #[test]
1049    fn add_cursor_at_same_position_merges_to_one() {
1050        let mut mc = state(5);
1051        let b = mc.add_cursor(c(5));
1052        assert_eq!(mc.len(), 1);
1053        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(5)));
1054        assert_eq!(mc.selections[0].id, b, "merge keeps the newer id");
1055        assert_primary_resolves(&mc);
1056        assert_ids_unique(&mc);
1057    }
1058
1059    #[test]
1060    fn add_cursor_distinct_positions_stay_separate_and_sorted() {
1061        let mut mc = state(30);
1062        let _ = mc.add_cursor(c(10));
1063        let last = mc.add_cursor(c(20));
1064        assert_eq!(mc.len(), 3);
1065        // Sorted by position, NOT by insertion order.
1066        assert_eq!(mc.to_selections(), vec![
1067            Selection::Cursor(c(10)),
1068            Selection::Cursor(c(20)),
1069            Selection::Cursor(c(30)),
1070        ]);
1071        // Primary is the most recently added (byte 20), which is the *middle*
1072        // element — proving primary is tracked by id, not vec position.
1073        assert_eq!(mc.get_primary().unwrap().id, last);
1074        assert_eq!(mc.get_primary_cursor(), Some(c(20)));
1075        assert_sorted_nonoverlapping(&mc);
1076        assert_primary_resolves(&mc);
1077        assert_ids_unique(&mc);
1078    }
1079
1080    #[test]
1081    fn add_cursor_same_byte_different_affinity_does_not_merge() {
1082        // Leading < Trailing, so cur_start(Trailing) > last_end(Leading) and the
1083        // merge condition (`cur_start <= last_end`) is false. Two carets survive
1084        // at the same byte offset.
1085        let mut mc = MultiCursorState::new_with_cursor(
1086            c_full(0, 4, CursorAffinity::Leading),
1087            DomNodeId::ROOT,
1088            0,
1089        );
1090        let _ = mc.add_cursor(c_full(0, 4, CursorAffinity::Trailing));
1091        assert_eq!(mc.len(), 2);
1092        assert_sorted_nonoverlapping(&mc);
1093        assert_primary_resolves(&mc);
1094    }
1095
1096    #[test]
1097    fn add_selection_overlapping_ranges_merge_into_union() {
1098        let mut mc = empty_state();
1099        let _ = mc.add_selection(rng(0, 10));
1100        let _ = mc.add_selection(rng(5, 20));
1101        assert_eq!(mc.len(), 1);
1102        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 20)));
1103        assert_primary_resolves(&mc);
1104    }
1105
1106    #[test]
1107    fn add_selection_touching_ranges_merge() {
1108        // Adjacent (end == start) counts as overlapping: `cur_start <= last_end`.
1109        let mut mc = empty_state();
1110        let _ = mc.add_selection(rng(0, 10));
1111        let _ = mc.add_selection(rng(10, 20));
1112        assert_eq!(mc.len(), 1);
1113        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 20)));
1114    }
1115
1116    #[test]
1117    fn add_selection_disjoint_ranges_stay_separate() {
1118        let mut mc = empty_state();
1119        let _ = mc.add_selection(rng(0, 10));
1120        let _ = mc.add_selection(rng(11, 20));
1121        assert_eq!(mc.len(), 2);
1122        assert_sorted_nonoverlapping(&mc);
1123        assert_primary_resolves(&mc);
1124    }
1125
1126    #[test]
1127    fn add_selection_reversed_range_is_normalized_for_merging() {
1128        // Backwards selection: start (20) is logically after end (5).
1129        let mut mc = empty_state();
1130        let _ = mc.add_selection(SelectionRange {
1131            start: c(20),
1132            end: c(5),
1133        });
1134        // A cursor *inside* the backwards range must merge with it.
1135        let _ = mc.add_cursor(c(10));
1136        assert_eq!(mc.len(), 1);
1137        // The merged result is normalized to a forwards range.
1138        assert_eq!(mc.selections[0].selection, Selection::Range(rng(5, 20)));
1139        assert_primary_resolves(&mc);
1140    }
1141
1142    #[test]
1143    fn add_selection_at_u32_max_boundary_does_not_overflow() {
1144        let mut mc = empty_state();
1145        let _ = mc.add_selection(rng(u32::MAX - 1, u32::MAX));
1146        let _ = mc.add_cursor(c(u32::MAX));
1147        assert_eq!(mc.len(), 1);
1148        assert_eq!(
1149            mc.selections[0].selection,
1150            Selection::Range(rng(u32::MAX - 1, u32::MAX))
1151        );
1152        assert_primary_resolves(&mc);
1153    }
1154
1155    #[test]
1156    fn add_cursor_stress_500_distinct_positions() {
1157        let mut mc = state(0);
1158        for i in 1..=500u32 {
1159            let _ = mc.add_cursor(c(i * 2));
1160        }
1161        assert_eq!(mc.len(), 501);
1162        assert_sorted_nonoverlapping(&mc);
1163        assert_primary_resolves(&mc);
1164        assert_ids_unique(&mc);
1165    }
1166
1167    #[test]
1168    fn add_cursor_stress_same_position_never_grows() {
1169        let mut mc = state(9);
1170        for _ in 0..300 {
1171            let _ = mc.add_cursor(c(9));
1172        }
1173        assert_eq!(mc.len(), 1, "identical cursors must always collapse");
1174        assert_primary_resolves(&mc);
1175    }
1176
1177    // =====================================================================
1178    // remove_selection
1179    // =====================================================================
1180
1181    #[test]
1182    fn remove_selection_unknown_id_returns_false_and_changes_nothing() {
1183        let mut mc = state(1);
1184        let before = mc.clone();
1185        let ghost = SelectionId::new(); // never inserted anywhere
1186        assert!(!mc.remove_selection(ghost));
1187        assert_eq!(mc, before);
1188    }
1189
1190    #[test]
1191    fn remove_selection_twice_second_call_returns_false() {
1192        let mut mc = state(0);
1193        let b = mc.add_cursor(c(10));
1194        assert!(mc.remove_selection(b));
1195        assert!(!mc.remove_selection(b));
1196        assert_eq!(mc.len(), 1);
1197        assert_primary_resolves(&mc);
1198    }
1199
1200    #[test]
1201    fn remove_all_selections_leaves_a_safe_empty_state() {
1202        let mut mc = state(0);
1203        let b = mc.add_cursor(c(10));
1204        let a = mc.selections.iter().find(|s| s.id != b).unwrap().id;
1205        assert!(mc.remove_selection(a));
1206        assert!(mc.remove_selection(b));
1207        assert!(mc.is_empty());
1208        assert_eq!(mc.len(), 0);
1209        assert!(mc.get_primary().is_none());
1210        assert!(mc.get_primary_cursor().is_none());
1211        assert!(mc.to_selections().is_empty());
1212        // Further mutation of the empty state must not panic.
1213        mc.merge_overlapping();
1214        mc.move_all_cursors(true, |cur| *cur);
1215        assert!(mc.is_empty());
1216    }
1217
1218    #[test]
1219    fn remove_primary_from_three_repoints_to_a_survivor() {
1220        let mut mc = state(0);
1221        let _ = mc.add_cursor(c(10));
1222        let p = mc.add_cursor(c(20)); // primary
1223        assert_eq!(mc.get_primary().unwrap().id, p);
1224        assert!(mc.remove_selection(p));
1225        assert_eq!(mc.len(), 2);
1226        assert_primary_resolves(&mc);
1227    }
1228
1229    // =====================================================================
1230    // get_primary / get_primary_mut / get_primary_cursor / to_selections / len
1231    // =====================================================================
1232
1233    #[test]
1234    fn empty_state_getters_return_none_without_panicking() {
1235        let mut mc = empty_state();
1236        assert!(mc.is_empty());
1237        assert_eq!(mc.len(), 0);
1238        assert!(mc.get_primary().is_none());
1239        assert!(mc.get_primary_mut().is_none());
1240        assert!(mc.get_primary_cursor().is_none());
1241        assert!(mc.to_selections().is_empty());
1242        mc.merge_overlapping(); // early-returns on len <= 1
1243        mc.ensure_primary_valid(); // private: must not panic on empty vec
1244        assert!(mc.is_empty());
1245    }
1246
1247    #[test]
1248    fn get_primary_falls_back_to_last_when_primary_id_is_dangling() {
1249        let mut mc = state(0);
1250        let _ = mc.add_cursor(c(10));
1251        mc.primary_id = SelectionId::new(); // dangling: names nothing in the vec
1252        let p = mc.get_primary().expect("must fall back, not return None");
1253        assert_eq!(p.id, mc.selections.last().unwrap().id);
1254        assert_eq!(mc.get_primary_cursor(), Some(c(10)));
1255    }
1256
1257    #[test]
1258    fn ensure_primary_valid_adopts_last_id_when_dangling() {
1259        let mut mc = state(0);
1260        let _ = mc.add_cursor(c(10));
1261        let dangling = SelectionId::new();
1262        mc.primary_id = dangling;
1263        mc.ensure_primary_valid();
1264        assert_ne!(mc.primary_id, dangling);
1265        assert_eq!(mc.primary_id, mc.selections.last().unwrap().id);
1266        // Idempotent: a second call on a now-valid id is a no-op.
1267        let fixed = mc.primary_id;
1268        mc.ensure_primary_valid();
1269        assert_eq!(mc.primary_id, fixed);
1270    }
1271
1272    #[test]
1273    fn ensure_primary_valid_on_empty_leaves_id_untouched() {
1274        let mut mc = empty_state();
1275        let before = mc.primary_id;
1276        mc.ensure_primary_valid();
1277        assert_eq!(mc.primary_id, before, "nothing to adopt — id must not change");
1278        assert!(mc.get_primary().is_none());
1279    }
1280
1281    #[test]
1282    fn get_primary_cursor_of_a_range_is_its_end_field() {
1283        let mut mc = empty_state();
1284        mc.set_single_range(rng(3, 9));
1285        assert_eq!(mc.get_primary_cursor(), Some(c(9)));
1286
1287        // Backwards range: the raw `end` field is returned (the *focus*), even
1288        // though it is the lower position. This is deliberate — the caret sits
1289        // at the focus, not at the max boundary.
1290        let mut back = empty_state();
1291        back.set_single_range(SelectionRange {
1292            start: c(9),
1293            end: c(3),
1294        });
1295        assert_eq!(back.get_primary_cursor(), Some(c(3)));
1296    }
1297
1298    #[test]
1299    fn get_primary_mut_mutation_is_visible_through_get_primary() {
1300        let mut mc = state(0);
1301        let p = mc.add_cursor(c(50));
1302        {
1303            let prim = mc.get_primary_mut().expect("primary exists");
1304            assert_eq!(prim.id, p);
1305            prim.selection = Selection::Range(rng(50, 60));
1306        }
1307        assert_eq!(
1308            mc.get_primary().unwrap().selection,
1309            Selection::Range(rng(50, 60))
1310        );
1311        assert_eq!(mc.get_primary_cursor(), Some(c(60)));
1312    }
1313
1314    #[test]
1315    fn get_primary_mut_falls_back_to_last_when_dangling() {
1316        let mut mc = state(0);
1317        let _ = mc.add_cursor(c(10));
1318        mc.primary_id = SelectionId::new();
1319        let last_id = mc.selections.last().unwrap().id;
1320        let prim = mc.get_primary_mut().expect("fallback to last");
1321        assert_eq!(prim.id, last_id);
1322    }
1323
1324    #[test]
1325    fn to_selections_matches_the_internal_order_and_len() {
1326        let mut mc = state(30);
1327        let _ = mc.add_cursor(c(10));
1328        let _ = mc.add_selection(rng(15, 20));
1329        let sels = mc.to_selections();
1330        assert_eq!(sels.len(), mc.len());
1331        let inner: Vec<Selection> = mc.selections.iter().map(|s| s.selection).collect();
1332        assert_eq!(sels, inner);
1333    }
1334
1335    #[test]
1336    fn len_and_is_empty_always_agree() {
1337        let mut mc = empty_state();
1338        assert!(mc.is_empty() && mc.is_empty());
1339        let _ = mc.add_cursor(c(1));
1340        assert!(!mc.is_empty() && mc.len() == 1);
1341        for i in 2..20u32 {
1342            let _ = mc.add_cursor(c(i * 3));
1343        }
1344        assert_eq!(mc.len(), mc.selections.len());
1345        assert_eq!(mc.is_empty(), mc.is_empty());
1346        assert!(!mc.is_empty());
1347    }
1348
1349    // =====================================================================
1350    // update_from_edit_result
1351    // =====================================================================
1352
1353    #[test]
1354    fn update_from_edit_result_with_empty_slice_clears_everything() {
1355        let mut mc = state(0);
1356        let _ = mc.add_cursor(c(10));
1357        mc.update_from_edit_result(&[]);
1358        assert!(mc.is_empty());
1359        assert!(mc.get_primary().is_none());
1360        assert!(mc.get_primary_cursor().is_none());
1361    }
1362
1363    #[test]
1364    fn update_from_edit_result_preserves_ids_by_index_and_mints_extras() {
1365        let mut mc = state(0);
1366        let _ = mc.add_cursor(c(10));
1367        let old: Vec<SelectionId> = mc.selections.iter().map(|s| s.id).collect();
1368        assert_eq!(old.len(), 2);
1369
1370        mc.update_from_edit_result(&[
1371            Selection::Cursor(c(1)),
1372            Selection::Cursor(c(2)),
1373            Selection::Cursor(c(3)),
1374            Selection::Range(rng(4, 8)),
1375        ]);
1376        assert_eq!(mc.len(), 4);
1377        assert_eq!(mc.selections[0].id, old[0], "id preserved by index");
1378        assert_eq!(mc.selections[1].id, old[1], "id preserved by index");
1379        assert_ids_unique(&mc);
1380        assert_primary_resolves(&mc);
1381        assert_eq!(mc.selections[3].selection, Selection::Range(rng(4, 8)));
1382    }
1383
1384    #[test]
1385    fn update_from_edit_result_shrinking_keeps_primary_resolvable() {
1386        let mut mc = state(0);
1387        let _ = mc.add_cursor(c(10));
1388        let _ = mc.add_cursor(c(20)); // primary = the byte-20 cursor
1389        mc.update_from_edit_result(&[Selection::Cursor(c(99))]);
1390        assert_eq!(mc.len(), 1);
1391        // The primary's id is gone (only index 0's id survived), so
1392        // ensure_primary_valid must have re-pointed it at the survivor.
1393        assert_primary_resolves(&mc);
1394        assert_eq!(mc.get_primary_cursor(), Some(c(99)));
1395    }
1396
1397    #[test]
1398    fn update_from_edit_result_does_not_merge_overlaps() {
1399        // Documented: "Don't merge here — edit_text already returns correct positions"
1400        let mut mc = state(0);
1401        mc.update_from_edit_result(&[
1402            Selection::Range(rng(0, 10)),
1403            Selection::Range(rng(5, 15)),
1404        ]);
1405        assert_eq!(mc.len(), 2, "update must NOT merge");
1406        // ... but an explicit merge afterwards collapses them.
1407        mc.merge_overlapping();
1408        assert_eq!(mc.len(), 1);
1409        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 15)));
1410        assert_primary_resolves(&mc);
1411    }
1412
1413    #[test]
1414    fn update_from_edit_result_with_1000_selections() {
1415        let mut mc = state(0);
1416        let big: Vec<Selection> = (0..1000u32).map(|i| Selection::Cursor(c(i * 4))).collect();
1417        mc.update_from_edit_result(&big);
1418        assert_eq!(mc.len(), 1000);
1419        assert_ids_unique(&mc);
1420        assert_primary_resolves(&mc);
1421        assert_eq!(mc.to_selections(), big);
1422    }
1423
1424    // =====================================================================
1425    // set_single_cursor / set_single_range
1426    // =====================================================================
1427
1428    #[test]
1429    fn set_single_cursor_collapses_all_selections() {
1430        let mut mc = state(0);
1431        let _ = mc.add_cursor(c(10));
1432        let _ = mc.add_selection(rng(20, 30));
1433        mc.set_single_cursor(c(7));
1434        assert_eq!(mc.len(), 1);
1435        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(7)));
1436        assert_primary_resolves(&mc);
1437        assert_eq!(mc.get_primary_cursor(), Some(c(7)));
1438    }
1439
1440    #[test]
1441    fn set_single_range_collapses_all_selections() {
1442        let mut mc = state(0);
1443        let _ = mc.add_cursor(c(10));
1444        mc.set_single_range(rng(u32::MAX - 2, u32::MAX));
1445        assert_eq!(mc.len(), 1);
1446        assert_eq!(
1447            mc.selections[0].selection,
1448            Selection::Range(rng(u32::MAX - 2, u32::MAX))
1449        );
1450        assert_primary_resolves(&mc);
1451    }
1452
1453    #[test]
1454    fn set_single_cursor_on_empty_state_mints_a_fresh_id() {
1455        let mut mc = empty_state();
1456        let stale = mc.primary_id;
1457        mc.set_single_cursor(c(1));
1458        assert_eq!(mc.len(), 1);
1459        assert_ne!(mc.primary_id, stale, "no last element -> a new id is minted");
1460        assert_primary_resolves(&mc);
1461    }
1462
1463    #[test]
1464    fn set_single_range_on_empty_state_mints_a_fresh_id() {
1465        let mut mc = empty_state();
1466        mc.set_single_range(rng(0, 0));
1467        assert_eq!(mc.len(), 1);
1468        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 0)));
1469        assert_primary_resolves(&mc);
1470    }
1471
1472    #[test]
1473    fn set_single_cursor_is_idempotent() {
1474        let mut mc = state(0);
1475        mc.set_single_cursor(c(5));
1476        let first = mc.clone();
1477        mc.set_single_cursor(c(5));
1478        assert_eq!(mc, first, "re-setting the same cursor must reuse the id");
1479    }
1480
1481    // =====================================================================
1482    // merge_overlapping
1483    // =====================================================================
1484
1485    #[test]
1486    fn merge_overlapping_on_empty_and_single_is_a_noop() {
1487        let mut e = empty_state();
1488        e.merge_overlapping();
1489        assert!(e.is_empty());
1490
1491        let mut one = state(3);
1492        let before = one.clone();
1493        one.merge_overlapping();
1494        assert_eq!(one, before);
1495    }
1496
1497    #[test]
1498    fn merge_overlapping_collapses_a_whole_chain() {
1499        let mut mc = empty_state();
1500        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
1501        mc.selections = vec![
1502            ident(ids[0], Selection::Range(rng(25, 40))),
1503            ident(ids[1], Selection::Range(rng(0, 10))),
1504            ident(ids[2], Selection::Range(rng(12, 30))),
1505            ident(ids[3], Selection::Range(rng(5, 15))),
1506        ];
1507        mc.primary_id = ids[3];
1508        mc.merge_overlapping();
1509        // 0..10 ∪ 5..15 ∪ 12..30 ∪ 25..40 = one contiguous 0..40
1510        assert_eq!(mc.len(), 1);
1511        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 40)));
1512        assert_sorted_nonoverlapping(&mc);
1513        assert_primary_resolves(&mc);
1514    }
1515
1516    #[test]
1517    fn merge_overlapping_keeps_disjoint_selections_and_sorts_them() {
1518        let mut mc = empty_state();
1519        let ids: Vec<SelectionId> = (0..3).map(|_| SelectionId::new()).collect();
1520        mc.selections = vec![
1521            ident(ids[0], Selection::Cursor(c(100))),
1522            ident(ids[1], Selection::Range(rng(0, 5))),
1523            ident(ids[2], Selection::Cursor(c(50))),
1524        ];
1525        mc.primary_id = ids[0];
1526        mc.merge_overlapping();
1527        assert_eq!(mc.len(), 3);
1528        assert_eq!(mc.to_selections(), vec![
1529            Selection::Range(rng(0, 5)),
1530            Selection::Cursor(c(50)),
1531            Selection::Cursor(c(100)),
1532        ]);
1533        assert_sorted_nonoverlapping(&mc);
1534        // The primary (byte 100) survived the sort untouched.
1535        assert_eq!(mc.primary_id, ids[0]);
1536        assert_eq!(mc.get_primary_cursor(), Some(c(100)));
1537    }
1538
1539    #[test]
1540    fn merge_overlapping_zero_width_merge_yields_a_cursor_not_a_range() {
1541        let mut mc = empty_state();
1542        let ids: Vec<SelectionId> = (0..2).map(|_| SelectionId::new()).collect();
1543        mc.selections = vec![
1544            ident(ids[0], Selection::Cursor(c(8))),
1545            ident(ids[1], Selection::Range(rng(8, 8))),
1546        ];
1547        mc.primary_id = ids[1];
1548        mc.merge_overlapping();
1549        assert_eq!(mc.len(), 1);
1550        // new_start == new_end -> collapses back to a Cursor.
1551        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(8)));
1552        assert_primary_resolves(&mc);
1553    }
1554
1555    #[test]
1556    fn merge_overlapping_is_idempotent() {
1557        let mut mc = empty_state();
1558        let ids: Vec<SelectionId> = (0..5).map(|_| SelectionId::new()).collect();
1559        mc.selections = vec![
1560            ident(ids[0], Selection::Range(rng(0, 10))),
1561            ident(ids[1], Selection::Cursor(c(5))),
1562            ident(ids[2], Selection::Range(rng(30, 20))), // reversed
1563            ident(ids[3], Selection::Cursor(c(100))),
1564            ident(ids[4], Selection::Range(rng(99, 101))),
1565        ];
1566        mc.primary_id = ids[2];
1567        mc.merge_overlapping();
1568        let once = mc.clone();
1569        mc.merge_overlapping();
1570        assert_eq!(mc, once, "merge_overlapping must be a fixed point");
1571        assert_sorted_nonoverlapping(&mc);
1572        assert_primary_resolves(&mc);
1573    }
1574
1575    #[test]
1576    fn merge_overlapping_adversarial_200_selections_keeps_invariants() {
1577        let mut mc = empty_state();
1578        let mut seed: u32 = 0x1234_5678;
1579        let mut next = || {
1580            seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1581            seed
1582        };
1583        let mut sels = Vec::new();
1584        for i in 0..200u32 {
1585            let a = next() % 1000;
1586            let b = next() % 1000;
1587            let sel = match i % 4 {
1588                0 => Selection::Cursor(c(a)),
1589                1 => Selection::Range(SelectionRange {
1590                    start: c(a),
1591                    end: c(b),
1592                }), // may be reversed
1593                2 => Selection::Range(rng(a.min(b), a.max(b))),
1594                _ => Selection::Cursor(c_full(
1595                    0,
1596                    a,
1597                    if b % 2 == 0 {
1598                        CursorAffinity::Leading
1599                    } else {
1600                        CursorAffinity::Trailing
1601                    },
1602                )),
1603            };
1604            sels.push(ident(SelectionId::new(), sel));
1605        }
1606        // Throw in the absolute boundaries too.
1607        sels.push(ident(SelectionId::new(), Selection::Cursor(c(0))));
1608        sels.push(ident(SelectionId::new(), Selection::Cursor(c(u32::MAX))));
1609        sels.push(ident(
1610            SelectionId::new(),
1611            Selection::Range(rng(u32::MAX - 1, u32::MAX)),
1612        ));
1613        mc.primary_id = sels[7].id;
1614        mc.selections = sels;
1615
1616        mc.merge_overlapping();
1617
1618        assert!(!mc.is_empty());
1619        assert!(mc.len() <= 203);
1620        assert_sorted_nonoverlapping(&mc);
1621        assert_primary_resolves(&mc);
1622        assert_ids_unique(&mc);
1623    }
1624
1625    #[test]
1626    fn merge_overlapping_primary_inside_a_chain_still_resolves() {
1627        // Three cursors that all collapse into one, plus a far-away cursor.
1628        // The primary is the *first* link of the merge chain.
1629        let mut mc = empty_state();
1630        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
1631        mc.selections = vec![
1632            ident(ids[0], Selection::Cursor(c(0))),
1633            ident(ids[1], Selection::Cursor(c(0))),
1634            ident(ids[2], Selection::Cursor(c(0))),
1635            ident(ids[3], Selection::Cursor(c(100))),
1636        ];
1637        mc.primary_id = ids[0];
1638        mc.merge_overlapping();
1639
1640        assert_eq!(mc.len(), 2);
1641        // Whatever the merge does with ids, `primary_id` must never dangle.
1642        assert!(
1643            mc.selections.iter().any(|s| s.id == mc.primary_id),
1644            "primary_id must name a surviving selection"
1645        );
1646        assert!(mc.get_primary().is_some());
1647    }
1648
1649    #[test]
1650    fn merge_overlapping_primary_should_follow_its_merge_chain() {
1651        // Same setup as above. `merge_overlapping` records `new_primary = sel.id`
1652        // when the chain's head is the primary, but the head's id is then
1653        // overwritten by the *next* merge, so `new_primary` points at an id that
1654        // no longer exists. ensure_primary_valid() then silently adopts the
1655        // vector's LAST element — the unrelated cursor at byte 100.
1656        //
1657        // Expected: the primary follows the merged selection it was part of (byte 0).
1658        let mut mc = empty_state();
1659        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
1660        mc.selections = vec![
1661            ident(ids[0], Selection::Cursor(c(0))),
1662            ident(ids[1], Selection::Cursor(c(0))),
1663            ident(ids[2], Selection::Cursor(c(0))),
1664            ident(ids[3], Selection::Cursor(c(100))),
1665        ];
1666        mc.primary_id = ids[0];
1667        mc.merge_overlapping();
1668        assert_eq!(
1669            mc.get_primary_cursor(),
1670            Some(c(0)),
1671            "primary jumped to an unrelated selection after the merge"
1672        );
1673    }
1674
1675    // =====================================================================
1676    // move_all_cursors
1677    // =====================================================================
1678
1679    #[test]
1680    fn move_all_cursors_identity_leaves_positions_unchanged() {
1681        let mut mc = state(0);
1682        let _ = mc.add_cursor(c(10));
1683        let _ = mc.add_cursor(c(20));
1684        let before = mc.to_selections();
1685        mc.move_all_cursors(false, |cur| *cur);
1686        assert_eq!(mc.to_selections(), before);
1687        assert_eq!(mc.len(), 3);
1688        assert_primary_resolves(&mc);
1689    }
1690
1691    #[test]
1692    fn move_all_cursors_extend_with_no_movement_keeps_a_cursor() {
1693        // `*c != new_cursor` is false -> the selection must stay a Cursor,
1694        // not degenerate into a zero-width Range.
1695        let mut mc = state(4);
1696        mc.move_all_cursors(true, |cur| *cur);
1697        assert_eq!(mc.len(), 1);
1698        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(4)));
1699    }
1700
1701    #[test]
1702    fn move_all_cursors_extend_turns_a_cursor_into_a_range() {
1703        let mut mc = state(10);
1704        mc.move_all_cursors(true, |cur| c(cur.cluster_id.start_byte_in_run + 5));
1705        assert_eq!(mc.len(), 1);
1706        assert_eq!(mc.selections[0].selection, Selection::Range(rng(10, 15)));
1707        // The anchor stayed at 10, only the focus moved.
1708        assert_eq!(mc.get_primary_cursor(), Some(c(15)));
1709    }
1710
1711    #[test]
1712    fn move_all_cursors_bare_forward_arrow_collapses_range_to_max_boundary() {
1713        let mut mc = empty_state();
1714        mc.set_single_range(rng(3, 9));
1715        mc.move_all_cursors(false, |cur| c(cur.cluster_id.start_byte_in_run + 1));
1716        assert_eq!(mc.len(), 1);
1717        // Collapses to the boundary WITHOUT stepping past it (not byte 10).
1718        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(9)));
1719    }
1720
1721    #[test]
1722    fn move_all_cursors_bare_backward_arrow_collapses_range_to_min_boundary() {
1723        let mut mc = empty_state();
1724        mc.set_single_range(rng(3, 9));
1725        mc.move_all_cursors(false, |cur| {
1726            c(cur.cluster_id.start_byte_in_run.saturating_sub(1))
1727        });
1728        assert_eq!(mc.len(), 1);
1729        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(3)));
1730    }
1731
1732    #[test]
1733    fn move_all_cursors_collapses_a_backwards_range_by_direction_not_field_order() {
1734        // Backwards range (focus at 3, anchor at 9): a forward arrow must still
1735        // collapse to the max boundary (9), not to the `end` field (3).
1736        let mut mc = empty_state();
1737        mc.set_single_range(SelectionRange {
1738            start: c(9),
1739            end: c(3),
1740        });
1741        mc.move_all_cursors(false, |cur| c(cur.cluster_id.start_byte_in_run + 1));
1742        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(9)));
1743
1744        let mut back = empty_state();
1745        back.set_single_range(SelectionRange {
1746            start: c(9),
1747            end: c(3),
1748        });
1749        back.move_all_cursors(false, |cur| {
1750            c(cur.cluster_id.start_byte_in_run.saturating_sub(1))
1751        });
1752        assert_eq!(back.selections[0].selection, Selection::Cursor(c(3)));
1753    }
1754
1755    #[test]
1756    fn move_all_cursors_extend_back_onto_the_anchor_collapses_to_a_cursor() {
1757        let mut mc = empty_state();
1758        mc.set_single_range(rng(3, 4));
1759        // Shrink the focus back onto the anchor: r.start == new_end.
1760        mc.move_all_cursors(true, |_| c(3));
1761        assert_eq!(mc.len(), 1);
1762        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(3)));
1763    }
1764
1765    #[test]
1766    fn move_all_cursors_constant_move_fn_merges_everything_into_one() {
1767        let mut mc = state(0);
1768        for i in 1..5u32 {
1769            let _ = mc.add_cursor(c(i * 10));
1770        }
1771        assert_eq!(mc.len(), 5);
1772        mc.move_all_cursors(false, |_| c(7));
1773        assert_eq!(mc.len(), 1, "colliding cursors must be merged afterwards");
1774        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(7)));
1775        assert_primary_resolves(&mc);
1776        assert_sorted_nonoverlapping(&mc);
1777    }
1778
1779    #[test]
1780    fn move_all_cursors_saturating_at_u32_max_does_not_overflow() {
1781        let mut mc = empty_state();
1782        let ids: Vec<SelectionId> = (0..2).map(|_| SelectionId::new()).collect();
1783        mc.selections = vec![
1784            ident(ids[0], Selection::Cursor(c(u32::MAX - 1))),
1785            ident(ids[1], Selection::Cursor(c(u32::MAX))),
1786        ];
1787        mc.primary_id = ids[1];
1788        mc.move_all_cursors(false, |cur| {
1789            c(cur.cluster_id.start_byte_in_run.saturating_add(1))
1790        });
1791        // Both saturate to u32::MAX and merge.
1792        assert_eq!(mc.len(), 1);
1793        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(u32::MAX)));
1794        assert_primary_resolves(&mc);
1795    }
1796
1797    #[test]
1798    fn move_all_cursors_on_empty_state_does_not_panic() {
1799        let mut mc = empty_state();
1800        mc.move_all_cursors(false, |cur| *cur);
1801        mc.move_all_cursors(true, |_| c(u32::MAX));
1802        assert!(mc.is_empty());
1803    }
1804
1805    #[test]
1806    fn move_all_cursors_stress_keeps_invariants() {
1807        let mut mc = state(0);
1808        for i in 1..100u32 {
1809            let _ = mc.add_cursor(c(i * 5));
1810        }
1811        for _ in 0..10 {
1812            mc.move_all_cursors(false, |cur| {
1813                // Fold every cursor into a small window -> heavy merging.
1814                c(cur.cluster_id.start_byte_in_run % 7)
1815            });
1816            assert_sorted_nonoverlapping(&mc);
1817            assert_primary_resolves(&mc);
1818            assert_ids_unique(&mc);
1819        }
1820        assert!(mc.len() <= 7);
1821    }
1822
1823    // =====================================================================
1824    // remap_node_ids
1825    // =====================================================================
1826
1827    #[test]
1828    fn remap_node_ids_for_a_different_dom_is_a_noop() {
1829        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
1830        mc.node_id.dom = DomId { inner: 7 };
1831        let before = mc.clone();
1832        let mut map = BTreeMap::new();
1833        map.insert(NodeId::new(5), NodeId::new(9));
1834        mc.remap_node_ids(DomId::ROOT_ID, &map);
1835        assert_eq!(mc, before, "a foreign DomId must not touch this state");
1836    }
1837
1838    #[test]
1839    fn remap_node_ids_rewrites_a_surviving_node() {
1840        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
1841        let mut map = BTreeMap::new();
1842        map.insert(NodeId::new(5), NodeId::new(9));
1843        mc.remap_node_ids(DomId::ROOT_ID, &map);
1844        assert_eq!(
1845            mc.node_id.node.into_crate_internal(),
1846            Some(NodeId::new(9))
1847        );
1848        assert_eq!(mc.len(), 1, "selections survive a successful remap");
1849        assert_primary_resolves(&mc);
1850    }
1851
1852    #[test]
1853    fn remap_node_ids_clears_selections_when_the_node_was_removed() {
1854        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
1855        let _ = mc.add_cursor(c(20));
1856        let map: BTreeMap<NodeId, NodeId> = BTreeMap::new(); // node 5 is gone
1857        mc.remap_node_ids(DomId::ROOT_ID, &map);
1858        assert!(mc.is_empty(), "a removed node must drop its selections");
1859        assert!(mc.get_primary().is_none());
1860        // node_id itself is left alone (only selections are cleared).
1861        assert_eq!(
1862            mc.node_id.node.into_crate_internal(),
1863            Some(NodeId::new(5))
1864        );
1865    }
1866
1867    #[test]
1868    fn remap_node_ids_with_a_none_node_is_a_noop() {
1869        // DomNodeId::ROOT carries NodeHierarchyItemId::NONE -> into_crate_internal()
1870        // is None, so neither branch runs and the selections must survive.
1871        let mut mc = state(3);
1872        let map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
1873        mc.remap_node_ids(DomId::ROOT_ID, &map);
1874        assert_eq!(mc.len(), 1);
1875        assert_eq!(mc.node_id.node, NodeHierarchyItemId::NONE);
1876        assert_primary_resolves(&mc);
1877    }
1878
1879    #[test]
1880    fn remap_node_ids_handles_large_node_indices() {
1881        let big = 1_000_000usize;
1882        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(big), 0);
1883        let mut map = BTreeMap::new();
1884        map.insert(NodeId::new(big), NodeId::new(big * 2));
1885        mc.remap_node_ids(DomId::ROOT_ID, &map);
1886        assert_eq!(
1887            mc.node_id.node.into_crate_internal(),
1888            Some(NodeId::new(big * 2))
1889        );
1890    }
1891
1892    #[test]
1893    fn remap_node_ids_twice_is_stable() {
1894        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
1895        let mut map = BTreeMap::new();
1896        map.insert(NodeId::new(5), NodeId::new(9));
1897        map.insert(NodeId::new(9), NodeId::new(9)); // identity for the new id
1898        mc.remap_node_ids(DomId::ROOT_ID, &map);
1899        mc.remap_node_ids(DomId::ROOT_ID, &map);
1900        assert_eq!(
1901            mc.node_id.node.into_crate_internal(),
1902            Some(NodeId::new(9))
1903        );
1904        assert_eq!(mc.len(), 1);
1905    }
1906
1907    // =====================================================================
1908    // selection_start_pos / selection_end_pos  (private helpers)
1909    // =====================================================================
1910
1911    #[test]
1912    fn selection_pos_helpers_normalize_reversed_ranges() {
1913        let forward = Selection::Range(rng(3, 9));
1914        assert_eq!(selection_start_pos(&forward), c(3));
1915        assert_eq!(selection_end_pos(&forward), c(9));
1916
1917        let backward = Selection::Range(SelectionRange {
1918            start: c(9),
1919            end: c(3),
1920        });
1921        assert_eq!(selection_start_pos(&backward), c(3));
1922        assert_eq!(selection_end_pos(&backward), c(9));
1923
1924        let cursor = Selection::Cursor(c(5));
1925        assert_eq!(selection_start_pos(&cursor), c(5));
1926        assert_eq!(selection_end_pos(&cursor), c(5));
1927    }
1928
1929    #[test]
1930    fn selection_pos_helpers_start_never_exceeds_end() {
1931        let mut seed: u32 = 0xACE1_BEEF;
1932        let mut next = || {
1933            seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
1934            seed
1935        };
1936        let extremes = [0u32, 1, u32::MAX - 1, u32::MAX];
1937        let mut cases: Vec<Selection> = Vec::new();
1938        for a in extremes {
1939            for b in extremes {
1940                cases.push(Selection::Range(SelectionRange {
1941                    start: c_full(a, b, CursorAffinity::Trailing),
1942                    end: c_full(b, a, CursorAffinity::Leading),
1943                }));
1944                cases.push(Selection::Cursor(c_full(a, b, CursorAffinity::Leading)));
1945            }
1946        }
1947        for _ in 0..200 {
1948            cases.push(Selection::Range(SelectionRange {
1949                start: c(next()),
1950                end: c(next()),
1951            }));
1952        }
1953        for sel in &cases {
1954            assert!(
1955                selection_start_pos(sel) <= selection_end_pos(sel),
1956                "start must never sort after end: {sel:?}"
1957            );
1958        }
1959    }
1960
1961    #[test]
1962    fn selection_pos_helpers_respect_affinity_ordering() {
1963        // Same byte, different affinity: Leading < Trailing.
1964        let sel = Selection::Range(SelectionRange {
1965            start: c_full(0, 4, CursorAffinity::Trailing),
1966            end: c_full(0, 4, CursorAffinity::Leading),
1967        });
1968        assert_eq!(
1969            selection_start_pos(&sel),
1970            c_full(0, 4, CursorAffinity::Leading)
1971        );
1972        assert_eq!(
1973            selection_end_pos(&sel),
1974            c_full(0, 4, CursorAffinity::Trailing)
1975        );
1976    }
1977
1978    // =====================================================================
1979    // TextSelection
1980    // =====================================================================
1981
1982    fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
1983        LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
1984    }
1985
1986    #[test]
1987    fn new_collapsed_invariants_hold() {
1988        let node = NodeId::new(3);
1989        let sel = TextSelection::new_collapsed(
1990            DomId::ROOT_ID,
1991            node,
1992            c(7),
1993            rect(1.0, 2.0, 3.0, 4.0),
1994            LogicalPosition::new(5.0, 6.0),
1995        );
1996        assert!(sel.is_collapsed());
1997        assert!(sel.is_forward);
1998        assert_eq!(sel.dom_id, DomId::ROOT_ID);
1999        assert_eq!(sel.anchor.ifc_root_node_id, node);
2000        assert_eq!(sel.focus.ifc_root_node_id, node);
2001        assert_eq!(sel.anchor.cursor, c(7));
2002        assert_eq!(sel.focus.cursor, c(7));
2003        assert_eq!(sel.affected_nodes.len(), 1);
2004        // The collapsed node maps to a zero-width range at the cursor.
2005        assert_eq!(
2006            sel.get_range_for_node(&node),
2007            Some(&SelectionRange {
2008                start: c(7),
2009                end: c(7),
2010            })
2011        );
2012    }
2013
2014    #[test]
2015    fn new_collapsed_with_non_finite_geometry_does_not_panic() {
2016        let node = NodeId::new(0);
2017        let sel = TextSelection::new_collapsed(
2018            DomId::ROOT_ID,
2019            node,
2020            c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
2021            rect(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX),
2022            LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
2023        );
2024        // Geometry is carried verbatim; only the cursors decide collapsedness.
2025        assert!(sel.is_collapsed());
2026        assert!(sel.get_range_for_node(&node).is_some());
2027        assert!(sel.anchor.char_bounds.origin.x.is_nan());
2028    }
2029
2030    #[test]
2031    fn get_range_for_node_returns_none_for_an_unaffected_node() {
2032        let sel = TextSelection::new_collapsed(
2033            DomId::ROOT_ID,
2034            NodeId::new(3),
2035            c(0),
2036            rect(0.0, 0.0, 0.0, 0.0),
2037            LogicalPosition::new(0.0, 0.0),
2038        );
2039        assert!(sel.get_range_for_node(&NodeId::new(4)).is_none());
2040        assert!(sel.get_range_for_node(&NodeId::new(0)).is_none());
2041        assert!(sel.get_range_for_node(&NodeId::new(usize::MAX)).is_none());
2042    }
2043
2044    #[test]
2045    fn get_range_for_node_on_an_empty_map_returns_none() {
2046        let node = NodeId::new(3);
2047        let mut sel = TextSelection::new_collapsed(
2048            DomId::ROOT_ID,
2049            node,
2050            c(0),
2051            rect(0.0, 0.0, 0.0, 0.0),
2052            LogicalPosition::new(0.0, 0.0),
2053        );
2054        sel.affected_nodes.clear();
2055        assert!(sel.get_range_for_node(&node).is_none());
2056        assert!(sel.is_collapsed(), "collapsedness does not depend on the map");
2057    }
2058
2059    #[test]
2060    fn is_collapsed_is_false_when_the_focus_cursor_moves() {
2061        let node = NodeId::new(3);
2062        let mut sel = TextSelection::new_collapsed(
2063            DomId::ROOT_ID,
2064            node,
2065            c(7),
2066            rect(0.0, 0.0, 1.0, 1.0),
2067            LogicalPosition::new(0.0, 0.0),
2068        );
2069        assert!(sel.is_collapsed());
2070        sel.focus.cursor = c(8);
2071        assert!(!sel.is_collapsed());
2072    }
2073
2074    #[test]
2075    fn is_collapsed_is_false_when_the_focus_crosses_into_another_ifc() {
2076        let mut sel = TextSelection::new_collapsed(
2077            DomId::ROOT_ID,
2078            NodeId::new(3),
2079            c(7),
2080            rect(0.0, 0.0, 1.0, 1.0),
2081            LogicalPosition::new(0.0, 0.0),
2082        );
2083        sel.focus.ifc_root_node_id = NodeId::new(4); // same cursor, different node
2084        assert!(
2085            !sel.is_collapsed(),
2086            "same cursor offset in a different IFC is not a collapsed selection"
2087        );
2088    }
2089
2090    #[test]
2091    fn is_collapsed_only_looks_at_cursors_not_at_mouse_position() {
2092        let node = NodeId::new(1);
2093        let mut sel = TextSelection::new_collapsed(
2094            DomId::ROOT_ID,
2095            node,
2096            c(2),
2097            rect(0.0, 0.0, 1.0, 1.0),
2098            LogicalPosition::new(0.0, 0.0),
2099        );
2100        sel.focus.mouse_position = LogicalPosition::new(999.0, -999.0);
2101        assert!(sel.is_collapsed());
2102    }
2103}