1use pathdiff::diff_paths;
7use seq_map::SeqMap;
8use std::io::ErrorKind;
9use std::path::{Path, PathBuf};
10use std::{fs, io};
11use tracing::{info, trace};
12
13pub mod prelude;
14
15pub type FileId = u16;
16
17#[derive(Debug)]
18pub struct FileInfo {
19 pub mount_name: String,
20 pub relative_path: PathBuf,
21 pub contents: String,
22 pub line_offsets: Box<[u16]>,
23}
24
25#[derive(Debug)]
26pub struct SourceMap {
27 pub mounts: SeqMap<String, PathBuf>,
28 pub cache: SeqMap<FileId, FileInfo>,
29 pub next_file_id: FileId,
30}
31
32#[derive(Debug)]
33pub struct RelativePath(pub String);
34
35impl SourceMap {
36 pub fn new(mounts: &SeqMap<String, PathBuf>) -> io::Result<Self> {
39 let mut canonical_mounts = SeqMap::new();
40 for (mount_name, base_path) in mounts {
41 let canon_path = base_path.canonicalize().map_err(|_| {
42 io::Error::new(
43 io::ErrorKind::InvalidData,
44 format!("could not canonicalize {base_path:?}"),
45 )
46 })?;
47
48 if !canon_path.is_dir() {
49 return Err(io::Error::new(
50 ErrorKind::NotFound,
51 format!("{canon_path:?} is not a directory"),
52 ));
53 }
54 canonical_mounts
55 .insert(mount_name.clone(), canon_path)
56 .map_err(|_| {
57 io::Error::new(io::ErrorKind::InvalidData, "could not insert mount")
58 })?;
59 }
60 Ok(Self {
61 mounts: canonical_mounts,
62 cache: SeqMap::new(),
63 next_file_id: 1,
64 })
65 }
66
67 pub fn add_mount(&mut self, name: &str, path: &Path) -> io::Result<()> {
70 if !path.is_dir() {
71 return Err(io::Error::new(
72 ErrorKind::NotFound,
73 format!("{path:?} is not a directory"),
74 ));
75 }
76 self.mounts
77 .insert(name.to_string(), path.to_path_buf())
78 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "could not insert mount"))
79 }
80
81 #[must_use]
82 pub fn base_path(&self, name: &str) -> &Path {
83 self.mounts.get(&name.to_string()).map_or_else(
84 || {
85 panic!("could not find path {name}");
86 },
87 |found| found,
88 )
89 }
90
91 pub fn read_file(&mut self, path: &Path, mount_name: &str) -> io::Result<(FileId, String)> {
92 let found_base_path = self.base_path(mount_name);
93 let relative_path = diff_paths(path, found_base_path)
94 .unwrap_or_else(|| panic!("could not find relative path {path:?} {found_base_path:?}"));
95
96 let contents = fs::read_to_string(path)?;
97
98 let id = self.next_file_id;
99 self.next_file_id += 1;
100
101 self.add_manual(id, mount_name, &relative_path, &contents);
102
103 Ok((id, contents))
104 }
105
106 pub fn add_manual(
107 &mut self,
108 id: FileId,
109 mount_name: &str,
110 relative_path: &Path,
111 contents: &str,
112 ) {
113 let line_offsets = Self::compute_line_offsets(contents);
114
115 self.cache
116 .insert(
117 id,
118 FileInfo {
119 mount_name: mount_name.to_string(),
120 relative_path: relative_path.to_path_buf(),
121 contents: contents.to_string(),
122 line_offsets,
123 },
124 )
125 .expect("could not add file info");
126 }
127
128 pub fn add_manual_no_id(
129 &mut self,
130 mount_name: &str,
131 relative_path: &Path,
132 contents: &str,
133 ) -> FileId {
134 let line_offsets = Self::compute_line_offsets(contents);
135 let id = self.next_file_id;
136 self.next_file_id += 1;
137
138 self.cache
139 .insert(
140 id,
141 FileInfo {
142 mount_name: mount_name.to_string(),
143 relative_path: relative_path.to_path_buf(),
144 contents: contents.to_string(),
145 line_offsets,
146 },
147 )
148 .expect("could not add file info");
149 id
150 }
151
152 pub fn read_file_relative(
153 &mut self,
154 mount_name: &str,
155 relative_path: &str,
156 ) -> io::Result<(FileId, String)> {
157 let buf = self.to_file_system_path(mount_name, relative_path)?;
158 self.read_file(&buf, mount_name)
159 }
160
161 fn to_file_system_path(&self, mount_name: &str, relative_path: &str) -> io::Result<PathBuf> {
176 let base_path = self.base_path(mount_name).to_path_buf();
177 let mut path_buf = base_path;
178
179 path_buf.push(relative_path);
180
181 path_buf.canonicalize().map_err(|_| {
182 io::Error::new(
183 ErrorKind::Other,
184 format!("path is wrong mount:{mount_name} relative:{relative_path}",),
185 )
186 })
187 }
188
189 fn compute_line_offsets(contents: &str) -> Box<[u16]> {
190 let mut offsets = Vec::new();
191 offsets.push(0);
192 for (i, &byte) in contents.as_bytes().iter().enumerate() {
193 if byte == b'\n' {
194 let next_line_start = u16::try_from(i + 1).expect("too big file");
196 offsets.push(next_line_start);
197 }
198 }
199 offsets.into_boxed_slice()
200 }
201
202 #[must_use]
203 pub fn get_span_source(&self, file_id: FileId, offset: usize, length: usize) -> &str {
204 self.cache.get(&file_id).map_or_else(
205 || {
206 panic!("{}", &format!("Invalid file_id {file_id} in span"));
207 },
208 |file_info| {
209 let start = offset;
210 let end = start + length;
211 &file_info.contents[start..end]
212 },
213 )
214 }
215
216 #[must_use]
217 pub fn get_source_line(&self, file_id: FileId, line_number: usize) -> Option<&str> {
218 let file_info = self.cache.get(&file_id)?;
219
220 let start_offset = file_info.line_offsets[line_number - 1] as usize;
221 let end_offset = file_info.line_offsets[line_number] as usize;
222 Some(&file_info.contents[start_offset..end_offset - 1])
223 }
224
225 #[must_use]
226 pub fn get_span_location_utf8(&self, file_id: FileId, offset: usize) -> (usize, usize) {
227 let file_info = self.cache.get(&file_id).expect("Invalid file_id in span");
228
229 let offset = offset as u16;
230
231 let line_idx = file_info
233 .line_offsets
234 .binary_search(&offset)
235 .unwrap_or_else(|insert_point| insert_point.saturating_sub(1));
236
237 let line_start = file_info.line_offsets[line_idx] as usize;
239 let octet_offset = offset as usize;
240
241 let line_text = &file_info.contents[line_start..octet_offset];
243
244 let column_character_offset = line_text.chars().count();
246
247 (line_idx + 1, column_character_offset + 1)
249 }
250
251 #[must_use]
252 pub fn fetch_relative_filename(&self, file_id: FileId) -> &str {
253 self.cache
254 .get(&file_id)
255 .unwrap()
256 .relative_path
257 .to_str()
258 .unwrap()
259 }
260 pub fn minimal_relative_path(target: &Path, current_dir: &Path) -> io::Result<PathBuf> {
261 let current_dir_components = current_dir.components().collect::<Vec<_>>();
264 let target_components = target.components().collect::<Vec<_>>();
265
266 let mut common_prefix_len = 0;
267 for i in 0..std::cmp::min(current_dir_components.len(), target_components.len()) {
268 if current_dir_components[i] == target_components[i] {
269 common_prefix_len += 1;
270 } else {
271 break;
272 }
273 }
274
275 let mut relative_path = PathBuf::new();
276
277 for _ in 0..(current_dir_components.len() - common_prefix_len) {
278 relative_path.push("..");
279 }
280
281 for component in &target_components[common_prefix_len..] {
282 relative_path.push(component);
283 }
284
285 info!(
286 ?current_dir,
287 ?target,
288 ?relative_path,
289 "minimal relative path"
290 );
291
292 Ok(relative_path)
293 }
294 pub fn get_relative_path_to(&self, file_id: FileId, current_dir: &Path) -> io::Result<PathBuf> {
295 let file_info = self.cache.get(&file_id).unwrap();
296 let mount_path = self.mounts.get(&file_info.mount_name).unwrap();
297
298 let absolute_path = mount_path.join(&file_info.relative_path);
299
300 Self::minimal_relative_path(&absolute_path, current_dir)
301 }
302}