use crate::buffer::Buffer;
use std::cmp::min;
mod cur;
pub(crate) mod find;
mod range;
mod text_object;
pub use cur::Cur;
pub use range::Range;
pub use text_object::TextObject;
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum Dot {
Cur {
c: Cur,
},
Range {
r: Range,
},
}
impl Default for Dot {
fn default() -> Self {
Self::Cur { c: Cur::default() }
}
}
impl From<Cur> for Dot {
fn from(c: Cur) -> Self {
Self::Cur { c }
}
}
impl From<Range> for Dot {
fn from(r: Range) -> Self {
Self::Range { r }
}
}
impl Dot {
pub fn from_char_indices(from: usize, to: usize) -> Self {
Self::Range {
r: Range::from_cursors(Cur { idx: from }, Cur { idx: to }, false),
}
}
pub fn as_char_indices(&self) -> (usize, usize) {
match *self {
Self::Cur { c: Cur { idx } } => (idx, idx),
Self::Range {
r:
Range {
start: Cur { idx: from },
end: Cur { idx: to },
..
},
} => (from, to),
}
}
pub fn n_chars(&self) -> usize {
let (from, to) = self.as_char_indices();
to - from + 1
}
pub fn is_cur(&self) -> bool {
matches!(self, Dot::Cur { .. })
}
pub fn is_range(&self) -> bool {
matches!(self, Dot::Range { .. })
}
pub fn contains(&self, cur: &Cur) -> bool {
match self {
Dot::Cur { c } => cur == c,
Dot::Range { r } => r.contains(cur),
}
}
pub fn contains_range(&self, rng: &Range) -> bool {
self.contains(&rng.start) && self.contains(&rng.end)
}
pub fn with_offset_saturating(mut self, offset: isize) -> Self {
match &mut self {
Dot::Cur { c } if offset >= 0 => c.idx += offset as usize,
Dot::Cur { c } => c.idx = c.idx.saturating_sub(-offset as usize),
Dot::Range { r } if offset >= 0 => {
r.start.idx += offset as usize;
r.end.idx += offset as usize;
}
Dot::Range { r } => {
r.start.idx = r.start.idx.saturating_sub(-offset as usize);
r.end.idx = r.end.idx.saturating_sub(-offset as usize);
}
}
self
}
pub fn addr(&self, b: &Buffer) -> String {
match self {
Self::Cur { c } => c.as_string_addr(b),
Self::Range { r } => r.as_string_addr(b),
}
}
pub fn content(&self, b: &Buffer) -> String {
let len_chars = b.txt.len_chars();
if len_chars == 0 {
return String::new();
}
let (from, to) = self.as_char_indices();
b.txt.slice(from, min(to + 1, len_chars)).to_string()
}
#[inline]
pub fn active_cur(&self) -> Cur {
match self {
Self::Cur { c } => *c,
Self::Range { r } => r.active_cursor(),
}
}
pub fn set_active_cur(&mut self, cur: Cur) {
match self {
Self::Cur { c } => *c = cur,
Self::Range { r } => r.set_active_cursor(cur),
}
}
#[inline]
pub fn first_cur(&self) -> Cur {
match self {
Self::Cur { c } => *c,
Self::Range { r } => r.start,
}
}
#[inline]
pub fn last_cur(&self) -> Cur {
match self {
Self::Cur { c } => *c,
Self::Range { r } => r.end,
}
}
#[inline]
pub fn as_range(&self) -> Range {
match self {
Self::Cur { c } => Range {
start: *c,
end: *c,
start_active: false,
},
Self::Range { r } => *r,
}
}
#[inline]
pub fn collapse_to_first_cur(&self) -> Self {
Dot::Cur {
c: self.first_cur(),
}
}
#[inline]
pub fn collapse_to_last_cur(&self) -> Self {
Dot::Cur { c: self.last_cur() }
}
#[inline]
pub fn collapse_to_active_cur(&self) -> Self {
Dot::Cur {
c: self.active_cur(),
}
}
#[inline]
pub fn flip(&mut self) {
if let Dot::Range { r } = self {
r.flip();
}
}
pub fn collapse_null_range(self) -> Self {
match self {
Dot::Range {
r: Range { start, end, .. },
} if start == end => Dot::Cur { c: start },
_ => self,
}
}
pub fn clamp_idx(&mut self, max_idx: usize) {
match self {
Dot::Cur { c } => c.clamp_idx(max_idx),
Dot::Range { r } => {
r.start.clamp_idx(max_idx);
r.end.clamp_idx(max_idx);
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
text_object::TextObject::{self, *},
*,
};
use simple_test_case::test_case;
const EXAMPLE_TEXT: &str = "\
This is the first line of the file. Followed
by the second line. Some of the sentences are split
over multiple lines.
Others are not.
There is a second paragraph as well. But it
is quite short when compared to the first.
The third paragraph is even shorter.";
fn cur(y: usize, x: usize) -> Cur {
let y = if y == 0 {
0
} else {
EXAMPLE_TEXT
.lines()
.take(y)
.map(|line| line.len() + 1)
.sum()
};
Cur { idx: y + x }
}
fn c(y: usize, x: usize) -> Dot {
Dot::Cur { c: cur(y, x) }
}
fn r(y: usize, x: usize, y2: usize, x2: usize) -> Dot {
Dot::Range {
r: Range {
start: cur(y, x),
end: cur(y2, x2),
start_active: false,
},
}
}
#[test_case(BufferStart, c(0, 0); "buffer start")]
#[test_case(BufferEnd, c(9, 36); "buffer end")]
#[test_case(Character, c(5, 2); "character")]
#[test_case(Line, r(5, 0, 5, 43); "line")]
#[test_case(LineEnd, c(5, 43); "line end")]
#[test_case(LineStart, c(5, 0); "line start")]
#[test]
fn set_dot_works(to: TextObject, expected: Dot) {
let mut b = Buffer::new_virtual(0, "test", EXAMPLE_TEXT, Default::default());
b.dot = c(5, 1); to.set_dot(&mut b);
assert_eq!(b.dot, expected);
}
#[test_case(c(0, 0), "T"; "first character")]
#[test_case(r(0, 0, 0, 34), "This is the first line of the file."; "first sentence")]
#[test_case(
r(0, 0, 1, 18),
"This is the first line of the file. Followed\nby the second line.";
"spanning a newline"
)]
#[test]
fn dot_content_includes_expected_text(dot: Dot, expected: &str) {
let mut b = Buffer::new_virtual(0, "test", EXAMPLE_TEXT, Default::default());
b.dot = dot;
let content = b.dot_contents();
assert_eq!(content, expected);
}
}