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,
#[cfg(feature = "helix")]
BlockOverNewline,
}
impl RestPolicy {
pub(crate) fn is_block(self) -> bool {
match self {
RestPolicy::Block => true,
#[cfg(feature = "helix")]
RestPolicy::BlockOverNewline => true,
RestPolicy::Between | RestPolicy::OnGrapheme => false,
}
}
pub(crate) fn covers_terminator(self) -> bool {
match self {
RestPolicy::Between | RestPolicy::OnGrapheme | RestPolicy::Block => false,
#[cfg(feature = "helix")]
RestPolicy::BlockOverNewline => true,
}
}
}
pub(crate) 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 => block(buf, c, false),
#[cfg(feature = "helix")]
RestPolicy::BlockOverNewline => block(buf, c, true),
}
}
fn block(buf: &str, c: Cursor, over_newline: bool) -> Cursor {
if c.is_empty() {
let head = c.head();
let next = next_grapheme_boundary(buf, head);
if next > head && (over_newline || !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)
);
}
#[cfg(feature = "helix")]
#[test]
fn block_over_newline_widens_onto_the_terminator() {
assert_eq!(
commit("ab\ncd", Cursor::point(2), RestPolicy::BlockOverNewline),
Cursor::new(2, 3)
);
assert_eq!(
commit("ab\ncd", Cursor::point(2), RestPolicy::Block),
Cursor::new(1, 2)
);
}
#[cfg(feature = "helix")]
#[test]
fn block_over_newline_covers_a_whole_crlf() {
assert_eq!(
commit("ab\r\ncd", Cursor::point(2), RestPolicy::BlockOverNewline),
Cursor::new(2, 4)
);
}
#[cfg(feature = "helix")]
#[test]
fn block_over_newline_still_will_not_widen_across_a_line() {
assert_eq!(
commit("abc\n", Cursor::point(4), RestPolicy::BlockOverNewline),
Cursor::point(4)
);
assert_eq!(
commit("a\n\nb", Cursor::point(2), RestPolicy::BlockOverNewline),
Cursor::new(2, 3)
);
}
#[cfg(feature = "helix")]
#[test]
fn block_over_newline_matches_block_away_from_terminators() {
for (buf, pos) in [("hello", 2), ("caf\u{e9}", 3), ("hello", 5), ("", 0)] {
assert_eq!(
commit(buf, Cursor::point(pos), RestPolicy::BlockOverNewline),
commit(buf, Cursor::point(pos), RestPolicy::Block),
"buf={buf:?} pos={pos}"
);
}
}
#[test]
fn commit_is_idempotent() {
const MULTILINE: &str = "ab\ncd\r\n\n日\n";
for policy in [
RestPolicy::Between,
RestPolicy::OnGrapheme,
RestPolicy::Block,
#[cfg(feature = "helix")]
RestPolicy::BlockOverNewline,
] {
for buf in [MIXED, MULTILINE] {
for a in (0..=buf.len()).filter(|&i| buf.is_char_boundary(i)) {
for h in (0..=buf.len()).filter(|&i| buf.is_char_boundary(i)) {
let once = commit(buf, Cursor::new(a, h), policy);
assert_eq!(
commit(buf, once, policy),
once,
"policy={policy:?} buf={buf:?} anchor={a} head={h}"
);
}
}
}
}
}
}