Skip to main content

microcad_lang_base/
lib.rs

1// Copyright © 2024-2026 The µcad authors <info@microcad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4//! µcad language base components for error handling etc.
5
6use std::str::FromStr;
7
8use miette::{MietteError, MietteSpanContents, SourceCode, SourceSpan, SpanContents};
9
10mod code_display;
11mod diag;
12mod identifier;
13mod ord_map;
14mod output;
15mod rc;
16mod source;
17mod src_ref;
18mod tree_display;
19
20pub use compact_str::{CompactString, ToCompactString};
21
22/// Id type (base of all identifiers)
23pub type Id = CompactString;
24
25/// URL to locate sources.
26pub use url::Url;
27
28pub fn virtual_url(name: &str) -> Url {
29    Url::from_str(&format!("virtual://{name}")).unwrap()
30}
31
32pub trait ResourceLocation {
33    /// The canonical identity of the resource.
34    fn url(&self) -> &Url;
35
36    /// Attempts to convert the location to a physical filesystem path.
37    /// Returns None if the resource is virtual (e.g., ucad-std:// or snippet://).
38    fn to_file_path(&self) -> Option<std::path::PathBuf> {
39        if self.url().scheme() == "file" {
40            self.url().to_file_path().ok()
41        } else {
42            None
43        }
44    }
45
46    /// Helper to identify if the resource exists on disk.
47    fn is_local(&self) -> bool {
48        self.url().scheme() == "file"
49    }
50
51    /// Return the relative file path from current directory.
52    fn relative_path(&self) -> Option<std::path::PathBuf> {
53        self.to_file_path().map(|path| {
54            let current_dir = std::env::current_dir().expect("current dir");
55            if let Ok(path) = path.canonicalize() {
56                pathdiff::diff_paths(path, current_dir).unwrap_or_default()
57            } else {
58                path.to_path_buf()
59            }
60        })
61    }
62
63    /// The source name
64    fn source_name(&self) -> String {
65        self.relative_path()
66            .map(|s| s.to_string_lossy().to_string())
67            .unwrap_or(self.url().path().to_string())
68    }
69}
70
71/// List of valid µcad extensions.
72pub const MICROCAD_EXTENSIONS: &[&str] = &["mu", "µcad", "mcad", "ucad"];
73
74/// Default extension for µcad files.
75pub const MICROCAD_EXTENSION: &str = "µcad";
76
77pub use code_display::*;
78pub use diag::{
79    Diag, DiagError, DiagHandler, DiagRenderOptions, DiagResult, Diagnostic, Diagnostics, Level,
80    Level as DiagLevel, PushDiag,
81};
82pub use identifier::Identifier;
83pub use ord_map::{OrdMap, OrdMapValue};
84pub use output::{Capture, Output, Stdout};
85pub use rc::{Rc, RcMut};
86pub use src_ref::{LineCol, LineIndex, Refer, Span, Spanned, SrcRef, SrcReferrer};
87pub use tree_display::{FormatTree, TreeDisplay, TreeState};
88
89pub use microcad_core::hash::{ComputedHash, HashId, HashMap, HashSet, Hashed, Hasher};
90pub use source::Source;
91
92/// The possible type of workbenches
93#[derive(Debug, PartialEq, Copy, Clone)]
94pub enum WorkbenchKind {
95    /// `sketch`
96    Sketch,
97    /// `part`
98    Part,
99    /// `op`
100    Op,
101}
102
103impl std::fmt::Display for WorkbenchKind {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(
106            f,
107            "{}",
108            match &self {
109                WorkbenchKind::Sketch => "sketch",
110                WorkbenchKind::Part => "part",
111                WorkbenchKind::Op => "op",
112            }
113        )
114    }
115}
116
117/// A compatibility layer for using SourceFile with miette
118pub struct SourceLocInfo<'a> {
119    /// The source text.
120    pub code: &'a str,
121    /// Name of of file
122    pub url: Url,
123    /// Line offset (e.g. used when source comes from a markdown file).
124    pub line_offset: u32,
125}
126
127impl SourceLocInfo<'static> {
128    /// Create an invalid source file for when we can't load the source
129    pub fn invalid() -> Self {
130        SourceLocInfo {
131            code: "NO FILE",
132            url: virtual_url("invalid"),
133            line_offset: 0,
134        }
135    }
136}
137
138impl<'a> ResourceLocation for SourceLocInfo<'a> {
139    fn url(&self) -> &Url {
140        &self.url
141    }
142}
143
144impl SourceCode for SourceLocInfo<'_> {
145    fn read_span<'a>(
146        &'a self,
147        span: &SourceSpan,
148        context_lines_before: usize,
149        context_lines_after: usize,
150    ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
151        let inner_contents =
152            self.code
153                .read_span(span, context_lines_before, context_lines_after)?;
154        let contents = MietteSpanContents::new_named(
155            self.source_name(),
156            inner_contents.data(),
157            *inner_contents.span(),
158            inner_contents.line() + self.line_offset as usize,
159            inner_contents.column(),
160            inner_contents.line_count(),
161        )
162        .with_language("µcad");
163        Ok(Box::new(contents))
164    }
165}
166
167/// Trait that can fetch for a file by it's hash value.
168pub trait GetSourceLocInfoByHash {
169    /// Get a source string by it's hash value.
170    fn get_source_loc_info_by_hash(&'_ self, hash: HashId) -> Option<SourceLocInfo<'_>>;
171}
172
173/// Shortens given string to it's first line and to `max_chars` characters.
174pub fn shorten(what: &str, max_chars: usize) -> String {
175    let short: String = what
176        .chars()
177        .enumerate()
178        .filter_map(|(p, ch)| {
179            if p == max_chars {
180                Some('…')
181            } else if p < max_chars {
182                if ch == '\n' { Some('⏎') } else { Some(ch) }
183            } else {
184                None
185            }
186        })
187        .collect();
188
189    if cfg!(feature = "ansi-color") && short.contains('\x1b') {
190        short + "\x1b[0m"
191    } else {
192        short
193    }
194}
195
196/// Create a marker string which is colored with ANSI.
197#[cfg(feature = "ansi-color")]
198#[macro_export]
199macro_rules! mark {
200    (FOUND!) => {
201        color_print::cformat!("<G!,k,s> FOUND </>")
202    };
203    (FOUND) => {
204        color_print::cformat!("<W!,k,s> FOUND </>")
205    };
206    (MATCH) => {
207        color_print::cformat!("<Y!,k,s> MATCH </>")
208    };
209    (NO_MATCH) => {
210        color_print::cformat!("<Y,k,s> NO MATCH </>")
211    };
212    (MATCH!) => {
213        color_print::cformat!("<G!,k,s> MATCH </>")
214    };
215    (NO_MATCH!) => {
216        color_print::cformat!("<R,k,s> NO MATCH </>")
217    };
218    (CALL) => {
219        color_print::cformat!("<B,k,s> CALL </>")
220    };
221    (LOOKUP) => {
222        color_print::cformat!("<c,s>LOOKUP</>")
223    };
224    (LOAD) => {
225        color_print::cformat!("<Y,k,s> LOADING </>")
226    };
227    (RESOLVE) => {
228        color_print::cformat!("<M,k,s> RESOLVE </>")
229    };
230    (AMBIGUOUS) => {
231        color_print::cformat!("<R,k,s> AMBIGUOUS </>")
232    };
233    (NOT_FOUND!) => {
234        color_print::cformat!("<R,k,s> NOT FOUND </>")
235    };
236    (NOT_FOUND) => {
237        color_print::cformat!("<Y,k,s> NOT FOUND </>")
238    };
239}
240
241/// Trait to write something with Display trait into a file.
242pub trait WriteToFile: std::fmt::Display {
243    /// Write something to a file.
244    fn write_to_file(&self, filename: &impl AsRef<std::path::Path>) -> std::io::Result<()> {
245        use std::io::Write;
246        let file = std::fs::File::create(filename)?;
247        let mut writer = std::io::BufWriter::new(file);
248        write!(writer, "{self}")
249    }
250}