// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 The Gleam contributors use debug_ignore::DebugIgnore; use ecow::EcoString; use itertools::Itertools; use gleam_core::{ Error, Result, Warning, analyse::TargetSupport, build::{self, Mode, Module, NullTelemetry, Outcome, ProjectCompiler}, config::PackageConfig, io::{BeamCompilerIO, CommandExecutor, FileSystemReader, FileSystemWriter, Stdio}, line_numbers::LineNumbers, manifest::Manifest, paths::ProjectPaths, type_::ModuleInterface, warning::VectorWarningEmitterIO, }; use std::{collections::HashMap, rc::Rc}; use camino::Utf8PathBuf; use super::{LockGuard, Locker}; /// A wrapper around the project compiler which makes it possible to repeatedly /// recompile the top level package, reusing the information about the already /// compiled dependency packages. /// #[derive(Debug)] pub struct LspProjectCompiler { pub project_compiler: ProjectCompiler, /// Information on compiled modules. pub modules: HashMap, pub sources: HashMap, /// The storage for the warning emitter. pub warnings: Rc, /// A lock to ensure that multiple instances of the LSP don't try and use /// build directory at the same time. pub locker: DebugIgnore>, } impl LspProjectCompiler where IO: CommandExecutor + FileSystemWriter + FileSystemReader + BeamCompilerIO + Clone, { pub fn new( manifest: Manifest, config: PackageConfig, paths: ProjectPaths, io: IO, locker: Box, ) -> Result { let target = config.target; let name = config.name.clone(); let warnings = Rc::new(VectorWarningEmitterIO::default()); // The build caches do not contain all the information we need in the // LSP (e.g. the typed AST) so delete the caches for the top level // package before we run for the first time. { let _guard: LockGuard = locker.lock_for_build()?; let path = paths.build_directory_for_package(Mode::Lsp, target, &name); io.delete_directory(&path)?; } let options = build::Options { warnings_as_errors: false, mode: Mode::Lsp, target: None, codegen: build::Codegen::None, compile: build::Compile::All, root_target_support: TargetSupport::Enforced, no_print_progress: false, }; let mut project_compiler = ProjectCompiler::new( config, options, manifest.packages, &NullTelemetry, warnings.clone(), paths, io, ); // To avoid the Erlang compiler printing to stdout (and thus // violating LSP which is currently using stdout) we silence it. project_compiler.subprocess_stdio = Stdio::Null; Ok(Self { locker: locker.into(), warnings, project_compiler, modules: HashMap::new(), sources: HashMap::new(), }) } pub fn compile(&mut self) -> Outcome, Error> { // Lock the build directory to ensure to ensure we are the only one compiling let _lock_guard: LockGuard = match self.locker.lock_for_build() { Ok(it) => it, Err(err) => return err.into(), }; // Verify that the build directory was created using the same version of // Gleam as we are running. If it is not then we discard the build // directory as the cache files may be in a different format. if let Err(e) = self .project_compiler .check_gleam_version_and_build_configuration() { return e.into(); } self.project_compiler.reset_state_for_new_compile_run(); let compiled_dependencies = match self.project_compiler.compile_dependencies() { Ok(it) => it, Err(err) => return err.into(), }; // Store the compiled dependency module information for module in &compiled_dependencies { let path = module.input_path.as_os_str().to_string_lossy().to_string(); // strip canonicalised windows prefix #[cfg(target_family = "windows")] let path = path .strip_prefix(r"\\?\") .map(|s| s.to_string()) .unwrap_or(path); let line_numbers = LineNumbers::new(&module.code); let source = ModuleSourceInformation { path, line_numbers }; _ = self.sources.insert(module.name.clone(), source); } // Since cached modules are not recompiled we need to manually add them for (name, module) in self.project_compiler.get_importable_modules() { // It we already have the source for an importable module it means // that we already have all the information we are adding here, so // we can skip past to to avoid doing extra work for no gain. if self.sources.contains_key(name) || name == "gleam" { continue; } // Create the source information let path = module.src_path.to_string(); // strip canonicalised windows prefix #[cfg(target_family = "windows")] let path = path .strip_prefix(r"\\?\") .map(|s| s.to_string()) .unwrap_or(path); let line_numbers = module.line_numbers.clone(); let source = ModuleSourceInformation { path, line_numbers }; _ = self.sources.insert(name.clone(), source); } // Warnings from dependencies are not fixable by the programmer so // we don't bother them with diagnostics for them. let _ = self.take_warnings(); // Compile the root package, that is, the one that the programmer is // working in. let (modules, error) = match self.project_compiler.compile_root_package() { Outcome::Ok(package) => (package.modules, None), Outcome::PartialFailure(package, error) => (package.modules, Some(error)), Outcome::TotalFailure(error) => (vec![], Some(error)), }; // Record the compiled dependency modules let mut compiled_modules = compiled_dependencies .into_iter() .map(|m| m.input_path) .collect_vec(); // Store the compiled module information for module in modules { let path = module.input_path.as_os_str().to_string_lossy().to_string(); let line_numbers = LineNumbers::new(&module.code); let source = ModuleSourceInformation { path, line_numbers }; // Record that this one has been compiled. This is returned by this // function and is used to determine what diagnostics to reset. compiled_modules.push(module.input_path.clone()); // Register information for the LS to use _ = self.sources.insert(module.name.clone(), source); _ = self.modules.insert(module.name.clone(), module); } match error { None => Outcome::Ok(compiled_modules), Some(error) => Outcome::PartialFailure(compiled_modules, error), } } } impl LspProjectCompiler { pub fn take_warnings(&mut self) -> Vec { self.warnings.take() } pub fn get_source(&self, module: &str) -> Option<&ModuleSourceInformation> { self.sources.get(module) } pub fn get_module_interface(&self, name: &str) -> Option<&ModuleInterface> { self.project_compiler.get_importable_modules().get(name) } } #[derive(Debug)] pub struct ModuleSourceInformation { /// The path to the source file from within the project root pub path: String, /// Useful for converting from Gleam's byte index offsets to the LSP line /// and column number positions. pub line_numbers: LineNumbers, }