Skip to main content

perl_position_tracking/
span.rs

1//! Byte-based span types for source location tracking.
2//!
3//! This module provides foundational span types used throughout the Perl LSP
4//! ecosystem for tracking source locations. These types use byte offsets,
5//! which are efficient for the parser but must be converted to line/character
6//! positions for LSP communication.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use std::ops::Range;
11
12/// A byte-based span representing a range in source text.
13///
14/// `ByteSpan` uses byte offsets (not character or line positions) for precise
15/// and efficient source location tracking. For LSP communication, use
16/// [`WireRange`](crate::WireRange) or convert via [`LineStartsCache`](crate::LineStartsCache).
17///
18/// # Invariants
19///
20/// - `start <= end` (enforced by constructors, but not at type level for Copy)
21/// - Both `start` and `end` are valid byte offsets in the source text
22/// - Spans are half-open intervals: `[start, end)`
23///
24/// # Example
25///
26/// ```
27/// use perl_position_tracking::ByteSpan;
28///
29/// let span = ByteSpan::new(0, 10);
30/// assert_eq!(span.len(), 10);
31/// assert!(!span.is_empty());
32///
33/// // Extract the spanned text
34/// let source = "hello world";
35/// let text = span.slice(source);
36/// assert_eq!(text, "hello worl");
37/// ```
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
39pub struct ByteSpan {
40    /// Starting byte offset in the source text (inclusive)
41    pub start: usize,
42    /// Ending byte offset in the source text (exclusive)
43    pub end: usize,
44}
45
46impl ByteSpan {
47    /// Creates a new `ByteSpan` with the given start and end offsets.
48    ///
49    /// # Panics
50    ///
51    /// Panics in debug mode if `start > end`.
52    #[inline]
53    pub fn new(start: usize, end: usize) -> Self {
54        debug_assert!(start <= end, "ByteSpan: start ({}) > end ({})", start, end);
55        Self { start, end }
56    }
57
58    /// Creates an empty span at the given position.
59    #[inline]
60    pub const fn empty(pos: usize) -> Self {
61        Self { start: pos, end: pos }
62    }
63
64    /// Creates a span covering the entire source text.
65    #[inline]
66    pub fn whole(source: &str) -> Self {
67        Self { start: 0, end: source.len() }
68    }
69
70    /// Returns the length of this span in bytes.
71    #[inline]
72    pub const fn len(&self) -> usize {
73        self.end - self.start
74    }
75
76    /// Returns true if this span is empty (start == end).
77    #[inline]
78    pub const fn is_empty(&self) -> bool {
79        self.start == self.end
80    }
81
82    /// Returns true if this span contains the given byte offset.
83    #[inline]
84    pub const fn contains(&self, offset: usize) -> bool {
85        offset >= self.start && offset < self.end
86    }
87
88    /// Returns true if this span contains the given span entirely.
89    #[inline]
90    pub const fn contains_span(&self, other: ByteSpan) -> bool {
91        self.start <= other.start && other.end <= self.end
92    }
93
94    /// Returns true if this span overlaps with the given span.
95    #[inline]
96    pub const fn overlaps(&self, other: ByteSpan) -> bool {
97        self.start < other.end && other.start < self.end
98    }
99
100    /// Returns the intersection of two spans, or None if they don't overlap.
101    pub fn intersection(&self, other: ByteSpan) -> Option<ByteSpan> {
102        let start = self.start.max(other.start);
103        let end = self.end.min(other.end);
104        if start < end { Some(ByteSpan { start, end }) } else { None }
105    }
106
107    /// Returns a new span that covers both this span and the given span.
108    #[inline]
109    pub fn union(&self, other: ByteSpan) -> ByteSpan {
110        ByteSpan { start: self.start.min(other.start), end: self.end.max(other.end) }
111    }
112
113    /// Extracts the slice of source text covered by this span.
114    ///
115    /// # Panics
116    ///
117    /// Panics if the span is out of bounds for the source text.
118    #[inline]
119    pub fn slice<'a>(&self, source: &'a str) -> &'a str {
120        &source[self.start..self.end]
121    }
122
123    /// Safely extracts the slice of source text, returning None if out of bounds.
124    #[inline]
125    pub fn try_slice<'a>(&self, source: &'a str) -> Option<&'a str> {
126        source.get(self.start..self.end)
127    }
128
129    /// Converts to a standard Range.
130    #[inline]
131    pub const fn to_range(&self) -> Range<usize> {
132        self.start..self.end
133    }
134}
135
136impl fmt::Display for ByteSpan {
137    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138        write!(f, "{}..{}", self.start, self.end)
139    }
140}
141
142impl From<Range<usize>> for ByteSpan {
143    #[inline]
144    fn from(range: Range<usize>) -> Self {
145        Self::new(range.start, range.end)
146    }
147}
148
149impl From<ByteSpan> for Range<usize> {
150    #[inline]
151    fn from(span: ByteSpan) -> Self {
152        span.start..span.end
153    }
154}
155
156impl From<(usize, usize)> for ByteSpan {
157    #[inline]
158    fn from((start, end): (usize, usize)) -> Self {
159        Self::new(start, end)
160    }
161}
162
163impl From<ByteSpan> for (usize, usize) {
164    #[inline]
165    fn from(span: ByteSpan) -> Self {
166        (span.start, span.end)
167    }
168}
169
170/// Type alias for backward compatibility with `SourceLocation`.
171///
172/// New code should use [`ByteSpan`] directly.
173pub type SourceLocation = ByteSpan;
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_byte_span_basics() {
181        let span = ByteSpan::new(5, 10);
182        assert_eq!(span.start, 5);
183        assert_eq!(span.end, 10);
184        assert_eq!(span.len(), 5);
185        assert!(!span.is_empty());
186    }
187
188    #[test]
189    fn test_empty_span() {
190        let span = ByteSpan::empty(5);
191        assert_eq!(span.start, 5);
192        assert_eq!(span.end, 5);
193        assert_eq!(span.len(), 0);
194        assert!(span.is_empty());
195    }
196
197    #[test]
198    fn test_contains() {
199        let span = ByteSpan::new(5, 10);
200        assert!(!span.contains(4));
201        assert!(span.contains(5));
202        assert!(span.contains(9));
203        assert!(!span.contains(10)); // end is exclusive
204    }
205
206    #[test]
207    fn test_contains_span() {
208        let outer = ByteSpan::new(0, 20);
209        let inner = ByteSpan::new(5, 15);
210        let partial = ByteSpan::new(15, 25);
211
212        assert!(outer.contains_span(inner));
213        assert!(!inner.contains_span(outer));
214        assert!(!outer.contains_span(partial));
215    }
216
217    #[test]
218    fn test_overlaps() {
219        let a = ByteSpan::new(0, 10);
220        let b = ByteSpan::new(5, 15);
221        let c = ByteSpan::new(10, 20);
222        let d = ByteSpan::new(15, 25);
223
224        assert!(a.overlaps(b)); // partial overlap
225        assert!(!a.overlaps(c)); // adjacent (no overlap)
226        assert!(!a.overlaps(d)); // disjoint
227    }
228
229    #[test]
230    fn test_intersection() {
231        let a = ByteSpan::new(0, 10);
232        let b = ByteSpan::new(5, 15);
233
234        assert_eq!(a.intersection(b), Some(ByteSpan::new(5, 10)));
235        assert_eq!(a.intersection(ByteSpan::new(10, 20)), None);
236    }
237
238    #[test]
239    fn test_union() {
240        let a = ByteSpan::new(0, 10);
241        let b = ByteSpan::new(5, 15);
242
243        assert_eq!(a.union(b), ByteSpan::new(0, 15));
244    }
245
246    #[test]
247    fn test_slice() {
248        let source = "hello world";
249        let span = ByteSpan::new(0, 5);
250        assert_eq!(span.slice(source), "hello");
251    }
252
253    #[test]
254    fn test_conversions() {
255        let span = ByteSpan::new(5, 10);
256
257        // To/from Range
258        let range: Range<usize> = span.into();
259        assert_eq!(range, 5..10);
260        let span2: ByteSpan = (5..10).into();
261        assert_eq!(span, span2);
262
263        // To/from tuple
264        let tuple: (usize, usize) = span.into();
265        assert_eq!(tuple, (5, 10));
266        let span3: ByteSpan = (5, 10).into();
267        assert_eq!(span, span3);
268    }
269
270    #[test]
271    fn test_display() {
272        let span = ByteSpan::new(5, 10);
273        assert_eq!(format!("{}", span), "5..10");
274    }
275}