use serde::{Deserialize, Serialize};
use super::cursor::CursorPosition;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Selection {
pub start: CursorPosition,
pub end: CursorPosition,
}
impl Selection {
#[must_use]
pub const fn new(start: CursorPosition, end: CursorPosition) -> Self {
Self { start, end }
}
#[must_use]
pub const fn empty(position: CursorPosition) -> Self {
Self {
start: position,
end: position,
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.start == self.end
}
#[must_use]
pub fn contains(&self, position: CursorPosition) -> bool {
let (min, max) = self.normalized();
position >= min && position < max
}
#[must_use]
pub fn normalized(&self) -> (CursorPosition, CursorPosition) {
if self.start.is_before(&self.end) {
(self.start, self.end)
} else {
(self.end, self.start)
}
}
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
let (self_start, self_end) = self.normalized();
let (other_start, other_end) = other.normalized();
!(self_end <= other_start || other_end <= self_start)
}
#[must_use]
pub fn merge(&self, other: &Self) -> Option<Self> {
let (self_start, self_end) = self.normalized();
let (other_start, other_end) = other.normalized();
if self_end >= other_start && other_end >= self_start {
Some(Self {
start: self_start.min(other_start),
end: self_end.max(other_end),
})
} else {
None
}
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum SelectionMode {
#[default]
Character,
Word,
Line,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum SelectionDirection {
Forward,
Backward,
}
#[must_use]
#[allow(dead_code)]
pub fn word_at_position(text: &str, line: usize, column: usize) -> Option<(usize, usize)> {
let lines: Vec<&str> = text.lines().collect();
let line_text = lines.get(line)?;
if column > line_text.len() {
return None;
}
let mut start = column;
for (i, c) in line_text[..column].char_indices().rev() {
if !is_word_char(c) {
start = i + c.len_utf8();
break;
}
if i == 0 {
start = 0;
}
}
let mut end = column;
for (i, c) in line_text[column..].char_indices() {
if !is_word_char(c) {
end = column + i;
break;
}
end = column + i + c.len_utf8();
}
if start == end {
None
} else {
Some((start, end))
}
}
#[allow(dead_code)]
fn is_word_char(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_selection_normalized() {
let sel = Selection::new(CursorPosition::new(1, 5), CursorPosition::new(0, 3));
let (start, end) = sel.normalized();
assert_eq!(start, CursorPosition::new(0, 3));
assert_eq!(end, CursorPosition::new(1, 5));
}
#[test]
fn test_selection_overlaps() {
let a = Selection::new(CursorPosition::new(0, 0), CursorPosition::new(0, 5));
let b = Selection::new(CursorPosition::new(0, 3), CursorPosition::new(0, 10));
let c = Selection::new(CursorPosition::new(0, 6), CursorPosition::new(0, 10));
assert!(a.overlaps(&b));
assert!(!a.overlaps(&c));
}
#[test]
fn test_word_at_position() {
let text = "hello world foo_bar";
assert_eq!(word_at_position(text, 0, 2), Some((0, 5)));
assert_eq!(word_at_position(text, 0, 8), Some((6, 11)));
assert_eq!(word_at_position(text, 0, 15), Some((12, 19)));
}
}