Skip to main content

open_cypher/
span.rs

1//! Source locations used throughout the lexer, parser, and syntax tree.
2
3use std::fmt;
4use std::ops::Range;
5
6/// A half-open byte range into a UTF-8 source string.
7///
8/// `start` is inclusive and `end` is exclusive. Offsets are bytes, rather
9/// than character or display columns, so a span can be used directly to slice
10/// the original query after checking that both endpoints are UTF-8 boundaries.
11#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
12#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
13pub struct Span {
14    /// Inclusive byte offset.
15    pub start: usize,
16    /// Exclusive byte offset.
17    pub end: usize,
18}
19
20impl Span {
21    /// Creates a span from an inclusive start and exclusive end offset.
22    ///
23    /// # Panics
24    ///
25    /// Panics if `start > end`.
26    #[must_use]
27    pub const fn new(start: usize, end: usize) -> Self {
28        assert!(start <= end, "a span cannot end before it starts");
29        Self { start, end }
30    }
31
32    /// Creates an empty span at `offset`.
33    #[must_use]
34    pub const fn empty(offset: usize) -> Self {
35        Self::new(offset, offset)
36    }
37
38    /// Returns the inclusive start byte offset.
39    #[must_use]
40    pub const fn start(self) -> usize {
41        self.start
42    }
43
44    /// Returns the exclusive end byte offset.
45    #[must_use]
46    pub const fn end(self) -> usize {
47        self.end
48    }
49
50    /// Returns the number of source bytes covered by this span.
51    #[must_use]
52    pub const fn len(self) -> usize {
53        self.end - self.start
54    }
55
56    /// Returns `true` when this span covers no source bytes.
57    #[must_use]
58    pub const fn is_empty(self) -> bool {
59        self.start == self.end
60    }
61
62    /// Returns `true` when `offset` is inside this half-open span.
63    #[must_use]
64    pub const fn contains(self, offset: usize) -> bool {
65        self.start <= offset && offset < self.end
66    }
67
68    /// Returns `true` when `other` is entirely contained in this span.
69    #[must_use]
70    pub const fn contains_span(self, other: Self) -> bool {
71        self.start <= other.start && other.end <= self.end
72    }
73
74    /// Returns the smallest span covering both inputs.
75    #[must_use]
76    pub const fn cover(self, other: Self) -> Self {
77        Self::new(
78            if self.start < other.start {
79                self.start
80            } else {
81                other.start
82            },
83            if self.end > other.end {
84                self.end
85            } else {
86                other.end
87            },
88        )
89    }
90
91    /// Returns this span as a standard half-open range.
92    #[must_use]
93    pub const fn range(self) -> Range<usize> {
94        self.start..self.end
95    }
96
97    /// Returns the source text covered by this span.
98    ///
99    /// `None` is returned if the span is outside `source` or either endpoint
100    /// falls in the middle of a UTF-8 code point.
101    #[must_use]
102    pub fn text(self, source: &str) -> Option<&str> {
103        source.get(self.range())
104    }
105}
106
107impl From<Range<usize>> for Span {
108    fn from(range: Range<usize>) -> Self {
109        Self::new(range.start, range.end)
110    }
111}
112
113impl From<Span> for Range<usize> {
114    fn from(span: Span) -> Self {
115        span.range()
116    }
117}
118
119impl fmt::Debug for Span {
120    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
121        write!(formatter, "{}..{}", self.start, self.end)
122    }
123}
124
125impl fmt::Display for Span {
126    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
127        fmt::Debug::fmt(self, formatter)
128    }
129}