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
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}