1use std::ops::Range; 2 3use crate::diagnostic::{Diagnostic, LabelStyle}; 4use crate::files::{Error, Files, Location}; 5use crate::term::renderer::{Locus, MultiLabel, Renderer, SingleLabel}; 6use crate::term::Config; 7 8/// Count the number of decimal digits in `n`. 9fn count_digits(mut n: usize) -> usize { 10 let mut count = 0; 11 while n != 0 { 12 count += 1; 13 n /= 10; // remove last digit 14 } 15 count 16} 17 18/// Output a richly formatted diagnostic, with source code previews. 19pub struct RichDiagnostic<'diagnostic, 'config, FileId> { 20 diagnostic: &'diagnostic Diagnostic<FileId>, 21 config: &'config Config, 22} 23 24impl<'diagnostic, 'config, FileId> RichDiagnostic<'diagnostic, 'config, FileId> 25where 26 FileId: Copy + PartialEq, 27{ 28 pub fn new( 29 diagnostic: &'diagnostic Diagnostic<FileId>, 30 config: &'config Config, 31 ) -> RichDiagnostic<'diagnostic, 'config, FileId> { 32 RichDiagnostic { diagnostic, config } 33 } 34 35 pub fn render<'files>( 36 &self, 37 files: &'files impl Files<'files, FileId = FileId>, 38 renderer: &mut Renderer<'_, '_>, 39 ) -> Result<(), Error> 40 where 41 FileId: 'files, 42 { 43 use std::collections::BTreeMap; 44 45 struct LabeledFile<'diagnostic, FileId> { 46 file_id: FileId, 47 start: usize, 48 name: String, 49 location: Location, 50 num_multi_labels: usize, 51 lines: BTreeMap<usize, Line<'diagnostic>>, 52 max_label_style: LabelStyle, 53 } 54 55 impl<'diagnostic, FileId> LabeledFile<'diagnostic, FileId> { 56 fn get_or_insert_line( 57 &mut self, 58 line_index: usize, 59 line_range: Range<usize>, 60 line_number: usize, 61 ) -> &mut Line<'diagnostic> { 62 self.lines.entry(line_index).or_insert_with(|| Line { 63 range: line_range, 64 number: line_number, 65 single_labels: vec![], 66 multi_labels: vec![], 67 // This has to be false by default so we know if it must be rendered by another condition already. 68 must_render: false, 69 }) 70 } 71 } 72 73 struct Line<'diagnostic> { 74 number: usize, 75 range: std::ops::Range<usize>, 76 // TODO: How do we reuse these allocations? 77 single_labels: Vec<SingleLabel<'diagnostic>>, 78 multi_labels: Vec<(usize, LabelStyle, MultiLabel<'diagnostic>)>, 79 must_render: bool, 80 } 81 82 // TODO: Make this data structure external, to allow for allocation reuse 83 let mut labeled_files = Vec::<LabeledFile<'_, _>>::new(); 84 // Keep track of the outer padding to use when rendering the 85 // snippets of source code. 86 let mut outer_padding = 0; 87 88 // Group labels by file 89 for label in &self.diagnostic.labels { 90 let start_line_index = files.line_index(label.file_id, label.range.start)?; 91 let start_line_number = files.line_number(label.file_id, start_line_index)?; 92 let start_line_range = files.line_range(label.file_id, start_line_index)?; 93 let end_line_index = files.line_index(label.file_id, label.range.end)?; 94 let end_line_number = files.line_number(label.file_id, end_line_index)?; 95 let end_line_range = files.line_range(label.file_id, end_line_index)?; 96 97 outer_padding = std::cmp::max(outer_padding, count_digits(start_line_number)); 98 outer_padding = std::cmp::max(outer_padding, count_digits(end_line_number)); 99 100 // NOTE: This could be made more efficient by using an associative 101 // data structure like a hashmap or B-tree, but we use a vector to 102 // preserve the order that unique files appear in the list of labels. 103 let labeled_file = match labeled_files 104 .iter_mut() 105 .find(|labeled_file| label.file_id == labeled_file.file_id) 106 { 107 Some(labeled_file) => { 108 // another diagnostic also referenced this file 109 if labeled_file.max_label_style > label.style 110 || (labeled_file.max_label_style == label.style 111 && labeled_file.start > label.range.start) 112 { 113 // this label has a higher style or has the same style but starts earlier 114 labeled_file.start = label.range.start; 115 labeled_file.location = files.location(label.file_id, label.range.start)?; 116 labeled_file.max_label_style = label.style; 117 } 118 labeled_file 119 } 120 None => { 121 // no other diagnostic referenced this file yet 122 labeled_files.push(LabeledFile { 123 file_id: label.file_id, 124 start: label.range.start, 125 name: files.name(label.file_id)?.to_string(), 126 location: files.location(label.file_id, label.range.start)?, 127 num_multi_labels: 0, 128 lines: BTreeMap::new(), 129 max_label_style: label.style, 130 }); 131 // this unwrap should never fail because we just pushed an element 132 labeled_files 133 .last_mut() 134 .expect("just pushed an element that disappeared") 135 } 136 }; 137 138 if start_line_index == end_line_index { 139 // Single line 140 // 141 // ```text 142 // 2 │ (+ test "") 143 // │ ^^ expected `Int` but found `String` 144 // ``` 145 let label_start = label.range.start - start_line_range.start; 146 // Ensure that we print at least one caret, even when we 147 // have a zero-length source range. 148 let label_end = 149 usize::max(label.range.end - start_line_range.start, label_start + 1); 150 151 let line = labeled_file.get_or_insert_line( 152 start_line_index, 153 start_line_range, 154 start_line_number, 155 ); 156 157 // Ensure that the single line labels are lexicographically 158 // sorted by the range of source code that they cover. 159 let index = match line.single_labels.binary_search_by(|(_, range, _)| { 160 // `Range<usize>` doesn't implement `Ord`, so convert to `(usize, usize)` 161 // to piggyback off its lexicographic comparison implementation. 162 (range.start, range.end).cmp(&(label_start, label_end)) 163 }) { 164 // If the ranges are the same, order the labels in reverse 165 // to how they were originally specified in the diagnostic. 166 // This helps with printing in the renderer. 167 Ok(index) | Err(index) => index, 168 }; 169 170 line.single_labels 171 .insert(index, (label.style, label_start..label_end, &label.message)); 172 173 // If this line is not rendered, the SingleLabel is not visible. 174 line.must_render = true; 175 } else { 176 // Multiple lines 177 // 178 // ```text 179 // 4 │ fizz₁ num = case (mod num 5) (mod num 3) of 180 // │ ╭─────────────^ 181 // 5 │ │ 0 0 => "FizzBuzz" 182 // 6 │ │ 0 _ => "Fizz" 183 // 7 │ │ _ 0 => "Buzz" 184 // 8 │ │ _ _ => num 185 // │ ╰──────────────^ `case` clauses have incompatible types 186 // ``` 187 188 let label_index = labeled_file.num_multi_labels; 189 labeled_file.num_multi_labels += 1; 190 191 // First labeled line 192 let label_start = label.range.start - start_line_range.start; 193 194 let start_line = labeled_file.get_or_insert_line( 195 start_line_index, 196 start_line_range.clone(), 197 start_line_number, 198 ); 199 200 start_line.multi_labels.push(( 201 label_index, 202 label.style, 203 MultiLabel::Top(label_start), 204 )); 205 206 // The first line has to be rendered so the start of the label is visible. 207 start_line.must_render = true; 208 209 // Marked lines 210 // 211 // ```text 212 // 5 │ │ 0 0 => "FizzBuzz" 213 // 6 │ │ 0 _ => "Fizz" 214 // 7 │ │ _ 0 => "Buzz" 215 // ``` 216 for line_index in (start_line_index + 1)..end_line_index { 217 let line_range = files.line_range(label.file_id, line_index)?; 218 let line_number = files.line_number(label.file_id, line_index)?; 219 220 outer_padding = std::cmp::max(outer_padding, count_digits(line_number)); 221 222 let line = labeled_file.get_or_insert_line(line_index, line_range, line_number); 223 224 line.multi_labels 225 .push((label_index, label.style, MultiLabel::Left)); 226 227 // The line should be rendered to match the configuration of how much context to show. 228 line.must_render |= 229 // Is this line part of the context after the start of the label? 230 line_index - start_line_index <= self.config.start_context_lines 231 || 232 // Is this line part of the context before the end of the label? 233 end_line_index - line_index <= self.config.end_context_lines; 234 } 235 236 // Last labeled line 237 // 238 // ```text 239 // 8 │ │ _ _ => num 240 // │ ╰──────────────^ `case` clauses have incompatible types 241 // ``` 242 let label_end = label.range.end - end_line_range.start; 243 244 let end_line = labeled_file.get_or_insert_line( 245 end_line_index, 246 end_line_range, 247 end_line_number, 248 ); 249 250 end_line.multi_labels.push(( 251 label_index, 252 label.style, 253 MultiLabel::Bottom(label_end, &label.message), 254 )); 255 256 // The last line has to be rendered so the end of the label is visible. 257 end_line.must_render = true; 258 } 259 } 260 261 // Header and message 262 // 263 // ```text 264 // error[E0001]: unexpected type in `+` application 265 // ``` 266 renderer.render_header( 267 None, 268 self.diagnostic.severity, 269 self.diagnostic.code.as_deref(), 270 self.diagnostic.message.as_str(), 271 )?; 272 273 // Source snippets 274 // 275 // ```text 276 // ┌─ test:2:9 277 // │ 278 // 2 │ (+ test "") 279 // │ ^^ expected `Int` but found `String` 280 // │ 281 // ``` 282 let mut labeled_files = labeled_files.into_iter().peekable(); 283 while let Some(labeled_file) = labeled_files.next() { 284 let source = files.source(labeled_file.file_id)?; 285 let source = source.as_ref(); 286 287 // Top left border and locus. 288 // 289 // ```text 290 // ┌─ test:2:9 291 // ``` 292 if !labeled_file.lines.is_empty() { 293 renderer.render_snippet_start( 294 outer_padding, 295 &Locus { 296 name: labeled_file.name, 297 location: labeled_file.location, 298 }, 299 )?; 300 renderer.render_snippet_empty( 301 outer_padding, 302 self.diagnostic.severity, 303 labeled_file.num_multi_labels, 304 &[], 305 )?; 306 } 307 308 let mut lines = labeled_file 309 .lines 310 .iter() 311 .filter(|(_, line)| line.must_render) 312 .peekable(); 313 314 while let Some((line_index, line)) = lines.next() { 315 renderer.render_snippet_source( 316 outer_padding, 317 line.number, 318 &source[line.range.clone()], 319 self.diagnostic.severity, 320 &line.single_labels, 321 labeled_file.num_multi_labels, 322 &line.multi_labels, 323 )?; 324 325 // Check to see if we need to render any intermediate stuff 326 // before rendering the next line. 327 if let Some((next_line_index, _)) = lines.peek() { 328 match next_line_index.checked_sub(*line_index) { 329 // Consecutive lines 330 Some(1) => {} 331 // One line between the current line and the next line 332 Some(2) => { 333 // Write a source line 334 let file_id = labeled_file.file_id; 335 336 // This line was not intended to be rendered initially. 337 // To render the line right, we have to get back the original labels. 338 let labels = labeled_file 339 .lines 340 .get(&(line_index + 1)) 341 .map_or(&[][..], |line| &line.multi_labels[..]); 342 343 renderer.render_snippet_source( 344 outer_padding, 345 files.line_number(file_id, line_index + 1)?, 346 &source[files.line_range(file_id, line_index + 1)?], 347 self.diagnostic.severity, 348 &[], 349 labeled_file.num_multi_labels, 350 labels, 351 )?; 352 } 353 // More than one line between the current line and the next line. 354 Some(_) | None => { 355 // Source break 356 // 357 // ```text 358 // · 359 // ``` 360 renderer.render_snippet_break( 361 outer_padding, 362 self.diagnostic.severity, 363 labeled_file.num_multi_labels, 364 &line.multi_labels, 365 )?; 366 } 367 } 368 } 369 } 370 371 // Check to see if we should render a trailing border after the 372 // final line of the snippet. 373 if labeled_files.peek().is_none() && self.diagnostic.notes.is_empty() { 374 // We don't render a border if we are at the final newline 375 // without trailing notes, because it would end up looking too 376 // spaced-out in combination with the final new line. 377 } else { 378 // Render the trailing snippet border. 379 renderer.render_snippet_empty( 380 outer_padding, 381 self.diagnostic.severity, 382 labeled_file.num_multi_labels, 383 &[], 384 )?; 385 } 386 } 387 388 // Additional notes 389 // 390 // ```text 391 // = expected type `Int` 392 // found type `String` 393 // ``` 394 for note in &self.diagnostic.notes { 395 renderer.render_snippet_note(outer_padding, note)?; 396 } 397 renderer.render_empty() 398 } 399} 400 401/// Output a short diagnostic, with a line number, severity, and message. 402pub struct ShortDiagnostic<'diagnostic, FileId> { 403 diagnostic: &'diagnostic Diagnostic<FileId>, 404 show_notes: bool, 405} 406 407impl<'diagnostic, FileId> ShortDiagnostic<'diagnostic, FileId> 408where 409 FileId: Copy + PartialEq, 410{ 411 pub fn new( 412 diagnostic: &'diagnostic Diagnostic<FileId>, 413 show_notes: bool, 414 ) -> ShortDiagnostic<'diagnostic, FileId> { 415 ShortDiagnostic { 416 diagnostic, 417 show_notes, 418 } 419 } 420 421 pub fn render<'files>( 422 &self, 423 files: &'files impl Files<'files, FileId = FileId>, 424 renderer: &mut Renderer<'_, '_>, 425 ) -> Result<(), Error> 426 where 427 FileId: 'files, 428 { 429 // Located headers 430 // 431 // ```text 432 // test:2:9: error[E0001]: unexpected type in `+` application 433 // ``` 434 let mut primary_labels_encountered = 0; 435 let labels = self.diagnostic.labels.iter(); 436 for label in labels.filter(|label| label.style == LabelStyle::Primary) { 437 primary_labels_encountered += 1; 438 439 renderer.render_header( 440 Some(&Locus { 441 name: files.name(label.file_id)?.to_string(), 442 location: files.location(label.file_id, label.range.start)?, 443 }), 444 self.diagnostic.severity, 445 self.diagnostic.code.as_deref(), 446 self.diagnostic.message.as_str(), 447 )?; 448 } 449 450 // Fallback to printing a non-located header if no primary labels were encountered 451 // 452 // ```text 453 // error[E0002]: Bad config found 454 // ``` 455 if primary_labels_encountered == 0 { 456 renderer.render_header( 457 None, 458 self.diagnostic.severity, 459 self.diagnostic.code.as_deref(), 460 self.diagnostic.message.as_str(), 461 )?; 462 } 463 464 if self.show_notes { 465 // Additional notes 466 // 467 // ```text 468 // = expected type `Int` 469 // found type `String` 470 // ``` 471 for note in &self.diagnostic.notes { 472 renderer.render_snippet_note(0, note)?; 473 } 474 } 475 476 Ok(()) 477 } 478} 479