use crate::diagnostic::{Diagnostic, Severity};
use crate::interpolation::{
DocumentInterpolation, EnvironmentProvider, InterpolationOptions, interpolate_document_with_options,
};
use crate::model::{ComposeDocument, ModelParse};
use crate::source::SourceId;
use crate::syntax::{SyntaxDocument, SyntaxParseError};
use std::collections::BTreeMap;
use std::error::Error;
use std::fmt;
use std::path::{Path, PathBuf};
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentOrigin {
label: String,
directory: PathBuf,
}
impl DocumentOrigin {
#[must_use]
pub fn new(label: impl Into<String>, directory: impl Into<PathBuf>) -> Self {
Self {
label: label.into(),
directory: directory.into(),
}
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub fn directory(&self) -> &Path {
&self.directory
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DocumentInput {
source_id: SourceId,
origin: DocumentOrigin,
source: Arc<str>,
}
impl DocumentInput {
#[must_use]
pub fn new(source_id: SourceId, origin: DocumentOrigin, source: impl Into<Arc<str>>) -> Self {
Self {
source_id,
origin,
source: source.into(),
}
}
#[must_use]
pub const fn source_id(&self) -> SourceId {
self.source_id
}
#[must_use]
pub const fn origin(&self) -> &DocumentOrigin {
&self.origin
}
#[must_use]
pub fn source_text(&self) -> &str {
&self.source
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedDocument {
origin: DocumentOrigin,
syntax: SyntaxDocument,
syntax_diagnostics: Vec<Diagnostic>,
model: ModelParse,
}
impl LoadedDocument {
#[must_use]
pub const fn source_id(&self) -> SourceId {
self.syntax.source_id()
}
#[must_use]
pub const fn origin(&self) -> &DocumentOrigin {
&self.origin
}
#[must_use]
pub const fn syntax(&self) -> &SyntaxDocument {
&self.syntax
}
#[must_use]
pub fn syntax_diagnostics(&self) -> &[Diagnostic] {
&self.syntax_diagnostics
}
#[must_use]
pub const fn model(&self) -> &ModelParse {
&self.model
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.syntax_diagnostics
.iter()
.chain(self.model.diagnostics())
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LoadedProject {
documents: Vec<LoadedDocument>,
base_directory: PathBuf,
diagnostics: Vec<Diagnostic>,
}
impl LoadedProject {
pub fn load(inputs: impl IntoIterator<Item = DocumentInput>) -> Result<Self, ProjectLoadError> {
let inputs: Vec<_> = inputs.into_iter().collect();
let Some(first) = inputs.first() else {
return Err(ProjectLoadError::EmptyProject);
};
let mut source_origins = BTreeMap::new();
for input in &inputs {
if let Some(first_origin) = source_origins.insert(input.source_id, input.origin.label.clone()) {
return Err(ProjectLoadError::DuplicateSourceId {
source_id: input.source_id,
first_origin,
duplicate_origin: input.origin.label.clone(),
});
}
}
let base_directory = first.origin.directory.clone();
let mut documents = Vec::with_capacity(inputs.len());
let mut diagnostics = Vec::new();
for input in inputs {
let syntax = SyntaxDocument::parse(input.source_id, input.source).map_err(|error| {
ProjectLoadError::SyntaxCapacity {
origin: input.origin.clone(),
error,
}
})?;
let (syntax, syntax_diagnostics) = syntax.into_parts();
let model = ComposeDocument::parse(&syntax);
diagnostics.extend(syntax_diagnostics.iter().cloned());
diagnostics.extend(model.diagnostics().iter().cloned());
documents.push(LoadedDocument {
origin: input.origin,
syntax,
syntax_diagnostics,
model,
});
}
Ok(Self {
documents,
base_directory,
diagnostics,
})
}
#[must_use]
pub fn documents(&self) -> &[LoadedDocument] {
&self.documents
}
#[must_use]
pub fn document(&self, source_id: SourceId) -> Option<&LoadedDocument> {
self.documents.iter().find(|document| document.source_id() == source_id)
}
#[must_use]
pub fn base_directory(&self) -> &Path {
&self.base_directory
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
#[must_use]
pub fn interpolate(&self, environment: &dyn EnvironmentProvider) -> ProjectInterpolation {
self.interpolate_with_options(environment, InterpolationOptions::default())
}
#[must_use]
pub fn interpolate_with_options(
&self,
environment: &dyn EnvironmentProvider,
options: InterpolationOptions,
) -> ProjectInterpolation {
let documents: Vec<_> = self
.documents
.iter()
.map(|document| interpolate_document_with_options(document.syntax(), environment, options))
.collect();
let diagnostics = documents
.iter()
.flat_map(|document| document.diagnostics().iter().cloned())
.collect();
ProjectInterpolation { documents, diagnostics }
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProjectInterpolation {
documents: Vec<DocumentInterpolation>,
diagnostics: Vec<Diagnostic>,
}
impl ProjectInterpolation {
#[must_use]
pub fn documents(&self) -> &[DocumentInterpolation] {
&self.documents
}
#[must_use]
pub fn document(&self, source_id: SourceId) -> Option<&DocumentInterpolation> {
self.documents.iter().find(|document| document.source_id() == source_id)
}
#[must_use]
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
#[must_use]
pub fn is_valid(&self) -> bool {
self.diagnostics
.iter()
.all(|diagnostic| diagnostic.severity() != Severity::Error)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProjectLoadError {
EmptyProject,
DuplicateSourceId {
source_id: SourceId,
first_origin: String,
duplicate_origin: String,
},
SyntaxCapacity {
origin: DocumentOrigin,
error: SyntaxParseError,
},
}
impl fmt::Display for ProjectLoadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyProject => formatter.write_str("a Compose project requires at least one document"),
Self::DuplicateSourceId {
source_id,
first_origin,
duplicate_origin,
} => write!(
formatter,
"{source_id} is assigned to both `{first_origin}` and `{duplicate_origin}`"
),
Self::SyntaxCapacity { origin, error } => write!(formatter, "{}: {error}", origin.label),
}
}
}
impl Error for ProjectLoadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::SyntaxCapacity { error, .. } => Some(error),
Self::EmptyProject | Self::DuplicateSourceId { .. } => None,
}
}
}