use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use core::sync::atomic::{AtomicU64, Ordering};
use crate::dom::{DomId, DomNodeId, NodeId};
use crate::geom::{LogicalPosition, LogicalRect};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ContentIndex {
pub run_index: u32,
pub item_index: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct GraphemeClusterId {
pub source_run: u32,
pub start_byte_in_run: u32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[repr(C)]
pub enum CursorAffinity {
Leading,
Trailing,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
#[repr(C)]
pub struct TextCursor {
pub cluster_id: GraphemeClusterId,
pub affinity: CursorAffinity,
}
impl_option!(
TextCursor,
OptionTextCursor,
[Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
);
#[derive(Debug, PartialOrd, Ord, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct SelectionRange {
pub start: TextCursor,
pub end: TextCursor,
}
impl_option!(
SelectionRange,
OptionSelectionRange,
[Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd]
);
impl_vec!(SelectionRange, SelectionRangeVec, SelectionRangeVecDestructor, SelectionRangeVecDestructorType, SelectionRangeVecSlice, OptionSelectionRange);
impl_vec_debug!(SelectionRange, SelectionRangeVec);
impl_vec_clone!(
SelectionRange,
SelectionRangeVec,
SelectionRangeVecDestructor
);
impl_vec_partialeq!(SelectionRange, SelectionRangeVec);
impl_vec_partialord!(SelectionRange, SelectionRangeVec);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C, u8)]
pub enum Selection {
Cursor(TextCursor),
Range(SelectionRange),
}
impl_option!(
Selection,
OptionSelection,
[Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
);
impl_vec!(Selection, SelectionVec, SelectionVecDestructor, SelectionVecDestructorType, SelectionVecSlice, OptionSelection);
impl_vec_debug!(Selection, SelectionVec);
impl_vec_clone!(Selection, SelectionVec, SelectionVecDestructor);
impl_vec_partialeq!(Selection, SelectionVec);
impl_vec_partialord!(Selection, SelectionVec);
#[derive(Debug, Clone, PartialEq)]
#[repr(C)]
pub struct SelectionState {
pub selections: SelectionVec,
pub node_id: DomNodeId,
}
impl SelectionState {
pub fn add(&mut self, new_selection: Selection) {
let mut selections: Vec<Selection> = self.selections.as_ref().to_vec();
selections.push(new_selection);
selections.sort_unstable();
selections.dedup(); self.selections = selections.into();
}
}
impl_option!(
SelectionState,
OptionSelectionState,
copy = false,
clone = false,
[Debug, Clone, PartialEq]
);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[repr(C)]
pub struct SelectionId {
pub inner: u64,
}
impl SelectionId {
pub fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self { inner: COUNTER.fetch_add(1, Ordering::Relaxed) }
}
}
impl Default for SelectionId {
fn default() -> Self {
Self::new()
}
}
impl_option!(
SelectionId,
OptionSelectionId,
[Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord]
);
impl_vec!(SelectionId, SelectionIdVec, SelectionIdVecDestructor, SelectionIdVecDestructorType, SelectionIdVecSlice, OptionSelectionId);
impl_vec_debug!(SelectionId, SelectionIdVec);
impl_vec_clone!(SelectionId, SelectionIdVec, SelectionIdVecDestructor);
impl_vec_partialeq!(SelectionId, SelectionIdVec);
impl_vec_partialord!(SelectionId, SelectionIdVec);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct IdentifiedSelection {
pub id: SelectionId,
pub selection: Selection,
}
impl_option!(
IdentifiedSelection,
OptionIdentifiedSelection,
[Debug, Clone, Copy, PartialEq, Eq, Hash]
);
impl_vec!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor, IdentifiedSelectionVecDestructorType, IdentifiedSelectionVecSlice, OptionIdentifiedSelection);
impl_vec_debug!(IdentifiedSelection, IdentifiedSelectionVec);
impl_vec_clone!(IdentifiedSelection, IdentifiedSelectionVec, IdentifiedSelectionVecDestructor);
impl_vec_partialeq!(IdentifiedSelection, IdentifiedSelectionVec);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiCursorState {
pub selections: Vec<IdentifiedSelection>,
pub primary_id: SelectionId,
pub node_id: DomNodeId,
pub contenteditable_key: u64,
}
impl MultiCursorState {
#[must_use] pub fn new_with_cursor(cursor: TextCursor, node_id: DomNodeId, contenteditable_key: u64) -> Self {
let id = SelectionId::new();
Self {
selections: vec![IdentifiedSelection {
id,
selection: Selection::Cursor(cursor),
}],
primary_id: id,
node_id,
contenteditable_key,
}
}
#[must_use]
pub fn add_cursor(&mut self, cursor: TextCursor) -> SelectionId {
let id = SelectionId::new();
self.selections.push(IdentifiedSelection {
id,
selection: Selection::Cursor(cursor),
});
self.primary_id = id;
self.merge_overlapping();
id
}
#[must_use]
pub fn add_selection(&mut self, range: SelectionRange) -> SelectionId {
let id = SelectionId::new();
self.selections.push(IdentifiedSelection {
id,
selection: Selection::Range(range),
});
self.primary_id = id;
self.merge_overlapping();
id
}
#[must_use]
pub fn remove_selection(&mut self, id: SelectionId) -> bool {
let len_before = self.selections.len();
self.selections.retain(|s| s.id != id);
let removed = self.selections.len() < len_before;
if removed {
self.ensure_primary_valid();
}
removed
}
#[must_use] pub fn get_primary(&self) -> Option<&IdentifiedSelection> {
let pid = self.primary_id;
self.selections
.iter()
.find(|s| s.id == pid)
.or_else(|| self.selections.last())
}
pub fn get_primary_mut(&mut self) -> Option<&mut IdentifiedSelection> {
let pid = self.primary_id;
if let Some(pos) = self.selections.iter().position(|s| s.id == pid) {
return self.selections.get_mut(pos);
}
self.selections.last_mut()
}
fn ensure_primary_valid(&mut self) {
let pid = self.primary_id;
if !self.selections.iter().any(|s| s.id == pid) {
if let Some(last) = self.selections.last() {
self.primary_id = last.id;
}
}
}
#[must_use] pub fn get_primary_cursor(&self) -> Option<TextCursor> {
self.get_primary().map(|s| match &s.selection {
Selection::Cursor(c) => *c,
Selection::Range(r) => r.end,
})
}
#[must_use] pub fn to_selections(&self) -> Vec<Selection> {
self.selections.iter().map(|s| s.selection).collect()
}
pub fn update_from_edit_result(&mut self, new_selections: &[Selection]) {
let old_ids: Vec<SelectionId> = self.selections.iter().map(|s| s.id).collect();
self.selections.clear();
for (i, sel) in new_selections.iter().enumerate() {
let id = old_ids.get(i).copied().unwrap_or_else(SelectionId::new);
self.selections.push(IdentifiedSelection {
id,
selection: *sel,
});
}
self.ensure_primary_valid();
}
pub fn set_single_cursor(&mut self, cursor: TextCursor) {
let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
self.selections.clear();
self.selections.push(IdentifiedSelection {
id,
selection: Selection::Cursor(cursor),
});
self.primary_id = id;
}
pub fn set_single_range(&mut self, range: SelectionRange) {
let id = self.selections.last().map_or_else(SelectionId::new, |primary| primary.id);
self.selections.clear();
self.selections.push(IdentifiedSelection {
id,
selection: Selection::Range(range),
});
self.primary_id = id;
}
#[must_use] pub const fn len(&self) -> usize {
self.selections.len()
}
#[must_use] pub const fn is_empty(&self) -> bool {
self.selections.is_empty()
}
pub fn merge_overlapping(&mut self) {
if self.selections.len() <= 1 {
return;
}
let primary = self.primary_id;
let mut new_primary = primary;
self.selections.sort_by(|a, b| {
let pos_a = selection_start_pos(&a.selection);
let pos_b = selection_start_pos(&b.selection);
pos_a.cmp(&pos_b)
});
let mut merged: Vec<IdentifiedSelection> = Vec::with_capacity(self.selections.len());
for sel in self.selections.drain(..) {
if let Some(last) = merged.last_mut() {
let last_end = selection_end_pos(&last.selection);
let cur_start = selection_start_pos(&sel.selection);
if cur_start <= last_end {
let new_start = selection_start_pos(&last.selection);
let cur_end = selection_end_pos(&sel.selection);
let new_end = if cur_end > last_end { cur_end } else { last_end };
if new_start == new_end {
last.selection = Selection::Cursor(new_start);
} else {
last.selection = Selection::Range(SelectionRange {
start: new_start,
end: new_end,
});
}
if last.id == primary || sel.id == primary {
new_primary = sel.id;
}
last.id = sel.id;
continue;
}
}
merged.push(sel);
}
self.selections = merged;
self.primary_id = new_primary;
self.ensure_primary_valid();
}
pub fn move_all_cursors(
&mut self,
extend_selection: bool,
move_fn: impl Fn(&TextCursor) -> TextCursor,
) {
for sel in &mut self.selections {
match &sel.selection {
Selection::Cursor(c) => {
let new_cursor = move_fn(c);
if extend_selection {
if *c != new_cursor {
sel.selection = Selection::Range(SelectionRange {
start: *c,
end: new_cursor,
});
}
} else {
sel.selection = Selection::Cursor(new_cursor);
}
}
Selection::Range(r) => {
if extend_selection {
let new_end = move_fn(&r.end);
if r.start == new_end {
sel.selection = Selection::Cursor(r.start);
} else {
sel.selection = Selection::Range(SelectionRange {
start: r.start,
end: new_end,
});
}
} else {
let (lo, hi) = if r.start <= r.end {
(r.start, r.end)
} else {
(r.end, r.start)
};
let probe = move_fn(&r.end);
let collapsed = if probe >= r.end { hi } else { lo };
sel.selection = Selection::Cursor(collapsed);
}
}
}
}
self.merge_overlapping();
}
pub fn remap_node_ids(
&mut self,
dom_id: DomId,
node_id_map: &BTreeMap<NodeId, NodeId>,
) {
if self.node_id.dom != dom_id {
return;
}
if let Some(old_node_id) = self.node_id.node.into_crate_internal() {
if let Some(&new_node_id) = node_id_map.get(&old_node_id) {
self.node_id.node = crate::styled_dom::NodeHierarchyItemId::from_crate_internal(Some(new_node_id));
} else {
self.selections.clear();
}
}
}
}
fn selection_start_pos(sel: &Selection) -> TextCursor {
match sel {
Selection::Cursor(c) => *c,
Selection::Range(r) => {
if r.start <= r.end { r.start } else { r.end }
}
}
}
fn selection_end_pos(sel: &Selection) -> TextCursor {
match sel {
Selection::Cursor(c) => *c,
Selection::Range(r) => {
if r.end >= r.start { r.end } else { r.start }
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectionAnchor {
pub ifc_root_node_id: NodeId,
pub cursor: TextCursor,
pub char_bounds: LogicalRect,
pub mouse_position: LogicalPosition,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SelectionFocus {
pub ifc_root_node_id: NodeId,
pub cursor: TextCursor,
pub mouse_position: LogicalPosition,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TextSelection {
pub dom_id: DomId,
pub anchor: SelectionAnchor,
pub focus: SelectionFocus,
pub affected_nodes: BTreeMap<NodeId, SelectionRange>,
pub is_forward: bool,
}
impl TextSelection {
#[must_use] pub fn new_collapsed(
dom_id: DomId,
ifc_root_node_id: NodeId,
cursor: TextCursor,
char_bounds: LogicalRect,
mouse_position: LogicalPosition,
) -> Self {
let anchor = SelectionAnchor {
ifc_root_node_id,
cursor,
char_bounds,
mouse_position,
};
let focus = SelectionFocus {
ifc_root_node_id,
cursor,
mouse_position,
};
let mut affected_nodes = BTreeMap::new();
affected_nodes.insert(ifc_root_node_id, SelectionRange {
start: cursor,
end: cursor,
});
Self {
dom_id,
anchor,
focus,
affected_nodes,
is_forward: true, }
}
#[must_use] pub fn is_collapsed(&self) -> bool {
self.anchor.ifc_root_node_id == self.focus.ifc_root_node_id
&& self.anchor.cursor == self.focus.cursor
}
#[must_use] pub fn get_range_for_node(&self, ifc_root_node_id: &NodeId) -> Option<&SelectionRange> {
self.affected_nodes.get(ifc_root_node_id)
}
}
impl_option!(
TextSelection,
OptionTextSelection,
copy = false,
clone = false,
[Debug, Clone, PartialEq, Eq]
);
#[cfg(test)]
mod audit_tests {
use super::*;
fn cursor(byte: u32) -> TextCursor {
TextCursor {
cluster_id: GraphemeClusterId { source_run: 0, start_byte_in_run: byte },
affinity: CursorAffinity::Leading,
}
}
fn state(byte: u32) -> MultiCursorState {
MultiCursorState::new_with_cursor(cursor(byte), DomNodeId::ROOT, 0)
}
#[test]
fn primary_tracked_by_id_not_vec_position() {
let mut mc = state(100);
let b = mc.add_cursor(cursor(0));
assert_eq!(mc.len(), 2);
assert_eq!(mc.get_primary().unwrap().id, b);
assert_eq!(mc.get_primary_cursor().unwrap(), cursor(0));
}
#[test]
fn merge_preserves_primary() {
let mut mc = state(5);
let _b = mc.add_cursor(cursor(5)); assert_eq!(mc.len(), 1);
let primary = mc.get_primary().unwrap();
assert_eq!(primary.id, mc.selections[0].id);
}
#[test]
fn removing_primary_repoints_it() {
let mut mc = state(0);
let b = mc.add_cursor(cursor(10)); assert_eq!(mc.get_primary().unwrap().id, b);
assert!(mc.remove_selection(b));
let p = mc.get_primary().unwrap();
assert!(mc.selections.iter().any(|s| s.id == p.id));
}
}
#[cfg(test)]
mod autotest_generated {
use super::*;
use crate::geom::LogicalSize;
use crate::styled_dom::NodeHierarchyItemId;
fn c(byte: u32) -> TextCursor {
TextCursor {
cluster_id: GraphemeClusterId {
source_run: 0,
start_byte_in_run: byte,
},
affinity: CursorAffinity::Leading,
}
}
fn c_full(run: u32, byte: u32, affinity: CursorAffinity) -> TextCursor {
TextCursor {
cluster_id: GraphemeClusterId {
source_run: run,
start_byte_in_run: byte,
},
affinity,
}
}
fn rng(a: u32, b: u32) -> SelectionRange {
SelectionRange {
start: c(a),
end: c(b),
}
}
fn dom_node(index: usize) -> DomNodeId {
DomNodeId {
dom: DomId::ROOT_ID,
node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(index))),
}
}
fn state(byte: u32) -> MultiCursorState {
MultiCursorState::new_with_cursor(c(byte), DomNodeId::ROOT, 0)
}
fn empty_state() -> MultiCursorState {
MultiCursorState {
selections: Vec::new(),
primary_id: SelectionId::new(),
node_id: DomNodeId::ROOT,
contenteditable_key: 0,
}
}
fn ident(id: SelectionId, sel: Selection) -> IdentifiedSelection {
IdentifiedSelection { id, selection: sel }
}
fn assert_sorted_nonoverlapping(mc: &MultiCursorState) {
for w in mc.selections.windows(2) {
let prev_end = selection_end_pos(&w[0].selection);
let next_start = selection_start_pos(&w[1].selection);
assert!(
next_start > prev_end,
"selections must be sorted and non-overlapping after merge: {:?} then {:?}",
w[0],
w[1]
);
}
}
fn assert_primary_resolves(mc: &MultiCursorState) {
if mc.is_empty() {
assert!(mc.get_primary().is_none());
assert!(mc.get_primary_cursor().is_none());
} else {
let p = mc.get_primary().expect("non-empty state must have a primary");
assert!(
mc.selections.iter().any(|s| s.id == p.id),
"get_primary() returned a selection not in the vec"
);
assert_eq!(
mc.primary_id, p.id,
"primary_id must name an existing selection (not fall back to last)"
);
}
}
fn assert_ids_unique(mc: &MultiCursorState) {
for (i, a) in mc.selections.iter().enumerate() {
for b in mc.selections.iter().skip(i + 1) {
assert_ne!(a.id, b.id, "duplicate SelectionId in state");
}
}
}
#[test]
fn selection_id_new_is_unique_and_strictly_increasing() {
let mut prev = SelectionId::new();
assert!(prev.inner > 0, "counter starts at 1, never the 0 sentinel");
for _ in 0..1000 {
let next = SelectionId::new();
assert!(
next.inner > prev.inner,
"SelectionId counter must be strictly monotonic"
);
assert_ne!(next, prev);
prev = next;
}
}
#[test]
fn selection_id_default_mints_a_fresh_id() {
let a = SelectionId::default();
let b = SelectionId::default();
let d = SelectionId::new();
assert_ne!(a, b);
assert_ne!(b, d);
assert!(a.inner > 0 && b.inner > 0);
}
#[test]
fn selection_state_add_dedups_identical_cursors() {
let mut st = SelectionState {
selections: Vec::<Selection>::new().into(),
node_id: DomNodeId::ROOT,
};
for _ in 0..100 {
st.add(Selection::Cursor(c(42)));
}
assert_eq!(st.selections.as_ref().len(), 1);
assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(42)));
}
#[test]
fn selection_state_add_sorts_descending_input_ascending() {
let mut st = SelectionState {
selections: Vec::<Selection>::new().into(),
node_id: DomNodeId::ROOT,
};
for byte in [90u32, 10, 50, 0, 70] {
st.add(Selection::Cursor(c(byte)));
}
let got: Vec<Selection> = st.selections.as_ref().to_vec();
assert_eq!(got.len(), 5);
let want: Vec<Selection> = [0u32, 10, 50, 70, 90]
.iter()
.map(|b| Selection::Cursor(c(*b)))
.collect();
assert_eq!(got, want);
}
#[test]
fn selection_state_add_boundary_and_reversed_ranges_do_not_panic() {
let mut st = SelectionState {
selections: Vec::<Selection>::new().into(),
node_id: DomNodeId::ROOT,
};
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)));
assert_eq!(st.selections.as_ref().len(), 4);
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)));
assert_eq!(st.selections.as_ref().len(), 2);
assert_eq!(st.selections.as_ref()[0], Selection::Cursor(c(5)));
}
#[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);
}
#[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);
assert_eq!(mc.to_selections(), vec![
Selection::Cursor(c(10)),
Selection::Cursor(c(20)),
Selection::Cursor(c(30)),
]);
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() {
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() {
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() {
let mut mc = empty_state();
let _ = mc.add_selection(SelectionRange {
start: c(20),
end: c(5),
});
let _ = mc.add_cursor(c(10));
assert_eq!(mc.len(), 1);
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);
}
#[test]
fn remove_selection_unknown_id_returns_false_and_changes_nothing() {
let mut mc = state(1);
let before = mc.clone();
let ghost = SelectionId::new(); 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());
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)); assert_eq!(mc.get_primary().unwrap().id, p);
assert!(mc.remove_selection(p));
assert_eq!(mc.len(), 2);
assert_primary_resolves(&mc);
}
#[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(); mc.ensure_primary_valid(); 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(); 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);
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)));
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.len() == 0);
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.len() == 0);
assert!(!mc.is_empty());
}
#[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)); mc.update_from_edit_result(&[Selection::Cursor(c(99))]);
assert_eq!(mc.len(), 1);
assert_primary_resolves(&mc);
assert_eq!(mc.get_primary_cursor(), Some(c(99)));
}
#[test]
fn update_from_edit_result_does_not_merge_overlaps() {
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");
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);
}
#[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");
}
#[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();
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);
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);
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))), 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),
}), 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));
}
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() {
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);
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]
#[ignore = "known bug: 3+-link merge chain loses the primary; see report"]
fn merge_overlapping_primary_should_follow_its_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.get_primary_cursor(),
Some(c(0)),
"primary jumped to an unrelated selection after the merge"
);
}
#[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() {
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)));
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);
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() {
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_extend_back_onto_the_anchor_collapses_to_a_cursor() {
let mut mc = empty_state();
mc.set_single_range(rng(3, 4));
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))
});
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| {
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);
}
#[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(); 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());
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() {
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)); 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);
}
#[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() {
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)
);
}
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);
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),
);
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 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); 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());
}
}