1use crate::diagnostic::{Diagnostic, Severity};
4use crate::interpolation::{
5 DocumentInterpolation, EnvironmentProvider, InterpolationOptions, interpolate_document_with_options,
6};
7use crate::model::{ComposeDocument, ModelParse};
8use crate::source::SourceId;
9use crate::syntax::{SyntaxDocument, SyntaxParseError};
10use std::collections::BTreeMap;
11use std::error::Error;
12use std::fmt;
13use std::path::{Path, PathBuf};
14use std::sync::Arc;
15
16#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct DocumentOrigin {
23 label: String,
24 directory: PathBuf,
25}
26
27impl DocumentOrigin {
28 #[must_use]
30 pub fn new(label: impl Into<String>, directory: impl Into<PathBuf>) -> Self {
31 Self {
32 label: label.into(),
33 directory: directory.into(),
34 }
35 }
36
37 #[must_use]
39 pub fn label(&self) -> &str {
40 &self.label
41 }
42
43 #[must_use]
45 pub fn directory(&self) -> &Path {
46 &self.directory
47 }
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct DocumentInput {
53 source_id: SourceId,
54 origin: DocumentOrigin,
55 source: Arc<str>,
56}
57
58impl DocumentInput {
59 #[must_use]
61 pub fn new(source_id: SourceId, origin: DocumentOrigin, source: impl Into<Arc<str>>) -> Self {
62 Self {
63 source_id,
64 origin,
65 source: source.into(),
66 }
67 }
68
69 #[must_use]
71 pub const fn source_id(&self) -> SourceId {
72 self.source_id
73 }
74
75 #[must_use]
77 pub const fn origin(&self) -> &DocumentOrigin {
78 &self.origin
79 }
80
81 #[must_use]
83 pub fn source_text(&self) -> &str {
84 &self.source
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Eq)]
90pub struct LoadedDocument {
91 origin: DocumentOrigin,
92 syntax: SyntaxDocument,
93 syntax_diagnostics: Vec<Diagnostic>,
94 model: ModelParse,
95}
96
97impl LoadedDocument {
98 #[must_use]
100 pub const fn source_id(&self) -> SourceId {
101 self.syntax.source_id()
102 }
103
104 #[must_use]
106 pub const fn origin(&self) -> &DocumentOrigin {
107 &self.origin
108 }
109
110 #[must_use]
112 pub const fn syntax(&self) -> &SyntaxDocument {
113 &self.syntax
114 }
115
116 #[must_use]
118 pub fn syntax_diagnostics(&self) -> &[Diagnostic] {
119 &self.syntax_diagnostics
120 }
121
122 #[must_use]
124 pub const fn model(&self) -> &ModelParse {
125 &self.model
126 }
127
128 #[must_use]
130 pub fn is_valid(&self) -> bool {
131 self.syntax_diagnostics
132 .iter()
133 .chain(self.model.diagnostics())
134 .all(|diagnostic| diagnostic.severity() != Severity::Error)
135 }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq)]
144pub struct LoadedProject {
145 documents: Vec<LoadedDocument>,
146 base_directory: PathBuf,
147 diagnostics: Vec<Diagnostic>,
148}
149
150impl LoadedProject {
151 pub fn load(inputs: impl IntoIterator<Item = DocumentInput>) -> Result<Self, ProjectLoadError> {
161 let inputs: Vec<_> = inputs.into_iter().collect();
162 let Some(first) = inputs.first() else {
163 return Err(ProjectLoadError::EmptyProject);
164 };
165
166 let mut source_origins = BTreeMap::new();
167 for input in &inputs {
168 if let Some(first_origin) = source_origins.insert(input.source_id, input.origin.label.clone()) {
169 return Err(ProjectLoadError::DuplicateSourceId {
170 source_id: input.source_id,
171 first_origin,
172 duplicate_origin: input.origin.label.clone(),
173 });
174 }
175 }
176
177 let base_directory = first.origin.directory.clone();
178 let mut documents = Vec::with_capacity(inputs.len());
179 let mut diagnostics = Vec::new();
180 for input in inputs {
181 let syntax = SyntaxDocument::parse(input.source_id, input.source).map_err(|error| {
182 ProjectLoadError::SyntaxCapacity {
183 origin: input.origin.clone(),
184 error,
185 }
186 })?;
187 let (syntax, syntax_diagnostics) = syntax.into_parts();
188 let model = ComposeDocument::parse(&syntax);
189 diagnostics.extend(syntax_diagnostics.iter().cloned());
190 diagnostics.extend(model.diagnostics().iter().cloned());
191 documents.push(LoadedDocument {
192 origin: input.origin,
193 syntax,
194 syntax_diagnostics,
195 model,
196 });
197 }
198
199 Ok(Self {
200 documents,
201 base_directory,
202 diagnostics,
203 })
204 }
205
206 #[must_use]
208 pub fn documents(&self) -> &[LoadedDocument] {
209 &self.documents
210 }
211
212 #[must_use]
214 pub fn document(&self, source_id: SourceId) -> Option<&LoadedDocument> {
215 self.documents.iter().find(|document| document.source_id() == source_id)
216 }
217
218 #[must_use]
220 pub fn base_directory(&self) -> &Path {
221 &self.base_directory
222 }
223
224 #[must_use]
226 pub fn diagnostics(&self) -> &[Diagnostic] {
227 &self.diagnostics
228 }
229
230 #[must_use]
232 pub fn is_valid(&self) -> bool {
233 self.diagnostics
234 .iter()
235 .all(|diagnostic| diagnostic.severity() != Severity::Error)
236 }
237
238 #[must_use]
240 pub fn interpolate(&self, environment: &dyn EnvironmentProvider) -> ProjectInterpolation {
241 self.interpolate_with_options(environment, InterpolationOptions::default())
242 }
243
244 #[must_use]
246 pub fn interpolate_with_options(
247 &self,
248 environment: &dyn EnvironmentProvider,
249 options: InterpolationOptions,
250 ) -> ProjectInterpolation {
251 let documents: Vec<_> = self
252 .documents
253 .iter()
254 .map(|document| interpolate_document_with_options(document.syntax(), environment, options))
255 .collect();
256 let diagnostics = documents
257 .iter()
258 .flat_map(|document| document.diagnostics().iter().cloned())
259 .collect();
260 ProjectInterpolation { documents, diagnostics }
261 }
262}
263
264#[derive(Debug, Clone, PartialEq, Eq)]
266pub struct ProjectInterpolation {
267 documents: Vec<DocumentInterpolation>,
268 diagnostics: Vec<Diagnostic>,
269}
270
271impl ProjectInterpolation {
272 #[must_use]
274 pub fn documents(&self) -> &[DocumentInterpolation] {
275 &self.documents
276 }
277
278 #[must_use]
280 pub fn document(&self, source_id: SourceId) -> Option<&DocumentInterpolation> {
281 self.documents.iter().find(|document| document.source_id() == source_id)
282 }
283
284 #[must_use]
286 pub fn diagnostics(&self) -> &[Diagnostic] {
287 &self.diagnostics
288 }
289
290 #[must_use]
292 pub fn is_valid(&self) -> bool {
293 self.diagnostics
294 .iter()
295 .all(|diagnostic| diagnostic.severity() != Severity::Error)
296 }
297}
298
299#[derive(Debug, Clone, PartialEq, Eq)]
301pub enum ProjectLoadError {
302 EmptyProject,
304 DuplicateSourceId {
306 source_id: SourceId,
308 first_origin: String,
310 duplicate_origin: String,
312 },
313 SyntaxCapacity {
315 origin: DocumentOrigin,
317 error: SyntaxParseError,
319 },
320}
321
322impl fmt::Display for ProjectLoadError {
323 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
324 match self {
325 Self::EmptyProject => formatter.write_str("a Compose project requires at least one document"),
326 Self::DuplicateSourceId {
327 source_id,
328 first_origin,
329 duplicate_origin,
330 } => write!(
331 formatter,
332 "{source_id} is assigned to both `{first_origin}` and `{duplicate_origin}`"
333 ),
334 Self::SyntaxCapacity { origin, error } => write!(formatter, "{}: {error}", origin.label),
335 }
336 }
337}
338
339impl Error for ProjectLoadError {
340 fn source(&self) -> Option<&(dyn Error + 'static)> {
341 match self {
342 Self::SyntaxCapacity { error, .. } => Some(error),
343 Self::EmptyProject | Self::DuplicateSourceId { .. } => None,
344 }
345 }
346}