Skip to main content

croma_core/
source.rs

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;