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}