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

            
47
use alloc::collections::BTreeMap;
48
use alloc::vec::Vec;
49
use core::sync::atomic::{AtomicU64, Ordering};
50

            
51
use crate::dom::{DomId, DomNodeId, NodeId};
52
use 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)]
59
pub 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)]
72
pub 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)]
83
pub 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)]
93
pub 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

            
100
impl_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)]
110
pub struct SelectionRange {
111
    pub start: TextCursor,
112
    pub end: TextCursor,
113
}
114

            
115
impl_option!(
116
    SelectionRange,
117
    OptionSelectionRange,
118
    [Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
119
);
120

            
121
impl_vec!(SelectionRange, SelectionRangeVec, SelectionRangeVecDestructor, SelectionRangeVecDestructorType, SelectionRangeVecSlice, OptionSelectionRange);
122
impl_vec_debug!(SelectionRange, SelectionRangeVec);
123
impl_vec_clone!(
124
    SelectionRange,
125
    SelectionRangeVec,
126
    SelectionRangeVecDestructor
127
);
128
impl_vec_partialeq!(SelectionRange, SelectionRangeVec);
129
impl_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)]
134
pub enum Selection {
135
    Cursor(TextCursor),
136
    Range(SelectionRange),
137
}
138

            
139
impl_option!(
140
    Selection,
141
    OptionSelection,
142
    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
143
);
144

            
145
impl_vec!(Selection, SelectionVec, SelectionVecDestructor, SelectionVecDestructorType, SelectionVecSlice, OptionSelection);
146
impl_vec_debug!(Selection, SelectionVec);
147
impl_vec_clone!(Selection, SelectionVec, SelectionVecDestructor);
148
impl_vec_partialeq!(Selection, SelectionVec);
149
impl_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)]
154
pub 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

            
161
impl SelectionState {
162
    /// Adds a new selection, merging it with any existing selections it overlaps with.
163
111
    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
111
        let mut selections: Vec<Selection> = self.selections.as_ref().to_vec();
167
111
        selections.push(new_selection);
168
111
        selections.sort_unstable();
169
111
        selections.dedup(); // Removes duplicate cursors
170
111
        self.selections = selections.into();
171
111
    }
172

            
173
}
174

            
175
impl_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)]
193
pub struct SelectionId {
194
    pub inner: u64,
195
}
196

            
197
impl SelectionId {
198
    /// Generate a new unique `SelectionId`.
199
15768
    pub fn new() -> Self {
200
        static COUNTER: AtomicU64 = AtomicU64::new(1);
201
15768
        Self { inner: COUNTER.fetch_add(1, Ordering::Relaxed) }
202
15768
    }
203
}
204

            
205
/// Note: `Default` generates a new unique ID (increments global counter),
206
/// rather than returning a zero/sentinel value.
207
impl Default for SelectionId {
208
2
    fn default() -> Self {
209
2
        Self::new()
210
2
    }
211
}
212

            
213
impl_option!(
214
    SelectionId,
215
    OptionSelectionId,
216
    [Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
217
);
218

            
219
impl_vec!(SelectionId, SelectionIdVec, SelectionIdVecDestructor, SelectionIdVecDestructorType, SelectionIdVecSlice, OptionSelectionId);
220
impl_vec_debug!(SelectionId, SelectionIdVec);
221
impl_vec_clone!(SelectionId, SelectionIdVec, SelectionIdVecDestructor);
222
impl_vec_partialeq!(SelectionId, SelectionIdVec);
223
impl_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)]
228
pub struct IdentifiedSelection {
229
    pub id: SelectionId,
230
    pub selection: Selection,
231
}
232

            
233
impl_option!(
234
    IdentifiedSelection,
235
    OptionIdentifiedSelection,
236
    [Debug, Clone, Copy, PartialEq, Eq, Hash]
237
);
238

            
239
impl_vec!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor, IdentifiedSelectionVecDestructorType, IdentifiedSelectionVecSlice, OptionIdentifiedSelection);
240
impl_vec_debug!(IdentifiedSelection, IdentifiedSelectionVec);
241
impl_vec_clone!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor);
242
impl_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)]
257
pub 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

            
270
impl MultiCursorState {
271
    /// Create a new `MultiCursorState` with a single cursor.
272
1299
    #[must_use] pub fn new_with_cursor(cursor: TextCursor, node_id: DomNodeId, contenteditable_key: u64) -> Self {
273
1299
        let id = SelectionId::new();
274
1299
        Self {
275
1299
            selections: vec![IdentifiedSelection {
276
1299
                id,
277
1299
                selection: Selection::Cursor(cursor),
278
1299
            }],
279
1299
            primary_id: id,
280
1299
            node_id,
281
1299
            contenteditable_key,
282
1299
        }
283
1299
    }
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
949
    pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId {
289
949
        let id = SelectionId::new();
290
949
        self.selections.push(IdentifiedSelection {
291
949
            id,
292
949
            selection: Selection::Cursor(cursor),
293
949
        });
294
949
        self.primary_id = id;
295
949
        self.merge_overlapping();
296
949
        id
297
949
    }
298

            
299
    /// Add a selection range, merging if it overlaps.
300
    /// Returns the `SelectionId` of the new (or merged) selection.
301
    #[must_use]
302
65
    pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId {
303
65
        let id = SelectionId::new();
304
65
        self.selections.push(IdentifiedSelection {
305
65
            id,
306
65
            selection: Selection::Range(range),
307
65
        });
308
65
        self.primary_id = id;
309
65
        self.merge_overlapping();
310
65
        id
311
65
    }
312

            
313
    /// Remove a selection by its stable ID. Returns true if found and removed.
314
    #[must_use]
315
7
    pub fn remove_selection(&mut self, id: SelectionId) -> bool {
316
7
        let len_before = self.selections.len();
317
12
        self.selections.retain(|s| s.id != id);
318
7
        let removed = self.selections.len() < len_before;
319
7
        if removed {
320
5
            // If we just removed the primary, re-point it at a surviving one.
321
5
            self.ensure_primary_valid();
322
5
        }
323
7
        removed
324
7
    }
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
10889
    #[must_use] pub fn get_primary(&self) -> Option<&IdentifiedSelection> {
331
10889
        let pid = self.primary_id;
332
10889
        self.selections
333
10889
            .iter()
334
11451
            .find(|s| s.id == pid)
335
10889
            .or_else(|| self.selections.last())
336
10889
    }
337

            
338
    /// Get a mutable reference to the primary selection (see `get_primary`).
339
14
    pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection> {
340
14
        let pid = self.primary_id;
341
15
        if let Some(pos) = self.selections.iter().position(|s| s.id == pid) {
342
12
            return self.selections.get_mut(pos);
343
2
        }
344
2
        self.selections.last_mut()
345
14
    }
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
3299
    fn ensure_primary_valid(&mut self) {
350
3299
        let pid = self.primary_id;
351
133775
        if !self.selections.iter().any(|s| s.id == pid) {
352
9
            if let Some(last) = self.selections.last() {
353
5
                self.primary_id = last.id;
354
5
            }
355
3290
        }
356
3299
    }
357

            
358
    /// Get the primary cursor position (for scroll-into-view, IME, etc.)
359
7437
    #[must_use] pub fn get_primary_cursor(&self) -> Option<TextCursor> {
360
7437
        self.get_primary().map(|s| match &s.selection {
361
7419
            Selection::Cursor(c) => *c,
362
4
            Selection::Range(r) => r.end,
363
7423
        })
364
7437
    }
365

            
366
    /// Convert to a Vec<Selection> for passing to `edit_text()`.
367
2341
    #[must_use] pub fn to_selections(&self) -> Vec<Selection> {
368
2341
        self.selections.iter().map(|s| s.selection).collect()
369
2341
    }
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
2304
    pub fn update_from_edit_result(&mut self, new_selections: &[Selection]) {
375
2304
        let old_ids: Vec<SelectionId> = self.selections.iter().map(|s| s.id).collect();
376
2304
        self.selections.clear();
377
3306
        for (i, sel) in new_selections.iter().enumerate() {
378
3306
            let id = old_ids.get(i).copied().unwrap_or_else(SelectionId::new);
379
3306
            self.selections.push(IdentifiedSelection {
380
3306
                id,
381
3306
                selection: *sel,
382
3306
            });
383
3306
        }
384
        // IDs are reassigned by index; make sure primary_id still resolves.
385
2304
        self.ensure_primary_valid();
386
        // Don't merge here — edit_text already returns correct positions
387
2304
    }
388

            
389
    /// Set all selections to a single cursor (e.g., on plain click without Ctrl).
390
26
    pub fn set_single_cursor(&mut self, cursor: TextCursor) {
391
26
        let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
392
26
        self.selections.clear();
393
26
        self.selections.push(IdentifiedSelection {
394
26
            id,
395
26
            selection: Selection::Cursor(cursor),
396
26
        });
397
26
        self.primary_id = id;
398
26
    }
399

            
400
    /// Set all selections to a single range.
401
78
    pub fn set_single_range(&mut self, range: SelectionRange) {
402
78
        let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
403
78
        self.selections.clear();
404
78
        self.selections.push(IdentifiedSelection {
405
78
            id,
406
78
            selection: Selection::Range(range),
407
78
        });
408
78
        self.primary_id = id;
409
78
    }
410

            
411
    /// Number of active cursors/selections.
412
50
    #[must_use] pub const fn len(&self) -> usize {
413
50
        self.selections.len()
414
50
    }
415

            
416
    /// Whether there are no selections (should not normally happen).
417
56
    #[must_use] pub const fn is_empty(&self) -> bool {
418
56
        self.selections.is_empty()
419
56
    }
420

            
421
    /// Sort selections by position and merge any that overlap.
422
1077
    pub fn merge_overlapping(&mut self) {
423
1077
        if self.selections.len() <= 1 {
424
91
            return;
425
986
        }
426

            
427
        // Capture the primary before sorting/merging reorders and rewrites IDs.
428
986
        let primary = self.primary_id;
429
986
        let mut new_primary = primary;
430

            
431
        // Sort by the start position of each selection
432
133162
        self.selections.sort_by(|a, b| {
433
133162
            let pos_a = selection_start_pos(&a.selection);
434
133162
            let pos_b = selection_start_pos(&b.selection);
435
133162
            pos_a.cmp(&pos_b)
436
133162
        });
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
986
        let mut merged: Vec<IdentifiedSelection> = Vec::with_capacity(self.selections.len());
441
132097
        for sel in self.selections.drain(..) {
442
132097
            if let Some(last) = merged.last_mut() {
443
131111
                let last_end = selection_end_pos(&last.selection);
444
131111
                let cur_start = selection_start_pos(&sel.selection);
445
131111
                if cur_start <= last_end {
446
                    // Overlap — merge into one range covering both
447
615
                    let new_start = selection_start_pos(&last.selection);
448
615
                    let cur_end = selection_end_pos(&sel.selection);
449
615
                    let new_end = if cur_end > last_end { cur_end } else { last_end };
450
615
                    if new_start == new_end {
451
405
                        last.selection = Selection::Cursor(new_start);
452
405
                    } else {
453
210
                        last.selection = Selection::Range(SelectionRange {
454
210
                            start: new_start,
455
210
                            end: new_end,
456
210
                        });
457
210
                    }
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
615
                    let inherits_primary =
466
615
                        last.id == primary || sel.id == primary || last.id == new_primary;
467
                    // Keep the newer ID (the one being merged in)
468
615
                    last.id = sel.id;
469
615
                    if inherits_primary {
470
469
                        new_primary = sel.id;
471
469
                    }
472
615
                    continue;
473
130496
                }
474
986
            }
475
131482
            merged.push(sel);
476
        }
477
986
        self.selections = merged;
478

            
479
        // Point primary at a surviving selection (fallback: last element).
480
986
        self.primary_id = new_primary;
481
986
        self.ensure_primary_valid();
482
1077
    }
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
    ///
490
    /// A bare (non-extending) move over an active range COLLAPSES to the range
491
    /// boundary — the arrow-key rule. Use [`Self::move_all_cursors_with`] for
492
    /// steps where that is wrong (Home/End, document jumps).
493
23
    pub fn move_all_cursors(
494
23
        &mut self,
495
23
        extend_selection: bool,
496
23
        move_fn: impl Fn(&TextCursor) -> TextCursor,
497
23
    ) {
498
23
        self.move_all_cursors_with(extend_selection, true, move_fn);
499
23
    }
500

            
501
    /// [`Self::move_all_cursors`], with control over what a bare move does to
502
    /// an active range.
503
    ///
504
    /// `collapse_range_to_boundary` is the arrow-key rule: Left/Right with a
505
    /// selection put the caret on the selection's edge and go no further.
506
    /// Every OTHER step — Home/End, Ctrl+Home/End, a visual line, a word — is a
507
    /// MOVEMENT and must be performed: collapsing them to the nearest edge is
508
    /// how pressing End with text selected used to leave the caret sitting at
509
    /// the end of the selection instead of the end of the line.
510
46
    pub fn move_all_cursors_with(
511
46
        &mut self,
512
46
        extend_selection: bool,
513
46
        collapse_range_to_boundary: bool,
514
46
        move_fn: impl Fn(&TextCursor) -> TextCursor,
515
46
    ) {
516
249
        for sel in &mut self.selections {
517
203
            match &sel.selection {
518
177
                Selection::Cursor(c) => {
519
177
                    let new_cursor = move_fn(c);
520
177
                    if extend_selection {
521
2
                        if *c != new_cursor {
522
1
                            sel.selection = Selection::Range(SelectionRange {
523
1
                                start: *c,
524
1
                                end: new_cursor,
525
1
                            });
526
1
                        }
527
175
                    } else {
528
175
                        sel.selection = Selection::Cursor(new_cursor);
529
175
                    }
530
                }
531
26
                Selection::Range(r) => {
532
26
                    if extend_selection {
533
1
                        let new_end = move_fn(&r.end);
534
1
                        if r.start == new_end {
535
1
                            sel.selection = Selection::Cursor(r.start);
536
1
                        } else {
537
                            sel.selection = Selection::Range(SelectionRange {
538
                                start: r.start,
539
                                end: new_end,
540
                            });
541
                        }
542
25
                    } else if collapse_range_to_boundary {
543
                        // Bare arrow with an active selection collapses the caret
544
                        // to the selection boundary in the arrow's direction WITHOUT
545
                        // advancing a character (standard editor behavior). Running
546
                        // move_fn on the focus and using that as the caret would step
547
                        // one unit past the edge. We don't get the arrow direction
548
                        // here, so probe it: apply move_fn to the focus and compare —
549
                        // a forward move collapses to the max boundary, a backward
550
                        // move to the min boundary.
551
14
                        let (lo, hi) = if r.start <= r.end {
552
12
                            (r.start, r.end)
553
                        } else {
554
2
                            (r.end, r.start)
555
                        };
556
14
                        let probe = move_fn(&r.end);
557
14
                        let collapsed = if probe >= r.end { hi } else { lo };
558
14
                        sel.selection = Selection::Cursor(collapsed);
559
11
                    } else {
560
11
                        // Home / End / Ctrl+Home / Ctrl+End / a visual line step:
561
11
                        // the caret goes where the step points, measured from the
562
11
                        // focus. The boundary collapse above would strand it on
563
11
                        // the selection's edge instead.
564
11
                        sel.selection = Selection::Cursor(move_fn(&r.end));
565
11
                    }
566
                }
567
            }
568
        }
569
46
        self.merge_overlapping();
570
46
    }
571

            
572
    /// Remap the `NodeId` in `node_id` after DOM reconciliation.
573
    ///
574
    /// If the node was removed (not in the map), the multi-cursor state is cleared.
575
40
    pub fn remap_node_ids(
576
40
        &mut self,
577
40
        dom_id: DomId,
578
40
        node_id_map: &BTreeMap<NodeId, NodeId>,
579
40
    ) {
580
40
        if self.node_id.dom != dom_id {
581
1
            return;
582
39
        }
583
39
        if let Some(old_node_id) = self.node_id.node.into_crate_internal() {
584
38
            if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
585
37
                self.node_id.node = crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
586
37
            } else {
587
1
                // Node removed — clear selections
588
1
                self.selections.clear();
589
1
            }
590
1
        }
591
40
    }
592
}
593

            
594
/// Helper: get the start position of a Selection for sorting.
595
398856
fn selection_start_pos(sel: &Selection) -> TextCursor {
596
398856
    match sel {
597
396648
        Selection::Cursor(c) => *c,
598
2208
        Selection::Range(r) => {
599
2208
            if r.start <= r.end { r.start } else { r.end }
600
        }
601
    }
602
398856
}
603

            
604
/// Helper: get the end position of a Selection for merging.
605
132532
fn selection_end_pos(sel: &Selection) -> TextCursor {
606
132532
    match sel {
607
131973
        Selection::Cursor(c) => *c,
608
559
        Selection::Range(r) => {
609
559
            if r.end >= r.start { r.end } else { r.start }
610
        }
611
    }
612
132532
}
613

            
614
// ============================================================================
615
// MULTI-NODE SELECTION (Browser-style Anchor/Focus model)
616
// ============================================================================
617

            
618
/// The anchor point of a text selection - where the user started selecting.
619
///
620
/// This is the fixed point during a drag operation. It records:
621
/// - The IFC root node (where the `UnifiedLayout` lives)
622
/// - The exact cursor position within that layout
623
/// - The visual bounds of the anchor character (for logical rectangle calculations)
624
///
625
/// The anchor remains constant during a drag; only the focus moves.
626
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627
pub struct SelectionAnchor {
628
    /// The IFC root node ID where selection started.
629
    /// This is the node that has `inline_layout_result` (e.g., `<p>`, `<div>`).
630
    pub ifc_root_node_id: NodeId,
631
    
632
    /// The exact cursor position within the IFC's `UnifiedLayout`.
633
    pub cursor: TextCursor,
634
    
635
    /// Visual bounds of the anchor character in viewport coordinates.
636
    /// Used for computing the logical selection rectangle during multi-line/multi-node selection.
637
    pub char_bounds: LogicalRect,
638
    
639
    /// The mouse position when the selection started (viewport coordinates).
640
    pub mouse_position: LogicalPosition,
641
}
642

            
643
/// The focus point of a text selection - where the selection currently ends.
644
///
645
/// This is the movable point during a drag operation. It updates on every mouse move.
646
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
647
pub struct SelectionFocus {
648
    /// The IFC root node ID where selection currently ends.
649
    /// May differ from anchor's IFC root during cross-node selection.
650
    pub ifc_root_node_id: NodeId,
651
    
652
    /// The exact cursor position within the IFC's `UnifiedLayout`.
653
    pub cursor: TextCursor,
654
    
655
    /// Current mouse position in viewport coordinates.
656
    pub mouse_position: LogicalPosition,
657
}
658

            
659
/// Complete selection state spanning potentially multiple DOM nodes.
660
///
661
/// This implements the W3C Selection API model with anchor/focus endpoints.
662
/// The selection can span multiple IFC roots (e.g., multiple `<p>` elements).
663
///
664
/// ## Storage Model
665
///
666
/// Uses `BTreeMap<NodeId, Vec<SelectionRange>>` for O(log N) lookup during rendering.
667
/// The key is the **IFC root `NodeId`**, and the value is every `SelectionRange`
668
/// that IFC contributes.
669
///
670
/// ## Example
671
///
672
/// ```text
673
/// <p id="1">Hello [World</p>     <- Anchor in IFC 1, partial selection
674
/// <p id="2">Complete line</p>    <- InBetween, fully selected
675
/// <p id="3">Partial] end</p>     <- Focus in IFC 3, partial selection
676
/// ```
677
#[derive(Debug, Clone, PartialEq, Eq)]
678
pub struct TextSelection {
679
    /// The DOM this selection belongs to.
680
    pub dom_id: DomId,
681
    
682
    /// The anchor point - where the selection started (fixed during drag).
683
    pub anchor: SelectionAnchor,
684
    
685
    /// The focus point - where the selection currently ends (moves during drag).
686
    pub focus: SelectionFocus,
687
    
688
    /// Map from IFC root `NodeId` to the `SelectionRange`s for that IFC.
689
    /// This allows O(log N) lookup during rendering.
690
    ///
691
    /// Each `SelectionRange` contains the actual `TextCursor` positions for that IFC,
692
    /// ready to be passed to `UnifiedLayout::get_selection_rects()`.
693
    ///
694
    /// A node carries SEVERAL ranges when a multi-cursor session selects several
695
    /// occurrences in it (Ctrl+D); the ranges are disjoint and in document order.
696
    pub affected_nodes: BTreeMap<NodeId, Vec<SelectionRange>>,
697
    
698
    /// Indicates whether anchor comes before focus in document order.
699
    /// True = forward selection (left-to-right), False = backward selection.
700
    pub is_forward: bool,
701
}
702

            
703
impl TextSelection {
704
    /// Create a new collapsed selection (cursor) at the given position.
705
8
    #[must_use] pub fn new_collapsed(
706
8
        dom_id: DomId,
707
8
        ifc_root_node_id: NodeId,
708
8
        cursor: TextCursor,
709
8
        char_bounds: LogicalRect,
710
8
        mouse_position: LogicalPosition,
711
8
    ) -> Self {
712
8
        let anchor = SelectionAnchor {
713
8
            ifc_root_node_id,
714
8
            cursor,
715
8
            char_bounds,
716
8
            mouse_position,
717
8
        };
718
        
719
8
        let focus = SelectionFocus {
720
8
            ifc_root_node_id,
721
8
            cursor,
722
8
            mouse_position,
723
8
        };
724
        
725
        // For a collapsed selection, the anchor node has a zero-width range
726
8
        let mut affected_nodes = BTreeMap::new();
727
8
        affected_nodes.insert(ifc_root_node_id, vec![SelectionRange {
728
8
            start: cursor,
729
8
            end: cursor,
730
8
        }]);
731
        
732
8
        Self {
733
8
            dom_id,
734
8
            anchor,
735
8
            focus,
736
8
            affected_nodes,
737
8
            is_forward: true, // Direction doesn't matter for collapsed selection
738
8
        }
739
8
    }
740
    
741
    /// Check if this is a collapsed selection (cursor with no range).
742
2227
    #[must_use] pub fn is_collapsed(&self) -> bool {
743
2227
        self.anchor.ifc_root_node_id == self.focus.ifc_root_node_id
744
226
            && self.anchor.cursor == self.focus.cursor
745
2227
    }
746
    
747
    /// Get the FIRST selection range for a specific IFC root node.
748
    /// Returns `None` if this node is not part of the selection.
749
    ///
750
    /// A multi-range node has more; [`Self::ranges_for_node`] returns all of them.
751
74
    #[must_use] pub fn get_range_for_node(&self, ifc_root_node_id: &NodeId) -> Option<&SelectionRange> {
752
74
        self.affected_nodes.get(ifc_root_node_id).and_then(|r| r.first())
753
74
    }
754

            
755
    /// Every range this IFC root contributes (empty slice when unaffected).
756
80
    #[must_use] pub fn ranges_for_node(&self, ifc_root_node_id: &NodeId) -> &[SelectionRange] {
757
80
        self.affected_nodes.get(ifc_root_node_id).map_or(&[], Vec::as_slice)
758
80
    }
759
}
760

            
761
impl_option!(
762
    TextSelection,
763
    OptionTextSelection,
764
    copy = false,
765
    clone = false,
766
    [Debug, Clone, PartialEq, Eq]
767
);
768

            
769
#[cfg(test)]
770
mod audit_tests {
771
    use super::*;
772

            
773
7
    fn cursor(byte: u32) -> TextCursor {
774
7
        TextCursor {
775
7
            cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: byte },
776
7
            affinity: CursorAffinity::Leading,
777
7
        }
778
7
    }
779

            
780
3
    fn state(byte: u32) -> MultiCursorState {
781
3
        MultiCursorState::new_with_cursor(cursor(byte), DomNodeId::ROOT, 0)
782
3
    }
783

            
784
    #[test]
785
1
    fn primary_tracked_by_id_not_vec_position() {
786
1
        let mut mc = state(100);
787
        // Add a cursor at an EARLIER position; after merge_overlapping's sort it
788
        // becomes the vector's FIRST element, but it is the primary (last added).
789
1
        let b = mc.add_cursor(cursor(0));
790
1
        assert_eq!(mc.len(), 2);
791
        // The primary must be the just-added cursor at byte 0, not the
792
        // position-last cursor at byte 100.
793
1
        assert_eq!(mc.get_primary().unwrap().id, b);
794
1
        assert_eq!(mc.get_primary_cursor().unwrap(), cursor(0));
795
1
    }
796

            
797
    #[test]
798
1
    fn merge_preserves_primary() {
799
1
        let mut mc = state(5);
800
1
        let _b = mc.add_cursor(cursor(5)); // same position -> merges to one
801
1
        assert_eq!(mc.len(), 1);
802
        // primary_id must resolve to the surviving selection.
803
1
        let primary = mc.get_primary().unwrap();
804
1
        assert_eq!(primary.id, mc.selections[0].id);
805
1
    }
806

            
807
    #[test]
808
1
    fn removing_primary_repoints_it() {
809
1
        let mut mc = state(0);
810
1
        let b = mc.add_cursor(cursor(10)); // primary = b
811
1
        assert_eq!(mc.get_primary().unwrap().id, b);
812
1
        assert!(mc.remove_selection(b));
813
        // primary must now be a still-existing selection, not a dangling id.
814
1
        let p = mc.get_primary().unwrap();
815
1
        assert!(mc.selections.iter().any(|s| s.id == p.id));
816
1
    }
817
}
818

            
819
#[cfg(test)]
820
mod autotest_generated {
821
    use super::*;
822
    use crate::geom::LogicalSize;
823
    use crate::styled_dom::NodeHierarchyItemId;
824

            
825
    // ---------------------------------------------------------------------
826
    // Fixtures
827
    // ---------------------------------------------------------------------
828

            
829
    /// Cursor in run 0 at `byte`, Leading affinity.
830
    fn c(byte: u32) -> TextCursor {
831
        TextCursor {
832
            cluster_id: GraphemeClusterId {
833
                source_run: 0,
834
                start_byte_in_run: byte,
835
            },
836
            affinity: CursorAffinity::Leading,
837
        }
838
    }
839

            
840
    /// Cursor with explicit run + affinity (for ordering / boundary probes).
841
    fn c_full(run: u32, byte: u32, affinity: CursorAffinity) -> TextCursor {
842
        TextCursor {
843
            cluster_id: GraphemeClusterId {
844
                source_run: run,
845
                start_byte_in_run: byte,
846
            },
847
            affinity,
848
        }
849
    }
850

            
851
    fn rng(a: u32, b: u32) -> SelectionRange {
852
        SelectionRange {
853
            start: c(a),
854
            end: c(b),
855
        }
856
    }
857

            
858
    fn dom_node(index: usize) -> DomNodeId {
859
        DomNodeId {
860
            dom: DomId::ROOT_ID,
861
            node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(index))),
862
        }
863
    }
864

            
865
    fn state(byte: u32) -> MultiCursorState {
866
        MultiCursorState::new_with_cursor(c(byte), DomNodeId::ROOT, 0)
867
    }
868

            
869
    /// A `MultiCursorState` with zero selections — "should not normally happen",
870
    /// so every getter must survive it.
871
    fn empty_state() -> MultiCursorState {
872
        MultiCursorState {
873
            selections: Vec::new(),
874
            primary_id: SelectionId::new(),
875
            node_id: DomNodeId::ROOT,
876
            contenteditable_key: 0,
877
        }
878
    }
879

            
880
    fn ident(id: SelectionId, sel: Selection) -> IdentifiedSelection {
881
        IdentifiedSelection { id, selection: sel }
882
    }
883

            
884
    // ---------------------------------------------------------------------
885
    // Invariant checkers (documented in `MultiCursorState`'s `## Invariants`)
886
    // ---------------------------------------------------------------------
887

            
888
    /// `selections` is sorted by position and non-overlapping.
889
    fn assert_sorted_nonoverlapping(mc: &MultiCursorState) {
890
        for w in mc.selections.windows(2) {
891
            let prev_end = selection_end_pos(&w[0].selection);
892
            let next_start = selection_start_pos(&w[1].selection);
893
            assert!(
894
                next_start > prev_end,
895
                "selections must be sorted and non-overlapping after merge: {:?} then {:?}",
896
                w[0],
897
                w[1]
898
            );
899
        }
900
    }
901

            
902
    /// `primary_id` must always name a selection that actually exists (or the
903
    /// state must be empty). A dangling `primary_id` makes `get_primary()` lie.
904
    fn assert_primary_resolves(mc: &MultiCursorState) {
905
        if mc.is_empty() {
906
            assert!(mc.get_primary().is_none());
907
            assert!(mc.get_primary_cursor().is_none());
908
        } else {
909
            let p = mc.get_primary().expect("non-empty state must have a primary");
910
            assert!(
911
                mc.selections.iter().any(|s| s.id == p.id),
912
                "get_primary() returned a selection not in the vec"
913
            );
914
            assert_eq!(
915
                mc.primary_id, p.id,
916
                "primary_id must name an existing selection (not fall back to last)"
917
            );
918
        }
919
    }
920

            
921
    /// All selection IDs must be distinct.
922
    fn assert_ids_unique(mc: &MultiCursorState) {
923
        for (i, a) in mc.selections.iter().enumerate() {
924
            for b in mc.selections.iter().skip(i + 1) {
925
                assert_ne!(a.id, b.id, "duplicate SelectionId in state");
926
            }
927
        }
928
    }
929

            
930
    // =====================================================================
931
    // SelectionId::new  (constructor)
932
    // =====================================================================
933

            
934
    #[test]
935
    fn selection_id_new_is_unique_and_strictly_increasing() {
936
        let mut prev = SelectionId::new();
937
        assert!(prev.inner > 0, "counter starts at 1, never the 0 sentinel");
938
        for _ in 0..1000 {
939
            let next = SelectionId::new();
940
            // Other tests share the global atomic, so ids may skip — but within
941
            // one thread they must be strictly increasing and never repeat.
942
            assert!(
943
                next.inner > prev.inner,
944
                "SelectionId counter must be strictly monotonic"
945
            );
946
            assert_ne!(next, prev);
947
            prev = next;
948
        }
949
    }
950

            
951
    #[test]
952
    fn selection_id_default_mints_a_fresh_id() {
953
        // Documented: Default does NOT return a zero/sentinel value.
954
        let a = SelectionId::default();
955
        let b = SelectionId::default();
956
        let d = SelectionId::new();
957
        assert_ne!(a, b);
958
        assert_ne!(b, d);
959
        assert!(a.inner > 0 && b.inner > 0);
960
    }
961

            
962
    // =====================================================================
963
    // SelectionState::add
964
    // =====================================================================
965

            
966
    #[test]
967
    fn selection_state_add_dedups_identical_cursors() {
968
        let mut st = SelectionState {
969
            selections: Vec::<Selection>::new().into(),
970
            node_id: DomNodeId::ROOT,
971
        };
972
        for _ in 0..100 {
973
            st.add(Selection::Cursor(c(42)));
974
        }
975
        assert_eq!(st.selections.as_ref().len(), 1);
976
        assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(42)));
977
    }
978

            
979
    #[test]
980
    fn selection_state_add_sorts_descending_input_ascending() {
981
        let mut st = SelectionState {
982
            selections: Vec::<Selection>::new().into(),
983
            node_id: DomNodeId::ROOT,
984
        };
985
        for byte in [90u32, 10, 50, 0, 70] {
986
            st.add(Selection::Cursor(c(byte)));
987
        }
988
        let got: Vec<Selection> = st.selections.as_ref().to_vec();
989
        assert_eq!(got.len(), 5);
990
        let want: Vec<Selection> = [0u32, 10, 50, 70, 90]
991
            .iter()
992
            .map(|b| Selection::Cursor(c(*b)))
993
            .collect();
994
        assert_eq!(got, want);
995
    }
996

            
997
    #[test]
998
    fn selection_state_add_boundary_and_reversed_ranges_do_not_panic() {
999
        let mut st = SelectionState {
            selections: Vec::<Selection>::new().into(),
            node_id: DomNodeId::ROOT,
        };
        // u32::MAX bytes, max run index, both affinities, and a *reversed* range
        // (start logically after end — explicitly allowed by SelectionRange docs).
        st.add(Selection::Cursor(c_full(
            u32::MAX,
            u32::MAX,
            CursorAffinity::Trailing,
        )));
        st.add(Selection::Cursor(c_full(0, 0, CursorAffinity::Leading)));
        st.add(Selection::Range(SelectionRange {
            start: c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
            end: c_full(0, 0, CursorAffinity::Leading),
        }));
        st.add(Selection::Range(rng(0, u32::MAX)));
        // add() only sorts + dedups; it does not normalize or merge, so all 4 stay.
        assert_eq!(st.selections.as_ref().len(), 4);
        // ... and the result is sorted.
        let got: Vec<Selection> = st.selections.as_ref().to_vec();
        let mut sorted = got.clone();
        sorted.sort_unstable();
        assert_eq!(got, sorted);
    }
    #[test]
    fn selection_state_add_cursor_and_range_at_same_pos_are_distinct() {
        let mut st = SelectionState {
            selections: Vec::<Selection>::new().into(),
            node_id: DomNodeId::ROOT,
        };
        st.add(Selection::Range(rng(5, 5)));
        st.add(Selection::Cursor(c(5)));
        // A zero-width Range and a Cursor are different `Selection` variants,
        // so dedup() cannot collapse them.
        assert_eq!(st.selections.as_ref().len(), 2);
        // Cursor variant sorts before Range variant.
        assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(5)));
    }
    // =====================================================================
    // MultiCursorState::new_with_cursor  (constructor)
    // =====================================================================
    #[test]
    fn new_with_cursor_invariants_hold() {
        let node = dom_node(7);
        let mc = MultiCursorState::new_with_cursor(c(3), node, 0xDEAD_BEEF);
        assert_eq!(mc.len(), 1);
        assert!(!mc.is_empty());
        assert_eq!(mc.selections.len(), mc.len());
        assert_eq!(mc.primary_id, mc.selections[0].id);
        assert_eq!(mc.node_id, node);
        assert_eq!(mc.contenteditable_key, 0xDEAD_BEEF);
        assert_eq!(mc.get_primary_cursor(), Some(c(3)));
        assert_eq!(mc.to_selections(), vec![Selection::Cursor(c(3))]);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn new_with_cursor_extreme_args_do_not_panic() {
        let mc = MultiCursorState::new_with_cursor(
            c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
            dom_node(usize::MAX / 4),
            u64::MAX,
        );
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.contenteditable_key, u64::MAX);
        assert_eq!(
            mc.get_primary_cursor(),
            Some(c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing))
        );
        assert_primary_resolves(&mc);
    }
    #[test]
    fn two_states_get_distinct_ids() {
        let a = state(0);
        let b = state(0);
        assert_ne!(a.primary_id, b.primary_id);
    }
    // =====================================================================
    // add_cursor / add_selection
    // =====================================================================
    #[test]
    fn add_cursor_at_same_position_merges_to_one() {
        let mut mc = state(5);
        let b = mc.add_cursor(c(5));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(5)));
        assert_eq!(mc.selections[0].id, b, "merge keeps the newer id");
        assert_primary_resolves(&mc);
        assert_ids_unique(&mc);
    }
    #[test]
    fn add_cursor_distinct_positions_stay_separate_and_sorted() {
        let mut mc = state(30);
        let _ = mc.add_cursor(c(10));
        let last = mc.add_cursor(c(20));
        assert_eq!(mc.len(), 3);
        // Sorted by position, NOT by insertion order.
        assert_eq!(mc.to_selections(), vec![
            Selection::Cursor(c(10)),
            Selection::Cursor(c(20)),
            Selection::Cursor(c(30)),
        ]);
        // Primary is the most recently added (byte 20), which is the *middle*
        // element — proving primary is tracked by id, not vec position.
        assert_eq!(mc.get_primary().unwrap().id, last);
        assert_eq!(mc.get_primary_cursor(), Some(c(20)));
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
        assert_ids_unique(&mc);
    }
    #[test]
    fn add_cursor_same_byte_different_affinity_does_not_merge() {
        // Leading < Trailing, so cur_start(Trailing) > last_end(Leading) and the
        // merge condition (`cur_start <= last_end`) is false. Two carets survive
        // at the same byte offset.
        let mut mc = MultiCursorState::new_with_cursor(
            c_full(0, 4, CursorAffinity::Leading),
            DomNodeId::ROOT,
            0,
        );
        let _ = mc.add_cursor(c_full(0, 4, CursorAffinity::Trailing));
        assert_eq!(mc.len(), 2);
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn add_selection_overlapping_ranges_merge_into_union() {
        let mut mc = empty_state();
        let _ = mc.add_selection(rng(0, 10));
        let _ = mc.add_selection(rng(5, 20));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 20)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn add_selection_touching_ranges_merge() {
        // Adjacent (end == start) counts as overlapping: `cur_start <= last_end`.
        let mut mc = empty_state();
        let _ = mc.add_selection(rng(0, 10));
        let _ = mc.add_selection(rng(10, 20));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 20)));
    }
    #[test]
    fn add_selection_disjoint_ranges_stay_separate() {
        let mut mc = empty_state();
        let _ = mc.add_selection(rng(0, 10));
        let _ = mc.add_selection(rng(11, 20));
        assert_eq!(mc.len(), 2);
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn add_selection_reversed_range_is_normalized_for_merging() {
        // Backwards selection: start (20) is logically after end (5).
        let mut mc = empty_state();
        let _ = mc.add_selection(SelectionRange {
            start: c(20),
            end: c(5),
        });
        // A cursor *inside* the backwards range must merge with it.
        let _ = mc.add_cursor(c(10));
        assert_eq!(mc.len(), 1);
        // The merged result is normalized to a forwards range.
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(5, 20)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn add_selection_at_u32_max_boundary_does_not_overflow() {
        let mut mc = empty_state();
        let _ = mc.add_selection(rng(u32::MAX - 1, u32::MAX));
        let _ = mc.add_cursor(c(u32::MAX));
        assert_eq!(mc.len(), 1);
        assert_eq!(
            mc.selections[0].selection,
            Selection::Range(rng(u32::MAX - 1, u32::MAX))
        );
        assert_primary_resolves(&mc);
    }
    #[test]
    fn add_cursor_stress_500_distinct_positions() {
        let mut mc = state(0);
        for i in 1..=500u32 {
            let _ = mc.add_cursor(c(i * 2));
        }
        assert_eq!(mc.len(), 501);
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
        assert_ids_unique(&mc);
    }
    #[test]
    fn add_cursor_stress_same_position_never_grows() {
        let mut mc = state(9);
        for _ in 0..300 {
            let _ = mc.add_cursor(c(9));
        }
        assert_eq!(mc.len(), 1, "identical cursors must always collapse");
        assert_primary_resolves(&mc);
    }
    // =====================================================================
    // remove_selection
    // =====================================================================
    #[test]
    fn remove_selection_unknown_id_returns_false_and_changes_nothing() {
        let mut mc = state(1);
        let before = mc.clone();
        let ghost = SelectionId::new(); // never inserted anywhere
        assert!(!mc.remove_selection(ghost));
        assert_eq!(mc, before);
    }
    #[test]
    fn remove_selection_twice_second_call_returns_false() {
        let mut mc = state(0);
        let b = mc.add_cursor(c(10));
        assert!(mc.remove_selection(b));
        assert!(!mc.remove_selection(b));
        assert_eq!(mc.len(), 1);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn remove_all_selections_leaves_a_safe_empty_state() {
        let mut mc = state(0);
        let b = mc.add_cursor(c(10));
        let a = mc.selections.iter().find(|s| s.id != b).unwrap().id;
        assert!(mc.remove_selection(a));
        assert!(mc.remove_selection(b));
        assert!(mc.is_empty());
        assert_eq!(mc.len(), 0);
        assert!(mc.get_primary().is_none());
        assert!(mc.get_primary_cursor().is_none());
        assert!(mc.to_selections().is_empty());
        // Further mutation of the empty state must not panic.
        mc.merge_overlapping();
        mc.move_all_cursors(true, |cur| *cur);
        assert!(mc.is_empty());
    }
    #[test]
    fn remove_primary_from_three_repoints_to_a_survivor() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let p = mc.add_cursor(c(20)); // primary
        assert_eq!(mc.get_primary().unwrap().id, p);
        assert!(mc.remove_selection(p));
        assert_eq!(mc.len(), 2);
        assert_primary_resolves(&mc);
    }
    // =====================================================================
    // get_primary / get_primary_mut / get_primary_cursor / to_selections / len
    // =====================================================================
    #[test]
    fn empty_state_getters_return_none_without_panicking() {
        let mut mc = empty_state();
        assert!(mc.is_empty());
        assert_eq!(mc.len(), 0);
        assert!(mc.get_primary().is_none());
        assert!(mc.get_primary_mut().is_none());
        assert!(mc.get_primary_cursor().is_none());
        assert!(mc.to_selections().is_empty());
        mc.merge_overlapping(); // early-returns on len <= 1
        mc.ensure_primary_valid(); // private: must not panic on empty vec
        assert!(mc.is_empty());
    }
    #[test]
    fn get_primary_falls_back_to_last_when_primary_id_is_dangling() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        mc.primary_id = SelectionId::new(); // dangling: names nothing in the vec
        let p = mc.get_primary().expect("must fall back, not return None");
        assert_eq!(p.id, mc.selections.last().unwrap().id);
        assert_eq!(mc.get_primary_cursor(), Some(c(10)));
    }
    #[test]
    fn ensure_primary_valid_adopts_last_id_when_dangling() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let dangling = SelectionId::new();
        mc.primary_id = dangling;
        mc.ensure_primary_valid();
        assert_ne!(mc.primary_id, dangling);
        assert_eq!(mc.primary_id, mc.selections.last().unwrap().id);
        // Idempotent: a second call on a now-valid id is a no-op.
        let fixed = mc.primary_id;
        mc.ensure_primary_valid();
        assert_eq!(mc.primary_id, fixed);
    }
    #[test]
    fn ensure_primary_valid_on_empty_leaves_id_untouched() {
        let mut mc = empty_state();
        let before = mc.primary_id;
        mc.ensure_primary_valid();
        assert_eq!(mc.primary_id, before, "nothing to adopt — id must not change");
        assert!(mc.get_primary().is_none());
    }
    #[test]
    fn get_primary_cursor_of_a_range_is_its_end_field() {
        let mut mc = empty_state();
        mc.set_single_range(rng(3, 9));
        assert_eq!(mc.get_primary_cursor(), Some(c(9)));
        // Backwards range: the raw `end` field is returned (the *focus*), even
        // though it is the lower position. This is deliberate — the caret sits
        // at the focus, not at the max boundary.
        let mut back = empty_state();
        back.set_single_range(SelectionRange {
            start: c(9),
            end: c(3),
        });
        assert_eq!(back.get_primary_cursor(), Some(c(3)));
    }
    #[test]
    fn get_primary_mut_mutation_is_visible_through_get_primary() {
        let mut mc = state(0);
        let p = mc.add_cursor(c(50));
        {
            let prim = mc.get_primary_mut().expect("primary exists");
            assert_eq!(prim.id, p);
            prim.selection = Selection::Range(rng(50, 60));
        }
        assert_eq!(
            mc.get_primary().unwrap().selection,
            Selection::Range(rng(50, 60))
        );
        assert_eq!(mc.get_primary_cursor(), Some(c(60)));
    }
    #[test]
    fn get_primary_mut_falls_back_to_last_when_dangling() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        mc.primary_id = SelectionId::new();
        let last_id = mc.selections.last().unwrap().id;
        let prim = mc.get_primary_mut().expect("fallback to last");
        assert_eq!(prim.id, last_id);
    }
    #[test]
    fn to_selections_matches_the_internal_order_and_len() {
        let mut mc = state(30);
        let _ = mc.add_cursor(c(10));
        let _ = mc.add_selection(rng(15, 20));
        let sels = mc.to_selections();
        assert_eq!(sels.len(), mc.len());
        let inner: Vec<Selection> = mc.selections.iter().map(|s| s.selection).collect();
        assert_eq!(sels, inner);
    }
    #[test]
    fn len_and_is_empty_always_agree() {
        let mut mc = empty_state();
        assert!(mc.is_empty() && mc.is_empty());
        let _ = mc.add_cursor(c(1));
        assert!(!mc.is_empty() && mc.len() == 1);
        for i in 2..20u32 {
            let _ = mc.add_cursor(c(i * 3));
        }
        assert_eq!(mc.len(), mc.selections.len());
        assert_eq!(mc.is_empty(), mc.is_empty());
        assert!(!mc.is_empty());
    }
    // =====================================================================
    // update_from_edit_result
    // =====================================================================
    #[test]
    fn update_from_edit_result_with_empty_slice_clears_everything() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        mc.update_from_edit_result(&[]);
        assert!(mc.is_empty());
        assert!(mc.get_primary().is_none());
        assert!(mc.get_primary_cursor().is_none());
    }
    #[test]
    fn update_from_edit_result_preserves_ids_by_index_and_mints_extras() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let old: Vec<SelectionId> = mc.selections.iter().map(|s| s.id).collect();
        assert_eq!(old.len(), 2);
        mc.update_from_edit_result(&[
            Selection::Cursor(c(1)),
            Selection::Cursor(c(2)),
            Selection::Cursor(c(3)),
            Selection::Range(rng(4, 8)),
        ]);
        assert_eq!(mc.len(), 4);
        assert_eq!(mc.selections[0].id, old[0], "id preserved by index");
        assert_eq!(mc.selections[1].id, old[1], "id preserved by index");
        assert_ids_unique(&mc);
        assert_primary_resolves(&mc);
        assert_eq!(mc.selections[3].selection, Selection::Range(rng(4, 8)));
    }
    #[test]
    fn update_from_edit_result_shrinking_keeps_primary_resolvable() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let _ = mc.add_cursor(c(20)); // primary = the byte-20 cursor
        mc.update_from_edit_result(&[Selection::Cursor(c(99))]);
        assert_eq!(mc.len(), 1);
        // The primary's id is gone (only index 0's id survived), so
        // ensure_primary_valid must have re-pointed it at the survivor.
        assert_primary_resolves(&mc);
        assert_eq!(mc.get_primary_cursor(), Some(c(99)));
    }
    #[test]
    fn update_from_edit_result_does_not_merge_overlaps() {
        // Documented: "Don't merge here — edit_text already returns correct positions"
        let mut mc = state(0);
        mc.update_from_edit_result(&[
            Selection::Range(rng(0, 10)),
            Selection::Range(rng(5, 15)),
        ]);
        assert_eq!(mc.len(), 2, "update must NOT merge");
        // ... but an explicit merge afterwards collapses them.
        mc.merge_overlapping();
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 15)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn update_from_edit_result_with_1000_selections() {
        let mut mc = state(0);
        let big: Vec<Selection> = (0..1000u32).map(|i| Selection::Cursor(c(i * 4))).collect();
        mc.update_from_edit_result(&big);
        assert_eq!(mc.len(), 1000);
        assert_ids_unique(&mc);
        assert_primary_resolves(&mc);
        assert_eq!(mc.to_selections(), big);
    }
    // =====================================================================
    // set_single_cursor / set_single_range
    // =====================================================================
    #[test]
    fn set_single_cursor_collapses_all_selections() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let _ = mc.add_selection(rng(20, 30));
        mc.set_single_cursor(c(7));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(7)));
        assert_primary_resolves(&mc);
        assert_eq!(mc.get_primary_cursor(), Some(c(7)));
    }
    #[test]
    fn set_single_range_collapses_all_selections() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        mc.set_single_range(rng(u32::MAX - 2, u32::MAX));
        assert_eq!(mc.len(), 1);
        assert_eq!(
            mc.selections[0].selection,
            Selection::Range(rng(u32::MAX - 2, u32::MAX))
        );
        assert_primary_resolves(&mc);
    }
    #[test]
    fn set_single_cursor_on_empty_state_mints_a_fresh_id() {
        let mut mc = empty_state();
        let stale = mc.primary_id;
        mc.set_single_cursor(c(1));
        assert_eq!(mc.len(), 1);
        assert_ne!(mc.primary_id, stale, "no last element -> a new id is minted");
        assert_primary_resolves(&mc);
    }
    #[test]
    fn set_single_range_on_empty_state_mints_a_fresh_id() {
        let mut mc = empty_state();
        mc.set_single_range(rng(0, 0));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 0)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn set_single_cursor_is_idempotent() {
        let mut mc = state(0);
        mc.set_single_cursor(c(5));
        let first = mc.clone();
        mc.set_single_cursor(c(5));
        assert_eq!(mc, first, "re-setting the same cursor must reuse the id");
    }
    // =====================================================================
    // merge_overlapping
    // =====================================================================
    #[test]
    fn merge_overlapping_on_empty_and_single_is_a_noop() {
        let mut e = empty_state();
        e.merge_overlapping();
        assert!(e.is_empty());
        let mut one = state(3);
        let before = one.clone();
        one.merge_overlapping();
        assert_eq!(one, before);
    }
    #[test]
    fn merge_overlapping_collapses_a_whole_chain() {
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Range(rng(25, 40))),
            ident(ids[1], Selection::Range(rng(0, 10))),
            ident(ids[2], Selection::Range(rng(12, 30))),
            ident(ids[3], Selection::Range(rng(5, 15))),
        ];
        mc.primary_id = ids[3];
        mc.merge_overlapping();
        // 0..10 ∪ 5..15 ∪ 12..30 ∪ 25..40 = one contiguous 0..40
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(0, 40)));
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn merge_overlapping_keeps_disjoint_selections_and_sorts_them() {
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..3).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Cursor(c(100))),
            ident(ids[1], Selection::Range(rng(0, 5))),
            ident(ids[2], Selection::Cursor(c(50))),
        ];
        mc.primary_id = ids[0];
        mc.merge_overlapping();
        assert_eq!(mc.len(), 3);
        assert_eq!(mc.to_selections(), vec![
            Selection::Range(rng(0, 5)),
            Selection::Cursor(c(50)),
            Selection::Cursor(c(100)),
        ]);
        assert_sorted_nonoverlapping(&mc);
        // The primary (byte 100) survived the sort untouched.
        assert_eq!(mc.primary_id, ids[0]);
        assert_eq!(mc.get_primary_cursor(), Some(c(100)));
    }
    #[test]
    fn merge_overlapping_zero_width_merge_yields_a_cursor_not_a_range() {
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..2).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Cursor(c(8))),
            ident(ids[1], Selection::Range(rng(8, 8))),
        ];
        mc.primary_id = ids[1];
        mc.merge_overlapping();
        assert_eq!(mc.len(), 1);
        // new_start == new_end -> collapses back to a Cursor.
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(8)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn merge_overlapping_is_idempotent() {
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..5).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Range(rng(0, 10))),
            ident(ids[1], Selection::Cursor(c(5))),
            ident(ids[2], Selection::Range(rng(30, 20))), // reversed
            ident(ids[3], Selection::Cursor(c(100))),
            ident(ids[4], Selection::Range(rng(99, 101))),
        ];
        mc.primary_id = ids[2];
        mc.merge_overlapping();
        let once = mc.clone();
        mc.merge_overlapping();
        assert_eq!(mc, once, "merge_overlapping must be a fixed point");
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn merge_overlapping_adversarial_200_selections_keeps_invariants() {
        let mut mc = empty_state();
        let mut seed: u32 = 0x1234_5678;
        let mut next = || {
            seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
            seed
        };
        let mut sels = Vec::new();
        for i in 0..200u32 {
            let a = next() % 1000;
            let b = next() % 1000;
            let sel = match i % 4 {
                0 => Selection::Cursor(c(a)),
                1 => Selection::Range(SelectionRange {
                    start: c(a),
                    end: c(b),
                }), // may be reversed
                2 => Selection::Range(rng(a.min(b), a.max(b))),
                _ => Selection::Cursor(c_full(
                    0,
                    a,
                    if b % 2 == 0 {
                        CursorAffinity::Leading
                    } else {
                        CursorAffinity::Trailing
                    },
                )),
            };
            sels.push(ident(SelectionId::new(), sel));
        }
        // Throw in the absolute boundaries too.
        sels.push(ident(SelectionId::new(), Selection::Cursor(c(0))));
        sels.push(ident(SelectionId::new(), Selection::Cursor(c(u32::MAX))));
        sels.push(ident(
            SelectionId::new(),
            Selection::Range(rng(u32::MAX - 1, u32::MAX)),
        ));
        mc.primary_id = sels[7].id;
        mc.selections = sels;
        mc.merge_overlapping();
        assert!(!mc.is_empty());
        assert!(mc.len() <= 203);
        assert_sorted_nonoverlapping(&mc);
        assert_primary_resolves(&mc);
        assert_ids_unique(&mc);
    }
    #[test]
    fn merge_overlapping_primary_inside_a_chain_still_resolves() {
        // Three cursors that all collapse into one, plus a far-away cursor.
        // The primary is the *first* link of the merge chain.
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Cursor(c(0))),
            ident(ids[1], Selection::Cursor(c(0))),
            ident(ids[2], Selection::Cursor(c(0))),
            ident(ids[3], Selection::Cursor(c(100))),
        ];
        mc.primary_id = ids[0];
        mc.merge_overlapping();
        assert_eq!(mc.len(), 2);
        // Whatever the merge does with ids, `primary_id` must never dangle.
        assert!(
            mc.selections.iter().any(|s| s.id == mc.primary_id),
            "primary_id must name a surviving selection"
        );
        assert!(mc.get_primary().is_some());
    }
    #[test]
    fn merge_overlapping_primary_should_follow_its_merge_chain() {
        // Same setup as above. `merge_overlapping` records `new_primary = sel.id`
        // when the chain's head is the primary, but the head's id is then
        // overwritten by the *next* merge, so `new_primary` points at an id that
        // no longer exists. ensure_primary_valid() then silently adopts the
        // vector's LAST element — the unrelated cursor at byte 100.
        //
        // Expected: the primary follows the merged selection it was part of (byte 0).
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..4).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Cursor(c(0))),
            ident(ids[1], Selection::Cursor(c(0))),
            ident(ids[2], Selection::Cursor(c(0))),
            ident(ids[3], Selection::Cursor(c(100))),
        ];
        mc.primary_id = ids[0];
        mc.merge_overlapping();
        assert_eq!(
            mc.get_primary_cursor(),
            Some(c(0)),
            "primary jumped to an unrelated selection after the merge"
        );
    }
    // =====================================================================
    // move_all_cursors
    // =====================================================================
    #[test]
    fn move_all_cursors_identity_leaves_positions_unchanged() {
        let mut mc = state(0);
        let _ = mc.add_cursor(c(10));
        let _ = mc.add_cursor(c(20));
        let before = mc.to_selections();
        mc.move_all_cursors(false, |cur| *cur);
        assert_eq!(mc.to_selections(), before);
        assert_eq!(mc.len(), 3);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn move_all_cursors_extend_with_no_movement_keeps_a_cursor() {
        // `*c != new_cursor` is false -> the selection must stay a Cursor,
        // not degenerate into a zero-width Range.
        let mut mc = state(4);
        mc.move_all_cursors(true, |cur| *cur);
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(4)));
    }
    #[test]
    fn move_all_cursors_extend_turns_a_cursor_into_a_range() {
        let mut mc = state(10);
        mc.move_all_cursors(true, |cur| c(cur.cluster_id.start_byte_in_run + 5));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Range(rng(10, 15)));
        // The anchor stayed at 10, only the focus moved.
        assert_eq!(mc.get_primary_cursor(), Some(c(15)));
    }
    #[test]
    fn move_all_cursors_bare_forward_arrow_collapses_range_to_max_boundary() {
        let mut mc = empty_state();
        mc.set_single_range(rng(3, 9));
        mc.move_all_cursors(false, |cur| c(cur.cluster_id.start_byte_in_run + 1));
        assert_eq!(mc.len(), 1);
        // Collapses to the boundary WITHOUT stepping past it (not byte 10).
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(9)));
    }
    #[test]
    fn move_all_cursors_bare_backward_arrow_collapses_range_to_min_boundary() {
        let mut mc = empty_state();
        mc.set_single_range(rng(3, 9));
        mc.move_all_cursors(false, |cur| {
            c(cur.cluster_id.start_byte_in_run.saturating_sub(1))
        });
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(3)));
    }
    #[test]
    fn move_all_cursors_collapses_a_backwards_range_by_direction_not_field_order() {
        // Backwards range (focus at 3, anchor at 9): a forward arrow must still
        // collapse to the max boundary (9), not to the `end` field (3).
        let mut mc = empty_state();
        mc.set_single_range(SelectionRange {
            start: c(9),
            end: c(3),
        });
        mc.move_all_cursors(false, |cur| c(cur.cluster_id.start_byte_in_run + 1));
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(9)));
        let mut back = empty_state();
        back.set_single_range(SelectionRange {
            start: c(9),
            end: c(3),
        });
        back.move_all_cursors(false, |cur| {
            c(cur.cluster_id.start_byte_in_run.saturating_sub(1))
        });
        assert_eq!(back.selections[0].selection, Selection::Cursor(c(3)));
    }
    #[test]
    fn move_all_cursors_without_boundary_collapse_performs_the_step_from_the_focus() {
        // Home over an active range: the caret goes where the step points, not
        // to the range's near edge. `move_fn` here is "go to byte 0".
        let mut mc = empty_state();
        mc.set_single_range(rng(3, 9));
        mc.move_all_cursors_with(false, false, |_| c(0));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(0)));
        // The same step with the arrow-key rule still answers the boundary.
        let mut arrow = empty_state();
        arrow.set_single_range(rng(3, 9));
        arrow.move_all_cursors_with(false, true, |_| c(0));
        assert_eq!(arrow.selections[0].selection, Selection::Cursor(c(3)));
        // End over the same range: byte 20 is past `hi`, which the boundary
        // rule would have clamped to 9.
        let mut end = empty_state();
        end.set_single_range(rng(3, 9));
        end.move_all_cursors_with(false, false, |_| c(20));
        assert_eq!(end.selections[0].selection, Selection::Cursor(c(20)));
        // A bare cursor is unaffected by the flag either way.
        for collapse in [false, true] {
            let mut bare = state(5);
            bare.move_all_cursors_with(false, collapse, |_| c(0));
            assert_eq!(bare.selections[0].selection, Selection::Cursor(c(0)));
        }
    }
    #[test]
    fn move_all_cursors_extend_back_onto_the_anchor_collapses_to_a_cursor() {
        let mut mc = empty_state();
        mc.set_single_range(rng(3, 4));
        // Shrink the focus back onto the anchor: r.start == new_end.
        mc.move_all_cursors(true, |_| c(3));
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(3)));
    }
    #[test]
    fn move_all_cursors_constant_move_fn_merges_everything_into_one() {
        let mut mc = state(0);
        for i in 1..5u32 {
            let _ = mc.add_cursor(c(i * 10));
        }
        assert_eq!(mc.len(), 5);
        mc.move_all_cursors(false, |_| c(7));
        assert_eq!(mc.len(), 1, "colliding cursors must be merged afterwards");
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(7)));
        assert_primary_resolves(&mc);
        assert_sorted_nonoverlapping(&mc);
    }
    #[test]
    fn move_all_cursors_saturating_at_u32_max_does_not_overflow() {
        let mut mc = empty_state();
        let ids: Vec<SelectionId> = (0..2).map(|_| SelectionId::new()).collect();
        mc.selections = vec![
            ident(ids[0], Selection::Cursor(c(u32::MAX - 1))),
            ident(ids[1], Selection::Cursor(c(u32::MAX))),
        ];
        mc.primary_id = ids[1];
        mc.move_all_cursors(false, |cur| {
            c(cur.cluster_id.start_byte_in_run.saturating_add(1))
        });
        // Both saturate to u32::MAX and merge.
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.selections[0].selection, Selection::Cursor(c(u32::MAX)));
        assert_primary_resolves(&mc);
    }
    #[test]
    fn move_all_cursors_on_empty_state_does_not_panic() {
        let mut mc = empty_state();
        mc.move_all_cursors(false, |cur| *cur);
        mc.move_all_cursors(true, |_| c(u32::MAX));
        assert!(mc.is_empty());
    }
    #[test]
    fn move_all_cursors_stress_keeps_invariants() {
        let mut mc = state(0);
        for i in 1..100u32 {
            let _ = mc.add_cursor(c(i * 5));
        }
        for _ in 0..10 {
            mc.move_all_cursors(false, |cur| {
                // Fold every cursor into a small window -> heavy merging.
                c(cur.cluster_id.start_byte_in_run % 7)
            });
            assert_sorted_nonoverlapping(&mc);
            assert_primary_resolves(&mc);
            assert_ids_unique(&mc);
        }
        assert!(mc.len() <= 7);
    }
    // =====================================================================
    // remap_node_ids
    // =====================================================================
    #[test]
    fn remap_node_ids_for_a_different_dom_is_a_noop() {
        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
        mc.node_id.dom = DomId { inner: 7 };
        let before = mc.clone();
        let mut map = BTreeMap::new();
        map.insert(NodeId::new(5), NodeId::new(9));
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert_eq!(mc, before, "a foreign DomId must not touch this state");
    }
    #[test]
    fn remap_node_ids_rewrites_a_surviving_node() {
        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
        let mut map = BTreeMap::new();
        map.insert(NodeId::new(5), NodeId::new(9));
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert_eq!(
            mc.node_id.node.into_crate_internal(),
            Some(NodeId::new(9))
        );
        assert_eq!(mc.len(), 1, "selections survive a successful remap");
        assert_primary_resolves(&mc);
    }
    #[test]
    fn remap_node_ids_clears_selections_when_the_node_was_removed() {
        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
        let _ = mc.add_cursor(c(20));
        let map: BTreeMap<NodeId, NodeId> = BTreeMap::new(); // node 5 is gone
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert!(mc.is_empty(), "a removed node must drop its selections");
        assert!(mc.get_primary().is_none());
        // node_id itself is left alone (only selections are cleared).
        assert_eq!(
            mc.node_id.node.into_crate_internal(),
            Some(NodeId::new(5))
        );
    }
    #[test]
    fn remap_node_ids_with_a_none_node_is_a_noop() {
        // DomNodeId::ROOT carries NodeHierarchyItemId::NONE -> into_crate_internal()
        // is None, so neither branch runs and the selections must survive.
        let mut mc = state(3);
        let map: BTreeMap<NodeId, NodeId> = BTreeMap::new();
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert_eq!(mc.len(), 1);
        assert_eq!(mc.node_id.node, NodeHierarchyItemId::NONE);
        assert_primary_resolves(&mc);
    }
    #[test]
    fn remap_node_ids_handles_large_node_indices() {
        let big = 1_000_000usize;
        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(big), 0);
        let mut map = BTreeMap::new();
        map.insert(NodeId::new(big), NodeId::new(big * 2));
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert_eq!(
            mc.node_id.node.into_crate_internal(),
            Some(NodeId::new(big * 2))
        );
    }
    #[test]
    fn remap_node_ids_twice_is_stable() {
        let mut mc = MultiCursorState::new_with_cursor(c(1), dom_node(5), 0);
        let mut map = BTreeMap::new();
        map.insert(NodeId::new(5), NodeId::new(9));
        map.insert(NodeId::new(9), NodeId::new(9)); // identity for the new id
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        mc.remap_node_ids(DomId::ROOT_ID, &map);
        assert_eq!(
            mc.node_id.node.into_crate_internal(),
            Some(NodeId::new(9))
        );
        assert_eq!(mc.len(), 1);
    }
    // =====================================================================
    // selection_start_pos / selection_end_pos  (private helpers)
    // =====================================================================
    #[test]
    fn selection_pos_helpers_normalize_reversed_ranges() {
        let forward = Selection::Range(rng(3, 9));
        assert_eq!(selection_start_pos(&forward), c(3));
        assert_eq!(selection_end_pos(&forward), c(9));
        let backward = Selection::Range(SelectionRange {
            start: c(9),
            end: c(3),
        });
        assert_eq!(selection_start_pos(&backward), c(3));
        assert_eq!(selection_end_pos(&backward), c(9));
        let cursor = Selection::Cursor(c(5));
        assert_eq!(selection_start_pos(&cursor), c(5));
        assert_eq!(selection_end_pos(&cursor), c(5));
    }
    #[test]
    fn selection_pos_helpers_start_never_exceeds_end() {
        let mut seed: u32 = 0xACE1_BEEF;
        let mut next = || {
            seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
            seed
        };
        let extremes = [0u32, 1, u32::MAX - 1, u32::MAX];
        let mut cases: Vec<Selection> = Vec::new();
        for a in extremes {
            for b in extremes {
                cases.push(Selection::Range(SelectionRange {
                    start: c_full(a, b, CursorAffinity::Trailing),
                    end: c_full(b, a, CursorAffinity::Leading),
                }));
                cases.push(Selection::Cursor(c_full(a, b, CursorAffinity::Leading)));
            }
        }
        for _ in 0..200 {
            cases.push(Selection::Range(SelectionRange {
                start: c(next()),
                end: c(next()),
            }));
        }
        for sel in &cases {
            assert!(
                selection_start_pos(sel) <= selection_end_pos(sel),
                "start must never sort after end: {sel:?}"
            );
        }
    }
    #[test]
    fn selection_pos_helpers_respect_affinity_ordering() {
        // Same byte, different affinity: Leading < Trailing.
        let sel = Selection::Range(SelectionRange {
            start: c_full(0, 4, CursorAffinity::Trailing),
            end: c_full(0, 4, CursorAffinity::Leading),
        });
        assert_eq!(
            selection_start_pos(&sel),
            c_full(0, 4, CursorAffinity::Leading)
        );
        assert_eq!(
            selection_end_pos(&sel),
            c_full(0, 4, CursorAffinity::Trailing)
        );
    }
    // =====================================================================
    // TextSelection
    // =====================================================================
    fn rect(x: f32, y: f32, w: f32, h: f32) -> LogicalRect {
        LogicalRect::new(LogicalPosition::new(x, y), LogicalSize::new(w, h))
    }
    #[test]
    fn new_collapsed_invariants_hold() {
        let node = NodeId::new(3);
        let sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c(7),
            rect(1.0, 2.0, 3.0, 4.0),
            LogicalPosition::new(5.0, 6.0),
        );
        assert!(sel.is_collapsed());
        assert!(sel.is_forward);
        assert_eq!(sel.dom_id, DomId::ROOT_ID);
        assert_eq!(sel.anchor.ifc_root_node_id, node);
        assert_eq!(sel.focus.ifc_root_node_id, node);
        assert_eq!(sel.anchor.cursor, c(7));
        assert_eq!(sel.focus.cursor, c(7));
        assert_eq!(sel.affected_nodes.len(), 1);
        // The collapsed node maps to a zero-width range at the cursor.
        assert_eq!(
            sel.get_range_for_node(&node),
            Some(&SelectionRange {
                start: c(7),
                end: c(7),
            })
        );
    }
    #[test]
    fn new_collapsed_with_non_finite_geometry_does_not_panic() {
        let node = NodeId::new(0);
        let sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c_full(u32::MAX, u32::MAX, CursorAffinity::Trailing),
            rect(f32::NAN, f32::INFINITY, f32::NEG_INFINITY, f32::MAX),
            LogicalPosition::new(f32::NAN, f32::NEG_INFINITY),
        );
        // Geometry is carried verbatim; only the cursors decide collapsedness.
        assert!(sel.is_collapsed());
        assert!(sel.get_range_for_node(&node).is_some());
        assert!(sel.anchor.char_bounds.origin.x.is_nan());
    }
    #[test]
    fn get_range_for_node_returns_none_for_an_unaffected_node() {
        let sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            NodeId::new(3),
            c(0),
            rect(0.0, 0.0, 0.0, 0.0),
            LogicalPosition::new(0.0, 0.0),
        );
        assert!(sel.get_range_for_node(&NodeId::new(4)).is_none());
        assert!(sel.get_range_for_node(&NodeId::new(0)).is_none());
        assert!(sel.get_range_for_node(&NodeId::new(usize::MAX)).is_none());
    }
    #[test]
    fn get_range_for_node_on_an_empty_map_returns_none() {
        let node = NodeId::new(3);
        let mut sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c(0),
            rect(0.0, 0.0, 0.0, 0.0),
            LogicalPosition::new(0.0, 0.0),
        );
        sel.affected_nodes.clear();
        assert!(sel.get_range_for_node(&node).is_none());
        assert!(sel.is_collapsed(), "collapsedness does not depend on the map");
    }
    #[test]
    fn ranges_for_node_returns_every_range_the_node_carries() {
        // A Ctrl+D session puts all of its occurrences on ONE node, so the
        // carrier has to be a list — the map used to hold a single range and
        // every occurrence but one was unexpressible.
        let node = NodeId::new(3);
        let mut sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c(0),
            rect(0.0, 0.0, 0.0, 0.0),
            LogicalPosition::new(0.0, 0.0),
        );
        let first = SelectionRange { start: c(0), end: c(2) };
        let second = SelectionRange { start: c(5), end: c(7) };
        sel.affected_nodes.insert(node, vec![first, second]);
        assert_eq!(sel.ranges_for_node(&node), &[first, second]);
        assert_eq!(
            sel.get_range_for_node(&node),
            Some(&first),
            "the single-range accessor answers with the FIRST range"
        );
        assert!(sel.ranges_for_node(&NodeId::new(4)).is_empty());
        sel.affected_nodes.insert(node, Vec::new());
        assert!(sel.ranges_for_node(&node).is_empty());
        assert!(
            sel.get_range_for_node(&node).is_none(),
            "an empty list is not a range"
        );
    }
    #[test]
    fn is_collapsed_is_false_when_the_focus_cursor_moves() {
        let node = NodeId::new(3);
        let mut sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c(7),
            rect(0.0, 0.0, 1.0, 1.0),
            LogicalPosition::new(0.0, 0.0),
        );
        assert!(sel.is_collapsed());
        sel.focus.cursor = c(8);
        assert!(!sel.is_collapsed());
    }
    #[test]
    fn is_collapsed_is_false_when_the_focus_crosses_into_another_ifc() {
        let mut sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            NodeId::new(3),
            c(7),
            rect(0.0, 0.0, 1.0, 1.0),
            LogicalPosition::new(0.0, 0.0),
        );
        sel.focus.ifc_root_node_id = NodeId::new(4); // same cursor, different node
        assert!(
            !sel.is_collapsed(),
            "same cursor offset in a different IFC is not a collapsed selection"
        );
    }
    #[test]
    fn is_collapsed_only_looks_at_cursors_not_at_mouse_position() {
        let node = NodeId::new(1);
        let mut sel = TextSelection::new_collapsed(
            DomId::ROOT_ID,
            node,
            c(2),
            rect(0.0, 0.0, 1.0, 1.0),
            LogicalPosition::new(0.0, 0.0),
        );
        sel.focus.mouse_position = LogicalPosition::new(999.0, -999.0);
        assert!(sel.is_collapsed());
    }
}