1use crate::Span;
2
3const UTF8_BOM: &str = "\u{feff}";
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub struct SourceText {
7 text: String,
8 file_name: Option<String>,
9 content_start: usize,
10 line_starts: Vec<usize>,
11 lines: Vec<SourceLine>,
12}
13
14impl SourceText {
15 pub fn new(text: impl Into<String>) -> Self {
16 Self::from_parts(text.into(), None)
17 }
18
19 pub fn with_file_name(text: impl Into<String>, file_name: impl Into<String>) -> Self {
20 Self::from_parts(text.into(), Some(file_name.into()))
21 }
22
23 fn from_parts(text: String, file_name: Option<String>) -> Self {
24 let content_start = text.strip_prefix(UTF8_BOM).map_or(0, |_| UTF8_BOM.len());
25 let (line_starts, lines) = collect_lines(&text, content_start);
26
27 Self {
28 text,
29 file_name,
30 content_start,
31 line_starts,
32 lines,
33 }
34 }
35
36 pub fn as_str(&self) -> &str {
37 &self.text
38 }
39
40 pub fn file_name(&self) -> Option<&str> {
41 self.file_name.as_deref()
42 }
43
44 pub fn content(&self) -> &str {
45 &self.text[self.content_start..]
46 }
47
48 pub fn content_start(&self) -> usize {
49 self.content_start
50 }
51
52 pub fn content_span(&self) -> Span {
53 Span::new(self.content_start, self.text.len())
54 }
55
56 pub fn has_leading_bom(&self) -> bool {
57 self.content_start > 0
58 }
59
60 pub fn len(&self) -> usize {
61 self.text.len()
62 }
63
64 pub fn is_empty(&self) -> bool {
65 self.content().is_empty()
66 }
67
68 pub fn line_count(&self) -> usize {
69 self.lines.len()
70 }
71
72 pub fn line_starts(&self) -> &[usize] {
73 &self.line_starts
74 }
75
76 pub fn line(&self, index: usize) -> Option<SourceLine> {
77 self.lines.get(index).copied()
78 }
79
80 pub fn line_text(&self, index: usize) -> Option<&str> {
81 self.line(index)
82 .and_then(|line| self.slice(line.text_span()))
83 }
84
85 pub fn line_with_ending(&self, index: usize) -> Option<&str> {
86 self.line(index)
87 .and_then(|line| self.slice(line.full_span()))
88 }
89
90 pub fn line_ending(&self, index: usize) -> Option<LineEnding> {
91 self.line(index).and_then(|line| line.ending())
92 }
93
94 pub fn slice(&self, span: Span) -> Option<&str> {
95 if span.start > span.end || span.end > self.text.len() {
96 return None;
97 }
98 if !self.text.is_char_boundary(span.start) || !self.text.is_char_boundary(span.end) {
99 return None;
100 }
101 Some(&self.text[span.start..span.end])
102 }
103
104 pub fn line_column(&self, offset: usize) -> Option<LineColumn> {
105 if offset < self.content_start || offset > self.text.len() {
106 return None;
107 }
108 if !self.text.is_char_boundary(offset) {
109 return None;
110 }
111
112 let line_index = match self.line_starts.binary_search(&offset) {
113 Ok(index) => index,
114 Err(0) => return None,
115 Err(index) => index - 1,
116 };
117 let line = self.lines[line_index];
118 let column_offset = offset.min(line.text_span.end);
119 let column = self.text[line.text_span.start..column_offset]
120 .chars()
121 .count()
122 + 1;
123
124 Some(LineColumn::new(line_index + 1, column))
125 }
126
127 pub fn line_column_span(&self, span: Span) -> Option<LineColumnSpan> {
128 if span.start > span.end {
129 return None;
130 }
131
132 Some(LineColumnSpan {
133 start: self.line_column(span.start)?,
134 end: self.line_column(span.end)?,
135 })
136 }
137
138 pub fn byte_offset(&self, position: LineColumn) -> Option<usize> {
139 if position.line == 0 || position.column == 0 {
140 return None;
141 }
142
143 let line = self.lines.get(position.line - 1)?;
144 if position.column == 1 {
145 return Some(line.text_span.start);
146 }
147
148 let mut column = 1;
149 for (relative_offset, ch) in
150 self.text[line.text_span.start..line.text_span.end].char_indices()
151 {
152 column += 1;
153 let next_offset = line.text_span.start + relative_offset + ch.len_utf8();
154 if column == position.column {
155 return Some(next_offset);
156 }
157 }
158
159 None
160 }
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq)]
164pub struct SourceLine {
165 text_span: Span,
166 full_span: Span,
167 ending: Option<LineEnding>,
168}
169
170impl SourceLine {
171 pub fn text_span(self) -> Span {
172 self.text_span
173 }
174
175 pub fn full_span(self) -> Span {
176 self.full_span
177 }
178
179 pub fn start(self) -> usize {
180 self.text_span.start
181 }
182
183 pub fn text_end(self) -> usize {
184 self.text_span.end
185 }
186
187 pub fn end(self) -> usize {
188 self.full_span.end
189 }
190
191 pub fn ending(self) -> Option<LineEnding> {
192 self.ending
193 }
194}
195
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum LineEnding {
198 Lf,
199 Crlf,
200 Cr,
201}
202
203impl LineEnding {
204 pub fn as_str(self) -> &'static str {
205 match self {
206 Self::Lf => "\n",
207 Self::Crlf => "\r\n",
208 Self::Cr => "\r",
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214pub struct LineColumn {
215 pub line: usize,
216 pub column: usize,
217}
218
219impl LineColumn {
220 pub fn new(line: usize, column: usize) -> Self {
221 Self { line, column }
222 }
223}
224
225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
226pub struct LineColumnSpan {
227 pub start: LineColumn,
228 pub end: LineColumn,
229}
230
231fn collect_lines(text: &str, content_start: usize) -> (Vec<usize>, Vec<SourceLine>) {
232 let mut line_starts = Vec::new();
233 let mut lines = Vec::new();
234 let bytes = text.as_bytes();
235 let mut line_start = content_start;
236 let mut index = content_start;
237
238 while index < bytes.len() {
239 let (ending, ending_len) = match bytes[index] {
240 b'\n' => (Some(LineEnding::Lf), 1),
241 b'\r' if bytes.get(index + 1) == Some(&b'\n') => (Some(LineEnding::Crlf), 2),
242 b'\r' => (Some(LineEnding::Cr), 1),
243 _ => {
244 index += 1;
245 continue;
246 }
247 };
248
249 push_line(
250 &mut line_starts,
251 &mut lines,
252 line_start,
253 index,
254 index + ending_len,
255 ending,
256 );
257 index += ending_len;
258 line_start = index;
259 }
260
261 push_line(
262 &mut line_starts,
263 &mut lines,
264 line_start,
265 text.len(),
266 text.len(),
267 None,
268 );
269
270 (line_starts, lines)
271}
272
273fn push_line(
274 line_starts: &mut Vec<usize>,
275 lines: &mut Vec<SourceLine>,
276 start: usize,
277 text_end: usize,
278 end: usize,
279 ending: Option<LineEnding>,
280) {
281 line_starts.push(start);
282 lines.push(SourceLine {
283 text_span: Span::new(start, text_end),
284 full_span: Span::new(start, end),
285 ending,
286 });
287}
288
289#[cfg(test)]
290#[path = "source_tests.rs"]
291mod tests;