Skip to content

Commit

Permalink
Create intermediate ‘mark’ data structures
Browse files Browse the repository at this point in the history
  • Loading branch information
brendanzab committed Feb 25, 2020
1 parent 229218b commit d6c1f25
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 88 deletions.
85 changes: 62 additions & 23 deletions codespan-reporting/src/term.rs
@@ -1,6 +1,7 @@
//! Terminal back-end for emitting diagnostics.

use std::io;
use std::ops::Range;
use std::str::FromStr;
use termcolor::{ColorChoice, WriteColor};

Expand Down Expand Up @@ -84,6 +85,12 @@ fn count_digits(mut n: usize) -> usize {
count
}

fn merge(range0: &Range<usize>, range1: &Range<usize>) -> Range<usize> {
let start = std::cmp::min(range0.start, range1.start);
let end = std::cmp::max(range0.end, range1.end);
start..end
}

/// Emit a diagnostic using the given writer, context, config, and files.
pub fn emit<F: Files>(
writer: &mut (impl WriteColor + ?Sized),
Expand All @@ -93,44 +100,76 @@ pub fn emit<F: Files>(
) -> io::Result<()> {
use std::collections::BTreeMap;

use self::views::MarkStyle;
use self::views::{Header, NewLine, Note, SourceSnippet};

// Emit the title
//
// ```text
// error[E0001]: unexpected type in `+` application
// ```
Header::new(diagnostic).emit(writer, config)?;
NewLine::new().emit(writer, config)?;

// Group labels by file
// Group marks by file

let mut label_groups = BTreeMap::new();
let mut max_line_number = 0;
let mut mark_groups = BTreeMap::new();
let mut max_line_number = None;

for label in &diagnostic.labels {
use std::collections::btree_map::Entry;

use self::views::{Mark, MarkGroup, MarkStyle};

let mark_style = match label.style {
LabelStyle::Primary => MarkStyle::Primary(diagnostic.severity),
LabelStyle::Secondary => MarkStyle::Secondary,
};

label_groups
.entry(label.file_id)
.or_insert(vec![])
.push((label, mark_style));
let end = files.location(label.file_id, label.range.end).expect("end");
let end_line_number = end.line + 1;
max_line_number = Some(max_line_number.map_or(end_line_number, |number| {
std::cmp::max(number, end_line_number)
}));

if let Some(location) = files.location(label.file_id, label.range.end) {
max_line_number = std::cmp::max(max_line_number, location.line + 1);
let mark = Mark {
style: mark_style,
range: label.range.clone(),
message: label.message.as_str(),
};

// TODO: Sort snippets by the mark group origin
// TODO: Group contiguous line index ranges using some sort of interval set algorithm
// TODO: Flatten mark groups to overlapping underlines that can be easily rendered.
match mark_groups.entry(label.file_id) {
Entry::Vacant(entry) => {
entry.insert(MarkGroup {
origin: files.origin(label.file_id).expect("origin"),
range: label.range.clone(),
marks: vec![mark],
});
},
Entry::Occupied(mut entry) => {
let mark_group = entry.get_mut();
mark_group.range = merge(&mark_group.range, &mark.range);
mark_group.marks.push(mark);
},
}
}

// Compute the width of the gutter for the following source snippets and notes.
let gutter_padding = count_digits(max_line_number);
// Compute the width of the gutter for the following source snippets and notes
let gutter_padding = max_line_number.map_or(0, count_digits);

// Emit the title
//
// ```text
// error[E0001]: unexpected type in `+` application
// ```
Header::new(diagnostic).emit(writer, config)?;
NewLine::new().emit(writer, config)?;

// Emit the snippets, starting with the one that contains the primary label
for (file_id, labels) in label_groups {
SourceSnippet::new(file_id, gutter_padding, labels).emit(files, writer, config)?;
// Emit the source snippets
//
// ```text
// ┌── test:2:9 ───
// │
// 2 │ (+ test "")
// │ ^^ expected `Int` but found `String`
// │
// ```
for (file_id, mark_group) in mark_groups {
SourceSnippet::new(gutter_padding, file_id, mark_group).emit(files, writer, config)?;
}

// Additional notes
Expand Down
99 changes: 38 additions & 61 deletions codespan-reporting/src/term/views/source_snippet.rs
Expand Up @@ -2,17 +2,26 @@ use std::io;
use std::ops::Range;
use termcolor::WriteColor;

use crate::diagnostic::{Files, Label};
use crate::diagnostic::Files;
use crate::term::Config;

use super::{Locus, NewLine};

pub use super::underline::MarkStyle;
use super::{
BorderLeft, BorderLeftBreak, BorderTop, BorderTopLeft, Gutter, Underline, UnderlineBottom,
UnderlineLeft, UnderlineTop, UnderlineTopLeft,
BorderLeft, BorderLeftBreak, BorderTop, BorderTopLeft, Gutter, Locus, MarkStyle, NewLine,
Underline, UnderlineBottom, UnderlineLeft, UnderlineTop, UnderlineTopLeft,
};

pub struct MarkGroup<'a, F: Files> {
pub origin: F::Origin,
pub range: Range<usize>,
pub marks: Vec<Mark<'a>>,
}

pub struct Mark<'a> {
pub style: MarkStyle,
pub range: Range<usize>,
pub message: &'a str,
}

/// An underlined snippet of source code.
///
/// ```text
Expand All @@ -23,49 +32,22 @@ use super::{
/// │
/// ```
pub struct SourceSnippet<'a, F: Files> {
file_id: F::FileId,
gutter_padding: usize,
ranges: Vec<(&'a Label<F::FileId>, MarkStyle)>,
file_id: F::FileId,
mark_group: MarkGroup<'a, F>,
}

impl<'a, F: Files> SourceSnippet<'a, F> {
pub fn new(
file_id: F::FileId,
gutter_padding: usize,
ranges: Vec<(&'a Label<F::FileId>, MarkStyle)>,
file_id: F::FileId,
mark_group: MarkGroup<'a, F>,
) -> SourceSnippet<'a, F> {
SourceSnippet {
file_id,
gutter_padding,
ranges,
}
}

fn locus_range(&self) -> Range<usize> {
fn merge(range0: Range<usize>, range1: Range<usize>) -> Range<usize> {
let start = std::cmp::min(range0.start, range1.start);
let end = std::cmp::max(range0.end, range1.end);
start..end
}

let mut source_range = None;
let mut locus_range = None;

for (label, mark_style) in &self.ranges {
source_range = Some(source_range.map_or(label.range.clone(), |range| {
merge(range, label.range.clone())
}));
if let MarkStyle::Primary(_) = mark_style {
locus_range = Some(locus_range.map_or(label.range.clone(), |range| {
merge(range, label.range.clone())
}));
}
file_id,
mark_group,
}

let source_range = source_range.unwrap_or(0..0);
let locus_range = locus_range.unwrap_or(source_range.clone());

locus_range
}

pub fn emit(
Expand All @@ -76,14 +58,10 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
) -> io::Result<()> {
use std::io::Write;

let origin = files.origin(self.file_id).expect("origin");
let location = |byte_index| files.location(self.file_id, byte_index);
let line_index = |byte_index| files.line_index(self.file_id, byte_index);
let line = |line_index| files.line(self.file_id, line_index);

let locus_range = self.locus_range();
let source_end_line_index = line_index(source_range.end).expect("source_end_line_index");

// Top left border and locus.
//
// ```text
Expand All @@ -95,21 +73,20 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
BorderTop::new(2).emit(writer, config)?;
write!(writer, " ")?;

let locus_location = location(locus_range.start).expect("locus_location");
Locus::new(origin, locus_location).emit(writer, config)?;
let locus_location = location(self.mark_group.range.start).expect("locus_location");
Locus::new(&self.mark_group.origin, locus_location).emit(writer, config)?;

write!(writer, " ")?;
BorderTop::new(3).emit(writer, config)?;
NewLine::new().emit(writer, config)?;

// TODO: Better grouping
for (i, (label, mark_style)) in self.ranges.iter().enumerate() {
let start_line_index = line_index(label.range.start).expect("start_line_index");
let end_line_index = line_index(label.range.end).expect("end_line_index");
for (i, mark) in self.mark_group.marks.iter().enumerate() {
let start_line_index = line_index(mark.range.start).expect("start_line_index");
let end_line_index = line_index(mark.range.end).expect("end_line_index");
let start_line = line(start_line_index).expect("start_line");
let end_line = line(end_line_index).expect("end_line");

let label_style = mark_style.label_style(config);
let label_style = mark.style.label_style(config);

// Code snippet
//
Expand Down Expand Up @@ -137,8 +114,8 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// │ ^^ expected `Int` but found `String`
// ```

let highlight_start = label.range.start - start_line.start;
let highlight_end = label.range.end - start_line.start;
let highlight_start = mark.range.start - start_line.start;
let highlight_end = mark.range.end - start_line.start;
let prefix_source = &start_line.source.as_ref()[..highlight_start];
let highlighted_source =
&start_line.source.as_ref()[highlight_start..highlight_end];
Expand All @@ -160,10 +137,10 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
Gutter::new(None, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
Underline::new(
*mark_style,
mark.style,
&prefix_source,
&highlighted_source,
&label.message,
mark.message,
)
.emit(writer, config)?;
NewLine::new().emit(writer, config)?;
Expand All @@ -180,7 +157,7 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// │ ╰──────────────^ `case` clauses have incompatible types
// ```

let highlight_start = label.range.start - start_line.start;
let highlight_start = mark.range.start - start_line.start;
let prefix_source = &start_line.source.as_ref()[..highlight_start];
let highlighted_source = &start_line.source.as_ref()[highlight_start..];

Expand All @@ -195,7 +172,7 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// Write line number, border, and underline
Gutter::new(start_line_index, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
UnderlineTopLeft::new(*mark_style).emit(writer, config)?;
UnderlineTopLeft::new(mark.style).emit(writer, config)?;

// Write source line
write!(config.source(writer), " {}", prefix_source)?;
Expand Down Expand Up @@ -226,7 +203,7 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// Write border and underline
Gutter::new(None, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
UnderlineTop::new(*mark_style, &prefix_source).emit(writer, config)?;
UnderlineTop::new(mark.style, &prefix_source).emit(writer, config)?;
NewLine::new().emit(writer, config)?;
}

Expand All @@ -244,7 +221,7 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// Write line number, border, and underline
Gutter::new(line_index, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
UnderlineLeft::new(*mark_style).emit(writer, config)?;
UnderlineLeft::new(mark.style).emit(writer, config)?;

// Write highlighted source
writer.set_color(label_style)?;
Expand All @@ -260,14 +237,14 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// │ ╰──────────────^ `case` clauses have incompatible types
// ```

let highlight_end = label.range.end - end_line.start;
let highlight_end = mark.range.end - end_line.start;
let highlighted_source = &end_line.source.as_ref()[..highlight_end];
let suffix_source = &end_line.source.as_ref()[highlight_end..];

// Write line number, border, and underline
Gutter::new(end_line_index, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
UnderlineLeft::new(*mark_style).emit(writer, config)?;
UnderlineLeft::new(mark.style).emit(writer, config)?;

// Write line source
writer.set_color(label_style)?;
Expand All @@ -279,7 +256,7 @@ impl<'a, F: Files> SourceSnippet<'a, F> {
// Write border, underline, and label
Gutter::new(None, self.gutter_padding).emit(writer, config)?;
BorderLeft::new().emit(writer, config)?;
UnderlineBottom::new(*mark_style, &highlighted_source, &label.message)
UnderlineBottom::new(mark.style, &highlighted_source, mark.message)
.emit(writer, config)?;
NewLine::new().emit(writer, config)?;
}
Expand Down
Expand Up @@ -4,7 +4,7 @@ expression: TEST_DATA.emit_color(&test_config())
---
{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}

{fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:8:12 {fg:Blue}───{/}
{fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:3:15 {fg:Blue}───{/}
{fg:Blue}│{/}
{fg:Blue}8{/} {fg:Blue}│{/} _ _ => {fg:Red}num{/}
{fg:Blue}│{/} {fg:Red}^^^ expected `String`, found `Nat`{/}
Expand All @@ -25,7 +25,7 @@ expression: TEST_DATA.emit_color(&test_config())

{fg:Red bold bright}error[E0308]{bold bright}: `case` clauses have incompatible types{/}

{fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:15:16 {fg:Blue}───{/}
{fg:Blue}┌{/}{fg:Blue}──{/} FizzBuzz.fun:11:5 {fg:Blue}───{/}
{fg:Blue}│{/}
{fg:Blue}15{/} {fg:Blue}│{/} _ _ => {fg:Red}num{/}
{fg:Blue}│{/} {fg:Red}^^^ expected `String`, found `Nat`{/}
Expand Down
Expand Up @@ -4,7 +4,7 @@ expression: TEST_DATA.emit_no_color(&test_config())
---
error[E0308]: `case` clauses have incompatible types

┌── FizzBuzz.fun:8:12 ───
┌── FizzBuzz.fun:3:15 ───
8_ _ => num
^^^ expected `String`, found `Nat`
Expand All @@ -25,7 +25,7 @@ error[E0308]: `case` clauses have incompatible types

error[E0308]: `case` clauses have incompatible types

┌── FizzBuzz.fun:15:16 ───
┌── FizzBuzz.fun:11:5 ───
15_ _ => num
^^^ expected `String`, found `Nat`
Expand Down

0 comments on commit d6c1f25

Please sign in to comment.