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