use std::cmp::Ordering;
use crate::core_editor::{
graphemes::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
Cursor,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum RestPolicy {
Between,
OnGrapheme,
Block,
}
fn recohere(buf: &str, c: Cursor) -> Cursor {
let len = buf.len();
let head = c.head().min(len);
let anchor = c.anchor().min(len);
let (anchor, head) = match anchor.cmp(&head) {
Ordering::Equal => {
let pos = ensure_grapheme_boundary_prev(buf, anchor);
(pos, pos)
}
Ordering::Less => (
ensure_grapheme_boundary_prev(buf, anchor),
ensure_grapheme_boundary_next(buf, head),
),
Ordering::Greater => (
ensure_grapheme_boundary_next(buf, anchor),
ensure_grapheme_boundary_prev(buf, head),
),
};
Cursor::new(anchor, head)
}
pub(crate) fn commit(buf: &str, c: Cursor, policy: RestPolicy) -> Cursor {
let c = recohere(buf, c);
let len = buf.len();
match policy {
RestPolicy::Between => c,
RestPolicy::OnGrapheme => {
let head = c.head();
let past_last_grapheme = head == len || buf[head..].starts_with(['\n', '\r']);
let line_has_grapheme = head > 0 && !buf[..head].ends_with(['\n', '\r']);
if c.is_empty() && past_last_grapheme && line_has_grapheme {
Cursor::point(prev_grapheme_boundary(buf, head))
} else {
c
}
}
RestPolicy::Block => {
if c.is_empty() {
let head = c.head();
let next = next_grapheme_boundary(buf, head);
if next > head && !buf[head..].starts_with(['\n', '\r']) {
c.move_head(next)
} else if head > 0 && !buf[..head].ends_with(['\n', '\r']) {
Cursor::new(prev_grapheme_boundary(buf, head), head)
} else {
c
}
} else {
c
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const MIXED: &str = "a日e\u{0301}👨👩👧";
#[test]
fn recohere_is_identity_on_aligned_cursors() {
let b = "hello";
assert_eq!(recohere(b, Cursor::new(1, 3)), Cursor::new(1, 3));
assert_eq!(recohere(b, Cursor::new(3, 1)), Cursor::new(3, 1));
assert_eq!(recohere(b, Cursor::point(2)), Cursor::point(2));
}
#[test]
fn recohere_clamps_past_end_to_point_at_len() {
assert_eq!(recohere("hello", Cursor::new(10, 12)), Cursor::point(5));
}
#[test]
fn recohere_floors_mid_grapheme_point() {
assert_eq!(recohere("ae\u{0301}", Cursor::point(2)), Cursor::point(1));
}
#[test]
fn recohere_expands_selection_outward() {
assert_eq!(recohere("ae\u{0301}", Cursor::new(0, 2)), Cursor::new(0, 4));
}
#[test]
fn recohere_is_idempotent() {
for a in (0..=MIXED.len()).filter(|&i| MIXED.is_char_boundary(i)) {
for h in (0..=MIXED.len()).filter(|&i| MIXED.is_char_boundary(i)) {
let once = recohere(MIXED, Cursor::new(a, h));
assert_eq!(recohere(MIXED, once), once, "anchor={a} head={h}");
}
}
}
#[test]
fn between_leaves_head_past_last_grapheme() {
assert_eq!(
commit("hello", Cursor::point(5), RestPolicy::Between),
Cursor::point(5)
);
}
#[test]
fn between_only_recoheres() {
assert_eq!(
commit("ae\u{0301}", Cursor::point(2), RestPolicy::Between),
Cursor::point(1)
);
}
#[test]
fn on_grapheme_pulls_point_back_from_end() {
assert_eq!(
commit("hello", Cursor::point(5), RestPolicy::OnGrapheme),
Cursor::point(4)
);
}
#[test]
fn on_grapheme_pulls_back_over_multibyte_grapheme() {
assert_eq!(
commit("caf\u{e9}", Cursor::point(5), RestPolicy::OnGrapheme),
Cursor::point(3)
);
}
#[test]
fn on_grapheme_leaves_midbuffer_point() {
assert_eq!(
commit("hello", Cursor::point(2), RestPolicy::OnGrapheme),
Cursor::point(2)
);
}
#[test]
fn on_grapheme_rests_on_trailing_empty_line() {
assert_eq!(
commit("a\n", Cursor::point(2), RestPolicy::OnGrapheme),
Cursor::point(2)
);
assert_eq!(
commit("hello\n", Cursor::point(6), RestPolicy::OnGrapheme),
Cursor::point(6)
);
}
#[test]
fn on_grapheme_pulls_back_from_interior_line_end() {
assert_eq!(
commit("abc\ndef", Cursor::point(3), RestPolicy::OnGrapheme),
Cursor::point(2)
);
assert_eq!(
commit("ab\r\ncd", Cursor::point(2), RestPolicy::OnGrapheme),
Cursor::point(1)
);
}
#[test]
fn on_grapheme_rests_on_interior_empty_line() {
assert_eq!(
commit("abc\n\ndef", Cursor::point(4), RestPolicy::OnGrapheme),
Cursor::point(4)
);
}
#[test]
fn on_grapheme_empty_buffer_is_noop() {
assert_eq!(
commit("", Cursor::point(0), RestPolicy::OnGrapheme),
Cursor::point(0)
);
}
#[test]
fn on_grapheme_leaves_selection_head_at_end() {
assert_eq!(
commit("hello", Cursor::new(0, 5), RestPolicy::OnGrapheme),
Cursor::new(0, 5)
);
}
#[test]
fn block_widens_point_to_one_grapheme() {
assert_eq!(
commit("hello", Cursor::point(2), RestPolicy::Block),
Cursor::new(2, 3)
);
}
#[test]
fn block_widens_over_multibyte_grapheme() {
assert_eq!(
commit("caf\u{e9}", Cursor::point(3), RestPolicy::Block),
Cursor::new(3, 5)
);
}
#[test]
fn block_point_at_end_widens_backward() {
assert_eq!(
commit("hello", Cursor::point(5), RestPolicy::Block),
Cursor::new(4, 5)
);
}
#[test]
fn block_widens_backward_over_multibyte_at_end() {
assert_eq!(
commit("caf\u{e9}", Cursor::point(5), RestPolicy::Block),
Cursor::new(3, 5)
);
}
#[test]
fn block_empty_buffer_stays_empty() {
assert_eq!(
commit("", Cursor::point(0), RestPolicy::Block),
Cursor::point(0)
);
}
#[test]
fn block_does_not_rest_on_crlf_terminator() {
assert_eq!(
commit("ab\r\ncd", Cursor::point(2), RestPolicy::Block),
Cursor::new(1, 2)
);
}
#[test]
fn block_leaves_existing_selection() {
assert_eq!(
commit("hello", Cursor::new(1, 3), RestPolicy::Block),
Cursor::new(1, 3)
);
}
#[test]
fn commit_is_idempotent() {
for policy in [
RestPolicy::Between,
RestPolicy::OnGrapheme,
RestPolicy::Block,
] {
for a in (0..=MIXED.len()).filter(|&i| MIXED.is_char_boundary(i)) {
for h in (0..=MIXED.len()).filter(|&i| MIXED.is_char_boundary(i)) {
let once = commit(MIXED, Cursor::new(a, h), policy);
assert_eq!(
commit(MIXED, once, policy),
once,
"policy={policy:?} anchor={a} head={h}"
);
}
}
}
}
}