Fork of daniellemaywood.uk/gleam — Wasm codegen work
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}