source_map_cache/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/swamp
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5use pathdiff::diff_paths;
6use seq_map::SeqMap;
7use source_map_node::{Node, Span};
8use std::fmt::Debug;
9use std::io::ErrorKind;
10use std::path::{Path, PathBuf};
11use std::{fs, io};
12pub mod prelude;
13pub type FileId = u16;
14
15#[derive(Debug)]
16pub struct FileInfo {
17    pub mount_name: String,
18    pub relative_path: PathBuf,
19    pub contents: String,
20    pub line_offsets: Box<[u16]>,
21}
22
23#[derive(Debug)]
24pub struct SourceMap {
25    pub mounts: SeqMap<String, PathBuf>,
26    pub cache: SeqMap<FileId, FileInfo>,
27    pub file_cache: SeqMap<(String, String), FileId>,
28    pub next_file_id: FileId,
29}
30
31#[derive(Debug)]
32pub struct RelativePath(pub String);
33
34impl SourceMap {
35    /// # Errors
36    ///
37    pub fn new(mounts: &SeqMap<String, PathBuf>) -> io::Result<Self> {
38        let mut canonical_mounts = SeqMap::new();
39        for (mount_name, base_path) in mounts {
40            let canon_path = base_path.canonicalize().map_err(|_| {
41                io::Error::new(
42                    io::ErrorKind::InvalidData,
43                    format!("could not canonicalize {base_path:?}"),
44                )
45            })?;
46
47            if !canon_path.is_dir() {
48                return Err(io::Error::new(
49                    ErrorKind::NotFound,
50                    format!("{canon_path:?} is not a directory"),
51                ));
52            }
53            canonical_mounts
54                .insert(mount_name.clone(), canon_path)
55                .map_err(|_| {
56                    io::Error::new(io::ErrorKind::InvalidData, "could not insert mount")
57                })?;
58        }
59        Ok(Self {
60            mounts: canonical_mounts,
61            cache: SeqMap::new(),
62            file_cache: SeqMap::new(),
63            next_file_id: 1,
64        })
65    }
66
67    /// # Errors
68    ///
69    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_to_cache(
107        &mut self,
108        mount_name: &str,
109        relative_path: &Path,
110        contents: &str,
111        file_id: FileId,
112    ) {
113        self.add_manual(file_id, mount_name, relative_path, contents);
114        self.file_cache
115            .insert(
116                (
117                    mount_name.to_string(),
118                    relative_path.to_str().unwrap().to_string(),
119                ),
120                file_id,
121            )
122            .unwrap();
123    }
124
125    pub fn add_manual(
126        &mut self,
127        id: FileId,
128        mount_name: &str,
129        relative_path: &Path,
130        contents: &str,
131    ) {
132        let line_offsets = Self::compute_line_offsets(contents);
133
134        self.cache
135            .insert(
136                id,
137                FileInfo {
138                    mount_name: mount_name.to_string(),
139                    relative_path: relative_path.to_path_buf(),
140                    contents: contents.to_string(),
141                    line_offsets,
142                },
143            )
144            .expect("could not add file info");
145    }
146
147    pub fn add_manual_no_id(
148        &mut self,
149        mount_name: &str,
150        relative_path: &Path,
151        contents: &str,
152    ) -> FileId {
153        let line_offsets = Self::compute_line_offsets(contents);
154        let id = self.next_file_id;
155        self.next_file_id += 1;
156
157        self.cache
158            .insert(
159                id,
160                FileInfo {
161                    mount_name: mount_name.to_string(),
162                    relative_path: relative_path.to_path_buf(),
163                    contents: contents.to_string(),
164                    line_offsets,
165                },
166            )
167            .expect("could not add file info");
168        id
169    }
170
171    pub fn read_file_relative(
172        &mut self,
173        mount_name: &str,
174        relative_path: &str,
175    ) -> io::Result<(FileId, String)> {
176        if let Some(found_in_cache) = self
177            .file_cache
178            .get(&(mount_name.to_string(), relative_path.to_string()))
179        {
180            let contents = self.cache.get(found_in_cache).unwrap().contents.clone();
181            return Ok((found_in_cache.clone(), contents));
182        }
183
184        let buf = self.to_file_system_path(mount_name, relative_path)?;
185        self.read_file(&buf, mount_name)
186    }
187
188    fn to_file_system_path(&self, mount_name: &str, relative_path: &str) -> io::Result<PathBuf> {
189        let base_path = self.base_path(mount_name).to_path_buf();
190        let mut path_buf = base_path;
191
192        path_buf.push(relative_path);
193
194        path_buf.canonicalize().map_err(|_| {
195            io::Error::new(
196                ErrorKind::Other,
197                format!("path is wrong mount:{mount_name} relative:{relative_path}",),
198            )
199        })
200    }
201
202    fn compute_line_offsets(contents: &str) -> Box<[u16]> {
203        let mut offsets = Vec::new();
204        offsets.push(0);
205        for (i, &byte) in contents.as_bytes().iter().enumerate() {
206            if byte == b'\n' {
207                // Safety: new line is always encoded as single octet
208                let next_line_start = u16::try_from(i + 1).expect("too big file");
209                offsets.push(next_line_start);
210            }
211        }
212        offsets.into_boxed_slice()
213    }
214
215    #[must_use]
216    pub fn get_span_source(&self, file_id: FileId, offset: usize, length: usize) -> &str {
217        self.cache.get(&file_id).map_or_else(
218            || {
219                "ERROR"
220                //panic!("{}", &format!("Invalid file_id {file_id} in span"));
221            },
222            |file_info| {
223                let start = offset;
224                let end = start + length;
225                &file_info.contents[start..end]
226            },
227        )
228    }
229
230    #[must_use]
231    pub fn get_source_line(&self, file_id: FileId, line_number: usize) -> Option<&str> {
232        let file_info = self.cache.get(&file_id)?;
233
234        let start_offset = file_info.line_offsets[line_number - 1] as usize;
235        let end_offset = file_info.line_offsets[line_number] as usize;
236        Some(&file_info.contents[start_offset..end_offset - 1])
237    }
238
239    #[must_use]
240    pub fn get_span_location_utf8(&self, file_id: FileId, offset: usize) -> (usize, usize) {
241        let file_info = self.cache.get(&file_id).expect("Invalid file_id in span");
242
243        let offset = offset as u16;
244
245        // Find the line containing 'offset' via binary search.
246        let line_idx = file_info
247            .line_offsets
248            .binary_search(&offset)
249            .unwrap_or_else(|insert_point| insert_point.saturating_sub(1));
250
251        // Determine the start of the line in bytes
252        let line_start = file_info.line_offsets[line_idx] as usize;
253        let octet_offset = offset as usize;
254
255        // Extract the line slice from line_start to offset
256        let line_text = &file_info.contents[line_start..octet_offset];
257
258        // Count UTF-8 characters in that range, because that is what the end user sees in their editor.
259        let column_character_offset = line_text.chars().count();
260
261        // Add one so it makes more sense to the end user
262        (line_idx + 1, column_character_offset + 1)
263    }
264
265    #[must_use]
266    pub fn fetch_relative_filename(&self, file_id: FileId) -> &str {
267        self.cache
268            .get(&file_id)
269            .unwrap()
270            .relative_path
271            .to_str()
272            .unwrap()
273    }
274
275    pub fn minimal_relative_path(target: &Path, current_dir: &Path) -> io::Result<PathBuf> {
276        let current_dir_components = current_dir.components().collect::<Vec<_>>();
277        let target_components = target.components().collect::<Vec<_>>();
278
279        let mut common_prefix_len = 0;
280        for i in 0..std::cmp::min(current_dir_components.len(), target_components.len()) {
281            if current_dir_components[i] == target_components[i] {
282                common_prefix_len += 1;
283            } else {
284                break;
285            }
286        }
287
288        let mut relative_path = PathBuf::new();
289
290        for _ in 0..(current_dir_components.len() - common_prefix_len) {
291            relative_path.push("..");
292        }
293
294        for component in &target_components[common_prefix_len..] {
295            relative_path.push(component);
296        }
297        Ok(relative_path)
298    }
299
300    pub fn get_relative_path_to(&self, file_id: FileId, current_dir: &Path) -> io::Result<PathBuf> {
301        let file_info = self.cache.get(&file_id).unwrap();
302        let mount_path = self.mounts.get(&file_info.mount_name).unwrap();
303
304        let absolute_path = mount_path.join(&file_info.relative_path);
305
306        Self::minimal_relative_path(&absolute_path, current_dir)
307    }
308
309    fn get_text(&self, node: &Node) -> &str {
310        self.get_span_source(
311            node.span.file_id,
312            node.span.offset as usize,
313            node.span.length as usize,
314        )
315    }
316
317    fn get_text_span(&self, span: &Span) -> &str {
318        self.get_span_source(span.file_id, span.offset as usize, span.length as usize)
319    }
320
321    fn get_line(&self, span: &Span, current_dir: &Path) -> FileLineInfo {
322        let relative_file_name = self
323            .get_relative_path_to(span.file_id, current_dir)
324            .unwrap();
325        let (row, col) = self.get_span_location_utf8(span.file_id, span.offset as usize);
326        let line = self.get_source_line(span.file_id, row).unwrap();
327
328        FileLineInfo {
329            row,
330            col,
331            line: line.to_string(),
332            relative_file_name: relative_file_name.to_str().unwrap().to_string(),
333        }
334    }
335}
336
337pub struct FileLineInfo {
338    pub row: usize,
339    pub col: usize,
340    pub line: String,
341    pub relative_file_name: String,
342}
343
344pub trait SourceMapLookup: Debug {
345    fn get_text(&self, node: &Node) -> &str;
346    fn get_text_span(&self, span: &Span) -> &str;
347    fn get_line(&self, span: &Span) -> FileLineInfo;
348}
349
350#[derive(Debug)]
351pub struct SourceMapWrapper<'a> {
352    pub source_map: &'a SourceMap,
353    pub current_dir: PathBuf,
354}
355
356impl SourceMapLookup for SourceMapWrapper<'_> {
357    fn get_text(&self, resolved_node: &Node) -> &str {
358        self.source_map.get_text(resolved_node)
359    }
360
361    fn get_text_span(&self, span: &Span) -> &str {
362        self.source_map.get_text_span(span)
363    }
364
365    fn get_line(&self, span: &Span) -> FileLineInfo {
366        self.source_map.get_line(span, &self.current_dir)
367    }
368}