1use std::{
2 fmt::{Debug, Display, Formatter},
3 ops::Range,
4 path::{Path, PathBuf},
5};
6
7use serde::{Deserialize, Serialize};
8
9use crate::{SourceCode, SourceContainer};
10
11#[derive(Clone, Default)]
12pub struct SourceLocationFactory {
13 file: Option<&'static str>,
14 newlines: NewLines,
15}
16
17impl SourceLocationFactory {
18 pub fn internal(src: &str) -> Self {
20 SourceLocationFactory { file: None, newlines: NewLines::build(src) }
21 }
22
23 pub fn for_source(source_code: &SourceCode) -> Self {
25 SourceLocationFactory {
26 file: Some(source_code.get_location_str()),
27 newlines: NewLines::build(&source_code.source),
28 }
29 }
30
31 pub fn create_range(&self, range: core::ops::Range<usize>) -> SourceLocation {
33 let start = TextLocation::from_offset(range.start, &self.newlines);
34 let end = TextLocation::from_offset(range.end, &self.newlines);
35 SourceLocation { span: CodeSpan::Range(start..end), file: self.file.into() }
36 }
37
38 pub fn create_block_location(&self, local_id: usize) -> SourceLocation {
39 SourceLocation { span: CodeSpan::Block { local_id }, file: self.file.into() }
40 }
41
42 pub fn create_file_only_location(&self) -> SourceLocation {
43 SourceLocation { span: CodeSpan::None, file: self.file.into() }
44 }
45
46 pub fn create_range_to_end_of_line(&self, line: usize, column: usize) -> SourceLocation {
47 let start = TextLocation::new(line, column, 0);
48 let end = TextLocation::new(line, self.newlines.get_end_of_line(line), 0);
49 SourceLocation { span: CodeSpan::Range(start..end), file: self.file.into() }
50 }
51}
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
56pub struct TextLocation {
57 line: usize,
59 column: usize,
61 offset: usize,
63}
64
65impl TextLocation {
66 pub fn new(line: usize, column: usize, offset: usize) -> Self {
67 TextLocation { line, column, offset }
68 }
69
70 pub fn from_offset(offset: usize, newlines: &NewLines) -> Self {
71 let line = newlines.get_line_nr(offset);
72 let column = newlines.get_column(line, offset);
73 TextLocation { line, column, offset }
74 }
75}
76
77#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
79pub enum CodeSpan {
80 Block { local_id: usize },
82 Combined(Vec<CodeSpan>),
84 Range(Range<TextLocation>),
86 None,
88}
89
90impl std::fmt::Debug for CodeSpan {
91 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Self::Block { local_id } => f.debug_struct("Block").field("local_id", local_id).finish(),
94 Self::Combined(arg0) => f.debug_tuple("Combined").field(arg0).finish(),
95 Self::None => write!(f, "None"),
96
97 Self::Range(range) => {
100 write!(
101 f,
102 "Range({}:{} - {}:{})",
103 range.start.line, range.start.column, range.end.line, range.end.column
104 )
105 }
106 }
107 }
108}
109
110impl Display for CodeSpan {
111 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
112 match self {
113 CodeSpan::Block { .. } => write!(f, "Block {}", self.get_line()),
114 CodeSpan::Combined(spans) => {
115 write!(f, "{}", spans.iter().map(|it| it.to_string()).collect::<String>())
116 }
117 CodeSpan::Range(range) => write!(
118 f,
119 "{}:{}:{{{}:{}-{}:{}}}: ",
120 range.start.line,
121 range.start.column,
122 range.start.line,
123 range.start.column,
124 range.end.line,
125 range.end.column,
126 ),
127 CodeSpan::None => Ok(()),
128 }
129 }
130}
131
132impl CodeSpan {
133 pub fn from_text_info(start: TextLocation, end: TextLocation) -> Self {
135 CodeSpan::Range(start..end)
136 }
137
138 pub fn get_line(&self) -> usize {
142 match self {
143 Self::Range(range) => range.start.line,
144 Self::Block { local_id } => *local_id,
145 _ => 0,
146 }
147 }
148
149 pub fn get_line_end(&self) -> usize {
152 match self {
153 Self::Range(range) => range.end.line,
154 _ => 0,
155 }
156 }
157
158 pub fn get_line_plus_one(&self) -> usize {
159 match self {
160 Self::Range(range) => range.start.line + 1,
161 Self::Block { local_id } => *local_id,
162 _ => 0,
163 }
164 }
165
166 pub fn get_column(&self) -> usize {
169 match self {
170 Self::Range(range) => range.start.column,
171 _ => 0,
172 }
173 }
174
175 pub fn get_column_end(&self) -> usize {
178 match self {
179 Self::Range(range) => range.end.column,
180 _ => 0,
181 }
182 }
183
184 pub fn to_range(&self) -> Option<Range<usize>> {
185 match self {
186 CodeSpan::Range(range) => Some(range.start.offset..range.end.offset),
187 _ => None,
188 }
189 }
190}
191
192#[derive(Copy, Clone, PartialEq, Eq, Hash, Default, Debug, Serialize, Deserialize)]
193pub enum FileMarker {
194 File(&'static str),
195 #[default]
196 Undefined,
197 Internal(&'static str),
198}
199
200impl From<&'static str> for FileMarker {
201 fn from(value: &'static str) -> Self {
202 Self::File(value)
203 }
204}
205
206impl From<Option<&'static str>> for FileMarker {
207 fn from(value: Option<&'static str>) -> Self {
208 value.map(FileMarker::from).unwrap_or_default()
209 }
210}
211
212impl From<&FileMarker> for PathBuf {
213 fn from(val: &FileMarker) -> Self {
214 match val {
215 FileMarker::File(f) | FileMarker::Internal(f) => f.replace(['<', '>'], "__").into(),
217 FileMarker::Undefined => Path::new("").to_path_buf(),
218 }
219 }
220}
221
222impl FileMarker {
223 pub fn get_name(&self) -> Option<&'static str> {
224 match self {
225 FileMarker::File(f) | FileMarker::Internal(f) => Some(f),
226 FileMarker::Undefined => None,
227 }
228 }
229
230 pub fn is_undefined(&self) -> bool {
231 matches!(self, Self::Undefined)
232 }
233
234 pub fn is_internal(&self) -> bool {
235 matches!(self, Self::Internal(_))
236 }
237}
238
239#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
240#[serde(bound(deserialize = "'de: 'static"))]
241pub struct SourceLocation {
242 span: CodeSpan,
243 file: FileMarker,
247}
248
249impl From<&SourceLocation> for SourceLocation {
250 fn from(value: &SourceLocation) -> Self {
251 value.clone()
252 }
253}
254
255impl Debug for SourceLocation {
256 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
257 let mut f = f.debug_struct("SourceLocation");
258 f.field("span", &self.span);
259 if !(self.file.is_undefined() || self.file.is_internal()) {
260 f.field("file", &self.file.get_name());
261 }
262 f.finish()
263 }
264}
265
266impl Display for SourceLocation {
267 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
268 if !(self.file.is_internal() || self.file.is_undefined()) {
269 write!(f, "{}", self.get_file_name().unwrap())?;
270 }
271 write!(f, ":{}", self.span)
272 }
273}
274
275impl Default for SourceLocation {
276 fn default() -> Self {
277 Self::internal()
278 }
279}
280
281impl SourceLocation {
282 pub fn into_internal(self) -> Self {
283 let SourceLocation { span, file } = self;
284 SourceLocation { span, file: FileMarker::Internal(file.get_name().unwrap_or_default()) }
285 }
286 pub fn undefined() -> SourceLocation {
288 SourceLocation { span: CodeSpan::None, file: FileMarker::default() }
289 }
290
291 pub fn get_line(&self) -> usize {
295 self.span.get_line()
296 }
297
298 pub fn get_line_end(&self) -> usize {
302 self.span.get_line_end()
303 }
304
305 pub fn get_line_plus_one(&self) -> usize {
307 self.span.get_line_plus_one()
308 }
309
310 pub fn get_column(&self) -> usize {
313 self.span.get_column()
314 }
315
316 pub fn get_column_end(&self) -> usize {
319 self.span.get_column_end()
320 }
321
322 pub fn span(&self, other: &SourceLocation) -> SourceLocation {
325 let span = match (&self.span, &other.span) {
326 (CodeSpan::Block { local_id }, CodeSpan::Block { local_id: other }) if local_id == other => {
328 CodeSpan::Block { local_id: *local_id }
329 }
330 (CodeSpan::Block { .. }, CodeSpan::Block { .. }) => {
331 CodeSpan::Combined(vec![self.span.clone(), other.span.clone()])
332 }
333 (CodeSpan::Range(start), CodeSpan::Range(end)) => CodeSpan::Range(start.start..end.end),
335 (CodeSpan::Block { local_id }, CodeSpan::Range(_))
337 | (CodeSpan::Range(_), CodeSpan::Block { local_id }) => CodeSpan::Block { local_id: *local_id },
338 (CodeSpan::None, _) | (_, CodeSpan::None) => CodeSpan::None,
340 (CodeSpan::Combined(inner), CodeSpan::Combined(other)) => {
341 let mut inner = inner.clone();
342 inner.extend_from_slice(other);
343 CodeSpan::Combined(inner)
344 }
345 (CodeSpan::Combined(data), other) | (other, CodeSpan::Combined(data)) => {
346 let mut data = data.clone();
347 data.push(other.clone());
348 CodeSpan::Combined(data)
349 }
350 };
351 SourceLocation { span, file: self.file }
352 }
353
354 pub fn to_range(&self) -> Option<Range<usize>> {
356 self.span.to_range()
357 }
358
359 pub fn get_file_name(&self) -> Option<&'static str> {
360 self.file.get_name()
361 }
362
363 pub fn is_undefined(&self) -> bool {
366 self.file.is_undefined() && self.span == CodeSpan::None
367 }
368
369 pub fn get_span(&self) -> &CodeSpan {
370 &self.span
371 }
372
373 pub fn replace_with(&mut self, new_location: SourceLocation) {
374 self.span = new_location.span;
375 self.file = new_location.file;
376 }
377 pub fn internal() -> Self {
380 SourceLocation { span: CodeSpan::None, file: FileMarker::Internal("<internal>") }
381 }
382
383 pub fn internal_in_unit(unit: Option<&'static str>) -> Self {
387 let Some(unit) = unit else { return Self::internal() };
388 SourceLocation { span: CodeSpan::None, file: FileMarker::File(unit) }
389 }
390
391 pub fn is_internal(&self) -> bool {
392 matches!(self.file, FileMarker::Internal(_)) | matches!(self.span, CodeSpan::None)
393 }
394
395 pub fn is_builtin_internal(&self) -> bool {
396 self.file.is_internal() && self.span == CodeSpan::None
397 }
398
399 pub fn is_in_unit(&self, unit: impl AsRef<str>) -> bool {
400 if let Some(filename) = self.get_file_name() {
401 filename == unit.as_ref()
402 } else {
403 true
405 }
406 }
407}
408
409#[derive(Clone, Default, Debug, PartialEq, Eq)]
414pub struct NewLines {
415 line_breaks: Vec<usize>,
416}
417
418impl NewLines {
419 pub fn build(str: &str) -> NewLines {
420 let mut line_breaks = Vec::new();
421 let mut total_offset: usize = 0;
422 if !str.is_empty() {
423 for l in str.split('\n') {
425 total_offset += l.len() + 1;
426 line_breaks.push(total_offset);
427 }
428 }
429 NewLines { line_breaks }
430 }
431
432 pub fn get_line_nr(&self, offset: usize) -> usize {
436 match self.line_breaks.binary_search(&offset) {
437 Ok(line) => line + 1,
439 Err(line) => line,
440 }
441 }
442
443 pub fn get_column(&self, line: usize, offset: usize) -> usize {
447 if line > 0 {
448 self.line_breaks.get(line - 1).map(|l| offset - *l).unwrap_or(0)
449 } else {
450 offset
451 }
452 }
453
454 pub fn get_end_of_line(&self, line: usize) -> usize {
457 self.line_breaks.get(line).copied().unwrap_or_default()
458 }
459}
460
461#[cfg(test)]
462mod tests {
463 use crate::source_location::{NewLines, TextLocation};
464 use insta::assert_debug_snapshot;
465
466 use super::{CodeSpan, SourceLocation};
467
468 #[test]
469 fn new_lines_test_empty_string() {
470 let text = "";
471 let nl = NewLines::build(text);
472
473 assert_eq!(nl.get_line_nr(0), 0);
474 assert_eq!(nl.get_line_nr(1), 0);
475 assert_eq!(nl.get_line_nr(2), 0);
476 assert_eq!(nl.get_line_nr(3), 0);
477 }
478
479 #[test]
480 fn new_lines_test_three_lines_with_crlf() {
481 let text = "A\r\nB\r\nC";
482 let nl = NewLines::build(text);
483 assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
484 assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 1);
485 assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 2);
486 }
487
488 #[test]
489 fn new_lines_test_three_lines_with_lf() {
490 let text = "A\nB\nC";
491 let nl = NewLines::build(text);
492 assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
493 assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 1);
494 assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 2);
495 }
496
497 #[test]
498 fn new_lines_test_three_long_lines_with_lf() {
499 let text = "xxxx A xxxx
500
501 xxxx B xxxx
502
503 xxxx C xxxxx";
504 let nl = NewLines::build(text);
505 assert_eq!(nl.get_line_nr(text.find('A').unwrap()), 0);
506 assert_eq!(nl.get_line_nr(text.find('B').unwrap()), 2);
507 assert_eq!(nl.get_line_nr(text.find('C').unwrap()), 4);
508 }
509
510 #[test]
511 fn new_lines_and_columns_test() {
512 let text = "xxxx A xxxx
513
514 xxxx B xxxx
515
516 xxxx C xxxxx";
517 let nl = NewLines::build(text);
518 assert_eq!(nl.get_column(0, text.find('A').unwrap()), 5);
519 assert_eq!(nl.get_column(2, text.find('B').unwrap()), 9);
520 assert_eq!(nl.get_column(4, text.find('C').unwrap()), 9);
521 }
522
523 #[test]
524 fn span_two_blocks() {
525 let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
526 let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 2 } };
527 assert_debug_snapshot!(loc1.span(&loc2));
528 }
529
530 #[test]
531 fn span_two_blocks_with_same_id() {
532 let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
533 let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
534 assert_debug_snapshot!(loc1.span(&loc2));
535 }
536
537 #[test]
538 fn span_id_and_range() {
539 let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
540 let loc2 = SourceLocation {
541 file: None.into(),
542 span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(2, 0, 4)),
543 };
544 assert_debug_snapshot!(loc1.span(&loc2));
545 }
546
547 #[test]
548 fn span_two_ranges() {
549 let loc1 = SourceLocation {
550 file: None.into(),
551 span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(2, 0, 4)),
552 };
553 let loc2 = SourceLocation {
554 file: None.into(),
555 span: CodeSpan::from_text_info(TextLocation::new(1, 0, 0), TextLocation::new(5, 30, 10)),
556 };
557 assert_debug_snapshot!(loc1.span(&loc2));
558 }
559
560 #[test]
561 fn span_combined() {
562 let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
563 let loc2 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 2 } };
564 let loc3 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 3 } };
565 let loc4 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 4 } };
566 assert_debug_snapshot!(loc1.span(&loc2).span(&loc3.span(&loc4)));
567 }
568
569 #[test]
570 fn span_none() {
571 let loc1 = SourceLocation { file: None.into(), span: CodeSpan::Block { local_id: 1 } };
572 let loc2 = SourceLocation { file: None.into(), span: CodeSpan::None };
573 assert_debug_snapshot!(loc1.span(&loc2));
574 }
575}