roan_error/
span.rs

1use crate::position::Position;
2
3/// Represents a span of text between two positions, including the literal text.
4#[derive(Clone, PartialEq, Eq)]
5pub struct TextSpan {
6    /// The starting position of the text span.
7    pub start: Position,
8    /// The ending position of the text span.
9    pub end: Position,
10    /// The literal text contained in the span.
11    pub literal: String,
12}
13
14impl TextSpan {
15    /// Creates a new `TextSpan` from a starting position, an ending position, and a literal string.
16    ///
17    /// # Arguments
18    ///
19    /// * `start` - The starting position of the text span.
20    /// * `end` - The ending position of the text span.
21    /// * `literal` - The literal text represented by the span.
22    ///
23    /// # Example
24    ///
25    /// ```
26    /// use roan_error::{Position, TextSpan};
27    /// let start = Position::new(1, 1, 0);
28    /// let end = Position::new(1, 5, 4);
29    /// let span = TextSpan::new(start, end, "test".to_string());
30    /// assert_eq!(span.length(), 4);
31    /// ```
32    pub fn new(start: Position, end: Position, literal: String) -> Self {
33        Self {
34            start,
35            end,
36            literal,
37        }
38    }
39
40    /// Combines multiple `TextSpan` objects into one. The spans are sorted by their starting positions.
41    ///
42    /// # Panics
43    ///
44    /// Panics if the input vector is empty.
45    ///
46    /// # Arguments
47    ///
48    /// * `spans` - A vector of `TextSpan` objects to combine.
49    ///
50    /// # Returns
51    ///
52    /// A new `TextSpan` that spans from the start of the first span to the end of the last span,
53    /// with the concatenated literal text.
54    ///
55    /// # Example
56    ///
57    /// ```
58    /// use roan_error::{Position, TextSpan};
59    /// let span1 = TextSpan::new(Position::new(1, 1, 0), Position::new(1, 5, 4), "test".to_string());
60    /// let span2 = TextSpan::new(Position::new(1, 6, 5), Position::new(1, 10, 9), "span".to_string());
61    /// let combined = TextSpan::combine(vec![span1, span2]);
62    /// assert_eq!(combined.unwrap().literal, "testspan");
63    /// ```
64    pub fn combine(mut spans: Vec<TextSpan>) -> Option<TextSpan> {
65        if spans.is_empty() {
66            return None;
67        }
68
69        spans.sort_by(|a, b| a.start.index.cmp(&b.start.index));
70
71        let start = spans.first().unwrap().start;
72        let end = spans.last().unwrap().end;
73
74        Some(TextSpan::new(
75            start,
76            end,
77            spans.into_iter().map(|span| span.literal).collect(),
78        ))
79    }
80
81    /// Returns the length of the span, calculated as the difference between the end and start indices.
82    ///
83    /// # Returns
84    ///
85    /// The length of the text span in bytes.
86    pub fn length(&self) -> usize {
87        self.end.index - self.start.index
88    }
89
90    /// Extracts the literal text from the given input string based on the start and end positions.
91    ///
92    /// # Arguments
93    ///
94    /// * `input` - The input string from which to extract the literal text.
95    ///
96    /// # Returns
97    ///
98    /// A slice of the input string that corresponds to the span's range.
99    pub fn literal<'a>(&self, input: &'a str) -> &'a str {
100        &input[self.start.index..self.end.index]
101    }
102}
103
104impl Default for TextSpan {
105    /// Creates a new `TextSpan` with default values.
106    ///
107    /// # Returns
108    ///
109    /// A new `TextSpan` with the starting and ending positions set to `(0, 0, 0)` and an empty string.
110    fn default() -> Self {
111        Self {
112            start: Position::default(),
113            end: Position::default(),
114            literal: String::new(),
115        }
116    }
117}
118
119impl std::fmt::Debug for TextSpan {
120    /// Formats the `TextSpan` as `"literal" (line:column)`.
121    ///
122    /// # Example
123    ///
124    /// ```
125    /// use roan_error::{Position, TextSpan};
126    /// let span = TextSpan::new(Position::new(1, 1, 0), Position::new(1, 5, 4), "test".to_string());
127    /// assert_eq!(format!("{:?}", span), "\"test\" (1:1)");
128    /// ```
129    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
130        write!(
131            f,
132            "\"{}\" ({}:{})",
133            self.literal, self.start.line, self.start.column
134        )
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn test_new() {
144        let start = Position::new(1, 1, 0);
145        let end = Position::new(1, 5, 4);
146        let span = TextSpan::new(start, end, "test".to_string());
147        assert_eq!(span.start, start);
148        assert_eq!(span.end, end);
149        assert_eq!(span.literal, "test");
150    }
151
152    #[test]
153    fn test_combine() {
154        let span1 = TextSpan::new(
155            Position::new(1, 1, 0),
156            Position::new(1, 5, 4),
157            "test".to_string(),
158        );
159        let span2 = TextSpan::new(
160            Position::new(1, 6, 5),
161            Position::new(1, 10, 9),
162            "span".to_string(),
163        );
164        let combined = TextSpan::combine(vec![span1, span2]).unwrap();
165        assert_eq!(combined.start, Position::new(1, 1, 0));
166        assert_eq!(combined.end, Position::new(1, 10, 9));
167        assert_eq!(combined.literal, "testspan");
168    }
169
170    #[test]
171    fn test_combine_empty() {
172        assert_eq!(TextSpan::combine(vec![]), None);
173    }
174
175    #[test]
176    fn test_length() {
177        let span = TextSpan::new(
178            Position::new(1, 1, 0),
179            Position::new(1, 5, 4),
180            "test".to_string(),
181        );
182        assert_eq!(span.length(), 4);
183    }
184
185    #[test]
186    fn test_literal() {
187        let span = TextSpan::new(
188            Position::new(1, 1, 0),
189            Position::new(1, 5, 4),
190            "test".to_string(),
191        );
192        assert_eq!(span.literal("test string"), "test");
193    }
194
195    #[test]
196    fn test_default() {
197        let span = TextSpan::default();
198        assert_eq!(span.start, Position::default());
199        assert_eq!(span.end, Position::default());
200        assert_eq!(span.literal, "");
201    }
202
203    #[test]
204    fn test_debug() {
205        let span = TextSpan::new(
206            Position::new(1, 1, 0),
207            Position::new(1, 5, 4),
208            "test".to_string(),
209        );
210
211        assert_eq!(format!("{:?}", span), "\"test\" (1:1)");
212    }
213}