use crate::TextSource;
use unicode_width::UnicodeWidthStr;
pub trait Geometry {
fn width(&self, grapheme: &str, column: usize) -> usize;
}
pub struct TerminalGeometry {
pub tabstop: usize,
}
impl Geometry for TerminalGeometry {
fn width(&self, grapheme: &str, column: usize) -> usize {
if grapheme == "\t" {
let tab = self.tabstop.max(1);
tab - column % tab
} else {
UnicodeWidthStr::width(grapheme).max(1)
}
}
}
pub static DEFAULT_GEOMETRY: TerminalGeometry = TerminalGeometry { tabstop: 8 };
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ByteOffset(
pub usize,
);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct TextRange {
pub start: ByteOffset,
pub end: ByteOffset,
}
impl TextRange {
pub const fn new(start: usize, end: usize) -> Self {
Self {
start: ByteOffset(start),
end: ByteOffset(end),
}
}
pub const fn is_empty(self) -> bool {
self.start.0 == self.end.0
}
pub fn as_range(self) -> std::ops::Range<usize> {
self.start.0..self.end.0
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub enum Shape {
#[default]
Character,
Line,
Block {
width: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Selection {
pub anchor: ByteOffset,
pub cursor: ByteOffset,
pub shape: Shape,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)]
pub struct DocumentVersion {
pub id: u64,
pub revision: u64,
}
#[derive(Clone, Copy)]
pub struct DocumentView<'a> {
pub text: &'a dyn TextSource,
pub version: DocumentVersion,
pub cursor: ByteOffset,
pub selection: Option<Selection>,
pub opaque: &'a [TextRange],
pub geometry: &'a dyn Geometry,
}
impl<'a> DocumentView<'a> {
pub fn new(text: &'a dyn TextSource, version: DocumentVersion, cursor: ByteOffset) -> Self {
Self {
text,
version,
cursor,
selection: None,
opaque: &[],
geometry: &DEFAULT_GEOMETRY,
}
}
pub fn is_valid(self) -> bool {
let valid = |p: ByteOffset| {
p.0 <= self.text.len_bytes()
&& self.text.is_char_boundary(p.0)
&& (p.0 == 0 || self.text.next_grapheme(self.text.prev_grapheme(p.0)) == p.0)
};
valid(self.cursor)
&& self
.selection
.is_none_or(|s| valid(s.anchor) && valid(s.cursor))
&& self
.opaque
.iter()
.all(|r| valid(r.start) && valid(r.end) && r.start < r.end)
&& self.opaque.windows(2).all(|r| r[0].end <= r[1].start)
}
}