use crate::{
buffer::Buffer,
dot::{
Cur, Dot, Range,
find::{Find, find_backward_start, find_forward_end},
},
key::Arrow,
};
use std::cmp::min;
#[allow(dead_code)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum TextObject {
Arr(Arrow),
BufferEnd,
BufferStart,
Character,
FindChar(char),
Delimited(char, char),
Line,
LineEnd,
LineStart,
Paragraph,
Word,
}
impl TextObject {
pub fn as_dot(&self, b: &Buffer) -> Dot {
use TextObject::*;
match self {
Arr(arr) => b.dot.active_cur().arr(*arr, b).into(),
BufferEnd => Cur::buffer_end(b).into(),
BufferStart => Cur::buffer_start().into(),
Character => b.dot.active_cur().arr(Arrow::Right, b).into(),
FindChar(ch) => find_forward_end(ch, b.dot.active_cur(), b).into(),
Delimited(l, r) => FindDelimited::new(*l, *r).expand(b.dot, b),
LineEnd => b.dot.active_cur().move_to_line_end(b).into(),
LineStart => b.dot.active_cur().move_to_line_start(b).into(),
Line => Dot::from(
b.dot
.as_range()
.extend_to_line_start(b)
.extend_to_line_end(b),
)
.collapse_null_range(),
Paragraph => FindParagraph::Fwd.expand(b.dot, b),
Word => FindWord::Fwd.expand(b.dot, b),
}
}
pub fn set_dot(&self, b: &mut Buffer) {
b.dot = self.as_dot(b);
}
pub fn extend_dot_forward(&self, b: &mut Buffer) {
use TextObject::*;
let Range {
mut start,
mut end,
start_active,
} = b.dot.as_range();
(start, end) = match (self, start_active) {
(Arr(arr), _) => (start, end.arr(*arr, b)),
(BufferEnd, true) => (end, Cur::buffer_end(b)),
(BufferEnd, false) => (start, Cur::buffer_end(b)),
(Character, true) => (start.arr_w_count(Arrow::Right, 1, b), end),
(Character, false) => (start, end.arr_w_count(Arrow::Right, 1, b)),
(FindChar(ch), true) => (find_forward_end(ch, start, b), end),
(FindChar(ch), false) => (start, find_forward_end(ch, end, b)),
(Line, true) => (start.arr_w_count(Arrow::Down, 1, b), end),
(Line, false) => (start, end.arr_w_count(Arrow::Down, 1, b)),
(LineEnd, true) => (start.move_to_line_end(b), end),
(LineEnd, false) => (start, end.move_to_line_end(b)),
(LineStart, true) => (start.move_to_line_start(b), end),
(LineStart, false) => (start, end.move_to_line_start(b)),
(Paragraph, true) => (find_forward_end(&FindParagraph::Fwd, start, b), end),
(Paragraph, false) => (start, find_forward_end(&FindParagraph::Fwd, end, b)),
(Word, true) => (find_forward_end(&FindWord::Fwd, start, b), end),
(Word, false) => (start, find_forward_end(&FindWord::Fwd, end, b)),
(BufferStart | Delimited(_, _), _) => return,
};
b.dot = Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range();
}
pub fn extend_dot_backward(&self, b: &mut Buffer) {
use TextObject::*;
let Range {
mut start,
mut end,
start_active,
} = b.dot.as_range();
(start, end) = match (self, start_active) {
(Arr(arr), _) => (start.arr(arr.flip(), b), end),
(BufferStart, true) => (Cur::buffer_start(), end),
(BufferStart, false) => (Cur::buffer_start(), start),
(Character, true) => (start.arr_w_count(Arrow::Left, 1, b), end),
(Character, false) => (start, end.arr_w_count(Arrow::Left, 1, b)),
(FindChar(ch), true) => (find_backward_start(ch, start, b), end),
(FindChar(ch), false) => (start, find_backward_start(ch, end, b)),
(Line, true) => (start.arr_w_count(Arrow::Up, 1, b), end),
(Line, false) => (start, end.arr_w_count(Arrow::Up, 1, b)),
(LineEnd, true) => (start.move_to_line_end(b), end),
(LineEnd, false) => (start, end.move_to_line_end(b)),
(LineStart, true) => (start.move_to_line_start(b), end),
(LineStart, false) => (start, end.move_to_line_start(b)),
(Paragraph, true) => (find_backward_start(&FindParagraph::Fwd, start, b), end),
(Paragraph, false) => (start, find_backward_start(&FindParagraph::Fwd, end, b)),
(Word, true) => (find_backward_start(&FindWord::Fwd, start, b), end),
(Word, false) => (start, find_backward_start(&FindWord::Fwd, end, b)),
(BufferEnd | Delimited(_, _), _) => return,
};
b.dot = Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range();
}
}
pub struct FindDelimited {
l: String,
r: String,
rev: bool,
}
impl FindDelimited {
pub fn new(l: impl Into<String>, r: impl Into<String>) -> Self {
Self {
l: l.into(),
r: r.into(),
rev: false,
}
}
}
impl Find for FindDelimited {
type Reversed = FindDelimited;
fn reversed(&self) -> Self::Reversed {
Self {
l: self.l.clone(),
r: self.r.clone(),
rev: !self.rev,
}
}
fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
where
I: Iterator<Item = (usize, char)>,
{
let (target, other) = if self.rev {
(&self.l, &self.r)
} else {
(&self.r, &self.l)
};
let mut skips = 0;
for (i, ch) in it {
if other.contains(ch) && target != other {
skips += 1;
} else if skips == 0 && target.contains(ch) {
let ix = if self.rev { i + 1 } else { i - 1 };
return Some((ix, ix));
} else if target.contains(ch) {
skips -= 1;
}
}
None
}
}
enum FindParagraph {
Fwd,
Bck,
}
impl Find for FindParagraph {
type Reversed = FindParagraph;
fn reversed(&self) -> Self::Reversed {
match self {
Self::Fwd => Self::Bck,
Self::Bck => Self::Fwd,
}
}
fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
where
I: Iterator<Item = (usize, char)>,
{
let mut prev_was_newline = false;
let mut pos = 0;
for (i, ch) in it {
match ch {
'\n' if prev_was_newline => {
return match self {
Self::Fwd => Some((i, i)),
Self::Bck => Some((i + 1, i + 1)),
};
}
'\n' => prev_was_newline = true,
_ => prev_was_newline = false,
}
pos = i;
}
Some((pos, pos))
}
}
enum FindWord {
Fwd,
Bck,
}
impl Find for FindWord {
type Reversed = FindWord;
fn reversed(&self) -> Self::Reversed {
match self {
Self::Fwd => Self::Bck,
Self::Bck => Self::Fwd,
}
}
fn try_find<I>(&self, it: I) -> Option<(usize, usize)>
where
I: Iterator<Item = (usize, char)>,
{
use CharKind::*;
let mut it = it.peekable();
let mut prev = CharKind::from(it.peek()?.1);
if matches!((prev, self), (Word | Punctuation, FindWord::Fwd)) {
it.next();
prev = CharKind::from(it.peek()?.1);
}
for (i, ch) in it {
let kind = CharKind::from(ch);
match (prev, kind) {
(Word, Punctuation) | (Punctuation, Word) | (Word | Punctuation, Whitespace) => {
return match self {
Self::Fwd => Some((i - 1, i - 1)),
Self::Bck => Some((i + 1, i + 1)),
};
}
_ => prev = kind,
}
}
None
}
fn expand(&self, dot: Dot, b: &Buffer) -> Dot {
use CharKind::*;
let Range {
mut start,
mut end,
start_active,
} = dot.as_range();
let max_idx = b.txt.len_chars() - 1;
start.idx = min(start.idx, max_idx);
end.idx = min(end.idx, max_idx);
if start.idx > 0 {
let current = CharKind::from(b.txt.char(start.idx));
let prev = CharKind::from(b.txt.char(start.idx - 1));
match (prev, current) {
(Whitespace, Word | Punctuation) => (),
(_, Whitespace) if start.idx < max_idx => {
while matches!(CharKind::from(b.txt.char(start.idx)), Whitespace) {
start.idx += 1;
if start.idx == max_idx {
end.idx = max_idx;
break;
}
}
}
_ => start = find_backward_start(self, start, b),
}
}
if end.idx < max_idx {
let current = CharKind::from(b.txt.char(end.idx));
let next = CharKind::from(b.txt.char(end.idx + 1));
match (current, next) {
(Word | Punctuation, Whitespace) => (),
_ => end = find_forward_end(self, end, b),
}
}
Dot::from(Range::from_cursors(start, end, start_active)).collapse_null_range()
}
}
#[derive(Debug, Clone, Copy)]
enum CharKind {
Word,
Punctuation,
Whitespace,
}
impl From<char> for CharKind {
fn from(ch: char) -> Self {
if ch.is_alphanumeric() || ch == '_' {
CharKind::Word
} else if ch.is_whitespace() {
CharKind::Whitespace
} else {
CharKind::Punctuation
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use simple_test_case::test_case;
#[test_case(FindWord::Fwd, 0, "this"; "forward start of buffer")]
#[test_case(FindWord::Fwd, 3, "this"; "forward end of first word")]
#[test_case(FindWord::Fwd, 4, "is"; "forward after first word")]
#[test_case(FindWord::Fwd, 5, "is"; "forward start of second word")]
#[test_case(FindWord::Fwd, 6, "is"; "forward end of second word")]
#[test_case(FindWord::Fwd, 9, "test"; "forward before last word")]
#[test_case(FindWord::Fwd, 13, "test"; "forward end of buffer")]
#[test]
fn expand_word(fw: FindWord, idx: usize, expected: &str) {
let b = Buffer::new_virtual(0, "test", "this is a test", Default::default());
let dot = Dot::Cur { c: Cur { idx } };
let expanded = fw.expand(dot, &b);
let content = expanded.content(&b);
assert_eq!(content, expected);
}
#[test]
fn expand_word_for_buffer_with_trailing_spaces() {
let b = Buffer::new_virtual(0, "test", "this is a test ", Default::default());
let dot = Dot::Cur { c: Cur { idx: 14 } };
let expanded = FindWord::Fwd.expand(dot, &b);
let content = expanded.content(&b);
assert!(matches!(expanded, Dot::Cur { c: Cur { idx: 16 } }));
assert_eq!(content, " ");
}
}