Skip to main content

cyrs_syntax/
range_ext.rs

1//! Extension trait sugar for [`TextRange`].
2//!
3//! cyrs uses byte offsets for every span (spec §4); line/column conversion
4//! is owned by [`LineIndex`](crate::LineIndex) and only happens at LSP and
5//! diagnostic-render boundaries. The helpers in this module deduplicate the
6//! handful of one-line conversions that consumer crates were rolling
7//! themselves (`TextRange` → `Range<usize>`, `TextRange` → `Range<u32>`,
8//! and the half-open intersection predicate).
9
10use crate::TextRange;
11
12/// Convenience conversions on top of [`TextRange`].
13///
14/// `TextRange` already exposes [`start()`](TextRange::start) and
15/// [`end()`](TextRange::end) returning `TextSize`, plus `Into<u32>` /
16/// `Into<usize>` on `TextSize`. This trait just bundles the two-line
17/// adapter that consumers kept duplicating into a single call site.
18pub trait TextRangeExt {
19    /// Convert to a half-open byte range suitable for slicing `&str` or
20    /// passing to `codespan_reporting`. Equivalent to
21    /// `usize::from(self.start())..usize::from(self.end())`.
22    fn as_byte_range(&self) -> std::ops::Range<usize>;
23
24    /// Convert to a half-open `u32` range. Useful for FFI / wire formats
25    /// that prefer `u32` offsets over `usize`.
26    fn as_u32_range(&self) -> std::ops::Range<u32>;
27
28    /// Returns `true` if the two ranges share at least one byte position
29    /// under the half-open convention. Zero-width ranges are handled
30    /// correctly — a caret at `10..10` intersects `10..15` but not
31    /// `5..9`.
32    fn intersects(&self, other: TextRange) -> bool;
33}
34
35impl TextRangeExt for TextRange {
36    #[inline]
37    fn as_byte_range(&self) -> std::ops::Range<usize> {
38        usize::from(self.start())..usize::from(self.end())
39    }
40
41    #[inline]
42    fn as_u32_range(&self) -> std::ops::Range<u32> {
43        u32::from(self.start())..u32::from(self.end())
44    }
45
46    #[inline]
47    fn intersects(&self, other: TextRange) -> bool {
48        self.start() <= other.end() && other.start() <= self.end()
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::TextSize;
56
57    fn range(start: u32, end: u32) -> TextRange {
58        TextRange::new(TextSize::from(start), TextSize::from(end))
59    }
60
61    #[test]
62    fn as_byte_range_matches_manual_conversion() {
63        let r = range(3, 17);
64        assert_eq!(r.as_byte_range(), 3usize..17usize);
65        // Equivalent to the inline helper consumers used to roll.
66        assert_eq!(
67            r.as_byte_range(),
68            usize::from(r.start())..usize::from(r.end())
69        );
70    }
71
72    #[test]
73    fn as_u32_range_matches_manual_conversion() {
74        let r = range(0, 42);
75        assert_eq!(r.as_u32_range(), 0u32..42u32);
76    }
77
78    #[test]
79    fn intersects_overlapping_ranges() {
80        assert!(range(0, 10).intersects(range(5, 15)));
81        assert!(range(5, 15).intersects(range(0, 10)));
82    }
83
84    #[test]
85    fn intersects_touching_endpoints_is_true() {
86        // Half-open: [0,10) and [10,20) touch at 10. The legacy helpers
87        // returned true for this and we preserve that.
88        assert!(range(0, 10).intersects(range(10, 20)));
89    }
90
91    #[test]
92    fn intersects_disjoint_returns_false() {
93        assert!(!range(0, 5).intersects(range(6, 10)));
94        assert!(!range(6, 10).intersects(range(0, 5)));
95    }
96
97    #[test]
98    fn zero_width_caret_intersects_containing_range() {
99        // Caret at 10..10 inside 10..15 → true.
100        assert!(range(10, 10).intersects(range(10, 15)));
101        // Caret at 10..10 outside 5..9 → false.
102        assert!(!range(10, 10).intersects(range(5, 9)));
103    }
104}