ad_astra/format/snippet.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::fmt::{Display, Formatter};
36
37use lady_deirdre::{
38 arena::{Id, Identifiable},
39 format::{AnnotationPriority, SnippetConfig, SnippetFormatter, Style, TerminalString},
40 lexis::{SiteSpan, ToSpan, TokenBuffer},
41};
42
43use crate::{
44 format::highlight::ScriptHighlighter,
45 runtime::PackageMeta,
46 syntax::{ScriptDoc, ScriptToken},
47};
48
49/// A configuration of options for drawing the [ScriptSnippet] object.
50///
51/// The [Default] implementation of this object provides canonical configuration
52/// options.
53#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
54#[non_exhaustive]
55pub struct ScriptSnippetConfig {
56 /// Whether the boxed frame should surround the code content from all sides.
57 ///
58 /// The default value is `true`.
59 pub show_outer_frame: bool,
60
61 /// Whether line numbers should be shown on the left of the code content.
62 ///
63 /// The default value is `true`.
64 pub show_line_numbers: bool,
65
66 /// Whether the full canonical script module path should be shown in the
67 /// caption of the printed snippet.
68 ///
69 /// If set to true, the snippet caption will look like:
70 /// `‹package name›.‹module name› [<custom caption>]`.
71 ///
72 /// Otherwise, the printer will only use the
73 /// [custom caption](ScriptSnippet::set_caption) if specified.
74 ///
75 /// The default value is `true`.
76 pub show_module_path: bool,
77
78 /// If set to true, syntax highlighting will be applied to the source code.
79 /// Otherwise, the source code will be monochrome.
80 ///
81 /// The default value is `true`.
82 pub highlight_code: bool,
83
84 /// Whether the snippet printer should use Unicode
85 /// [box drawing characters](https://en.wikipedia.org/wiki/Box-drawing_characters#Box_Drawing)
86 /// for decorative elements. Otherwise, the printer uses only ASCII box-drawing
87 /// characters.
88 ///
89 /// The default value is `true`.
90 pub unicode_drawing: bool,
91}
92
93impl Default for ScriptSnippetConfig {
94 #[inline(always)]
95 fn default() -> Self {
96 Self::new()
97 }
98}
99
100impl From<ScriptSnippetConfig> for SnippetConfig {
101 #[inline(always)]
102 fn from(value: ScriptSnippetConfig) -> Self {
103 let mut config = Self::verbose();
104
105 config.draw_frame = value.show_outer_frame;
106 config.show_numbers = value.show_line_numbers;
107 config.ascii_drawing = !value.unicode_drawing;
108
109 if !value.highlight_code {
110 config.dim_code = false;
111 config.style = false;
112 }
113
114 config
115 }
116}
117
118impl ScriptSnippetConfig {
119 /// The default constructor for the configuration.
120 #[inline(always)]
121 pub const fn new() -> Self {
122 Self {
123 show_outer_frame: true,
124 show_line_numbers: true,
125 show_module_path: true,
126 highlight_code: true,
127 unicode_drawing: true,
128 }
129 }
130
131 /// The constructor for the configuration object that returns a
132 /// configuration with all advanced drawing options disabled.
133 #[inline(always)]
134 pub const fn minimal() -> Self {
135 Self {
136 show_outer_frame: false,
137 show_line_numbers: false,
138 show_module_path: false,
139 highlight_code: false,
140 unicode_drawing: false,
141 }
142 }
143}
144
145/// A drawing object that renders the source code text of a
146/// [ScriptModule](crate::analysis::ScriptModule) with syntax highlighting and
147/// annotated source code ranges.
148///
149/// The intended use of this object is printing script source code to the
150/// terminal.
151///
152/// ```text
153/// ╭──╢ ‹doctest›.‹my_module.adastra› ╟────────────────────────────────────────╮
154/// 1 │ │
155/// 2 │ let foo = 10; │
156/// │ ╰╴ Annotation text. │
157/// 3 │ let bar = foo + 20; │
158/// 4 │ │
159/// ╰───────────────────────────────────────────────────────────────────────────╯
160/// ```
161///
162/// There are several crate API functions that create this object, such as
163/// [ModuleText::snippet](crate::analysis::ModuleText::snippet) and
164/// [ModuleDiagnostics::highlight](crate::analysis::ModuleDiagnostics::highlight).
165///
166/// The [Display] implementation of this object performs the actual snippet
167/// rendering. For example, you can print the snippet to the terminal using the
168/// `println` macro: `println!("{my_snippet}")`
169pub struct ScriptSnippet<'a> {
170 code: SnippetCode<'a>,
171 config: ScriptSnippetConfig,
172 caption: Option<String>,
173 annotations: Vec<(SiteSpan, AnnotationPriority, String)>,
174 summary: Option<String>,
175}
176
177impl<'a> Display for ScriptSnippet<'a> {
178 #[inline(always)]
179 fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
180 let mut caption = String::with_capacity(512);
181
182 match self.config.show_module_path {
183 true => {
184 if let Some(prefix) = &self.caption {
185 caption.push_str(&prefix.apply(Style::new().bold()));
186 caption.push_str(" [");
187 }
188
189 let id = match &self.code {
190 SnippetCode::Borrowed(code) => code.id(),
191 SnippetCode::Owned(code) => code.id(),
192 };
193
194 caption.push_str(
195 format_script_path(id, PackageMeta::by_id(id))
196 .apply(Style::new().bright_cyan())
197 .as_str(),
198 );
199
200 if self.caption.is_some() {
201 caption.push(']');
202 }
203 }
204
205 false => {
206 if let Some(prefix) = &self.caption {
207 caption.push_str(prefix)
208 }
209 }
210 }
211
212 match &self.code {
213 SnippetCode::Borrowed(code) => {
214 let config = self.config.into();
215
216 let mut snippet = formatter.snippet(*code);
217
218 snippet.set_config(&config).set_caption(caption);
219
220 if self.config.highlight_code {
221 snippet.set_highlighter(ScriptHighlighter::new());
222 }
223
224 if let Some(summary) = &self.summary {
225 snippet.set_summary(summary.as_str());
226 }
227
228 for (span, priority, message) in &self.annotations {
229 snippet.annotate(span, *priority, message.as_str());
230 }
231
232 snippet.finish()?;
233 }
234
235 SnippetCode::Owned(code) => {
236 let config = self.config.into();
237
238 let mut snippet = formatter.snippet(code);
239
240 snippet.set_config(&config).set_caption(caption);
241
242 if self.config.highlight_code {
243 snippet.set_highlighter(ScriptHighlighter::new());
244 }
245
246 if let Some(summary) = &self.summary {
247 snippet.set_summary(summary.as_str());
248 }
249
250 for (span, priority, message) in &self.annotations {
251 snippet.annotate(span, *priority, message.as_str());
252 }
253
254 snippet.finish()?;
255 }
256 }
257
258 Ok(())
259 }
260}
261
262impl<S: AsRef<str>> From<S> for ScriptSnippet<'static> {
263 #[inline(always)]
264 fn from(string: S) -> Self {
265 let buffer = TokenBuffer::from(string);
266
267 Self::new(SnippetCode::Owned(buffer))
268 }
269}
270
271impl<'a> ScriptSnippet<'a> {
272 #[inline(always)]
273 fn new(code: SnippetCode<'a>) -> Self {
274 Self {
275 code,
276 config: ScriptSnippetConfig::default(),
277 caption: None,
278 annotations: Vec::new(),
279 summary: None,
280 }
281 }
282
283 #[inline(always)]
284 pub(crate) fn from_doc(doc: &'a ScriptDoc) -> Self {
285 Self::new(SnippetCode::Borrowed(doc))
286 }
287
288 /// Sets the configuration for snippet drawing features.
289 ///
290 /// See [ScriptSnippetConfig] for details.
291 pub fn set_config(&mut self, config: ScriptSnippetConfig) -> &mut Self {
292 self.config = config;
293
294 self
295 }
296
297 /// Sets the caption of the printed content.
298 ///
299 /// The `caption` string will be printed in the header of the snippet.
300 /// By default, the caption is an empty string, and in this case, the
301 /// renderer does not include a custom caption in the header.
302 ///
303 /// The `caption` parameter must be a single-line string. Any additional
304 /// caption lines (separated by the `\n` character) will be ignored.
305 pub fn set_caption(&mut self, caption: impl AsRef<str>) -> &mut Self {
306 self.caption = caption
307 .as_ref()
308 .lines()
309 .next()
310 .map(|line| String::from(line));
311
312 self
313 }
314
315 /// Sets the footer summary text of the printed content.
316 ///
317 /// The `summary` string will be printed below the source code. By default,
318 /// the summary is an empty string, and in this case, the renderer does not
319 /// print any footer text.
320 ///
321 /// Unlike the [caption](Self::set_caption) and
322 /// [annotation](Self::annotate) text, the summary text can have
323 /// multiple lines.
324 pub fn set_summary(&mut self, summary: impl AsRef<str>) -> &mut Self {
325 self.summary = Some(String::from(summary.as_ref()));
326
327 self
328 }
329
330 /// Adds an annotation to the source code.
331 ///
332 /// The `span` argument specifies the source code range intended for
333 /// annotation. You can use a `10..20` absolute Unicode character range, the
334 /// [line-column](lady_deirdre::lexis::Position) range
335 /// `Position::new(10, 3)..Position::new(12, 4)`, or the [ScriptOrigin]
336 /// instance. The span argument must represent a
337 /// [valid](ToSpan::is_valid_span) value (e.g., `20..10` is not a valid
338 /// range because the upper bound is less than the lower bound). Otherwise,
339 /// the annotation will be silently ignored.
340 ///
341 /// The `priority` argument specifies the annotation priority. The snippet
342 /// interface supports the following priority types:
343 ///
344 /// - [AnnotationPriority::Default]: A default annotation. The spanned text
345 /// will be simply inverted (e.g., white text on a black background).
346 /// - [AnnotationPriority::Primary]: The spanned text will be inverted with
347 /// a red background.
348 /// - [AnnotationPriority::Secondary]: The spanned text will be inverted
349 /// with a blue background.
350 /// - [AnnotationPriority::Note]: The spanned text will be inverted with a
351 /// yellow background.
352 ///
353 /// The `message` argument specifies the text that should label the spanned
354 /// range. The message should be a single-line string. Any additional message
355 /// lines (separated by the `\n` character) will be ignored.
356 ///
357 /// You can leave the message as an empty string. In this case, the renderer
358 /// will not label the spanned text.
359 ///
360 /// Note that if the ScriptSnippet does not have any annotations, the object
361 /// will render the entire source code. Otherwise, the renderer will output
362 /// only the annotated lines plus a few lines of surrounding context.
363 pub fn annotate(
364 &mut self,
365 span: impl ToSpan,
366 priority: AnnotationPriority,
367 message: impl AsRef<str>,
368 ) -> &mut Self {
369 let span = match &self.code {
370 SnippetCode::Borrowed(code) => span.to_site_span(*code),
371 SnippetCode::Owned(code) => span.to_site_span(code),
372 };
373
374 let Some(span) = span else {
375 return self;
376 };
377
378 let message = message
379 .as_ref()
380 .lines()
381 .next()
382 .map(|line| String::from(line))
383 .unwrap_or(String::new());
384
385 self.annotations.push((span, priority, message));
386
387 self
388 }
389}
390
391#[inline(always)]
392pub(crate) fn format_script_path(id: Id, package: Option<&'static PackageMeta>) -> String {
393 let mut path = String::with_capacity(512);
394
395 if let Some(package) = package {
396 path.push_str(&format!("‹{}›.", package));
397 }
398
399 let name = id.name();
400
401 match name.is_empty() {
402 true => path.push_str(&format!("‹#{}›", id.into_inner())),
403 false => path.push_str(&format!("‹{}›", name.escape_debug())),
404 }
405
406 path
407}
408
409enum SnippetCode<'a> {
410 Borrowed(&'a ScriptDoc),
411 Owned(TokenBuffer<ScriptToken>),
412}