microcad_lang/syntax/source_file/
mod.rs

1// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! µcad source file representation
5
6use miette::{MietteError, MietteSpanContents, SourceCode, SourceSpan, SpanContents};
7use crate::{src_ref::*, syntax::*};
8
9/// µcad source file
10#[derive(Clone, Default)]
11pub struct SourceFile {
12    /// Documentation.
13    pub doc: Option<DocBlock>,
14    /// Qualified name of the file if loaded from externals
15    pub name: QualifiedName,
16    /// Root code body.
17    pub statements: StatementList,
18    /// Name of loaded file.
19    filename: Option<std::path::PathBuf>,
20    /// Source file string, TODO: might be a &'a str in the future
21    pub source: String,
22
23    /// Hash of the source file
24    ///
25    /// This hash is calculated from the source code itself
26    /// This is used to map `SrcRef` -> `SourceFile`
27    pub hash: u64,
28}
29
30impl SourceFile {
31    /// Create new source file from existing source.
32    pub fn new(
33        doc: Option<DocBlock>,
34        statements: StatementList,
35        source: String,
36        hash: u64,
37    ) -> Self {
38        Self {
39            doc,
40            statements,
41            source,
42            hash,
43            ..Default::default()
44        }
45    }
46    /// Return filename of loaded file or `<no file>`
47    pub fn filename(&self) -> std::path::PathBuf {
48        self.filename
49            .clone()
50            .unwrap_or(std::path::PathBuf::from(crate::invalid_no_ansi!(SOURCE)))
51    }
52
53    /// Return filename of loaded file or `<no file>`
54    pub fn set_filename(&mut self, path: impl AsRef<std::path::Path>) {
55        assert!(self.filename.is_none());
56        self.filename = Some(
57            path.as_ref()
58                .canonicalize()
59                .unwrap_or(path.as_ref().to_path_buf()),
60        )
61    }
62
63    /// Return filename of loaded file or `<no file>`
64    pub fn filename_as_str(&self) -> &str {
65        self.filename
66            .as_ref()
67            .map(|f| f.to_str().expect("File name error {filename:?}"))
68            .unwrap_or(crate::invalid!(SOURCE))
69    }
70
71    /// Return the module name from the file name
72    pub fn id(&self) -> Identifier {
73        self.name.last().unwrap_or(&Identifier::none()).clone()
74    }
75
76    /// get a specific line
77    ///
78    /// - `line`: line number beginning at `0`
79    pub fn get_line(&self, line: usize) -> Option<&str> {
80        self.source.lines().nth(line)
81    }
82
83    /// get a specific line
84    ///
85    /// - `line`: line number beginning at `0`
86    pub fn get_code(&self, src_ref: &SrcRef) -> &str {
87        let range = &src_ref.as_ref().expect("source reference empty").range;
88        &self.source[range.start..range.end]
89    }
90
91    /// return number of source code lines
92    pub fn num_lines(&self) -> usize {
93        self.source.lines().count()
94    }
95
96    /// Set file name.
97    pub fn set_name(&mut self, name: QualifiedName) {
98        self.name = name
99    }
100}
101
102impl std::fmt::Display for SourceFile {
103    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104        self.statements.iter().try_for_each(|s| writeln!(f, "{s}"))
105    }
106}
107
108impl std::fmt::Debug for SourceFile {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        self.statements
111            .iter()
112            .try_for_each(|s| writeln!(f, "{s:?}"))
113    }
114}
115
116impl TreeDisplay for SourceFile {
117    fn tree_print(&self, f: &mut std::fmt::Formatter, mut depth: TreeState) -> std::fmt::Result {
118        writeln!(
119            f,
120            "{:depth$}SourceFile '{:?}' ({}):",
121            "",
122            self.id(),
123            self.filename_as_str()
124        )?;
125        depth.indent();
126        if let Some(doc) = &self.doc {
127            doc.tree_print(f, depth)?;
128        }
129        self.statements
130            .iter()
131            .try_for_each(|s| s.tree_print(f, depth))
132    }
133}
134
135impl SrcReferrer for SourceFile {
136    fn src_ref(&self) -> crate::src_ref::SrcRef {
137        SrcRef::new(0..self.num_lines(), 0, 0, self.hash)
138    }
139}
140
141impl Doc for SourceFile {
142    fn doc(&self) -> Option<DocBlock> {
143        self.doc.clone()
144    }
145}
146
147#[test]
148fn load_source_file_wrong_location() {
149    let source_file = SourceFile::load("I do not exist.µcad");
150    if let Err(err) = source_file {
151        log::info!("{err}");
152        //assert_eq!(format!("{err}"), "Cannot load source file");
153    } else {
154        panic!("Does file exist?");
155    }
156}
157
158/// A compatibility layer for using SourceFile with miette
159pub struct MietteSourceFile<'a> {
160    source: &'a str,
161    name: String,
162    line_offset: usize,
163}
164
165impl MietteSourceFile<'static> {
166    /// Create an invalid source file for when we can't load the source
167    pub fn invalid() -> Self {
168        MietteSourceFile {
169            source: crate::invalid_no_ansi!(FILE),
170            name: crate::invalid_no_ansi!(FILE).into(),
171            line_offset: 0,
172        }
173    }
174}
175
176impl SourceFile {
177    /// Get a miette source adapter for the SourceFile
178    pub fn miette_source<'a>(&'a self, path: String, line_offset: usize) -> MietteSourceFile<'a> {
179        MietteSourceFile {
180            source: &self.source,
181            name: path,
182            line_offset,
183        }
184    }
185}
186
187impl SourceCode for MietteSourceFile<'_> {
188    fn read_span<'a>(
189        &'a self,
190        span: &SourceSpan,
191        context_lines_before: usize,
192        context_lines_after: usize,
193    ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
194        let inner_contents = self.source.read_span(span, context_lines_before, context_lines_after)?;
195        let contents = MietteSpanContents::new_named(
196            self.name.clone(),
197            inner_contents.data(),
198            *inner_contents.span(),
199            inner_contents.line() + self.line_offset,
200            inner_contents.column(),
201            inner_contents.line_count(),
202        ).with_language("µcad");
203        Ok(Box::new(contents))
204    }
205}