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 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 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 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 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}