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}