Fork of daniellemaywood.uk/gleam — Wasm codegen work
2

Configure Feed

Select the types of activity you want to include in your feed.

gleam / language-server / src / router.rs
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}