1use std::path::{Path, PathBuf};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
12pub struct FileId(pub(crate) u32);
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct Span {
17 pub file: FileId,
18 pub start: u32,
19 pub end: u32,
20}
21
22impl Span {
23 pub(crate) fn new(file: FileId, start: usize, end: usize) -> Self {
24 Span {
27 file,
28 start: start as u32,
29 end: end as u32,
30 }
31 }
32
33 pub fn to(self, other: Span) -> Span {
36 if self.file == other.file {
37 Span {
38 file: self.file,
39 start: self.start.min(other.start),
40 end: self.end.max(other.end),
41 }
42 } else {
43 self
44 }
45 }
46}
47
48#[derive(Debug)]
50pub struct SourceFile {
51 pub name: PathBuf,
52 pub text: String,
53 line_starts: std::cell::OnceCell<Vec<u32>>,
55}
56
57#[derive(Debug, Default)]
59pub struct SourceMap {
60 files: Vec<SourceFile>,
61}
62
63#[derive(Clone, Debug, PartialEq, Eq)]
65pub struct Location {
66 pub file: PathBuf,
67 pub line: u32,
69 pub column: u32,
71}
72
73impl SourceMap {
74 pub fn new() -> Self {
75 SourceMap::default()
76 }
77
78 pub(crate) fn add(&mut self, name: impl Into<PathBuf>, text: String) -> Option<FileId> {
81 if text.len() >= u32::MAX as usize || self.files.len() >= u32::MAX as usize {
82 return None;
83 }
84 let id = FileId(self.files.len() as u32);
85 self.files.push(SourceFile {
86 name: name.into(),
87 text,
88 line_starts: std::cell::OnceCell::new(),
89 });
90 Some(id)
91 }
92
93 pub fn file(&self, id: FileId) -> &SourceFile {
94 &self.files[id.0 as usize]
95 }
96
97 pub fn snippet(&self, span: Span) -> &str {
99 let text = &self.file(span.file).text;
100 text.get(span.start as usize..span.end as usize)
101 .unwrap_or("")
102 }
103
104 pub fn locate(&self, span: Span) -> Location {
106 let file = self.file(span.file);
107 let starts = file.line_starts.get_or_init(|| {
108 let mut v = vec![0u32];
109 for (i, b) in file.text.bytes().enumerate() {
110 if b == b'\n' {
111 v.push(i as u32 + 1);
112 }
113 }
114 v
115 });
116 let line = match starts.binary_search(&span.start) {
117 Ok(i) => i,
118 Err(i) => i - 1,
119 };
120 Location {
121 file: file.name.clone(),
122 line: line as u32 + 1,
123 column: span.start - starts[line] + 1,
124 }
125 }
126}
127
128impl SourceFile {
129 pub fn path(&self) -> &Path {
130 &self.name
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn locate_lines_and_columns() {
140 let mut map = SourceMap::new();
141 let id = map.add("t.cir", "title\nr1 1 0 1k\n.end\n".into()).unwrap();
142 let loc = map.locate(Span::new(id, 6, 8));
143 assert_eq!(loc.line, 2);
144 assert_eq!(loc.column, 1);
145 let loc = map.locate(Span::new(id, 9, 10));
146 assert_eq!(loc.line, 2);
147 assert_eq!(loc.column, 4);
148 assert_eq!(map.snippet(Span::new(id, 6, 8)), "r1");
149 }
150
151 #[test]
152 fn locate_at_file_start_and_end() {
153 let mut map = SourceMap::new();
154 let id = map.add("t.cir", "a\nb".into()).unwrap();
155 assert_eq!(map.locate(Span::new(id, 0, 1)).line, 1);
156 let loc = map.locate(Span::new(id, 2, 3));
157 assert_eq!((loc.line, loc.column), (2, 1));
158 }
159}