ad_astra/analysis/
text.rs

1////////////////////////////////////////////////////////////////////////////////
2// This file is part of "Ad Astra", an embeddable scripting programming       //
3// language platform.                                                         //
4//                                                                            //
5// This work is proprietary software with source-available code.              //
6//                                                                            //
7// To copy, use, distribute, or contribute to this work, you must agree to    //
8// the terms of the General License Agreement:                                //
9//                                                                            //
10// https://github.com/Eliah-Lakhin/ad-astra/blob/master/EULA.md               //
11//                                                                            //
12// The agreement grants a Basic Commercial License, allowing you to use       //
13// this work in non-commercial and limited commercial products with a total   //
14// gross revenue cap. To remove this commercial limit for one of your         //
15// products, you must acquire a Full Commercial License.                      //
16//                                                                            //
17// If you contribute to the source code, documentation, or related materials, //
18// you must grant me an exclusive license to these contributions.             //
19// Contributions are governed by the "Contributions" section of the General   //
20// License Agreement.                                                         //
21//                                                                            //
22// Copying the work in parts is strictly forbidden, except as permitted       //
23// under the General License Agreement.                                       //
24//                                                                            //
25// If you do not or cannot agree to the terms of this Agreement,              //
26// do not use this work.                                                      //
27//                                                                            //
28// This work is provided "as is", without any warranties, express or implied, //
29// except where such disclaimers are legally invalid.                         //
30//                                                                            //
31// Copyright (c) 2024 Ilya Lakhin (Илья Александрович Лахин).                 //
32// All rights reserved.                                                       //
33////////////////////////////////////////////////////////////////////////////////
34
35use std::{
36    fmt::{Debug, Display, Formatter},
37    ops::Deref,
38};
39
40use ahash::RandomState;
41use lady_deirdre::{
42    analysis::DocumentReadGuard,
43    arena::{Id, Identifiable},
44    syntax::SyntaxTree,
45    units::Lexis,
46};
47
48use crate::{
49    format::{format_script_doc, format_script_path, ScriptFormatConfig, ScriptSnippet},
50    runtime::PackageMeta,
51    syntax::{ScriptDoc, ScriptNode},
52};
53
54/// A view into the [ScriptModule](crate::analysis::ScriptModule) source code
55/// text and lexis.
56///
57/// Created by the [ModuleRead::text](crate::analysis::ModuleRead::text)
58/// function.
59///
60/// To fetch the raw text of the source code or a substring, use the
61/// [ModuleText::substring](lady_deirdre::lexis::SourceCode::substring)
62/// function.
63///
64/// To print a snippet of the source code text with syntax highlighting
65/// and annotation messages to the terminal, use the [ModuleText::snippet]
66/// function. The [Display] implementation of this object also prints
67/// a highlighted snippet with default settings.
68pub struct ModuleText<'a> {
69    pub(super) package: &'static PackageMeta,
70    pub(super) doc_read: DocumentReadGuard<'a, ScriptNode, RandomState>,
71}
72
73impl<'a> Identifiable for ModuleText<'a> {
74    #[inline(always)]
75    fn id(&self) -> Id {
76        self.doc_read.id()
77    }
78}
79
80impl<'a> Lexis for ModuleText<'a> {
81    type Lexis = ScriptDoc;
82
83    #[inline(always)]
84    fn lexis(&self) -> &Self::Lexis {
85        self.doc_read.deref()
86    }
87}
88
89impl<'a> Debug for ModuleText<'a> {
90    #[inline(always)]
91    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
92        formatter.write_fmt(format_args!(
93            "ModuleText({})",
94            format_script_path(self.id(), Some(self.package))
95        ))
96    }
97}
98
99impl<'a> Display for ModuleText<'a> {
100    #[inline(always)]
101    fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
102        Display::fmt(&self.snippet().set_caption("module"), formatter)
103    }
104}
105
106impl<'a> ModuleText<'a> {
107    /// Returns an annotated snippet that prints the module's source code text with
108    /// syntax highlighting and annotations for specific code fragments with
109    /// string messages.
110    ///
111    /// ```
112    /// # use ad_astra::{
113    /// #     analysis::{ModuleRead, ScriptModule},
114    /// #     export,
115    /// #     lady_deirdre::{analysis::TriggerHandle, format::AnnotationPriority, lexis::Position},
116    /// #     runtime::ScriptPackage,
117    /// # };
118    /// #
119    /// # #[export(package)]
120    /// # #[derive(Default)]
121    /// # struct Package;
122    /// #
123    /// let module = ScriptModule::new(
124    ///     Package::meta(),
125    ///     r#"
126    ///     let foo = 10;
127    ///     let bar = foo + 20;
128    /// "#,
129    /// );
130    ///
131    /// module.rename("my_module.adastra");
132    ///
133    /// let handle = TriggerHandle::new();
134    /// let module_read = module.read(&handle, 1).unwrap();
135    /// let module_text = module_read.text();
136    ///
137    /// let mut snippet = module_text.snippet();
138    ///
139    /// snippet.annotate(
140    ///     Position::new(2, 9)..Position::new(2, 12),
141    ///     AnnotationPriority::Primary,
142    ///     "Annotation text.",
143    /// );
144    ///
145    /// println!("{snippet}");
146    /// ```
147    ///
148    /// Outputs:
149    /// ```text
150    ///    ╭──╢ ‹doctest›.‹my_module.adastra› ╟────────────────────────────────────────╮
151    ///  1 │                                                                           │
152    ///  2 │     let foo = 10;                                                         │
153    ///    │         ╰╴ Annotation text.                                               │
154    ///  3 │     let bar = foo + 20;                                                   │
155    ///  4 │                                                                           │
156    ///    ╰───────────────────────────────────────────────────────────────────────────╯
157    /// ```
158    #[inline(always)]
159    pub fn snippet(&self) -> ScriptSnippet {
160        ScriptSnippet::from_doc(self.doc_read.deref())
161    }
162
163    /// Returns true if the script module does not have syntax errors.
164    #[inline(always)]
165    pub fn is_well_formed(&self) -> bool {
166        self.doc_read.error_refs().next().is_none()
167    }
168
169    /// If the script module does not have syntax errors (i.e., the module is
170    /// [well-formed](Self::is_well_formed)), returns the reformatted source
171    /// code text according to the formatting rules.
172    ///
173    /// See [format_script_text](crate::format::format_script_text) for details.
174    #[inline(always)]
175    pub fn format(&self, config: ScriptFormatConfig) -> Option<String> {
176        format_script_doc(config, self.doc_read.deref())
177    }
178}
179
180/// An interface that provides access to script module texts by module [Id].
181///
182/// When displaying runtime errors (through the
183/// [RuntimeError::display](crate::runtime::RuntimeError::display) function),
184/// the inner algorithm needs access to the script module text of the modules
185/// mentioned in the error's description. You can implement this trait on
186/// your custom multi-module compiler to provide access to the module texts.
187///
188/// If your compiler consists of runtime-independent script modules, you can
189/// use a normal [ModuleText], which implements ModuleTextResolver out of the
190/// box.
191pub trait ModuleTextResolver {
192    /// If the underlying compiler contains a script module with `id`, returns
193    /// [ModuleText] of this module. Otherwise, returns None.
194    fn resolve(&self, id: Id) -> Option<&ModuleText>;
195}
196
197impl<'a> ModuleTextResolver for ModuleText<'a> {
198    #[inline(always)]
199    fn resolve(&self, id: Id) -> Option<&ModuleText> {
200        if self.id() != id {
201            return None;
202        }
203
204        Some(self)
205    }
206}