Fork of daniellemaywood.uk/gleam — Wasm codegen work
9.9 kB
297 lines
1use gleam_core::{
2 Error, Result,
3 build::SourceFingerprint,
4 error::{FileIoAction, FileKind},
5 io::{BeamCompiler, CommandExecutor, FileSystemReader, FileSystemWriter},
6 paths::ProjectPaths,
7};
8use std::{
9 collections::{HashMap, hash_map::Entry},
10 time::SystemTime,
11};
12
13use camino::{Utf8Path, Utf8PathBuf};
14
15use super::{
16 DownloadDependencies, MakeLocker, engine::LanguageServerEngine, feedback::FeedbackBookKeeper,
17 files::FileSystemProxy, progress::ProgressReporter,
18};
19
20/// The language server instance serves a language client, typically a text
21/// editor. The editor could have multiple Gleam projects open at once, so run
22/// an instance of the language server engine for each project.
23///
24/// This router is responsible for finding or creating an engine for a given
25/// file using the nearest parent `gleam.toml` file.
26///
27#[derive(Debug)]
28pub(crate) struct Router<IO, Reporter> {
29 io: FileSystemProxy<IO>,
30 engines: HashMap<Utf8PathBuf, Project<IO, Reporter>>,
31 progress_reporter: Reporter,
32}
33
34impl<IO, Reporter> Router<IO, Reporter>
35where
36 // IO to be supplied from outside of gleam-core
37 IO: FileSystemReader
38 + FileSystemWriter
39 + BeamCompiler
40 + CommandExecutor
41 + DownloadDependencies
42 + MakeLocker
43 + Clone,
44 // IO to be supplied from inside of gleam-core
45 Reporter: ProgressReporter + Clone,
46{
47 pub fn new(progress_reporter: Reporter, io: FileSystemProxy<IO>) -> Self {
48 Self {
49 io,
50 engines: HashMap::new(),
51 progress_reporter,
52 }
53 }
54
55 pub fn project_path(&self, path: &Utf8Path) -> Option<Utf8PathBuf> {
56 find_gleam_project_parent(&self.io, path)
57 }
58
59 pub fn project_for_path(
60 &mut self,
61 path: Utf8PathBuf,
62 ) -> Result<Option<&mut Project<IO, Reporter>>> {
63 // If the path is the root of a known project then return it. Otherwise
64 // find the nearest parent project.
65 let path = if self.engines.contains_key(&path) {
66 path
67 } else {
68 let Some(path) = find_gleam_project_parent(&self.io, &path) else {
69 return Ok(None);
70 };
71 path
72 };
73
74 // If the gleam.toml has changed or the build directory is missing
75 // (e.g. `gleam clean`), then discard the project as the target,
76 // deps, etc may have changed and we need to rebuild taking them into
77 // account.
78 if let Some(project) = self.engines.get(&path) {
79 let paths = ProjectPaths::new(path.clone());
80
81 if !self.io.exists(&paths.build_directory())
82 || Self::gleam_toml_changed(&paths, project, &self.io)?
83 {
84 let _ = self.engines.remove(&path);
85 }
86 }
87
88 // Look up the project, creating a new one if it does not exist.
89 Ok(Some(match self.engines.entry(path.clone()) {
90 Entry::Occupied(entry) => entry.into_mut(),
91 Entry::Vacant(entry) => {
92 let project =
93 Self::new_project(path, self.io.clone(), self.progress_reporter.clone())?;
94 entry.insert(project)
95 }
96 }))
97 }
98
99 /// Has gleam.toml changed since the last time we saw this project?
100 fn gleam_toml_changed(
101 paths: &ProjectPaths,
102 project: &Project<IO, Reporter>,
103 io: &FileSystemProxy<IO>,
104 ) -> Result<bool, Error> {
105 // Get the location of gleam.toml for this project
106 let config_path = paths.root_config();
107
108 // See if the file modification time has changed.
109 if io.modification_time(&config_path)? == project.gleam_toml_modification_time {
110 return Ok(false); // Not changed
111 }
112
113 // The mtime has changed. This might not be a content change, so let's
114 // check the hash.
115 let toml = io.read(&config_path)?;
116 let gleam_toml_changed = project.gleam_toml_fingerprint != SourceFingerprint::new(&toml);
117 Ok(gleam_toml_changed)
118 }
119
120 pub fn delete_engine_for_path(&mut self, path: &Utf8Path) {
121 if let Some(path) = find_gleam_project_parent(&self.io, path) {
122 _ = self.engines.remove(&path);
123 }
124 }
125
126 fn new_project(
127 path: Utf8PathBuf,
128 io: FileSystemProxy<IO>,
129 progress_reporter: Reporter,
130 ) -> Result<Project<IO, Reporter>, Error> {
131 tracing::info!(?path, "creating_new_language_server_engine");
132 let paths = ProjectPaths::new(path);
133 let config_path = paths.root_config();
134 let modification_time = io.modification_time(&config_path)?;
135 let toml = io.read(&config_path)?;
136 let config = toml::from_str(&toml).map_err(|e| Error::FileIo {
137 action: FileIoAction::Parse,
138 kind: FileKind::File,
139 path: config_path,
140 err: Some(e.to_string()),
141 })?;
142 let engine = LanguageServerEngine::new(config, progress_reporter, io, paths)?;
143 let project = Project {
144 engine,
145 feedback: FeedbackBookKeeper::default(),
146 gleam_toml_modification_time: modification_time,
147 gleam_toml_fingerprint: SourceFingerprint::new(&toml),
148 };
149 Ok(project)
150 }
151}
152
153/// Given a given path, find the nearest parent directory containing a
154/// `gleam.toml` file.
155///
156/// The file must be in either the `src`, `test` or `dev` directory if it is not a
157/// `.gleam` file.
158fn find_gleam_project_parent<IO>(io: &IO, path: &Utf8Path) -> Option<Utf8PathBuf>
159where
160 IO: FileSystemReader,
161{
162 let is_module = path.extension().map(|x| x == "gleam").unwrap_or(false);
163 let mut directory = path.to_path_buf();
164
165 // If we are finding the gleam project of a directory then we want to check the directory itself
166 let is_directory = path.extension().is_none();
167 if is_directory {
168 directory.push("src");
169 }
170
171 while let Some(root) = directory.parent() {
172 // If there's no gleam.toml in the root then we continue to the next parent.
173 if !io.is_file(&root.join("gleam.toml")) {
174 _ = directory.pop();
175 continue;
176 }
177
178 // If it is a Gleam module then it must reside in the src, test dev directory.
179 if is_module
180 && !(directory.ends_with("test")
181 || directory.ends_with("src")
182 || directory.ends_with("dev"))
183 {
184 _ = directory.pop();
185 continue;
186 }
187
188 return Some(root.to_path_buf());
189 }
190 None
191}
192
193#[derive(Debug)]
194pub(crate) struct Project<A, B> {
195 pub engine: LanguageServerEngine<A, B>,
196 pub feedback: FeedbackBookKeeper,
197 pub gleam_toml_modification_time: SystemTime,
198 pub gleam_toml_fingerprint: SourceFingerprint,
199}
200
201#[cfg(test)]
202mod find_gleam_project_parent_tests {
203 use super::*;
204 use gleam_core::io::{FileSystemWriter, memory::InMemoryFileSystem};
205
206 #[test]
207 fn root() {
208 let io = InMemoryFileSystem::new();
209 assert_eq!(find_gleam_project_parent(&io, Utf8Path::new("/")), None);
210 }
211
212 #[test]
213 fn outside_a_project() {
214 let io = InMemoryFileSystem::new();
215 assert_eq!(
216 find_gleam_project_parent(&io, Utf8Path::new("/app/src/one.gleam")),
217 None
218 );
219 }
220
221 #[test]
222 fn gleam_toml_itself() {
223 let io = InMemoryFileSystem::new();
224 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
225 assert_eq!(
226 find_gleam_project_parent(&io, Utf8Path::new("/app/gleam.toml")),
227 Some(Utf8PathBuf::from("/app"))
228 );
229 }
230
231 #[test]
232 fn directory_with_gleam_toml() {
233 let io = InMemoryFileSystem::new();
234 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
235 assert_eq!(
236 find_gleam_project_parent(&io, Utf8Path::new("/app")),
237 Some(Utf8PathBuf::from("/app"))
238 );
239 }
240
241 #[test]
242 fn test_module() {
243 let io = InMemoryFileSystem::new();
244 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
245 assert_eq!(
246 find_gleam_project_parent(&io, Utf8Path::new("/app/test/one/two/three.gleam")),
247 Some(Utf8PathBuf::from("/app"))
248 );
249 }
250
251 #[test]
252 fn src_module() {
253 let io = InMemoryFileSystem::new();
254 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
255 assert_eq!(
256 find_gleam_project_parent(&io, Utf8Path::new("/app/src/one/two/three.gleam")),
257 Some(Utf8PathBuf::from("/app"))
258 );
259 }
260
261 #[test]
262 fn dev_module() {
263 let io = InMemoryFileSystem::new();
264 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
265 assert_eq!(
266 find_gleam_project_parent(&io, Utf8Path::new("/app/dev/one/two/three.gleam")),
267 Some(Utf8PathBuf::from("/app"))
268 );
269 }
270
271 // https://github.com/gleam-lang/gleam/issues/2121
272 #[test]
273 fn module_in_project_but_not_src_or_test_or_dev() {
274 let io = InMemoryFileSystem::new();
275 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
276 assert_eq!(
277 find_gleam_project_parent(&io, Utf8Path::new("/app/other/one/two/three.gleam")),
278 None,
279 );
280 }
281
282 #[test]
283 fn nested_projects() {
284 let io = InMemoryFileSystem::new();
285 io.write(Utf8Path::new("/app/gleam.toml"), "").unwrap();
286 io.write(Utf8Path::new("/app/examples/wibble/gleam.toml"), "")
287 .unwrap();
288 assert_eq!(
289 find_gleam_project_parent(&io, Utf8Path::new("/app/src/one.gleam")),
290 Some(Utf8PathBuf::from("/app"))
291 );
292 assert_eq!(
293 find_gleam_project_parent(&io, Utf8Path::new("/app/examples/wibble/src/one.gleam")),
294 Some(Utf8PathBuf::from("/app/examples/wibble"))
295 );
296 }
297}