Skip to main content

praxis_syntax/
span_bridge.rs

1//! Bridge between Praxis spans and rowan's text offsets.
2//!
3//! Praxis `Span`/`BytePos` are the source of truth for diagnostics and never
4//! leak rowan types outward. Internally, rowan indexes the tree with its own
5//! `TextSize`/`TextRange` (also byte offsets into UTF-8). Neither type is local
6//! to this crate, so the orphan rule forbids `From` impls between them; these
7//! free functions are the only place the two meet (ADR-003).
8//!
9//! Prefer the explicit `span_to_range` / `range_to_span` calls at the tree
10//! boundary over implicit conversions, so every crossing is visible.
11
12use praxis_source::{BytePos, Span};
13use rowan::{TextRange, TextSize};
14
15/// Convert a Praxis [`Span`] into a rowan [`TextRange`].
16///
17/// `Span` is never inverted (start ≤ end by construction), so this cannot panic
18/// on `TextRange::new`'s internal assertion.
19#[inline]
20#[must_use]
21pub fn span_to_range(span: Span) -> TextRange {
22    TextRange::new(
23        TextSize::from(span.start().to_u32()),
24        TextSize::from(span.end().to_u32()),
25    )
26}
27
28/// Convert a rowan [`TextRange`] back into a Praxis [`Span`].
29#[inline]
30#[must_use]
31pub fn range_to_span(range: TextRange) -> Span {
32    Span::new(
33        BytePos::from(u32::from(range.start())),
34        BytePos::from(u32::from(range.end())),
35    )
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn span_round_trips_through_textrange() {
44        for (start, end) in [(0, 0), (0, 3), (5, 5), (10, 42)] {
45            let span = Span::new(BytePos::from(start), BytePos::from(end));
46            let range = span_to_range(span);
47            let back = range_to_span(range);
48            assert_eq!(back, span);
49        }
50    }
51
52    #[test]
53    fn empty_span_maps_to_empty_range() {
54        let span = Span::new(BytePos::from(7), BytePos::from(7));
55        let range = span_to_range(span);
56        assert!(range.is_empty());
57    }
58}