Skip to main content

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}