logix_type/
span.rs

1use std::{borrow::Cow, fmt, path::Path};
2
3use bstr::ByteSlice;
4use logix_vfs::LogixVfs;
5
6use crate::{loader::CachedFile, LogixLoader};
7
8#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
9struct Range {
10    start: u16,
11    end: u16,
12}
13
14impl Range {
15    fn len(&self) -> usize {
16        usize::from(self.end - self.start)
17    }
18}
19
20impl fmt::Debug for Range {
21    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
22        let range: std::ops::Range<u16> = self.start..self.end;
23        fmt::Debug::fmt(&range, f)
24    }
25}
26
27#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
28enum SpanRange {
29    SingleLine {
30        line: usize,
31        col: Range,
32    },
33    MultiLine {
34        start_line: usize,
35        start_col: u16,
36        last_line: usize,
37        last_col: u16,
38        end_pos: usize,
39    },
40}
41
42impl SpanRange {
43    fn get_range_for_line(&self, cur_line: usize) -> Option<std::ops::Range<usize>> {
44        match self {
45            Self::SingleLine { line, col } => {
46                (*line == cur_line).then(|| usize::from(col.start)..usize::from(col.end))
47            }
48            Self::MultiLine { .. } => todo!(),
49        }
50    }
51}
52
53#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
54pub struct SourceSpan {
55    file: CachedFile,
56    pos: usize,
57    range: SpanRange,
58}
59
60impl SourceSpan {
61    pub fn empty() -> Self {
62        Self {
63            file: CachedFile::empty(),
64            pos: 0,
65            range: SpanRange::SingleLine {
66                line: 0,
67                col: Range { start: 0, end: 0 },
68            },
69        }
70    }
71    pub fn new_for_test(
72        loader: &LogixLoader<impl LogixVfs>,
73        path: impl AsRef<Path>,
74        line: usize,
75        col: usize,
76        len: usize,
77    ) -> Self {
78        let file = loader.get_file(path).unwrap();
79        let mut pos = 0;
80        for (i, line_data) in file.data().lines_with_terminator().enumerate() {
81            if i + 1 == line {
82                pos += col;
83                break;
84            }
85            pos += line_data.len();
86        }
87
88        Self::new(&file, pos, line, col, len)
89    }
90
91    pub(crate) fn new(file: &CachedFile, pos: usize, line: usize, col: usize, len: usize) -> Self {
92        let scol = u16::try_from(col).unwrap();
93        let ecol = u16::try_from(col + len).unwrap();
94        Self {
95            file: file.clone(),
96            pos,
97            range: SpanRange::SingleLine {
98                line,
99                col: Range {
100                    start: scol,
101                    end: ecol,
102                },
103            },
104        }
105    }
106
107    pub fn path(&self) -> &Path {
108        self.file.path()
109    }
110
111    /// The first line in this span
112    pub fn line(&self) -> usize {
113        match self.range {
114            SpanRange::SingleLine { line, col: _ } => line,
115            SpanRange::MultiLine { start_line, .. } => start_line,
116        }
117    }
118
119    /// The last line in this span
120    pub fn last_line(&self) -> usize {
121        match self.range {
122            SpanRange::SingleLine { line, col: _ } => line,
123            SpanRange::MultiLine { last_line, .. } => last_line,
124        }
125    }
126    /// The start column of the start line
127    pub fn col(&self) -> usize {
128        match self.range {
129            SpanRange::SingleLine {
130                line: _,
131                col: Range { start, end: _ },
132            } => start.into(),
133            SpanRange::MultiLine { start_col, .. } => start_col.into(),
134        }
135    }
136
137    /// The value of this entire span
138    pub fn value(&self) -> Cow<str> {
139        let end_pos = match &self.range {
140            SpanRange::SingleLine { line: _, col } => self.pos + col.len(),
141            &SpanRange::MultiLine { end_pos, .. } => end_pos,
142        };
143        String::from_utf8_lossy(&self.file.data()[self.pos..end_pos])
144    }
145
146    pub fn lines(
147        &self,
148        context: usize,
149    ) -> impl Iterator<Item = (usize, Option<std::ops::Range<usize>>, Cow<str>)> {
150        self.file
151            .lines()
152            .enumerate()
153            .skip(self.line().saturating_sub(context + 1))
154            .map_while(move |(i, line)| {
155                let ln = i + 1;
156                if ln <= self.line() + context {
157                    Some((ln, self.range.get_range_for_line(ln), line.to_str_lossy()))
158                } else {
159                    None
160                }
161            })
162    }
163
164    pub fn with_off(&self, off: usize, len: usize) -> Self {
165        let off = u16::try_from(off).unwrap();
166        let len = u16::try_from(len).unwrap();
167        Self {
168            file: self.file.clone(),
169            pos: self.pos + usize::from(off),
170            range: match self.range {
171                SpanRange::SingleLine {
172                    line,
173                    col: Range { start, end: _ },
174                } => SpanRange::SingleLine {
175                    line,
176                    col: Range {
177                        start: start + off,
178                        end: start + off + len,
179                    },
180                },
181                SpanRange::MultiLine { .. } => todo!(),
182            },
183        }
184    }
185
186    pub fn calc_ln_width(&self, extra: usize) -> usize {
187        match self.last_line() + extra {
188            0..=999 => 3,
189            1000..=9999 => 4,
190            10000..=99999 => 5,
191            100000..=999999 => 6,
192            _ => 10,
193        }
194    }
195
196    pub(crate) fn join(&self, other: &Self) -> SourceSpan {
197        assert_eq!(self.file, other.file);
198
199        let mut ret = self.clone();
200
201        match (&self.range, &other.range) {
202            (
203                SpanRange::SingleLine {
204                    line: sline,
205                    col: scol,
206                },
207                SpanRange::SingleLine {
208                    line: oline,
209                    col: ocol,
210                },
211            ) => match sline.cmp(oline) {
212                std::cmp::Ordering::Less => {
213                    ret.pos = self.pos;
214                    ret.range = SpanRange::MultiLine {
215                        start_line: *sline,
216                        start_col: scol.start,
217                        last_line: *oline,
218                        last_col: ocol.end,
219                        end_pos: other.pos + ocol.len(),
220                    };
221                }
222                std::cmp::Ordering::Equal => {
223                    ret.pos = self.pos.min(other.pos);
224                    ret.range = SpanRange::SingleLine {
225                        line: *sline,
226                        col: Range {
227                            start: ocol.start.min(scol.start),
228                            end: ocol.end.max(scol.end),
229                        },
230                    };
231                }
232                std::cmp::Ordering::Greater => {
233                    ret.pos = self.pos;
234                    ret.range = SpanRange::MultiLine {
235                        start_line: *oline,
236                        start_col: ocol.start,
237                        last_line: *sline,
238                        last_col: scol.end,
239                        end_pos: self.pos + scol.len(),
240                    };
241                }
242            },
243            (SpanRange::SingleLine { .. }, SpanRange::MultiLine { .. }) => todo!(),
244            (SpanRange::MultiLine { .. }, SpanRange::SingleLine { .. }) => todo!(),
245            (SpanRange::MultiLine { .. }, SpanRange::MultiLine { .. }) => todo!(),
246        }
247
248        ret
249    }
250
251    pub(crate) fn from_pos(file: &CachedFile, pos: usize) -> SourceSpan {
252        let mut cur = 0;
253        let mut ln = 1;
254        let mut col = Range { start: 0, end: 0 };
255
256        for line in file.data().lines_with_terminator() {
257            let range = cur..cur + line.len();
258
259            if range.contains(&pos) {
260                let start = u16::try_from(pos - range.start).unwrap();
261                col = Range {
262                    start,
263                    end: start + 1,
264                };
265                break;
266            }
267
268            cur = range.end;
269            ln += 1;
270        }
271
272        Self {
273            file: file.clone(),
274            pos,
275            range: SpanRange::SingleLine { line: ln, col },
276        }
277    }
278}
279
280impl fmt::Display for SourceSpan {
281    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
282        write!(
283            f,
284            "{}:{}:{}",
285            self.file.path().display(),
286            self.line(),
287            self.col()
288        )
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    #[test]
297    fn coverage_hacks() {
298        assert_eq!(
299            SourceSpan {
300                file: CachedFile::from_slice("test.logix", b"hello world"),
301                pos: 6,
302                range: SpanRange::SingleLine {
303                    line: 1,
304                    col: Range { start: 6, end: 11 },
305                }
306            }
307            .value(),
308            "world"
309        );
310        assert_eq!(SourceSpan::empty().value(), "");
311    }
312
313    #[test]
314    fn line_width() {
315        assert_eq!(SourceSpan::empty().calc_ln_width(0), 3);
316        assert_eq!(SourceSpan::empty().calc_ln_width(10), 3);
317        assert_eq!(SourceSpan::empty().calc_ln_width(100), 3);
318        assert_eq!(SourceSpan::empty().calc_ln_width(1000), 4);
319        assert_eq!(SourceSpan::empty().calc_ln_width(10000), 5);
320        assert_eq!(SourceSpan::empty().calc_ln_width(100000), 6);
321        assert_eq!(SourceSpan::empty().calc_ln_width(1000000), 10);
322        assert_eq!(SourceSpan::empty().calc_ln_width(10000000), 10);
323        assert_eq!(SourceSpan::empty().calc_ln_width(100000000), 10);
324        assert_eq!(SourceSpan::empty().calc_ln_width(1000000000), 10);
325        assert_eq!(SourceSpan::empty().calc_ln_width(10000000000), 10);
326    }
327}