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}