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 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 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 (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 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 pub fn update_file_at_path(&mut self, path: &Path, content: String) {
187 self.update_file(self.mappings.0[path], content);
188 }
189
190 pub fn get_source_at_path(&self, path: &Path) -> Option<SourceId> {
192 self.mappings.0.get(path).copied()
193 }
194
195 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 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 self.0.get_source_by_id(id, |source| {
268 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}