1 use std::cmp::{max, min};
2 use std::fmt;
3 
4 use crate::formatter::{get_term_style, style::Stylesheet};
5 
6 /// List of lines to be displayed.
7 pub struct DisplayList<'a> {
8     pub body: Vec<DisplayLine<'a>>,
9     pub stylesheet: Box<dyn Stylesheet>,
10     pub anonymized_line_numbers: bool,
11     pub margin: Option<Margin>,
12 }
13 
14 impl<'a> From<Vec<DisplayLine<'a>>> for DisplayList<'a> {
from(body: Vec<DisplayLine<'a>>) -> DisplayList<'a>15     fn from(body: Vec<DisplayLine<'a>>) -> DisplayList<'a> {
16         Self {
17             body,
18             anonymized_line_numbers: false,
19             stylesheet: get_term_style(false),
20             margin: None,
21         }
22     }
23 }
24 
25 impl<'a> PartialEq for DisplayList<'a> {
eq(&self, other: &Self) -> bool26     fn eq(&self, other: &Self) -> bool {
27         self.body == other.body && self.anonymized_line_numbers == other.anonymized_line_numbers
28     }
29 }
30 
31 impl<'a> fmt::Debug for DisplayList<'a> {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result32     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33         f.debug_struct("DisplayList")
34             .field("body", &self.body)
35             .field("anonymized_line_numbers", &self.anonymized_line_numbers)
36             .finish()
37     }
38 }
39 
40 #[derive(Debug, Default, Copy, Clone)]
41 pub struct FormatOptions {
42     pub color: bool,
43     pub anonymized_line_numbers: bool,
44     pub margin: Option<Margin>,
45 }
46 
47 #[derive(Clone, Copy, Debug)]
48 pub struct Margin {
49     /// The available whitespace in the left that can be consumed when centering.
50     whitespace_left: usize,
51     /// The column of the beginning of left-most span.
52     span_left: usize,
53     /// The column of the end of right-most span.
54     span_right: usize,
55     /// The beginning of the line to be displayed.
56     computed_left: usize,
57     /// The end of the line to be displayed.
58     computed_right: usize,
59     /// The current width of the terminal. 140 by default and in tests.
60     column_width: usize,
61     /// The end column of a span label, including the span. Doesn't account for labels not in the
62     /// same line as the span.
63     label_right: usize,
64 }
65 
66 impl Margin {
new( whitespace_left: usize, span_left: usize, span_right: usize, label_right: usize, column_width: usize, max_line_len: usize, ) -> Self67     pub fn new(
68         whitespace_left: usize,
69         span_left: usize,
70         span_right: usize,
71         label_right: usize,
72         column_width: usize,
73         max_line_len: usize,
74     ) -> Self {
75         // The 6 is padding to give a bit of room for `...` when displaying:
76         // ```
77         // error: message
78         //   --> file.rs:16:58
79         //    |
80         // 16 | ... fn foo(self) -> Self::Bar {
81         //    |                     ^^^^^^^^^
82         // ```
83 
84         let mut m = Margin {
85             whitespace_left: whitespace_left.saturating_sub(6),
86             span_left: span_left.saturating_sub(6),
87             span_right: span_right + 6,
88             computed_left: 0,
89             computed_right: 0,
90             column_width,
91             label_right: label_right + 6,
92         };
93         m.compute(max_line_len);
94         m
95     }
96 
was_cut_left(&self) -> bool97     pub(crate) fn was_cut_left(&self) -> bool {
98         self.computed_left > 0
99     }
100 
was_cut_right(&self, line_len: usize) -> bool101     pub(crate) fn was_cut_right(&self, line_len: usize) -> bool {
102         let right =
103             if self.computed_right == self.span_right || self.computed_right == self.label_right {
104                 // Account for the "..." padding given above. Otherwise we end up with code lines that
105                 // do fit but end in "..." as if they were trimmed.
106                 self.computed_right - 6
107             } else {
108                 self.computed_right
109             };
110         right < line_len && self.computed_left + self.column_width < line_len
111     }
112 
compute(&mut self, max_line_len: usize)113     fn compute(&mut self, max_line_len: usize) {
114         // When there's a lot of whitespace (>20), we want to trim it as it is useless.
115         self.computed_left = if self.whitespace_left > 20 {
116             self.whitespace_left - 16 // We want some padding.
117         } else {
118             0
119         };
120         // We want to show as much as possible, max_line_len is the right-most boundary for the
121         // relevant code.
122         self.computed_right = max(max_line_len, self.computed_left);
123 
124         if self.computed_right - self.computed_left > self.column_width {
125             // Trimming only whitespace isn't enough, let's get craftier.
126             if self.label_right - self.whitespace_left <= self.column_width {
127                 // Attempt to fit the code window only trimming whitespace.
128                 self.computed_left = self.whitespace_left;
129                 self.computed_right = self.computed_left + self.column_width;
130             } else if self.label_right - self.span_left <= self.column_width {
131                 // Attempt to fit the code window considering only the spans and labels.
132                 let padding_left = (self.column_width - (self.label_right - self.span_left)) / 2;
133                 self.computed_left = self.span_left.saturating_sub(padding_left);
134                 self.computed_right = self.computed_left + self.column_width;
135             } else if self.span_right - self.span_left <= self.column_width {
136                 // Attempt to fit the code window considering the spans and labels plus padding.
137                 let padding_left = (self.column_width - (self.span_right - self.span_left)) / 5 * 2;
138                 self.computed_left = self.span_left.saturating_sub(padding_left);
139                 self.computed_right = self.computed_left + self.column_width;
140             } else {
141                 // Mostly give up but still don't show the full line.
142                 self.computed_left = self.span_left;
143                 self.computed_right = self.span_right;
144             }
145         }
146     }
147 
left(&self, line_len: usize) -> usize148     pub(crate) fn left(&self, line_len: usize) -> usize {
149         min(self.computed_left, line_len)
150     }
151 
right(&self, line_len: usize) -> usize152     pub(crate) fn right(&self, line_len: usize) -> usize {
153         if line_len.saturating_sub(self.computed_left) <= self.column_width {
154             line_len
155         } else {
156             min(line_len, self.computed_right)
157         }
158     }
159 }
160 
161 /// Inline annotation which can be used in either Raw or Source line.
162 #[derive(Debug, PartialEq)]
163 pub struct Annotation<'a> {
164     pub annotation_type: DisplayAnnotationType,
165     pub id: Option<&'a str>,
166     pub label: Vec<DisplayTextFragment<'a>>,
167 }
168 
169 /// A single line used in `DisplayList`.
170 #[derive(Debug, PartialEq)]
171 pub enum DisplayLine<'a> {
172     /// A line with `lineno` portion of the slice.
173     Source {
174         lineno: Option<usize>,
175         inline_marks: Vec<DisplayMark>,
176         line: DisplaySourceLine<'a>,
177     },
178 
179     /// A line indicating a folded part of the slice.
180     Fold { inline_marks: Vec<DisplayMark> },
181 
182     /// A line which is displayed outside of slices.
183     Raw(DisplayRawLine<'a>),
184 }
185 
186 /// A source line.
187 #[derive(Debug, PartialEq)]
188 pub enum DisplaySourceLine<'a> {
189     /// A line with the content of the Slice.
190     Content {
191         text: &'a str,
192         range: (usize, usize), // meta information for annotation placement.
193     },
194 
195     /// An annotation line which is displayed in context of the slice.
196     Annotation {
197         annotation: Annotation<'a>,
198         range: (usize, usize),
199         annotation_type: DisplayAnnotationType,
200         annotation_part: DisplayAnnotationPart,
201     },
202 
203     /// An empty source line.
204     Empty,
205 }
206 
207 /// Raw line - a line which does not have the `lineno` part and is not considered
208 /// a part of the snippet.
209 #[derive(Debug, PartialEq)]
210 pub enum DisplayRawLine<'a> {
211     /// A line which provides information about the location of the given
212     /// slice in the project structure.
213     Origin {
214         path: &'a str,
215         pos: Option<(usize, usize)>,
216         header_type: DisplayHeaderType,
217     },
218 
219     /// An annotation line which is not part of any snippet.
220     Annotation {
221         annotation: Annotation<'a>,
222 
223         /// If set to `true`, the annotation will be aligned to the
224         /// lineno delimiter of the snippet.
225         source_aligned: bool,
226         /// If set to `true`, only the label of the `Annotation` will be
227         /// displayed. It allows for a multiline annotation to be aligned
228         /// without displaing the meta information (`type` and `id`) to be
229         /// displayed on each line.
230         continuation: bool,
231     },
232 }
233 
234 /// An inline text fragment which any label is composed of.
235 #[derive(Debug, PartialEq)]
236 pub struct DisplayTextFragment<'a> {
237     pub content: &'a str,
238     pub style: DisplayTextStyle,
239 }
240 
241 /// A style for the `DisplayTextFragment` which can be visually formatted.
242 ///
243 /// This information may be used to emphasis parts of the label.
244 #[derive(Debug, Clone, Copy, PartialEq)]
245 pub enum DisplayTextStyle {
246     Regular,
247     Emphasis,
248 }
249 
250 /// An indicator of what part of the annotation a given `Annotation` is.
251 #[derive(Debug, Clone, PartialEq)]
252 pub enum DisplayAnnotationPart {
253     /// A standalone, single-line annotation.
254     Standalone,
255     /// A continuation of a multi-line label of an annotation.
256     LabelContinuation,
257     /// A consequitive annotation in case multiple annotations annotate a single line.
258     Consequitive,
259     /// A line starting a multiline annotation.
260     MultilineStart,
261     /// A line ending a multiline annotation.
262     MultilineEnd,
263 }
264 
265 /// A visual mark used in `inline_marks` field of the `DisplaySourceLine`.
266 #[derive(Debug, Clone, PartialEq)]
267 pub struct DisplayMark {
268     pub mark_type: DisplayMarkType,
269     pub annotation_type: DisplayAnnotationType,
270 }
271 
272 /// A type of the `DisplayMark`.
273 #[derive(Debug, Clone, PartialEq)]
274 pub enum DisplayMarkType {
275     /// A mark indicating a multiline annotation going through the current line.
276     AnnotationThrough,
277     /// A mark indicating a multiline annotation starting on the given line.
278     AnnotationStart,
279 }
280 
281 /// A type of the `Annotation` which may impact the sigils, style or text displayed.
282 ///
283 /// There are several ways to uses this information when formatting the `DisplayList`:
284 ///
285 /// * An annotation may display the name of the type like `error` or `info`.
286 /// * An underline for `Error` may be `^^^` while for `Warning` it coule be `---`.
287 /// * `ColorStylesheet` may use different colors for different annotations.
288 #[derive(Debug, Clone, PartialEq)]
289 pub enum DisplayAnnotationType {
290     None,
291     Error,
292     Warning,
293     Info,
294     Note,
295     Help,
296 }
297 
298 /// Information whether the header is the initial one or a consequitive one
299 /// for multi-slice cases.
300 // TODO: private
301 #[derive(Debug, Clone, PartialEq)]
302 pub enum DisplayHeaderType {
303     /// Initial header is the first header in the snippet.
304     Initial,
305 
306     /// Continuation marks all headers of following slices in the snippet.
307     Continuation,
308 }
309