Skip to main content

lutra_compiler/project/
source_tree.rs

1use std::{path, str::FromStr};
2
3use indexmap::IndexMap;
4use itertools::Itertools;
5
6/// Sources used to resolve the project.
7/// All paths are relative to the project root.
8// We use `SourceTree` to represent both a single file (including a "file" piped
9// from stdin), and a collection of files. (Possibly this could be implemented
10// as a Trait with a Struct for each type, which would use structure over values
11// (i.e. `Option<PathBuf>` below signifies whether it's a project or not). But
12// waiting until it's necessary before splitting it out.)
13#[derive(Debug, Clone)]
14pub struct SourceTree {
15    /// Path to the root of the source tree.
16    /// Can be a directory that contains module.lt or a .lt file.
17    root: path::PathBuf,
18
19    /// Mapping from file paths into into their contents.
20    /// Paths are relative to the root.
21    sources: IndexMap<path::PathBuf, String>,
22}
23
24impl SourceTree {
25    pub fn empty() -> Self {
26        SourceTree {
27            sources: Default::default(),
28            root: path::PathBuf::new(),
29        }
30    }
31
32    pub fn single(path: path::PathBuf, content: String) -> Self {
33        SourceTree {
34            sources: [(path.clone(), content)].into(),
35            root: path::PathBuf::new(),
36        }
37    }
38
39    pub fn new<I>(iter: I, root: path::PathBuf) -> Self
40    where
41        I: IntoIterator<Item = (path::PathBuf, String)>,
42    {
43        SourceTree {
44            sources: IndexMap::from_iter(iter),
45            root,
46        }
47    }
48
49    pub fn is_empty(&self) -> bool {
50        self.sources.len() == 0
51    }
52
53    pub fn insert(&mut self, path: path::PathBuf, content: String) {
54        self.sources.insert(path, content);
55    }
56
57    pub fn replace(&mut self, path: &path::Path, content: String) -> Option<String> {
58        let source = self.sources.get_mut(path)?;
59        Some(std::mem::replace(source, content))
60    }
61
62    pub fn get_ids(&self) -> impl Iterator<Item = u16> {
63        0..self.sources.len() as u16
64    }
65    pub fn get_sources(&self) -> impl Iterator<Item = (&path::PathBuf, &String)> {
66        self.sources.iter()
67    }
68    pub fn get_root(&self) -> &std::path::Path {
69        self.root.as_path()
70    }
71
72    pub fn get_by_id(&self, source_id: u16) -> Option<(&path::Path, &str)> {
73        self.sources
74            .get_index(source_id as usize)
75            .map(|(p, c)| (p.as_path(), c.as_str()))
76    }
77
78    pub fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
79        self.sources
80            .get_full(path)
81            .map(|(i, _, content)| (i as u16, content.as_str()))
82    }
83
84    pub fn get_source_display_paths(&self) -> impl Iterator<Item = &path::Path> {
85        self.sources
86            .keys()
87            .map(|path| self.get_display_path(path).unwrap())
88    }
89
90    /// Converts a "project path" into an absolute path in the file-system.
91    pub fn get_absolute_path(&self, path: impl AsRef<path::Path>) -> path::PathBuf {
92        let path = path.as_ref();
93        if path.as_os_str().is_empty() {
94            self.root.to_path_buf()
95        } else {
96            self.get_project_dir().join(path)
97        }
98    }
99
100    /// Converts an absolute path into a path relative to the root.
101    /// Not that this is not relative to "project dir".
102    pub fn get_relative_path<'a>(
103        &self,
104        absolute_path: &'a path::Path,
105    ) -> Result<&'a path::Path, path::StripPrefixError> {
106        absolute_path.strip_prefix(&self.root)
107    }
108
109    /// Returns project dir: the directory in which the project files reside.
110    /// For example,
111    /// - if root is `/some_path/project/`, then that is also the project dir,
112    /// - if root is `/some_path/my_file.lt`, then project dir is `/some_path/`.
113    pub fn get_project_dir(&self) -> &path::Path {
114        if self.root.extension().is_some_and(|e| e == "lt") {
115            self.root.parent().unwrap()
116        } else {
117            &self.root
118        }
119    }
120
121    /// Converts a path (either absolute or relative to project root) into a
122    /// "display path", which is path relative to "project dir".
123    /// This is equivalent to "relative path", except for single-file projects,
124    /// where root is a file and "project dir" is its parent directory.
125    pub fn get_display_path<'a>(&'a self, path: &'a path::Path) -> Option<&'a path::Path> {
126        if path.is_absolute() {
127            path.strip_prefix(self.get_project_dir()).ok()
128        } else if path.as_os_str().is_empty() {
129            self.root.file_name().map(path::Path::new)
130        } else {
131            Some(path)
132        }
133    }
134}
135
136impl std::fmt::Display for SourceTree {
137    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
138        let mut r = format!("path: {}\nsources:\n", self.root.to_string_lossy());
139
140        for source in self.sources.keys().sorted() {
141            r += "- ";
142            r += &source.to_string_lossy();
143            r += "\n";
144        }
145
146        f.write_str(&r)
147    }
148}
149
150/// Project source (in [SourceTree]) and a code snippet
151pub struct SourceOverlay<'a> {
152    tree: &'a SourceTree,
153
154    snippet_path: path::PathBuf,
155    snippet: &'a str,
156}
157
158impl<'a> SourceOverlay<'a> {
159    pub fn new(tree: &'a SourceTree, snippet: &'a str, snippet_path: Option<&str>) -> Self {
160        Self {
161            tree,
162            snippet,
163            snippet_path: snippet_path
164                .map(|s| path::PathBuf::from_str(s).unwrap())
165                .unwrap_or_default(),
166        }
167    }
168
169    pub const fn overlay_id() -> u16 {
170        u16::MAX
171    }
172}
173
174pub(crate) trait SourceProvider {
175    fn get_root(&self) -> &path::Path;
176
177    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)>;
178
179    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)>;
180}
181
182impl SourceProvider for SourceTree {
183    fn get_root(&self) -> &path::Path {
184        &self.root
185    }
186    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
187        SourceTree::get_by_id(self, id)
188    }
189
190    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
191        SourceTree::get_by_path(self, path)
192    }
193}
194
195impl<'a> SourceProvider for SourceOverlay<'a> {
196    fn get_root(&self) -> &path::Path {
197        &self.tree.root
198    }
199
200    fn get_by_id(&self, id: u16) -> Option<(&path::Path, &str)> {
201        if id == SourceOverlay::overlay_id() {
202            Some((self.snippet_path.as_path(), self.snippet))
203        } else {
204            self.tree.get_by_id(id)
205        }
206    }
207
208    fn get_by_path(&self, path: &path::Path) -> Option<(u16, &str)> {
209        if path == self.snippet_path {
210            Some((SourceOverlay::overlay_id(), self.snippet))
211        } else {
212            self.tree.get_by_path(path)
213        }
214    }
215}