use crate::doc::{Doc, Part};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Cursor {
pub block: usize,
pub part: Part,
pub offset: usize,
}
impl Cursor {
pub fn new(block: usize, part: Part, offset: usize) -> Self {
Self {
block,
part,
offset,
}
}
pub fn len_in(self, doc: &Doc) -> Option<usize> {
doc.blocks
.get(self.block)?
.text_at(self.part)
.map(|text| text.text.len())
}
pub fn clamp(self, doc: &Doc) -> Self {
if doc.blocks.is_empty() {
return Self::default();
}
let block = self.block.min(doc.blocks.len() - 1);
let part = match doc.blocks[block].text_at(self.part) {
Some(_) => self.part,
None => match doc.blocks[block].parts().first() {
Some(part) => *part,
None => return Self::new(block, Part::default(), 0),
},
};
let here = Self::new(block, part, self.offset);
let Some(text) = doc.blocks[block].text_at(part) else {
return here;
};
let mut offset = self.offset.min(text.text.len());
while offset > 0 && !text.text.is_char_boundary(offset) {
offset -= 1;
}
Self { offset, ..here }
}
fn next_editable(doc: &Doc, from: usize) -> Option<Self> {
(from..doc.blocks.len()).find_map(|ix| {
doc.blocks[ix]
.parts()
.first()
.map(|part| Self::new(ix, *part, 0))
})
}
fn previous_editable(doc: &Doc, from: usize) -> Option<Self> {
(0..=from.min(doc.blocks.len().saturating_sub(1)))
.rev()
.find_map(|ix| {
let part = *doc.blocks[ix].parts().last()?;
let at = Self::new(ix, part, 0);
Some(Self {
offset: at.len_in(doc).unwrap_or(0),
..at
})
})
}
fn step_part(self, doc: &Doc, by: isize) -> Option<Part> {
let parts = doc.blocks.get(self.block)?.parts();
let ix = parts.iter().position(|part| *part == self.part)?;
parts.get(ix.checked_add_signed(by)?).copied()
}
pub fn left(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
if here.offset > 0
&& let Some(text) = doc.blocks[here.block].text_at(here.part)
{
let mut offset = here.offset - 1;
while offset > 0 && !text.text.is_char_boundary(offset) {
offset -= 1;
}
return Self { offset, ..here };
}
if let Some(part) = here.step_part(doc, -1) {
let at = Self { part, ..here };
return Self {
offset: at.len_in(doc).unwrap_or(0),
..at
};
}
here.block
.checked_sub(1)
.and_then(|ix| Self::previous_editable(doc, ix))
.unwrap_or(here)
}
pub fn right(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
let len = here.len_in(doc).unwrap_or(0);
if here.offset < len
&& let Some(text) = doc.blocks[here.block].text_at(here.part)
{
let mut offset = here.offset + 1;
while offset < len && !text.text.is_char_boundary(offset) {
offset += 1;
}
return Self { offset, ..here };
}
if let Some(part) = here.step_part(doc, 1) {
return Self {
part,
offset: 0,
..here
};
}
Self::next_editable(doc, here.block + 1).unwrap_or(here)
}
pub fn up(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
if let Part::Cell { row, column } = here.part
&& row > 0
{
return Self {
part: Part::Cell {
row: row - 1,
column,
},
..here
}
.clamp(doc);
}
match here
.block
.checked_sub(1)
.and_then(|ix| Self::previous_editable(doc, ix))
{
Some(above) => Self {
offset: here.offset,
..above
}
.clamp(doc),
None => Self { offset: 0, ..here },
}
}
pub fn down(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
if let Part::Cell { row, column } = here.part {
let below = Self {
part: Part::Cell {
row: row + 1,
column,
},
..here
};
if below.len_in(doc).is_some() {
return below.clamp(doc);
}
}
match Self::next_editable(doc, here.block + 1) {
Some(below) => Self {
offset: here.offset,
..below
}
.clamp(doc),
None => Self {
offset: here.len_in(doc).unwrap_or(0),
..here
},
}
}
pub fn home(self) -> Self {
Self { offset: 0, ..self }
}
pub fn end(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
Self {
offset: here.len_in(doc).unwrap_or(0),
..here
}
}
pub fn word_left(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
let Some(text) = doc.blocks[here.block].text_at(here.part) else {
return here.left(doc);
};
if here.offset == 0 {
return here.left(doc);
}
let head = &text.text[..here.offset];
let trimmed = head.trim_end_matches(|c: char| !c.is_alphanumeric());
let offset = trimmed.trim_end_matches(char::is_alphanumeric).len();
Self { offset, ..here }
}
pub fn word_right(self, doc: &Doc) -> Self {
let here = self.clamp(doc);
let Some(text) = doc.blocks[here.block].text_at(here.part) else {
return here.right(doc);
};
if here.offset >= text.text.len() {
return here.right(doc);
}
let tail = &text.text[here.offset..];
let skipped = tail.len()
- tail
.trim_start_matches(|c: char| !c.is_alphanumeric())
.len();
let rest = &tail[skipped..];
let word = rest.len() - rest.trim_start_matches(char::is_alphanumeric).len();
Self {
offset: here.offset + skipped + word,
..here
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Selection {
pub anchor: Cursor,
pub head: Cursor,
}
impl From<Cursor> for Selection {
fn from(cursor: Cursor) -> Self {
Self::at(cursor)
}
}
impl Selection {
pub fn at(cursor: Cursor) -> Self {
Self {
anchor: cursor,
head: cursor,
}
}
pub fn new(anchor: Cursor, head: Cursor) -> Self {
Self { anchor, head }
}
pub fn is_collapsed(&self) -> bool {
self.anchor == self.head
}
pub fn ordered(&self) -> (Cursor, Cursor) {
(self.anchor.min(self.head), self.anchor.max(self.head))
}
pub fn extend_to(self, head: Cursor) -> Self {
Self { head, ..self }
}
pub fn clamp(self, doc: &Doc) -> Self {
Self {
anchor: self.anchor.clamp(doc),
head: self.head.clamp(doc),
}
}
pub fn all(doc: &Doc) -> Self {
let start = Cursor::next_editable(doc, 0).unwrap_or_default();
let end =
Cursor::previous_editable(doc, doc.blocks.len().saturating_sub(1)).unwrap_or(start);
Self::new(start, end)
}
}