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] = &["µcad", "mcad", "ucad"];
73
74pub use code_display::*;
75pub use diag::{
76    Diag, DiagError, DiagHandler, DiagRenderOptions, DiagResult, Diagnostic, Diagnostics, Level,
77    PushDiag,
78};
79pub use identifier::Identifier;
80pub use ord_map::{OrdMap, OrdMapValue};
81pub use output::{Capture, Output, Stdout};
82pub use rc::{Rc, RcMut};
83pub use src_ref::{LineCol, LineIndex, Refer, Span, SrcRef, SrcReferrer};
84pub use tree_display::{FormatTree, TreeDisplay, TreeState};
85
86pub use microcad_core::hash::{ComputedHash, HashId, HashMap, HashSet, Hashed};
87pub use source::Source;
88
89/// A compatibility layer for using SourceFile with miette
90pub struct SourceLocInfo<'a> {
91    /// The source text.
92    pub code: &'a str,
93    /// Name of of file
94    pub url: Url,
95    /// Line offset (e.g. used when source comes from a markdown file).
96    pub line_offset: u32,
97}
98
99impl SourceLocInfo<'static> {
100    /// Create an invalid source file for when we can't load the source
101    pub fn invalid() -> Self {
102        SourceLocInfo {
103            code: "NO FILE",
104            url: virtual_url("invalid"),
105            line_offset: 0,
106        }
107    }
108}
109
110impl<'a> ResourceLocation for SourceLocInfo<'a> {
111    fn url(&self) -> &Url {
112        &self.url
113    }
114}
115
116impl SourceCode for SourceLocInfo<'_> {
117    fn read_span<'a>(
118        &'a self,
119        span: &SourceSpan,
120        context_lines_before: usize,
121        context_lines_after: usize,
122    ) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError> {
123        let inner_contents =
124            self.code
125                .read_span(span, context_lines_before, context_lines_after)?;
126        let contents = MietteSpanContents::new_named(
127            self.source_name(),
128            inner_contents.data(),
129            *inner_contents.span(),
130            inner_contents.line() + self.line_offset as usize,
131            inner_contents.column(),
132            inner_contents.line_count(),
133        )
134        .with_language("µcad");
135        Ok(Box::new(contents))
136    }
137}
138
139/// Trait that can fetch for a file by it's hash value.
140pub trait GetSourceLocInfoByHash {
141    /// Get a source string by it's hash value.
142    fn get_source_loc_info_by_hash(&'_ self, hash: HashId) -> Option<SourceLocInfo<'_>>;
143}
144
145/// Shortens given string to it's first line and to `max_chars` characters.
146pub fn shorten(what: &str, max_chars: usize) -> String {
147    let short: String = what
148        .chars()
149        .enumerate()
150        .filter_map(|(p, ch)| {
151            if p == max_chars {
152                Some('…')
153            } else if p < max_chars {
154                if ch == '\n' { Some('⏎') } else { Some(ch) }
155            } else {
156                None
157            }
158        })
159        .collect();
160
161    if cfg!(feature = "ansi-color") && short.contains('\x1b') {
162        short + "\x1b[0m"
163    } else {
164        short
165    }
166}
167
168/// Create a marker string which is colored with ANSI.
169#[cfg(feature = "ansi-color")]
170#[macro_export]
171macro_rules! mark {
172    (FOUND!) => {
173        color_print::cformat!("<G!,k,s> FOUND </>")
174    };
175    (FOUND) => {
176        color_print::cformat!("<W!,k,s> FOUND </>")
177    };
178    (MATCH) => {
179        color_print::cformat!("<Y!,k,s> MATCH </>")
180    };
181    (NO_MATCH) => {
182        color_print::cformat!("<Y,k,s> NO MATCH </>")
183    };
184    (MATCH!) => {
185        color_print::cformat!("<G!,k,s> MATCH </>")
186    };
187    (NO_MATCH!) => {
188        color_print::cformat!("<R,k,s> NO MATCH </>")
189    };
190    (CALL) => {
191        color_print::cformat!("<B,k,s> CALL </>")
192    };
193    (LOOKUP) => {
194        color_print::cformat!("<c,s>LOOKUP</>")
195    };
196    (LOAD) => {
197        color_print::cformat!("<Y,k,s> LOADING </>")
198    };
199    (RESOLVE) => {
200        color_print::cformat!("<M,k,s> RESOLVE </>")
201    };
202    (AMBIGUOUS) => {
203        color_print::cformat!("<R,k,s> AMBIGUOUS </>")
204    };
205    (NOT_FOUND!) => {
206        color_print::cformat!("<R,k,s> NOT FOUND </>")
207    };
208    (NOT_FOUND) => {
209        color_print::cformat!("<Y,k,s> NOT FOUND </>")
210    };
211}
212
213/// Trait to write something with Display trait into a file.
214pub trait WriteToFile: std::fmt::Display {
215    /// Write something to a file.
216    fn write_to_file(&self, filename: &impl AsRef<std::path::Path>) -> std::io::Result<()> {
217        use std::io::Write;
218        let file = std::fs::File::create(filename)?;
219        let mut writer = std::io::BufWriter::new(file);
220        write!(writer, "{self}")
221    }
222}