use crate::core_editor::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
#[allow(dead_code)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Direction {
Forward,
Backward,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum Movement {
Move,
Extend,
}
impl Movement {
pub(crate) fn select(select: bool) -> Self {
if select {
Movement::Extend
} else {
Movement::Move
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum CaretGeometry {
Block,
Bar,
}
impl CaretGeometry {
pub(crate) fn is_inclusive(self) -> bool {
matches!(self, CaretGeometry::Block)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Cursor {
anchor: usize,
head: usize,
}
impl Cursor {
pub fn new(anchor: usize, head: usize) -> Self {
Self { anchor, head }
}
pub fn point(head: usize) -> Self {
Self::new(head, head)
}
pub fn anchor(&self) -> usize {
self.anchor
}
pub fn head(&self) -> usize {
self.head
}
pub fn start(&self) -> usize {
self.anchor.min(self.head)
}
pub fn end(&self) -> usize {
self.anchor.max(self.head)
}
pub fn is_empty(&self) -> bool {
self.anchor == self.head
}
}
#[allow(dead_code)]
impl Cursor {
pub fn to_range(self) -> std::ops::Range<usize> {
self.start()..self.end()
}
pub fn len(&self) -> usize {
self.end() - self.start()
}
pub fn direction(&self) -> Direction {
if self.head < self.anchor {
Direction::Backward
} else {
Direction::Forward
}
}
pub fn flip(self) -> Self {
Self {
anchor: self.head,
head: self.anchor,
}
}
pub fn with_direction(self, direction: Direction) -> Self {
if self.direction() == direction {
self
} else {
self.flip()
}
}
pub fn move_head(self, new_head: usize) -> Self {
Self {
anchor: self.anchor,
head: new_head,
}
}
pub fn collapse_to(self, head: usize) -> Self {
Self::point(head)
}
pub fn collapse(self) -> Self {
Self::point(self.head)
}
pub fn extend(self, from: usize, to: usize) -> Self {
debug_assert!(from <= to);
if self.anchor <= self.head {
Self {
anchor: self.anchor.min(from),
head: self.head.max(to),
}
} else {
Self {
anchor: self.anchor.max(to),
head: self.head.min(from),
}
}
}
pub fn contains(&self, pos: usize) -> bool {
self.start() <= pos && pos < self.end()
}
pub fn caret(&self, buf: &str) -> usize {
if self.head > self.anchor {
prev_grapheme_boundary(buf, self.head)
} else {
self.head
}
}
pub fn put_cursor(
self,
buf: &str,
target: usize,
movement: Movement,
geometry: CaretGeometry,
) -> Self {
if movement == Movement::Move {
return Self::point(target);
}
let inclusive = geometry.is_inclusive();
let anchor: usize = if !inclusive {
self.anchor
} else if self.head >= self.anchor && target < self.anchor {
next_grapheme_boundary(buf, self.anchor)
} else if self.head < self.anchor && target >= self.anchor {
prev_grapheme_boundary(buf, self.anchor)
} else {
self.anchor
};
let head = if anchor <= target && inclusive {
next_grapheme_boundary(buf, target)
} else {
target
};
Self::new(anchor, head)
}
}
impl Default for Cursor {
fn default() -> Self {
Self::point(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn contains() {
let range = Cursor::new(10, 12);
assert!(!range.contains(9));
assert!(range.contains(10));
assert!(range.contains(11));
assert!(!range.contains(12));
assert!(!range.contains(13));
let range = Cursor::new(9, 6);
assert!(!range.contains(9));
assert!(range.contains(7));
assert!(range.contains(6));
}
#[test]
fn point_constructs_empty_range_at_head() {
let range = Cursor::point(5);
assert_eq!(range.start(), 5);
assert_eq!(range.end(), 5);
assert!(range.is_empty());
}
#[test]
fn new_preserves_anchor_and_head_order() {
let forward = Cursor::new(2, 5);
assert_eq!(forward.direction(), Direction::Forward);
let backward = Cursor::new(5, 2);
assert_eq!(backward.direction(), Direction::Backward);
}
#[test]
fn start_returns_lower_of_anchor_and_head() {
assert_eq!(Cursor::new(2, 5).start(), 2);
assert_eq!(Cursor::new(5, 2).start(), 2);
}
#[test]
fn end_returns_higher_of_anchor_and_head() {
assert_eq!(Cursor::new(2, 5).end(), 5);
assert_eq!(Cursor::new(5, 2).end(), 5);
}
#[test]
fn start_and_end_agree_for_empty_range() {
let range = Cursor::point(7);
assert_eq!(range.start(), range.end());
}
#[test]
fn len_is_zero_for_empty_range() {
assert_eq!(Cursor::point(7).len(), 0);
}
#[test]
fn len_ignores_direction() {
assert_eq!(Cursor::new(2, 5).len(), 3);
assert_eq!(Cursor::new(5, 2).len(), 3);
}
#[test]
fn is_empty_true_when_anchor_equals_head() {
assert!(Cursor::new(5, 5).is_empty());
assert!(Cursor::point(0).is_empty());
}
#[test]
fn is_empty_false_for_nonzero_width() {
assert!(!Cursor::new(2, 5).is_empty());
assert!(!Cursor::new(5, 2).is_empty());
}
#[test]
fn direction_forward_when_head_greater_than_anchor() {
assert_eq!(Cursor::new(2, 5).direction(), Direction::Forward);
}
#[test]
fn direction_backward_when_head_less_than_anchor() {
assert_eq!(Cursor::new(5, 2).direction(), Direction::Backward);
}
#[test]
fn direction_forward_for_empty_range() {
assert_eq!(Cursor::point(5).direction(), Direction::Forward);
}
#[test]
fn flip_swaps_anchor_and_head() {
let flipped = Cursor::new(2, 5).flip();
assert_eq!(flipped, Cursor::new(5, 2));
}
#[test]
fn flip_twice_returns_original() {
let range = Cursor::new(2, 5);
assert_eq!(range.flip().flip(), range);
}
#[test]
fn flip_of_empty_range_is_unchanged() {
let range = Cursor::point(5);
assert_eq!(range.flip(), range);
}
#[test]
fn with_direction_noop_when_already_forward() {
let range = Cursor::new(2, 5);
assert_eq!(range.with_direction(Direction::Forward), range);
}
#[test]
fn with_direction_noop_when_already_backward() {
let range = Cursor::new(5, 2);
assert_eq!(range.with_direction(Direction::Backward), range);
}
#[test]
fn with_direction_flips_forward_to_backward() {
let range = Cursor::new(2, 5);
assert_eq!(range.with_direction(Direction::Backward), Cursor::new(5, 2));
}
#[test]
fn with_direction_flips_backward_to_forward() {
let range = Cursor::new(5, 2);
assert_eq!(range.with_direction(Direction::Forward), Cursor::new(2, 5));
}
#[test]
fn with_direction_on_empty_range_stays_forward() {
let range = Cursor::point(5);
assert_eq!(range.with_direction(Direction::Forward), range);
assert_eq!(range.with_direction(Direction::Backward), range);
}
#[test]
fn extend_forward_shrinks_anchor_left() {
let range = Cursor::new(5, 8);
assert_eq!(range.extend(2, 3), Cursor::new(2, 8));
}
#[test]
fn extend_forward_grows_head_right() {
let range = Cursor::new(2, 5);
assert_eq!(range.extend(6, 8), Cursor::new(2, 8));
}
#[test]
fn extend_forward_grows_both_sides() {
let range = Cursor::new(4, 6);
assert_eq!(range.extend(2, 8), Cursor::new(2, 8));
}
#[test]
fn extend_forward_noop_when_range_already_covers() {
let range = Cursor::new(1, 9);
assert_eq!(range.extend(3, 5), range);
}
#[test]
fn extend_backward_preserves_direction() {
let range = Cursor::new(8, 2);
let result = range.extend(4, 6);
assert_eq!(result.direction(), Direction::Backward);
}
#[test]
fn extend_backward_grows_head_left() {
let range = Cursor::new(8, 5);
assert_eq!(range.extend(2, 3), Cursor::new(8, 2));
}
#[test]
fn extend_backward_grows_anchor_right() {
let range = Cursor::new(5, 2);
assert_eq!(range.extend(6, 8), Cursor::new(8, 2));
}
#[test]
fn extend_from_empty_range_stays_forward() {
let range = Cursor::point(5);
let result = range.extend(3, 7);
assert_eq!(result.direction(), Direction::Forward);
assert_eq!(result, Cursor::new(3, 7));
}
#[test]
fn extend_with_zero_width_target_is_safe() {
let range = Cursor::new(2, 5);
assert_eq!(range.extend(3, 3), range);
}
#[test]
fn contains_false_for_empty_range() {
let range = Cursor::point(5);
assert!(!range.contains(5));
assert!(!range.contains(4));
assert!(!range.contains(6));
}
#[test]
fn contains_is_direction_agnostic() {
let forward = Cursor::new(2, 5);
let backward = Cursor::new(5, 2);
for pos in 0..=6 {
assert_eq!(
forward.contains(pos),
backward.contains(pos),
"mismatch at {pos}"
);
}
}
#[test]
fn anchor_and_head_accessors() {
let range = Cursor::new(3, 7);
assert_eq!(range.anchor(), 3);
assert_eq!(range.head(), 7);
}
#[test]
fn to_range_returns_start_to_end() {
assert_eq!(Cursor::new(2, 5).to_range(), 2..5);
assert_eq!(Cursor::new(5, 2).to_range(), 2..5);
assert_eq!(Cursor::point(4).to_range(), 4..4);
}
#[test]
fn move_head_preserves_anchor() {
let range = Cursor::new(2, 5);
assert_eq!(range.move_head(8), Cursor::new(2, 8));
assert_eq!(range.move_head(0), Cursor::new(2, 0));
}
#[test]
fn move_head_from_point_creates_range() {
assert_eq!(Cursor::point(3).move_head(7), Cursor::new(3, 7));
}
#[test]
fn collapse_to_drops_anchor_and_moves_head() {
let range = Cursor::new(2, 5);
assert_eq!(range.collapse_to(8), Cursor::point(8));
}
#[test]
fn collapse_drops_anchor_at_current_head() {
let range = Cursor::new(2, 5);
assert_eq!(range.collapse(), Cursor::point(5));
}
#[test]
fn collapse_on_point_is_noop() {
let range = Cursor::point(5);
assert_eq!(range.collapse(), range);
}
#[test]
fn default_is_point_at_zero() {
assert_eq!(Cursor::default(), Cursor::point(0));
}
#[test]
fn caret_of_point_is_head() {
assert_eq!(Cursor::point(2).caret("hello"), 2);
assert_eq!(Cursor::point(0).caret("hello"), 0);
}
#[test]
fn caret_of_forward_block_retreats_one_grapheme() {
assert_eq!(Cursor::new(0, 3).caret("hello"), 2);
}
#[test]
fn caret_of_backward_range_is_head() {
assert_eq!(Cursor::new(5, 2).caret("hello"), 2);
}
#[test]
fn caret_retreats_past_multibyte_grapheme() {
assert_eq!(Cursor::new(0, 5).caret("café"), 3);
}
#[test]
fn caret_retreats_past_zwj_emoji_as_one() {
let buf = "a👨👩👧";
assert_eq!(Cursor::new(0, buf.len()).caret(buf), 1);
}
#[test]
fn caret_of_empty_buffer_point_is_zero() {
assert_eq!(Cursor::point(0).caret(""), 0);
}
#[test]
fn put_cursor_non_extend_collapses_to_point() {
assert_eq!(
Cursor::new(2, 4).put_cursor("hello", 3, Movement::Move, CaretGeometry::Block),
Cursor::point(3)
);
}
#[test]
fn put_cursor_extend_forward_places_head_on_far_edge() {
let c = Cursor::new(0, 1).put_cursor("hello", 2, Movement::Extend, CaretGeometry::Block);
assert_eq!(c, Cursor::new(0, 3));
assert_eq!(c.caret("hello"), 2);
}
#[test]
fn put_cursor_extend_no_flip_stays_backward() {
let c = Cursor::new(4, 2).put_cursor("hello", 1, Movement::Extend, CaretGeometry::Block);
assert_eq!(c, Cursor::new(4, 1));
assert_eq!(c.caret("hello"), 1);
}
#[test]
fn put_cursor_extend_flip_forward_to_backward_keeps_anchor_grapheme() {
let c = Cursor::new(2, 4).put_cursor("hello", 0, Movement::Extend, CaretGeometry::Block);
assert_eq!(c, Cursor::new(3, 0));
assert_eq!(c.caret("hello"), 0);
assert!(c.contains(2)); }
#[test]
fn put_cursor_extend_flip_backward_to_forward_keeps_anchor_grapheme() {
let c = Cursor::new(4, 2).put_cursor("hello", 5, Movement::Extend, CaretGeometry::Block);
assert_eq!(c, Cursor::new(3, 5));
assert_eq!(c.caret("hello"), 4);
assert!(c.contains(3)); }
#[test]
fn put_cursor_extend_flip_across_multibyte_anchor() {
let c = Cursor::new(3, 5).put_cursor("café", 0, Movement::Extend, CaretGeometry::Block);
assert_eq!(c, Cursor::new(5, 0));
assert!(c.contains(3) && c.contains(4)); }
}