Skip to main content

harper_core/
span.rs

1use std::{fmt::Display, marker::PhantomData, ops::Range};
2
3use serde::{Deserialize, Serialize};
4
5use crate::Token;
6
7/// A window in a `T` sequence.
8///
9/// Note that the range covered by a [`Span`] is end-exclusive, meaning that the end index is not
10/// included in the range covered by the [`Span`]. If you're familiar with the Rust range syntax,
11/// you could say the span covers the equivalent of `start..end`, *not* `start..=end`.
12///
13/// For a [`Span`] to be correct, its end index must be greater than or equal to its start
14/// index. Creating or using a [`Span`] which does not follow this rule may lead to unexpected
15/// behavior or panics.
16///
17/// Although specific to `harper.js`, [this page may clear up any questions you have](https://writewithharper.com/docs/harperjs/spans).
18#[derive(Debug, Serialize, Deserialize, Default, PartialEq, Eq, Hash)]
19pub struct Span<T> {
20    /// The start index of the span.
21    pub start: usize,
22    /// The end index of the span.
23    ///
24    /// Note that [`Span`] represents an exclusive range. This means that a `Span::new(0, 5)` will
25    /// cover the values `0, 1, 2, 3, 4`; it will not cover the `5`.
26    pub end: usize,
27    #[serde(skip)]
28    span_type: PhantomData<T>,
29}
30
31impl<T> Span<T> {
32    /// A [`Span`] with a start and end index of 0.
33    pub const ZERO: Self = Self::empty(0);
34
35    /// A [`Span`] of `usize::MIN..usize::MAX`.
36    pub(crate) const FULL: Self = Self {
37        start: usize::MIN,
38        end: usize::MAX,
39        span_type: PhantomData,
40    };
41
42    /// Creates a new [`Span`] with the provided start and end indices.
43    ///
44    /// # Panics
45    ///
46    /// This will panic if `start` is greater than `end`.
47    pub fn new(start: usize, end: usize) -> Self {
48        if start > end {
49            panic!("{start} > {end}");
50        }
51        Self {
52            start,
53            end,
54            span_type: PhantomData,
55        }
56    }
57
58    /// Creates a new [`Span`] from the provided start position and length.
59    pub fn new_with_len(start: usize, len: usize) -> Self {
60        Self {
61            start,
62            end: start + len,
63            span_type: PhantomData,
64        }
65    }
66
67    /// Creates a new empty [`Span`] with the provided position.
68    pub const fn empty(pos: usize) -> Self {
69        Self {
70            start: pos,
71            end: pos,
72            span_type: PhantomData,
73        }
74    }
75
76    /// The length of the [`Span`].
77    pub fn len(&self) -> usize {
78        self.end - self.start
79    }
80
81    /// Checks whether the [`Span`] is empty.
82    ///
83    /// A [`Span`] is considered empty if it has a length of 0.
84    pub fn is_empty(&self) -> bool {
85        self.len() == 0
86    }
87
88    /// Checks whether `idx` is within the range of the span.
89    pub fn contains(&self, idx: usize) -> bool {
90        self.start <= idx && idx < self.end
91    }
92
93    /// Checks whether this span's range overlaps with `other`.
94    pub fn overlaps_with(&self, other: Self) -> bool {
95        (self.start < other.end) && (other.start < self.end)
96    }
97
98    /// Get the associated content. Will return [`None`] if the span is non-empty and any aspect is
99    /// invalid.
100    pub fn try_get_content<'a>(&self, source: &'a [T]) -> Option<&'a [T]> {
101        if self.is_empty() {
102            Some(&source[0..0])
103        } else {
104            source.get(self.start..self.end)
105        }
106    }
107
108    /// Expand the span by either modifying [`Self::start`] or [`Self::end`] to include the target
109    /// index.
110    ///
111    /// Does nothing if the span already includes the target.
112    pub fn expand_to_include(&mut self, target: usize) {
113        if target < self.start {
114            self.start = target;
115        } else if target >= self.end {
116            self.end = target + 1;
117        }
118    }
119
120    /// Get the associated content. Will panic if any aspect is invalid.
121    pub fn get_content<'a>(&self, source: &'a [T]) -> &'a [T] {
122        match self.try_get_content(source) {
123            Some(v) => v,
124            None => panic!("Failed to get content for span."),
125        }
126    }
127
128    /// Set the span's length.
129    pub fn set_len(&mut self, length: usize) {
130        self.end = self.start + length;
131    }
132
133    /// Returns a copy of this [`Span`] with a new length.
134    pub fn with_len(&self, length: usize) -> Self {
135        let mut cloned = *self;
136        cloned.set_len(length);
137        cloned
138    }
139
140    /// Add an amount to both [`Self::start`] and [`Self::end`]
141    pub fn push_by(&mut self, by: usize) {
142        self.start += by;
143        self.end += by;
144    }
145
146    /// Subtract an amount from both [`Self::start`] and [`Self::end`]
147    pub fn pull_by(&mut self, by: usize) {
148        self.start -= by;
149        self.end -= by;
150    }
151
152    /// Add an amount to a copy of both [`Self::start`] and [`Self::end`]
153    pub fn pushed_by(&self, by: usize) -> Self {
154        let mut clone = *self;
155        clone.start += by;
156        clone.end += by;
157        clone
158    }
159
160    /// Subtract an amount to a copy of both [`Self::start`] and [`Self::end`]
161    pub fn pulled_by(&self, by: usize) -> Option<Self> {
162        if by > self.start {
163            return None;
164        }
165
166        let mut clone = *self;
167        clone.start -= by;
168        clone.end -= by;
169        Some(clone)
170    }
171}
172
173/// Additional functions for types that implement [`std::fmt::Debug`] and [`Display`].
174impl<T: Display + std::fmt::Debug> Span<T> {
175    /// Gets the content of this [`Span<T>`] as a [`String`].
176    pub fn get_content_string(&self, source: &[T]) -> String {
177        if let Some(content) = self.try_get_content(source) {
178            content.iter().map(|t| t.to_string()).collect()
179        } else {
180            panic!("Could not get position {self:?} within \"{source:?}\"")
181        }
182    }
183}
184
185/// Functionality specific to [`Token`] spans.
186impl Span<Token> {
187    /// Converts the [`Span<Token>`] into a [`Span<char>`].
188    ///
189    /// This requires knowing the character spans of the tokens covered by this
190    /// [`Span<Token>`]. Because of this, a reference to the source token sequence used to create
191    /// this span is required.
192    pub fn to_char_span(&self, source_document_tokens: &[Token]) -> Span<char> {
193        if self.is_empty() {
194            Span::ZERO
195        } else {
196            let target_tokens = &source_document_tokens[self.start..self.end];
197            Span::new(
198                target_tokens.first().unwrap().span.start,
199                target_tokens.last().unwrap().span.end,
200            )
201        }
202    }
203}
204
205impl<T> From<Range<usize>> for Span<T> {
206    /// Reinterprets the provided [`std::ops::Range`] as a [`Span`].
207    fn from(value: Range<usize>) -> Self {
208        Self::new(value.start, value.end)
209    }
210}
211
212impl<T> From<Span<T>> for Range<usize> {
213    /// Converts the [`Span`] to an [`std::ops::Range`].
214    fn from(value: Span<T>) -> Self {
215        value.start..value.end
216    }
217}
218
219impl<T> IntoIterator for Span<T> {
220    type Item = usize;
221
222    type IntoIter = Range<usize>;
223
224    /// Converts the [`Span`] into an iterator that yields the indices covered by its range.
225    ///
226    /// Note that [`Span`] is half-open, meaning that the value [`Self::end`] will not be yielded
227    /// by this iterator: it will stop at the index immediately preceding [`Self::end`].
228    fn into_iter(self) -> Self::IntoIter {
229        self.start..self.end
230    }
231}
232
233impl<T> Clone for Span<T> {
234    // Note: manual implementation so we don't unnecessarily require `T` to impl `Clone`.
235    fn clone(&self) -> Self {
236        *self
237    }
238}
239impl<T> Copy for Span<T> {}
240
241#[cfg(test)]
242mod tests {
243    use crate::{
244        Document,
245        expr::{ExprExt, SequenceExpr},
246    };
247
248    use super::Span;
249
250    type UntypedSpan = Span<()>;
251
252    #[test]
253    fn overlaps() {
254        assert!(UntypedSpan::new(0, 5).overlaps_with(UntypedSpan::new(3, 6)));
255        assert!(UntypedSpan::new(0, 5).overlaps_with(UntypedSpan::new(2, 3)));
256        assert!(UntypedSpan::new(0, 5).overlaps_with(UntypedSpan::new(4, 5)));
257        assert!(UntypedSpan::new(0, 5).overlaps_with(UntypedSpan::new(4, 4)));
258
259        assert!(!UntypedSpan::new(0, 3).overlaps_with(UntypedSpan::new(3, 5)));
260    }
261
262    #[test]
263    fn expands_properly() {
264        let mut span = UntypedSpan::new(2, 2);
265
266        span.expand_to_include(1);
267        assert_eq!(span, UntypedSpan::new(1, 2));
268
269        span.expand_to_include(2);
270        assert_eq!(span, UntypedSpan::new(1, 3));
271    }
272
273    #[test]
274    fn to_char_span_converts_correctly() {
275        let doc = Document::new_plain_english_curated("Hello world!");
276
277        // Empty span.
278        let token_span = Span::ZERO;
279        let converted = token_span.to_char_span(doc.get_tokens());
280        assert!(converted.is_empty());
281
282        // Span from `Expr`.
283        let token_span = SequenceExpr::any_word()
284            .t_ws()
285            .then_any_word()
286            .iter_matches_in_doc(&doc)
287            .next()
288            .unwrap();
289        let converted = token_span.to_char_span(doc.get_tokens());
290        assert_eq!(
291            converted.get_content_string(doc.get_source()),
292            "Hello world"
293        );
294    }
295}