Skip to main content

microcad_lang/syntax/source_file/
mod.rs

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