ass_editor/core/position/range.rs
1//! Document range type built from start/end [`Position`] values.
2//!
3//! Defines [`Range`], a half-open interval `[start, end)` with
4//! containment, overlap, union, and intersection operations.
5
6use super::Position;
7use core::cmp::{max, min};
8use core::fmt;
9
10/// A range in a document represented by start and end positions
11///
12/// Ranges are half-open intervals [start, end) where start is inclusive
13/// and end is exclusive. This matches standard text editor conventions.
14///
15/// # Examples
16///
17/// ```
18/// use ass_editor::{Position, Range, EditorDocument};
19///
20/// let doc = EditorDocument::from_content("Hello World").unwrap();
21/// let range = Range::new(Position::new(0), Position::new(5)); // "Hello"
22///
23/// // Basic properties
24/// assert_eq!(range.len(), 5);
25/// assert!(!range.is_empty());
26/// assert!(range.contains(Position::new(2)));
27/// assert!(!range.contains(Position::new(5))); // End is exclusive
28///
29/// // Range operations
30/// let other = Range::new(Position::new(3), Position::new(8)); // "lo Wo"
31/// assert!(range.overlaps(&other));
32///
33/// let union = range.union(&other);
34/// assert_eq!(union.start.offset, 0);
35/// assert_eq!(union.end.offset, 8);
36/// ```
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub struct Range {
39 /// Start position (inclusive)
40 pub start: Position,
41 /// End position (exclusive)
42 pub end: Position,
43}
44
45impl Range {
46 /// Create a new range
47 ///
48 /// Automatically normalizes so start <= end
49 #[must_use]
50 pub fn new(start: Position, end: Position) -> Self {
51 if start.offset <= end.offset {
52 Self { start, end }
53 } else {
54 Self {
55 start: end,
56 end: start,
57 }
58 }
59 }
60
61 /// Create an empty range at position
62 #[must_use]
63 pub const fn empty(pos: Position) -> Self {
64 Self {
65 start: pos,
66 end: pos,
67 }
68 }
69
70 /// Check if range is empty (start == end)
71 #[must_use]
72 pub const fn is_empty(&self) -> bool {
73 self.start.offset == self.end.offset
74 }
75
76 /// Get the length of the range in bytes
77 #[must_use]
78 pub const fn len(&self) -> usize {
79 self.end.offset.saturating_sub(self.start.offset)
80 }
81
82 /// Check if range contains a position
83 #[must_use]
84 pub const fn contains(&self, pos: Position) -> bool {
85 pos.offset >= self.start.offset && pos.offset < self.end.offset
86 }
87
88 /// Check if this range overlaps with another
89 #[must_use]
90 pub const fn overlaps(&self, other: &Self) -> bool {
91 self.start.offset < other.end.offset && other.start.offset < self.end.offset
92 }
93
94 /// Extend range to include a position
95 #[must_use]
96 pub fn extend_to(&self, pos: Position) -> Self {
97 Self {
98 start: Position::new(min(self.start.offset, pos.offset)),
99 end: Position::new(max(self.end.offset, pos.offset)),
100 }
101 }
102
103 /// Get the union of two ranges (smallest range containing both)
104 #[must_use]
105 pub fn union(&self, other: &Self) -> Self {
106 Self {
107 start: Position::new(min(self.start.offset, other.start.offset)),
108 end: Position::new(max(self.end.offset, other.end.offset)),
109 }
110 }
111
112 /// Get the intersection of two ranges if they overlap
113 #[must_use]
114 pub fn intersection(&self, other: &Self) -> Option<Self> {
115 let start = max(self.start.offset, other.start.offset);
116 let end = min(self.end.offset, other.end.offset);
117
118 if start < end {
119 Some(Self::new(Position::new(start), Position::new(end)))
120 } else {
121 None
122 }
123 }
124}
125
126impl fmt::Display for Range {
127 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
128 if self.is_empty() {
129 write!(f, "{}", self.start)
130 } else {
131 write!(f, "{}-{}", self.start, self.end)
132 }
133 }
134}