tfparser_core/loader/
source_map.rs1use std::{
12 collections::HashMap,
13 path::{Path, PathBuf},
14 sync::{Arc, OnceLock, RwLock},
15};
16
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct LineCol {
24 pub line: u32,
26 pub column: u32,
28}
29
30#[derive(Clone, Debug)]
34pub struct LineIndex {
35 line_starts: Vec<u32>,
36}
37
38impl LineIndex {
39 #[must_use]
45 pub fn build(src: &str) -> Self {
46 let mut starts = Vec::with_capacity(src.len() / 32 + 4);
47 starts.push(0);
48 for (i, b) in src.bytes().enumerate() {
49 if b == b'\n' {
50 let next = i.saturating_add(1);
51 starts.push(u32::try_from(next).unwrap_or(u32::MAX));
52 }
53 }
54 Self {
55 line_starts: starts,
56 }
57 }
58
59 #[must_use]
64 pub fn locate(&self, byte: u32) -> LineCol {
65 let line_idx = self
66 .line_starts
67 .partition_point(|&start| start <= byte)
68 .saturating_sub(1);
69 let line_start = self.line_starts.get(line_idx).copied().unwrap_or_default();
70 LineCol {
71 line: u32::try_from(line_idx + 1).unwrap_or(u32::MAX),
72 column: byte.saturating_sub(line_start).saturating_add(1),
73 }
74 }
75}
76
77#[derive(Debug, Default)]
85pub struct SourceMap {
86 inner: RwLock<HashMap<PathBuf, Arc<SourceEntry>>>,
87}
88
89#[derive(Debug)]
91struct SourceEntry {
92 src: Arc<str>,
93 line_index: OnceLock<LineIndex>,
94}
95
96impl SourceMap {
97 #[must_use]
99 pub fn new() -> Self {
100 Self::default()
101 }
102
103 #[must_use]
105 pub fn get(&self, path: &Path) -> Option<Arc<str>> {
106 let read = self.inner.read().ok()?;
107 read.get(path).map(|entry| Arc::clone(&entry.src))
108 }
109
110 pub fn insert(&self, path: &Path, src: Arc<str>) -> Arc<str> {
113 let entry = Arc::new(SourceEntry {
114 src: Arc::clone(&src),
115 line_index: OnceLock::new(),
116 });
117 if let Ok(mut write) = self.inner.write() {
118 write.insert(path.to_path_buf(), entry);
119 }
120 src
121 }
122
123 #[must_use]
126 pub fn line_index(&self, path: &Path) -> Option<LineIndex> {
127 let read = self.inner.read().ok()?;
128 let entry = read.get(path)?;
129 let li = entry
130 .line_index
131 .get_or_init(|| LineIndex::build(&entry.src))
132 .clone();
133 Some(li)
134 }
135
136 #[must_use]
138 pub fn len(&self) -> usize {
139 self.inner.read().map_or(0, |m| m.len())
140 }
141
142 #[must_use]
144 pub fn is_empty(&self) -> bool {
145 self.len() == 0
146 }
147}
148
149#[cfg(test)]
150#[allow(
151 clippy::unwrap_used,
152 clippy::expect_used,
153 clippy::panic,
154 clippy::indexing_slicing
155)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn test_line_index_locates_first_byte_at_1_1() {
161 let li = LineIndex::build("");
162 assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
163 }
164
165 #[test]
166 fn test_line_index_locates_across_newlines() {
167 let src = "abc\ndefgh\ni";
168 let li = LineIndex::build(src);
169 assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
170 assert_eq!(li.locate(2), LineCol { line: 1, column: 3 });
171 assert_eq!(li.locate(3), LineCol { line: 1, column: 4 }); assert_eq!(li.locate(4), LineCol { line: 2, column: 1 });
173 assert_eq!(li.locate(8), LineCol { line: 2, column: 5 });
174 assert_eq!(li.locate(10), LineCol { line: 3, column: 1 });
175 }
176
177 #[test]
178 fn test_line_index_handles_trailing_newline() {
179 let src = "x\n";
180 let li = LineIndex::build(src);
181 assert_eq!(li.locate(0), LineCol { line: 1, column: 1 });
182 assert_eq!(li.locate(2), LineCol { line: 2, column: 1 });
184 }
185
186 #[test]
187 fn test_line_index_clamps_byte_past_end() {
188 let li = LineIndex::build("abc");
189 let pos = li.locate(1_000);
190 assert_eq!(pos.line, 1);
191 assert!(pos.column >= 1);
193 }
194
195 #[test]
196 fn test_source_map_round_trip() {
197 let map = SourceMap::new();
198 let path = PathBuf::from("/tmp/x.tf");
199 let src: Arc<str> = Arc::from("a\nb\nc");
200 let returned = map.insert(&path, Arc::clone(&src));
201 assert_eq!(&*returned, "a\nb\nc");
202 let cached = map.get(&path).unwrap();
203 assert!(Arc::ptr_eq(&cached, &src));
204 assert_eq!(map.len(), 1);
205 let li = map.line_index(&path).unwrap();
206 assert_eq!(li.locate(2), LineCol { line: 2, column: 1 });
207 }
208
209 #[test]
210 fn test_source_map_returns_none_for_uncached() {
211 let map = SourceMap::new();
212 assert!(map.get(Path::new("/nope")).is_none());
213 assert!(map.line_index(Path::new("/nope")).is_none());
214 }
215
216 #[test]
217 fn test_source_map_overwrite_replaces_entry() {
218 let map = SourceMap::new();
219 let path = PathBuf::from("/tmp/x.tf");
220 map.insert(&path, Arc::from("v1"));
221 map.insert(&path, Arc::from("v2"));
222 assert_eq!(&*map.get(&path).unwrap(), "v2");
223 }
224}