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)
}
#[must_use]
pub fn linewise_operator_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_end_including_newline(text, end)
}
#[must_use]
pub fn blockwise_byte_ranges(self, text: &str, cursor_byte_index: usize) -> Vec<Range<usize>> {
BlockSelection::new(text, self.anchor_byte_index, cursor_byte_index).byte_ranges(text)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ScreenColumnRange {
start: usize,
end: usize,
}
impl ScreenColumnRange {
#[must_use]
pub const fn new(first: usize, second: usize) -> Self {
if first <= second {
Self {
start: first,
end: second,
}
} else {
Self {
start: second,
end: first,
}
}
}
#[must_use]
pub const fn start(self) -> usize {
self.start
}
#[must_use]
pub const fn end(self) -> usize {
self.end
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BlockSelection {
first_line_start: usize,
last_line_start: usize,
columns: ScreenColumnRange,
}
impl BlockSelection {
#[must_use]
pub fn new(text: &str, anchor_byte_index: usize, cursor_byte_index: usize) -> Self {
let anchor = clamp_to_cursor_position(text, anchor_byte_index);
let cursor = clamp_to_cursor_position(text, cursor_byte_index);
let (anchor_line_start, anchor_column) = line_start_and_column(text, anchor);
let (cursor_line_start, cursor_column) = line_start_and_column(text, cursor);
Self {
first_line_start: anchor_line_start.min(cursor_line_start),
last_line_start: anchor_line_start.max(cursor_line_start),
columns: ScreenColumnRange::new(anchor_column, cursor_column),
}
}
#[must_use]
pub const fn columns(self) -> ScreenColumnRange {
self.columns
}
#[must_use]
pub fn byte_ranges(self, text: &str) -> Vec<Range<usize>> {
let mut ranges = Vec::new();
let mut line_start_index = self.first_line_start;
while line_start_index <= self.last_line_start && line_start_index <= text.len() {
let line_end_index = line_content_end(text, line_start_index);
if let Some(range) =
line_column_range(text, line_start_index, line_end_index, self.columns)
{
ranges.push(range);
}
if line_end_index >= text.len() {
break;
}
line_start_index = line_end_index + '\n'.len_utf8();
}
ranges
}
}
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_start_and_column(text: &str, index: usize) -> (usize, usize) {
let index = clamp_to_cursor_position(text, index);
let start = line_start(text, index);
let column = text[start..index].chars().count();
(start, column)
}
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)
}
fn line_column_range(
text: &str,
line_start_index: usize,
line_end_index: usize,
columns: ScreenColumnRange,
) -> Option<Range<usize>> {
let mut start = None;
let mut end = None;
for (column, (offset, character)) in text[line_start_index..line_end_index]
.char_indices()
.enumerate()
{
let byte_index = line_start_index + offset;
if column == columns.start {
start = Some(byte_index);
}
if column == columns.end {
end = Some(byte_index + character.len_utf8());
break;
}
}
match (start, end) {
(Some(start), Some(end)) => Some(start..end),
(Some(start), None) => Some(start..line_end_index),
(None, _) => None,
}
}
fn line_end_including_newline(text: &str, index: usize) -> usize {
let content_end = line_content_end(text, index);
text[content_end..]
.chars()
.next()
.filter(|character| *character == '\n')
.map_or(content_end, |newline| content_end + newline.len_utf8())
}
#[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
);
}
#[test]
fn linewise_operator_range_includes_trailing_newline_when_present() {
let text = "one\ntwo\nthree";
let selection = VimSelection::new(text, "one\nt".len());
assert_eq!(
selection.linewise_operator_byte_range(text, "one\ntwo".len()),
4..8
);
assert_eq!(
selection.linewise_operator_byte_range(text, "one\ntwo\nth".len()),
4..13
);
}
#[test]
fn blockwise_selection_returns_per_line_utf8_ranges() {
let text = "aλc\n12345\nxy\n";
let selection = VimSelection::new(text, 1);
assert_eq!(
selection.blockwise_byte_ranges(text, "aλc\n12".len()),
vec![1.."aλc".len(), "aλc\n1".len().."aλc\n123".len()]
);
}
#[test]
fn blockwise_selection_skips_lines_shorter_than_start_column() {
let text = "abcd\nx\nwxyz";
let selection = VimSelection::new(text, 2);
assert_eq!(
selection.blockwise_byte_ranges(text, "abcd\nx\nwxy".len()),
vec![2..4, "abcd\nx\nwx".len().."abcd\nx\nwxyz".len()]
);
}
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);
let operator_range = selection.linewise_operator_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));
prop_assert!(operator_range.start <= operator_range.end);
prop_assert!(operator_range.end <= text.len());
prop_assert!(text.is_char_boundary(operator_range.start));
prop_assert!(text.is_char_boundary(operator_range.end));
}
}
}