use crate::core::errors::{EditorError, Result};
use core::cmp::{max, min};
use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Position {
pub offset: usize,
}
impl Position {
#[must_use]
pub const fn new(offset: usize) -> Self {
Self { offset }
}
#[must_use]
pub const fn start() -> Self {
Self { offset: 0 }
}
#[must_use]
pub const fn is_start(&self) -> bool {
self.offset == 0
}
#[must_use]
pub const fn advance(&self, bytes: usize) -> Self {
Self {
offset: self.offset.saturating_add(bytes),
}
}
#[must_use]
pub const fn retreat(&self, bytes: usize) -> Self {
Self {
offset: self.offset.saturating_sub(bytes),
}
}
}
impl Default for Position {
fn default() -> Self {
Self::start()
}
}
impl fmt::Display for Position {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.offset)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct LineColumn {
pub line: usize,
pub column: usize,
}
impl LineColumn {
pub fn new(line: usize, column: usize) -> Result<Self> {
if line == 0 || column == 0 {
return Err(EditorError::InvalidPosition { line, column });
}
Ok(Self { line, column })
}
#[must_use]
pub const fn start() -> Self {
Self { line: 1, column: 1 }
}
}
impl fmt::Display for LineColumn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.column)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Range {
pub start: Position,
pub end: Position,
}
impl Range {
#[must_use]
pub fn new(start: Position, end: Position) -> Self {
if start.offset <= end.offset {
Self { start, end }
} else {
Self {
start: end,
end: start,
}
}
}
#[must_use]
pub const fn empty(pos: Position) -> Self {
Self {
start: pos,
end: pos,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.start.offset == self.end.offset
}
#[must_use]
pub const fn len(&self) -> usize {
self.end.offset.saturating_sub(self.start.offset)
}
#[must_use]
pub const fn contains(&self, pos: Position) -> bool {
pos.offset >= self.start.offset && pos.offset < self.end.offset
}
#[must_use]
pub const fn overlaps(&self, other: &Self) -> bool {
self.start.offset < other.end.offset && other.start.offset < self.end.offset
}
#[must_use]
pub fn extend_to(&self, pos: Position) -> Self {
Self {
start: Position::new(min(self.start.offset, pos.offset)),
end: Position::new(max(self.end.offset, pos.offset)),
}
}
#[must_use]
pub fn union(&self, other: &Self) -> Self {
Self {
start: Position::new(min(self.start.offset, other.start.offset)),
end: Position::new(max(self.end.offset, other.end.offset)),
}
}
#[must_use]
pub fn intersection(&self, other: &Self) -> Option<Self> {
let start = max(self.start.offset, other.start.offset);
let end = min(self.end.offset, other.end.offset);
if start < end {
Some(Self::new(Position::new(start), Position::new(end)))
} else {
None
}
}
}
impl fmt::Display for Range {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_empty() {
write!(f, "{}", self.start)
} else {
write!(f, "{}-{}", self.start, self.end)
}
}
}
#[derive(Debug, Clone, Default)]
pub struct PositionBuilder {
line: Option<usize>,
column: Option<usize>,
offset: Option<usize>,
}
impl PositionBuilder {
#[must_use]
pub const fn new() -> Self {
Self {
line: None,
column: None,
offset: None,
}
}
#[must_use]
pub const fn line(mut self, line: usize) -> Self {
self.line = Some(line);
self
}
#[must_use]
pub const fn column(mut self, column: usize) -> Self {
self.column = Some(column);
self
}
#[must_use]
pub const fn offset(mut self, offset: usize) -> Self {
self.offset = Some(offset);
self
}
#[must_use]
pub const fn at_line_start(mut self, line: usize) -> Self {
self.line = Some(line);
self.column = Some(1);
self
}
#[must_use]
pub const fn at_line_end(mut self, line: usize) -> Self {
self.line = Some(line);
self.column = None; self
}
#[must_use]
pub const fn at_start() -> Self {
Self {
line: Some(1),
column: Some(1),
offset: Some(0),
}
}
#[cfg(feature = "rope")]
pub fn build(self, rope: &ropey::Rope) -> Result<Position> {
if let Some(offset) = self.offset {
if offset > rope.len_bytes() {
return Err(EditorError::PositionOutOfBounds {
position: offset,
length: rope.len_bytes(),
});
}
Ok(Position::new(offset))
} else if let Some(line) = self.line {
let line_idx = line.saturating_sub(1);
if line_idx >= rope.len_lines() {
return Err(EditorError::InvalidPosition { line, column: 1 });
}
let line_start = rope.line_to_byte(line_idx);
if let Some(column) = self.column {
LineColumn::new(line, column)?;
let col_idx = column.saturating_sub(1);
let line = rope.line(line_idx);
let mut byte_pos = 0;
let mut char_pos = 0;
for ch in line.chars() {
if char_pos == col_idx {
break;
}
byte_pos += ch.len_utf8();
char_pos += 1;
}
if char_pos < col_idx {
return Err(EditorError::InvalidPosition {
line: self.line.unwrap_or(0),
column,
});
}
Ok(Position::new(line_start + byte_pos))
} else {
let line_end = if line_idx + 1 < rope.len_lines() {
rope.line_to_byte(line_idx + 1).saturating_sub(1)
} else {
rope.len_bytes()
};
Ok(Position::new(line_end))
}
} else {
Ok(Position::start())
}
}
#[cfg(not(feature = "rope"))]
pub fn build(self) -> Result<Position> {
if let Some(offset) = self.offset {
Ok(Position::new(offset))
} else {
Err(EditorError::FeatureNotEnabled {
feature: "line/column position".to_string(),
required_feature: "rope".to_string(),
})
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Selection {
pub anchor: Position,
pub cursor: Position,
}
impl Selection {
#[must_use]
pub const fn new(anchor: Position, cursor: Position) -> Self {
Self { anchor, cursor }
}
#[must_use]
pub const fn empty(pos: Position) -> Self {
Self {
anchor: pos,
cursor: pos,
}
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.anchor.offset == self.cursor.offset
}
#[must_use]
pub fn range(&self) -> Range {
Range::new(self.anchor, self.cursor)
}
#[must_use]
pub const fn is_reversed(&self) -> bool {
self.cursor.offset < self.anchor.offset
}
#[must_use]
pub const fn extend_to(&self, pos: Position) -> Self {
Self {
anchor: self.anchor,
cursor: pos,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn position_operations() {
let pos = Position::new(10);
assert_eq!(pos.advance(5).offset, 15);
assert_eq!(pos.retreat(5).offset, 5);
assert_eq!(pos.retreat(20).offset, 0); }
#[test]
fn line_column_validation() {
assert!(LineColumn::new(0, 1).is_err());
assert!(LineColumn::new(1, 0).is_err());
assert!(LineColumn::new(1, 1).is_ok());
}
#[test]
fn range_normalization() {
let r = Range::new(Position::new(10), Position::new(5));
assert_eq!(r.start.offset, 5);
assert_eq!(r.end.offset, 10);
}
#[test]
fn range_operations() {
let r1 = Range::new(Position::new(5), Position::new(10));
let r2 = Range::new(Position::new(8), Position::new(15));
assert!(r1.overlaps(&r2));
assert_eq!(r1.union(&r2).start.offset, 5);
assert_eq!(r1.union(&r2).end.offset, 15);
let intersection = r1.intersection(&r2).unwrap();
assert_eq!(intersection.start.offset, 8);
assert_eq!(intersection.end.offset, 10);
}
#[test]
fn selection_direction() {
let sel = Selection::new(Position::new(10), Position::new(5));
assert!(sel.is_reversed());
assert_eq!(sel.range().start.offset, 5);
assert_eq!(sel.range().end.offset, 10);
}
#[test]
#[cfg(feature = "rope")]
fn position_builder_with_rope() {
let rope = ropey::Rope::from_str("Line 1\nLine 2\nLine 3");
let pos = PositionBuilder::new()
.line(2)
.column(1)
.build(&rope)
.unwrap();
assert_eq!(pos.offset, 7); }
#[test]
#[cfg(not(feature = "rope"))]
fn position_builder_offset() {
let pos = PositionBuilder::new().offset(42).build().unwrap();
assert_eq!(pos.offset, 42);
}
}