| 1 | use std::{ |
| 2 | cmp, |
| 3 | fmt::{self, Display, Write}, |
| 4 | iter::once, |
| 5 | }; |
| 6 | |
| 7 | pub mod style; |
| 8 | |
| 9 | use self::style::{Style, StyleClass, Stylesheet}; |
| 10 | |
| 11 | #[cfg (feature = "color" )] |
| 12 | use crate::stylesheets::color::AnsiTermStylesheet; |
| 13 | use crate::{display_list::*, stylesheets::no_color::NoColorStylesheet}; |
| 14 | |
| 15 | fn format_repeat_char(c: char, n: usize, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 16 | for _ in 0..n { |
| 17 | f.write_char(c)?; |
| 18 | } |
| 19 | Ok(()) |
| 20 | } |
| 21 | |
| 22 | #[inline ] |
| 23 | fn is_annotation_empty(annotation: &Annotation<'_>) -> bool { |
| 24 | annotationIter<'_, DisplayTextFragment<'_>> |
| 25 | .label |
| 26 | .iter() |
| 27 | .all(|fragment: &DisplayTextFragment<'_>| fragment.content.is_empty()) |
| 28 | } |
| 29 | |
| 30 | #[cfg (feature = "color" )] |
| 31 | #[inline ] |
| 32 | pub fn get_term_style(color: bool) -> Box<dyn Stylesheet> { |
| 33 | if color { |
| 34 | Box::new(AnsiTermStylesheet) |
| 35 | } else { |
| 36 | Box::new(NoColorStylesheet) |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | #[cfg (not(feature = "color" ))] |
| 41 | #[inline ] |
| 42 | pub fn get_term_style(_color: bool) -> Box<dyn Stylesheet> { |
| 43 | Box::new(NoColorStylesheet) |
| 44 | } |
| 45 | |
| 46 | impl<'a> fmt::Display for DisplayList<'a> { |
| 47 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| 48 | let lineno_width = self.body.iter().fold(0, |max, line| match line { |
| 49 | DisplayLine::Source { |
| 50 | lineno: Some(lineno), |
| 51 | .. |
| 52 | } => { |
| 53 | // The largest line is the largest width. |
| 54 | cmp::max(*lineno, max) |
| 55 | } |
| 56 | _ => max, |
| 57 | }); |
| 58 | let lineno_width = if lineno_width == 0 { |
| 59 | lineno_width |
| 60 | } else if self.anonymized_line_numbers { |
| 61 | Self::ANONYMIZED_LINE_NUM.len() |
| 62 | } else { |
| 63 | ((lineno_width as f64).log10().floor() as usize) + 1 |
| 64 | }; |
| 65 | let inline_marks_width = self.body.iter().fold(0, |max, line| match line { |
| 66 | DisplayLine::Source { inline_marks, .. } => cmp::max(inline_marks.len(), max), |
| 67 | _ => max, |
| 68 | }); |
| 69 | |
| 70 | for (i, line) in self.body.iter().enumerate() { |
| 71 | self.format_line(line, lineno_width, inline_marks_width, f)?; |
| 72 | if i + 1 < self.body.len() { |
| 73 | f.write_char(' \n' )?; |
| 74 | } |
| 75 | } |
| 76 | Ok(()) |
| 77 | } |
| 78 | } |
| 79 | |
| 80 | impl<'a> DisplayList<'a> { |
| 81 | const ANONYMIZED_LINE_NUM: &'static str = "LL" ; |
| 82 | const ERROR_TXT: &'static str = "error" ; |
| 83 | const HELP_TXT: &'static str = "help" ; |
| 84 | const INFO_TXT: &'static str = "info" ; |
| 85 | const NOTE_TXT: &'static str = "note" ; |
| 86 | const WARNING_TXT: &'static str = "warning" ; |
| 87 | |
| 88 | #[inline ] |
| 89 | fn format_annotation_type( |
| 90 | annotation_type: &DisplayAnnotationType, |
| 91 | f: &mut fmt::Formatter<'_>, |
| 92 | ) -> fmt::Result { |
| 93 | match annotation_type { |
| 94 | DisplayAnnotationType::Error => f.write_str(Self::ERROR_TXT), |
| 95 | DisplayAnnotationType::Help => f.write_str(Self::HELP_TXT), |
| 96 | DisplayAnnotationType::Info => f.write_str(Self::INFO_TXT), |
| 97 | DisplayAnnotationType::Note => f.write_str(Self::NOTE_TXT), |
| 98 | DisplayAnnotationType::Warning => f.write_str(Self::WARNING_TXT), |
| 99 | DisplayAnnotationType::None => Ok(()), |
| 100 | } |
| 101 | } |
| 102 | |
| 103 | fn annotation_type_len(annotation_type: &DisplayAnnotationType) -> usize { |
| 104 | match annotation_type { |
| 105 | DisplayAnnotationType::Error => Self::ERROR_TXT.len(), |
| 106 | DisplayAnnotationType::Help => Self::HELP_TXT.len(), |
| 107 | DisplayAnnotationType::Info => Self::INFO_TXT.len(), |
| 108 | DisplayAnnotationType::Note => Self::NOTE_TXT.len(), |
| 109 | DisplayAnnotationType::Warning => Self::WARNING_TXT.len(), |
| 110 | DisplayAnnotationType::None => 0, |
| 111 | } |
| 112 | } |
| 113 | |
| 114 | fn get_annotation_style(&self, annotation_type: &DisplayAnnotationType) -> Box<dyn Style> { |
| 115 | self.stylesheet.get_style(match annotation_type { |
| 116 | DisplayAnnotationType::Error => StyleClass::Error, |
| 117 | DisplayAnnotationType::Warning => StyleClass::Warning, |
| 118 | DisplayAnnotationType::Info => StyleClass::Info, |
| 119 | DisplayAnnotationType::Note => StyleClass::Note, |
| 120 | DisplayAnnotationType::Help => StyleClass::Help, |
| 121 | DisplayAnnotationType::None => StyleClass::None, |
| 122 | }) |
| 123 | } |
| 124 | |
| 125 | fn format_label( |
| 126 | &self, |
| 127 | label: &[DisplayTextFragment<'_>], |
| 128 | f: &mut fmt::Formatter<'_>, |
| 129 | ) -> fmt::Result { |
| 130 | let emphasis_style = self.stylesheet.get_style(StyleClass::Emphasis); |
| 131 | |
| 132 | for fragment in label { |
| 133 | match fragment.style { |
| 134 | DisplayTextStyle::Regular => fragment.content.fmt(f)?, |
| 135 | DisplayTextStyle::Emphasis => emphasis_style.paint(fragment.content, f)?, |
| 136 | } |
| 137 | } |
| 138 | Ok(()) |
| 139 | } |
| 140 | |
| 141 | fn format_annotation( |
| 142 | &self, |
| 143 | annotation: &Annotation<'_>, |
| 144 | continuation: bool, |
| 145 | in_source: bool, |
| 146 | f: &mut fmt::Formatter<'_>, |
| 147 | ) -> fmt::Result { |
| 148 | let color = self.get_annotation_style(&annotation.annotation_type); |
| 149 | let formatted_len = if let Some(id) = &annotation.id { |
| 150 | 2 + id.len() + Self::annotation_type_len(&annotation.annotation_type) |
| 151 | } else { |
| 152 | Self::annotation_type_len(&annotation.annotation_type) |
| 153 | }; |
| 154 | |
| 155 | if continuation { |
| 156 | format_repeat_char(' ' , formatted_len + 2, f)?; |
| 157 | return self.format_label(&annotation.label, f); |
| 158 | } |
| 159 | if formatted_len == 0 { |
| 160 | self.format_label(&annotation.label, f) |
| 161 | } else { |
| 162 | color.paint_fn( |
| 163 | Box::new(|f| { |
| 164 | Self::format_annotation_type(&annotation.annotation_type, f)?; |
| 165 | if let Some(id) = &annotation.id { |
| 166 | f.write_char('[' )?; |
| 167 | f.write_str(id)?; |
| 168 | f.write_char(']' )?; |
| 169 | } |
| 170 | Ok(()) |
| 171 | }), |
| 172 | f, |
| 173 | )?; |
| 174 | if !is_annotation_empty(annotation) { |
| 175 | if in_source { |
| 176 | color.paint_fn( |
| 177 | Box::new(|f| { |
| 178 | f.write_str(": " )?; |
| 179 | self.format_label(&annotation.label, f) |
| 180 | }), |
| 181 | f, |
| 182 | )?; |
| 183 | } else { |
| 184 | f.write_str(": " )?; |
| 185 | self.format_label(&annotation.label, f)?; |
| 186 | } |
| 187 | } |
| 188 | Ok(()) |
| 189 | } |
| 190 | } |
| 191 | |
| 192 | #[inline ] |
| 193 | fn format_source_line( |
| 194 | &self, |
| 195 | line: &DisplaySourceLine<'_>, |
| 196 | f: &mut fmt::Formatter<'_>, |
| 197 | ) -> fmt::Result { |
| 198 | match line { |
| 199 | DisplaySourceLine::Empty => Ok(()), |
| 200 | DisplaySourceLine::Content { text, .. } => { |
| 201 | f.write_char(' ' )?; |
| 202 | if let Some(margin) = self.margin { |
| 203 | let line_len = text.chars().count(); |
| 204 | let mut left = margin.left(line_len); |
| 205 | let right = margin.right(line_len); |
| 206 | |
| 207 | if margin.was_cut_left() { |
| 208 | // We have stripped some code/whitespace from the beginning, make it clear. |
| 209 | "..." .fmt(f)?; |
| 210 | left += 3; |
| 211 | } |
| 212 | |
| 213 | // On long lines, we strip the source line, accounting for unicode. |
| 214 | let mut taken = 0; |
| 215 | let cut_right = if margin.was_cut_right(line_len) { |
| 216 | taken += 3; |
| 217 | true |
| 218 | } else { |
| 219 | false |
| 220 | }; |
| 221 | // Specifies that it will end on the next character, so it will return |
| 222 | // until the next one to the final condition. |
| 223 | let mut ended = false; |
| 224 | let range = text |
| 225 | .char_indices() |
| 226 | .skip(left) |
| 227 | // Complete char iterator with final character |
| 228 | .chain(once((text.len(), ' \0' ))) |
| 229 | // Take until the next one to the final condition |
| 230 | .take_while(|(_, ch)| { |
| 231 | // Fast return to iterate over final byte position |
| 232 | if ended { |
| 233 | return false; |
| 234 | } |
| 235 | // Make sure that the trimming on the right will fall within the terminal width. |
| 236 | // FIXME: `unicode_width` sometimes disagrees with terminals on how wide a `char` is. |
| 237 | // For now, just accept that sometimes the code line will be longer than desired. |
| 238 | taken += unicode_width::UnicodeWidthChar::width(*ch).unwrap_or(1); |
| 239 | if taken > right - left { |
| 240 | ended = true; |
| 241 | } |
| 242 | true |
| 243 | }) |
| 244 | // Reduce to start and end byte position |
| 245 | .fold((None, 0), |acc, (i, _)| { |
| 246 | if acc.0.is_some() { |
| 247 | (acc.0, i) |
| 248 | } else { |
| 249 | (Some(i), i) |
| 250 | } |
| 251 | }); |
| 252 | |
| 253 | // Format text with margins |
| 254 | text[range.0.expect("One character at line" )..range.1].fmt(f)?; |
| 255 | |
| 256 | if cut_right { |
| 257 | // We have stripped some code after the right-most span end, make it clear we did so. |
| 258 | "..." .fmt(f)?; |
| 259 | } |
| 260 | Ok(()) |
| 261 | } else { |
| 262 | text.fmt(f) |
| 263 | } |
| 264 | } |
| 265 | DisplaySourceLine::Annotation { |
| 266 | range, |
| 267 | annotation, |
| 268 | annotation_type, |
| 269 | annotation_part, |
| 270 | } => { |
| 271 | let indent_char = match annotation_part { |
| 272 | DisplayAnnotationPart::Standalone => ' ' , |
| 273 | DisplayAnnotationPart::LabelContinuation => ' ' , |
| 274 | DisplayAnnotationPart::Consequitive => ' ' , |
| 275 | DisplayAnnotationPart::MultilineStart => '_' , |
| 276 | DisplayAnnotationPart::MultilineEnd => '_' , |
| 277 | }; |
| 278 | let mark = match annotation_type { |
| 279 | DisplayAnnotationType::Error => '^' , |
| 280 | DisplayAnnotationType::Warning => '-' , |
| 281 | DisplayAnnotationType::Info => '-' , |
| 282 | DisplayAnnotationType::Note => '-' , |
| 283 | DisplayAnnotationType::Help => '-' , |
| 284 | DisplayAnnotationType::None => ' ' , |
| 285 | }; |
| 286 | let color = self.get_annotation_style(annotation_type); |
| 287 | let indent_length = match annotation_part { |
| 288 | DisplayAnnotationPart::LabelContinuation => range.1, |
| 289 | DisplayAnnotationPart::Consequitive => range.1, |
| 290 | _ => range.0, |
| 291 | }; |
| 292 | |
| 293 | color.paint_fn( |
| 294 | Box::new(|f| { |
| 295 | format_repeat_char(indent_char, indent_length + 1, f)?; |
| 296 | format_repeat_char(mark, range.1 - indent_length, f) |
| 297 | }), |
| 298 | f, |
| 299 | )?; |
| 300 | |
| 301 | if !is_annotation_empty(annotation) { |
| 302 | f.write_char(' ' )?; |
| 303 | color.paint_fn( |
| 304 | Box::new(|f| { |
| 305 | self.format_annotation( |
| 306 | annotation, |
| 307 | annotation_part == &DisplayAnnotationPart::LabelContinuation, |
| 308 | true, |
| 309 | f, |
| 310 | ) |
| 311 | }), |
| 312 | f, |
| 313 | )?; |
| 314 | } |
| 315 | |
| 316 | Ok(()) |
| 317 | } |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | #[inline ] |
| 322 | fn format_raw_line( |
| 323 | &self, |
| 324 | line: &DisplayRawLine<'_>, |
| 325 | lineno_width: usize, |
| 326 | f: &mut fmt::Formatter<'_>, |
| 327 | ) -> fmt::Result { |
| 328 | match line { |
| 329 | DisplayRawLine::Origin { |
| 330 | path, |
| 331 | pos, |
| 332 | header_type, |
| 333 | } => { |
| 334 | let header_sigil = match header_type { |
| 335 | DisplayHeaderType::Initial => "-->" , |
| 336 | DisplayHeaderType::Continuation => ":::" , |
| 337 | }; |
| 338 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); |
| 339 | |
| 340 | if let Some((col, row)) = pos { |
| 341 | format_repeat_char(' ' , lineno_width, f)?; |
| 342 | lineno_color.paint(header_sigil, f)?; |
| 343 | f.write_char(' ' )?; |
| 344 | path.fmt(f)?; |
| 345 | f.write_char(':' )?; |
| 346 | col.fmt(f)?; |
| 347 | f.write_char(':' )?; |
| 348 | row.fmt(f) |
| 349 | } else { |
| 350 | format_repeat_char(' ' , lineno_width, f)?; |
| 351 | lineno_color.paint(header_sigil, f)?; |
| 352 | f.write_char(' ' )?; |
| 353 | path.fmt(f) |
| 354 | } |
| 355 | } |
| 356 | DisplayRawLine::Annotation { |
| 357 | annotation, |
| 358 | source_aligned, |
| 359 | continuation, |
| 360 | } => { |
| 361 | if *source_aligned { |
| 362 | if *continuation { |
| 363 | format_repeat_char(' ' , lineno_width + 3, f)?; |
| 364 | } else { |
| 365 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); |
| 366 | format_repeat_char(' ' , lineno_width, f)?; |
| 367 | f.write_char(' ' )?; |
| 368 | lineno_color.paint("=" , f)?; |
| 369 | f.write_char(' ' )?; |
| 370 | } |
| 371 | } |
| 372 | self.format_annotation(annotation, *continuation, false, f) |
| 373 | } |
| 374 | } |
| 375 | } |
| 376 | |
| 377 | #[inline ] |
| 378 | fn format_line( |
| 379 | &self, |
| 380 | dl: &DisplayLine<'_>, |
| 381 | lineno_width: usize, |
| 382 | inline_marks_width: usize, |
| 383 | f: &mut fmt::Formatter<'_>, |
| 384 | ) -> fmt::Result { |
| 385 | match dl { |
| 386 | DisplayLine::Source { |
| 387 | lineno, |
| 388 | inline_marks, |
| 389 | line, |
| 390 | } => { |
| 391 | let lineno_color = self.stylesheet.get_style(StyleClass::LineNo); |
| 392 | if self.anonymized_line_numbers && lineno.is_some() { |
| 393 | lineno_color.paint_fn( |
| 394 | Box::new(|f| { |
| 395 | f.write_str(Self::ANONYMIZED_LINE_NUM)?; |
| 396 | f.write_str(" |" ) |
| 397 | }), |
| 398 | f, |
| 399 | )?; |
| 400 | } else { |
| 401 | lineno_color.paint_fn( |
| 402 | Box::new(|f| { |
| 403 | match lineno { |
| 404 | Some(n) => write!(f, " {:>width$}" , n, width = lineno_width), |
| 405 | None => format_repeat_char(' ' , lineno_width, f), |
| 406 | }?; |
| 407 | f.write_str(" |" ) |
| 408 | }), |
| 409 | f, |
| 410 | )?; |
| 411 | } |
| 412 | if *line != DisplaySourceLine::Empty { |
| 413 | if !inline_marks.is_empty() || 0 < inline_marks_width { |
| 414 | f.write_char(' ' )?; |
| 415 | self.format_inline_marks(inline_marks, inline_marks_width, f)?; |
| 416 | } |
| 417 | self.format_source_line(line, f)?; |
| 418 | } else if !inline_marks.is_empty() { |
| 419 | f.write_char(' ' )?; |
| 420 | self.format_inline_marks(inline_marks, inline_marks_width, f)?; |
| 421 | } |
| 422 | Ok(()) |
| 423 | } |
| 424 | DisplayLine::Fold { inline_marks } => { |
| 425 | f.write_str("..." )?; |
| 426 | if !inline_marks.is_empty() || 0 < inline_marks_width { |
| 427 | format_repeat_char(' ' , lineno_width, f)?; |
| 428 | self.format_inline_marks(inline_marks, inline_marks_width, f)?; |
| 429 | } |
| 430 | Ok(()) |
| 431 | } |
| 432 | DisplayLine::Raw(line) => self.format_raw_line(line, lineno_width, f), |
| 433 | } |
| 434 | } |
| 435 | |
| 436 | fn format_inline_marks( |
| 437 | &self, |
| 438 | inline_marks: &[DisplayMark], |
| 439 | inline_marks_width: usize, |
| 440 | f: &mut fmt::Formatter<'_>, |
| 441 | ) -> fmt::Result { |
| 442 | format_repeat_char(' ' , inline_marks_width - inline_marks.len(), f)?; |
| 443 | for mark in inline_marks { |
| 444 | self.get_annotation_style(&mark.annotation_type).paint_fn( |
| 445 | Box::new(|f| { |
| 446 | f.write_char(match mark.mark_type { |
| 447 | DisplayMarkType::AnnotationThrough => '|' , |
| 448 | DisplayMarkType::AnnotationStart => '/' , |
| 449 | }) |
| 450 | }), |
| 451 | f, |
| 452 | )?; |
| 453 | } |
| 454 | Ok(()) |
| 455 | } |
| 456 | } |
| 457 | |