swamp_script_source_map/
lib.rs

1/*
2 * Copyright (c) Peter Bjorklund. All rights reserved. https://github.com/swamp/script
3 * Licensed under the MIT License. See LICENSE in the project root for license information.
4 */
5
6use 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    /// # Errors
37    ///
38    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    /// # 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_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    /*
162
163    fn to_relative_path(path: &ModulePath) -> RelativePath {
164        RelativePath(
165            path.0
166                .iter()
167                .map(|local_type_identifier| local_type_identifier.as_str())
168                .collect::<Vec<_>>()
169                .join("/"),
170        )
171    }
172
173     */
174
175    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                // Safety: new line is always encoded as single octet
195                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        // Find the line containing 'offset' via binary search.
232        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        // Determine the start of the line in bytes
238        let line_start = file_info.line_offsets[line_idx] as usize;
239        let octet_offset = offset as usize;
240
241        // Extract the line slice from line_start to offset
242        let line_text = &file_info.contents[line_start..octet_offset];
243
244        // Count UTF-8 characters in that range, because that is what the end user sees in their editor.
245        let column_character_offset = line_text.chars().count();
246
247        // Add one so it makes more sense to the end user
248        (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 target = target.canonicalize()?;
262
263        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}