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 / compiler.rs
8.2 kB 221 lines
1// SPDX-License-Identifier: Apache-2.0 2// SPDX-FileCopyrightText: 2023 The Gleam contributors 3 4use debug_ignore::DebugIgnore; 5use ecow::EcoString; 6use itertools::Itertools; 7 8use gleam_core::{ 9 Error, Result, Warning, 10 analyse::TargetSupport, 11 build::{self, Mode, Module, NullTelemetry, Outcome, ProjectCompiler}, 12 config::PackageConfig, 13 io::{BeamCompilerIO, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio}, 14 line_numbers::LineNumbers, 15 manifest::Manifest, 16 paths::ProjectPaths, 17 type_::ModuleInterface, 18 warning::VectorWarningEmitterIO, 19}; 20use std::{collections::HashMap, rc::Rc}; 21 22use camino::Utf8PathBuf; 23 24use super::{LockGuard, Locker}; 25 26/// A wrapper around the project compiler which makes it possible to repeatedly 27/// recompile the top level package, reusing the information about the already 28/// compiled dependency packages. 29/// 30#[derive(Debug)] 31pub struct LspProjectCompiler<IO> { 32 pub project_compiler: ProjectCompiler<IO>, 33 34 /// Information on compiled modules. 35 pub modules: HashMap<EcoString, Module>, 36 pub sources: HashMap<EcoString, ModuleSourceInformation>, 37 38 /// The storage for the warning emitter. 39 pub warnings: Rc<VectorWarningEmitterIO>, 40 41 /// A lock to ensure that multiple instances of the LSP don't try and use 42 /// build directory at the same time. 43 pub locker: DebugIgnore<Box<dyn Locker>>, 44} 45 46impl<IO> LspProjectCompiler<IO> 47where 48 IO: CommandExecutor + FileSystemWriter + FileSystemReader + BeamCompilerIO + Clone, 49{ 50 pub fn new( 51 manifest: Manifest, 52 config: PackageConfig, 53 paths: ProjectPaths, 54 io: IO, 55 locker: Box<dyn Locker>, 56 ) -> Result<Self> { 57 let target = config.target; 58 let name = config.name.clone(); 59 let warnings = Rc::new(VectorWarningEmitterIO::default()); 60 61 // The build caches do not contain all the information we need in the 62 // LSP (e.g. the typed AST) so delete the caches for the top level 63 // package before we run for the first time. 64 { 65 let _guard: LockGuard = locker.lock_for_build()?; 66 let path = paths.build_directory_for_package(Mode::Lsp, target, &name); 67 io.delete_directory(&path)?; 68 } 69 70 let options = build::Options { 71 warnings_as_errors: false, 72 mode: Mode::Lsp, 73 target: None, 74 codegen: build::Codegen::None, 75 compile: build::Compile::All, 76 root_target_support: TargetSupport::Enforced, 77 no_print_progress: false, 78 }; 79 let mut project_compiler = ProjectCompiler::new( 80 config, 81 options, 82 manifest.packages, 83 &NullTelemetry, 84 warnings.clone(), 85 paths, 86 io, 87 ); 88 89 // To avoid the Erlang compiler printing to stdout (and thus 90 // violating LSP which is currently using stdout) we silence it. 91 project_compiler.subprocess_stdio = Stdio::Null; 92 93 Ok(Self { 94 locker: locker.into(), 95 warnings, 96 project_compiler, 97 modules: HashMap::new(), 98 sources: HashMap::new(), 99 }) 100 } 101 102 pub fn compile(&mut self) -> Outcome<Vec<Utf8PathBuf>, Error> { 103 // Lock the build directory to ensure to ensure we are the only one compiling 104 let _lock_guard: LockGuard = match self.locker.lock_for_build() { 105 Ok(it) => it, 106 Err(err) => return err.into(), 107 }; 108 109 // Verify that the build directory was created using the same version of 110 // Gleam as we are running. If it is not then we discard the build 111 // directory as the cache files may be in a different format. 112 if let Err(e) = self 113 .project_compiler 114 .check_gleam_version_and_build_configuration() 115 { 116 return e.into(); 117 } 118 119 self.project_compiler.reset_state_for_new_compile_run(); 120 121 let compiled_dependencies = match self.project_compiler.compile_dependencies() { 122 Ok(it) => it, 123 Err(err) => return err.into(), 124 }; 125 126 // Store the compiled dependency module information 127 for module in &compiled_dependencies { 128 let path = module.input_path.as_os_str().to_string_lossy().to_string(); 129 // strip canonicalised windows prefix 130 #[cfg(target_family = "windows")] 131 let path = path 132 .strip_prefix(r"\\?\") 133 .map(|s| s.to_string()) 134 .unwrap_or(path); 135 let line_numbers = LineNumbers::new(&module.code); 136 let source = ModuleSourceInformation { path, line_numbers }; 137 _ = self.sources.insert(module.name.clone(), source); 138 } 139 140 // Since cached modules are not recompiled we need to manually add them 141 for (name, module) in self.project_compiler.get_importable_modules() { 142 // It we already have the source for an importable module it means 143 // that we already have all the information we are adding here, so 144 // we can skip past to to avoid doing extra work for no gain. 145 if self.sources.contains_key(name) || name == "gleam" { 146 continue; 147 } 148 // Create the source information 149 let path = module.src_path.to_string(); 150 // strip canonicalised windows prefix 151 #[cfg(target_family = "windows")] 152 let path = path 153 .strip_prefix(r"\\?\") 154 .map(|s| s.to_string()) 155 .unwrap_or(path); 156 let line_numbers = module.line_numbers.clone(); 157 let source = ModuleSourceInformation { path, line_numbers }; 158 _ = self.sources.insert(name.clone(), source); 159 } 160 161 // Warnings from dependencies are not fixable by the programmer so 162 // we don't bother them with diagnostics for them. 163 let _ = self.take_warnings(); 164 165 // Compile the root package, that is, the one that the programmer is 166 // working in. 167 let (modules, error) = match self.project_compiler.compile_root_package() { 168 Outcome::Ok(package) => (package.modules, None), 169 Outcome::PartialFailure(package, error) => (package.modules, Some(error)), 170 Outcome::TotalFailure(error) => (vec![], Some(error)), 171 }; 172 173 // Record the compiled dependency modules 174 let mut compiled_modules = compiled_dependencies 175 .into_iter() 176 .map(|m| m.input_path) 177 .collect_vec(); 178 179 // Store the compiled module information 180 for module in modules { 181 let path = module.input_path.as_os_str().to_string_lossy().to_string(); 182 let line_numbers = LineNumbers::new(&module.code); 183 let source = ModuleSourceInformation { path, line_numbers }; 184 // Record that this one has been compiled. This is returned by this 185 // function and is used to determine what diagnostics to reset. 186 compiled_modules.push(module.input_path.clone()); 187 // Register information for the LS to use 188 _ = self.sources.insert(module.name.clone(), source); 189 _ = self.modules.insert(module.name.clone(), module); 190 } 191 192 match error { 193 None => Outcome::Ok(compiled_modules), 194 Some(error) => Outcome::PartialFailure(compiled_modules, error), 195 } 196 } 197} 198 199impl<IO> LspProjectCompiler<IO> { 200 pub fn take_warnings(&mut self) -> Vec<Warning> { 201 self.warnings.take() 202 } 203 204 pub fn get_source(&self, module: &str) -> Option<&ModuleSourceInformation> { 205 self.sources.get(module) 206 } 207 208 pub fn get_module_interface(&self, name: &str) -> Option<&ModuleInterface> { 209 self.project_compiler.get_importable_modules().get(name) 210 } 211} 212 213#[derive(Debug)] 214pub struct ModuleSourceInformation { 215 /// The path to the source file from within the project root 216 pub path: String, 217 218 /// Useful for converting from Gleam's byte index offsets to the LSP line 219 /// and column number positions. 220 pub line_numbers: LineNumbers, 221}