source_map/
filesystem.rs

1use std::{
2    collections::HashMap,
3    convert::TryInto,
4    path::{Path, PathBuf},
5};
6
7use crate::{lines_columns_indexes::LineStarts, SourceId, SpanWithSource};
8
9pub struct Source {
10    pub path: PathBuf,
11    pub content: String,
12    pub(crate) line_starts: LineStarts,
13}
14
15#[cfg(feature = "global-source-filesystem")]
16pub mod global_store {
17    use super::*;
18
19    pub struct GlobalStore;
20
21    #[cfg(feature = "global-source-filesystem")]
22    static SOURCE_IDS_TO_FILES: std::sync::RwLock<MapFileStore<NoPathMap>> =
23        std::sync::RwLock::new(MapFileStore {
24            sources: Vec::new(),
25            mappings: NoPathMap,
26        });
27
28    impl FileSystem for GlobalStore {
29        fn new_source_id_with_line_starts(
30            &mut self,
31            path: PathBuf,
32            content: String,
33        ) -> (SourceId, LineStarts) {
34            SOURCE_IDS_TO_FILES
35                .write()
36                .unwrap()
37                .new_source_id_with_line_starts(path, content)
38        }
39
40        fn get_source_by_id<T, F: for<'a> FnOnce(&'a Source) -> T>(
41            &self,
42            source_id: SourceId,
43            f: F,
44        ) -> T {
45            SOURCE_IDS_TO_FILES
46                .read()
47                .unwrap()
48                .get_source_by_id(source_id, f)
49        }
50    }
51}
52
53#[derive(Default)]
54pub struct MapFileStore<T> {
55    sources: Vec<Source>,
56    mappings: T,
57}
58
59pub trait FileSystem: Sized {
60    /// Generate a new [SourceId]
61    fn new_source_id(&mut self, path: PathBuf, content: String) -> SourceId {
62        self.new_source_id_with_line_starts(path, content).0
63    }
64
65    fn new_source_id_with_line_starts(
66        &mut self,
67        path: PathBuf,
68        content: String,
69    ) -> (SourceId, LineStarts);
70
71    fn get_source_by_id<T, F: for<'a> FnOnce(&'a Source) -> T>(
72        &self,
73        source_id: SourceId,
74        f: F,
75    ) -> T;
76
77    fn get_file_path_and_content(&self, source_id: SourceId) -> (PathBuf, String) {
78        self.get_source_by_id(source_id, |Source { path, content, .. }| {
79            (path.to_owned(), content.to_owned())
80        })
81    }
82
83    fn get_file_path(&self, source_id: SourceId) -> PathBuf {
84        self.get_source_by_id(source_id, |source| source.path.to_owned())
85    }
86
87    fn get_file_content(&self, source_id: SourceId) -> String {
88        self.get_source_by_id(source_id, |source| source.content.to_owned())
89    }
90
91    fn get_file_whole_span(&self, source_id: SourceId) -> SpanWithSource {
92        self.get_source_by_id(source_id, |source| SpanWithSource {
93            start: 0,
94            end: source
95                .content
96                .len()
97                .try_into()
98                .expect("File too large to convert into Span"),
99            source: source_id,
100        })
101    }
102
103    /// Note that this does clone the result
104    fn get_file_slice<I: std::slice::SliceIndex<str>>(
105        &self,
106        source_id: SourceId,
107        indexer: I,
108    ) -> Option<<I::Output as ToOwned>::Owned>
109    where
110        I::Output: Sized + ToOwned,
111    {
112        self.get_source_by_id(source_id, |s| s.content.get(indexer).map(|v| v.to_owned()))
113    }
114}
115
116impl<M: PathMap> FileSystem for MapFileStore<M> {
117    fn new_source_id_with_line_starts(
118        &mut self,
119        path: PathBuf,
120        content: String,
121    ) -> (SourceId, LineStarts) {
122        let line_starts = LineStarts::new(&content);
123        let source = Source {
124            path: path.clone(),
125            content,
126            line_starts: line_starts.clone(),
127        };
128        self.sources.push(source);
129        let source_id = SourceId(self.sources.len().try_into().unwrap());
130        self.mappings.set_path(path, source_id);
131
132        // Import that this is after. SourceId(0) is SourceId::NULL
133        (source_id, line_starts)
134    }
135
136    fn get_source_by_id<T, F: for<'a> FnOnce(&'a Source) -> T>(
137        &self,
138        source_id: SourceId,
139        f: F,
140    ) -> T {
141        f(&self.sources[source_id.0 as usize - 1])
142    }
143}
144
145#[derive(Default)]
146pub struct NoPathMap;
147
148#[derive(Default)]
149pub struct WithPathMap(pub(crate) HashMap<PathBuf, SourceId>);
150
151impl PathMap for NoPathMap {
152    fn set_path(&mut self, _path: PathBuf, _source: SourceId) {}
153}
154
155impl PathMap for WithPathMap {
156    fn set_path(&mut self, path: PathBuf, source: SourceId) {
157        self.0.insert(path, source);
158    }
159}
160
161pub trait PathMap {
162    fn set_path(&mut self, path: PathBuf, source: SourceId);
163}
164
165impl<T: PathMap> MapFileStore<T> {
166    pub fn update_file(&mut self, id: SourceId, content: String) {
167        let item = &mut self.sources[id.0 as usize - 1];
168        item.line_starts = LineStarts::new(&content);
169        item.content = content;
170    }
171
172    /// Returns the OLD and NEW length of the file's content
173    pub fn append_to_file(&mut self, id: SourceId, content: &str) -> (usize, usize) {
174        let existing = &mut self.sources[id.0 as usize - 1];
175        let old_length = existing.content.len();
176        existing.line_starts.append(old_length, content);
177        existing.content.push_str(content);
178        (old_length, existing.content.len())
179    }
180}
181
182impl MapFileStore<WithPathMap> {
183    /// Updates an **existing** entry
184    ///
185    /// TODO partial updates
186    pub fn update_file_at_path(&mut self, path: &Path, content: String) {
187        self.update_file(self.mappings.0[path], content);
188    }
189
190    /// Returns a possible [SourceId] for a path
191    pub fn get_source_at_path(&self, path: &Path) -> Option<SourceId> {
192        self.mappings.0.get(path).copied()
193    }
194
195    /// Either a rename or move. **Must already exist**
196    pub fn change_file_path(&mut self, from: &Path, to: PathBuf) {
197        let id = self.mappings.0[from];
198        self.sources[id.0 as usize - 1].path = to;
199        self.mappings.0.remove(from);
200        self.mappings.0.insert(from.to_path_buf(), id);
201    }
202
203    pub fn create_or_update_file_at_path(&mut self, path: &Path, content: String) {
204        if let Some(existing_id) = self.mappings.0.get(path) {
205            self.update_file(*existing_id, content);
206        } else {
207            self.new_source_id(path.to_path_buf(), content);
208        }
209    }
210
211    pub fn get_paths(&self) -> &HashMap<PathBuf, SourceId> {
212        &self.mappings.0
213    }
214}
215
216impl<T: PathMap> MapFileStore<T> {
217    #[cfg(feature = "codespan-reporting")]
218    pub fn into_code_span_store(&self) -> CodeSpanStore<T> {
219        CodeSpanStore(self)
220    }
221}
222
223#[cfg(feature = "codespan-reporting")]
224pub struct CodeSpanStore<'a, T>(&'a MapFileStore<T>);
225
226#[cfg(feature = "codespan-reporting")]
227impl<'a, T> codespan_reporting::files::Files<'a> for CodeSpanStore<'a, T>
228where
229    T: PathMap,
230{
231    type FileId = SourceId;
232    type Name = String;
233    type Source = &'a str;
234
235    fn name(&'a self, id: Self::FileId) -> Result<Self::Name, codespan_reporting::files::Error> {
236        Ok(self.0.get_file_path(id).display().to_string())
237    }
238
239    fn source(
240        &'a self,
241        id: Self::FileId,
242    ) -> Result<Self::Source, codespan_reporting::files::Error> {
243        Ok(&self.0.sources[id.0 as usize - 1].content)
244    }
245
246    // Implementation copied from codespan codebase
247    fn line_index(
248        &'a self,
249        id: Self::FileId,
250        byte_index: usize,
251    ) -> Result<usize, codespan_reporting::files::Error> {
252        Ok(self.0.get_source_by_id(id, |source| {
253            source
254                .line_starts
255                .0
256                .binary_search(&byte_index)
257                .unwrap_or_else(|next_line| next_line - 1)
258        }))
259    }
260
261    fn line_range(
262        &'a self,
263        id: Self::FileId,
264        line_index: usize,
265    ) -> Result<std::ops::Range<usize>, codespan_reporting::files::Error> {
266        // Implementation copied from codespan codebase
267        self.0.get_source_by_id(id, |source| {
268            // Copied from codespan-reporting
269            fn line_start(
270                line_starts: &[usize],
271                line_index: usize,
272                source_len: usize,
273            ) -> Result<usize, codespan_reporting::files::Error> {
274                use std::cmp::Ordering;
275
276                match line_index.cmp(&line_starts.len()) {
277                    Ordering::Less => Ok(line_starts
278                        .get(line_index)
279                        .cloned()
280                        .expect("failed despite previous check")),
281                    Ordering::Equal => Ok(source_len),
282                    Ordering::Greater => Err(codespan_reporting::files::Error::LineTooLarge {
283                        given: line_index,
284                        max: line_starts.len() - 1,
285                    }),
286                }
287            }
288
289            line_start(&source.line_starts.0, line_index, source.content.len()).and_then(
290                |prev_line_start| {
291                    line_start(&source.line_starts.0, line_index + 1, source.content.len())
292                        .map(|next_line_start| prev_line_start..next_line_start)
293                },
294            )
295        })
296    }
297}