use std::ops::Range;
use bevy::prelude::Resource;
use super::motion::{clamp_to_boundary, clamp_to_cursor_position};
#[derive(Clone, Debug, Default, Eq, PartialEq, Resource)]
pub struct VimSelectionState {
selection: Option<VimSelection>,
}
impl VimSelectionState {
pub fn start(&mut self, text: &str, anchor_byte_index: usize) {
self.selection = Some(VimSelection::new(text, anchor_byte_index));
}
pub const fn clear(&mut self) {
self.selection = None;
}
#[must_use]
pub const fn selection(&self) -> Option<VimSelection> {
self.selection
}
pub fn set_anchor(&mut self, text: &str, anchor_byte_index: usize) {
if self.selection.is_some() {
self.selection = Some(VimSelection::new(text, anchor_byte_index));
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct VimSelection {
anchor_byte_index: usize,
}
impl VimSelection {
#[must_use]
pub fn new(text: &str, anchor_byte_index: usize) -> Self {
Self {
anchor_byte_index: clamp_to_boundary(text, anchor_byte_index),
}
}
#[must_use]
pub const fn anchor_byte_index(&self) -> usize {
self.anchor_byte_index
}
#[must_use]
pub fn byte_range(self, text: &str, cursor_byte_index: usize) -> Range<usize> {
let anchor = clamp_to_boundary(text, self.anchor_byte_index);
let cursor = clamp_to_boundary(text, cursor_byte_index);
anchor.min(cursor)..anchor.max(cursor)
}
#[must_use]
pub fn characterwise_byte_range(self, text: &str, cursor_byte_index: usize) -> Range<usize> {
let anchor = clamp_to_cursor_position(text, self.anchor_byte_index);
let cursor = clamp_to_cursor_position(text, cursor_byte_index);
let start = anchor.min(cursor);
let end_cell = anchor.max(cursor);
let end = text[end_cell..]
.chars()
.next()
.map_or(end_cell, |character| end_cell + character.len_utf8());
start..end
}
#[must_use]
pub fn linewise_byte_range(self, text: &str, cursor_byte_index: usize) -> Range<usize> {
let anchor = clamp_to_boundary(text, self.anchor_byte_index);
let cursor = clamp_to_boundary(text, cursor_byte_index);
let start = anchor.min(cursor);
let end = anchor.max(cursor);
line_start(text, start)..line_content_end(text, end)
}
}
fn line_start(text: &str, index: usize) -> usize {
let index = clamp_to_boundary(text, index);
text[..index]
.rfind('\n')
.map_or(0, |newline_index| newline_index + '\n'.len_utf8())
}
fn line_content_end(text: &str, index: usize) -> usize {
let index = clamp_to_boundary(text, index);
text[index..]
.find('\n')
.map_or(text.len(), |newline_offset| index + newline_offset)
}
#[cfg(test)]
mod tests {
use super::VimSelection;
use proptest::prelude::*;
#[test]
fn selection_range_orders_anchor_and_cursor() {
let text = "AλBC";
let selection = VimSelection::new(text, "AλB".len());
assert_eq!(selection.byte_range(text, 1), 1.."AλB".len());
}
#[test]
fn characterwise_selection_includes_cursor_cell() {
let text = "ALMA";
let selection = VimSelection::new(text, 1);
assert_eq!(selection.characterwise_byte_range(text, 2), 1..3);
}
#[test]
fn linewise_selection_covers_touched_line_content() {
let text = "one\ntwo\nthree";
let selection = VimSelection::new(text, "one\nt".len());
assert_eq!(
selection.linewise_byte_range(text, "one\ntwo\nth".len()),
4..13
);
}
proptest! {
#[test]
fn selection_ranges_stay_on_utf8_boundaries(
text in "\\PC*",
anchor in any::<usize>(),
cursor in any::<usize>(),
) {
let selection = VimSelection::new(&text, anchor);
let range = selection.byte_range(&text, cursor);
prop_assert!(range.start <= range.end);
prop_assert!(range.end <= text.len());
prop_assert!(text.is_char_boundary(range.start));
prop_assert!(text.is_char_boundary(range.end));
}
}
}