revamp MultiSpan and introduce new snippet code

MultiSpan model is now:

- set of primary spans
- set of span+label pairs

Primary spans render with `^^^`, secondary spans with `---`.

Labels are placed next to the `^^^` or `---` marker as appropriate.
This commit is contained in:
Niko Matsakis 2016-04-20 14:52:31 -04:00
parent e1a575cb07
commit a20ee76b56
3 changed files with 1433 additions and 140 deletions

View File

@ -32,8 +32,6 @@ use serialize::{Encodable, Decodable, Encoder, Decoder};
use ast::Name;
use errors::emitter::MAX_HIGHLIGHT_LINES;
// _____________________________________________________________________________
// Pos, BytePos, CharPos
//
@ -51,7 +49,7 @@ pub struct BytePos(pub u32);
/// A character offset. Because of multibyte utf8 characters, a byte offset
/// is not equivalent to a character offset. The CodeMap will convert BytePos
/// values to CharPos values as necessary.
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Debug)]
#[derive(Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Debug)]
pub struct CharPos(pub usize);
// FIXME: Lots of boilerplate in these impls, but so far my attempts to fix
@ -132,13 +130,29 @@ pub struct Span {
pub expn_id: ExpnId
}
/// Spans are converted to MultiSpans just before error reporting, either automatically,
/// generated by line grouping, or manually constructed.
/// In the latter case care should be taken to ensure that spans are ordered, disjoint,
/// and point into the same FileMap.
/// A collection of spans. Spans have two orthogonal attributes:
///
/// - they can be *primary spans*. In this case they are the locus of
/// the error, and would be rendered with `^^^`.
/// - they can have a *label*. In this case, the label is written next
/// to the mark in the snippet when we render.
#[derive(Clone)]
pub struct MultiSpan {
pub spans: Vec<Span>
primary_spans: Vec<Span>,
span_labels: Vec<(Span, String)>,
}
#[derive(Clone, Debug)]
pub struct SpanLabel {
/// the span we are going to include in the final snippet
pub span: Span,
/// is this a primary span? This is the "locus" of the message,
/// and is indicated with a `^^^^` underline, versus `----`
pub is_primary: bool,
/// what label should we attach to this span (if any)?
pub label: Option<String>,
}
pub const DUMMY_SP: Span = Span { lo: BytePos(0), hi: BytePos(0), expn_id: NO_EXPANSION };
@ -276,97 +290,76 @@ pub fn original_sp(cm: &CodeMap, sp: Span, enclosing_sp: Span) -> Span {
impl MultiSpan {
pub fn new() -> MultiSpan {
MultiSpan { spans: Vec::new() }
}
pub fn to_span_bounds(&self) -> Span {
assert!(!self.spans.is_empty());
let Span { lo, expn_id, .. } = *self.spans.first().unwrap();
let Span { hi, .. } = *self.spans.last().unwrap();
Span { lo: lo, hi: hi, expn_id: expn_id }
}
/// Merges or inserts the given span into itself.
pub fn push_merge(&mut self, mut sp: Span) {
let mut idx_merged = None;
for idx in 0.. {
let cur = match self.spans.get(idx) {
Some(s) => *s,
None => break,
};
// Try to merge with a contained Span
if let Some(union) = cur.merge(sp) {
self.spans[idx] = union;
sp = union;
idx_merged = Some(idx);
break;
}
// Or insert into the first sorted position
if sp.hi <= cur.lo {
self.spans.insert(idx, sp);
idx_merged = Some(idx);
break;
}
}
if let Some(idx) = idx_merged {
// Merge with spans trailing the insertion/merging position
while (idx + 1) < self.spans.len() {
if let Some(union) = self.spans[idx + 1].merge(sp) {
self.spans[idx] = union;
self.spans.remove(idx + 1);
} else {
break;
}
}
} else {
self.spans.push(sp);
MultiSpan {
primary_spans: vec![],
span_labels: vec![]
}
}
/// Inserts the given span into itself, for use with `end_highlight_lines`.
pub fn push_trim(&mut self, mut sp: Span) {
let mut prev = mk_sp(BytePos(0), BytePos(0));
pub fn from_span(primary_span: Span) -> MultiSpan {
MultiSpan {
primary_spans: vec![primary_span],
span_labels: vec![]
}
}
if let Some(first) = self.spans.get_mut(0) {
if first.lo > sp.lo {
// Prevent us here from spanning fewer lines
// because of trimming the start of the span
// (this should not be visible, because this method ought
// to not be used in conjunction with `highlight_lines`)
first.lo = sp.lo;
pub fn from_spans(vec: Vec<Span>) -> MultiSpan {
MultiSpan {
primary_spans: vec,
span_labels: vec![]
}
}
pub fn push_primary_span(&mut self, span: Span) {
self.primary_spans.push(span);
}
pub fn push_span_label(&mut self, span: Span, label: String) {
self.span_labels.push((span, label));
}
/// Selects the first primary span (if any)
pub fn primary_span(&self) -> Option<Span> {
self.primary_spans.first().cloned()
}
/// Returns all primary spans.
pub fn primary_spans(&self) -> &[Span] {
&self.primary_spans
}
/// Returns the strings to highlight. If we have an explicit set,
/// return those, otherwise just give back an (unlabeled) version
/// of the primary span.
pub fn span_labels(&self) -> Vec<SpanLabel> {
let is_primary = |span| self.primary_spans.contains(&span);
let mut span_labels = vec![];
for &(span, ref label) in &self.span_labels {
span_labels.push(SpanLabel {
span: span,
is_primary: is_primary(span),
label: Some(label.clone())
});
}
for &span in &self.primary_spans {
if !span_labels.iter().any(|sl| sl.span == span) {
span_labels.push(SpanLabel {
span: span,
is_primary: true,
label: None
});
}
}
for idx in 0.. {
if let Some(sp_trim) = sp.trim_start(prev) {
// Implies `sp.hi > prev.hi`
let cur = match self.spans.get(idx) {
Some(s) => *s,
None => {
sp = sp_trim;
break;
}
};
// `cur` may overlap with `sp_trim`
if let Some(cur_trim) = cur.trim_start(sp_trim) {
// Implies `sp.hi < cur.hi`
self.spans.insert(idx, sp_trim);
self.spans[idx + 1] = cur_trim;
return;
} else if sp.hi == cur.hi {
return;
}
prev = cur;
}
}
self.spans.push(sp);
span_labels
}
}
impl From<Span> for MultiSpan {
fn from(span: Span) -> MultiSpan {
MultiSpan { spans: vec![span] }
MultiSpan::from_span(span)
}
}
@ -929,6 +922,10 @@ impl CodeMap {
}
pub fn span_to_string(&self, sp: Span) -> String {
if sp == COMMAND_LINE_SP {
return "<command line option>".to_string();
}
if self.files.borrow().is_empty() && sp.source_equal(&DUMMY_SP) {
return "no-location".to_string();
}
@ -1099,12 +1096,16 @@ impl CodeMap {
}
pub fn span_to_lines(&self, sp: Span) -> FileLinesResult {
debug!("span_to_lines(sp={:?})", sp);
if sp.lo > sp.hi {
return Err(SpanLinesError::IllFormedSpan(sp));
}
let lo = self.lookup_char_pos(sp.lo);
debug!("span_to_lines: lo={:?}", lo);
let hi = self.lookup_char_pos(sp.hi);
debug!("span_to_lines: hi={:?}", hi);
if lo.file.start_pos != hi.file.start_pos {
return Err(SpanLinesError::DistinctSources(DistinctSources {
@ -1184,59 +1185,6 @@ impl CodeMap {
}
}
/// Groups and sorts spans by lines into `MultiSpan`s, where `push` adds them to their group,
/// specifying the unification behaviour for overlapping spans.
/// Spans overflowing a line are put into their own one-element-group.
pub fn custom_group_spans<F>(&self, mut spans: Vec<Span>, push: F) -> Vec<MultiSpan>
where F: Fn(&mut MultiSpan, Span)
{
spans.sort_by(|a, b| a.lo.cmp(&b.lo));
let mut groups = Vec::<MultiSpan>::new();
let mut overflowing = vec![];
let mut prev_expn = ExpnId(!2u32);
let mut prev_file = !0usize;
let mut prev_line = !0usize;
let mut err_size = 0;
for sp in spans {
let line = self.lookup_char_pos(sp.lo).line;
let line_hi = self.lookup_char_pos(sp.hi).line;
if line != line_hi {
overflowing.push(sp.into());
continue
}
let file = self.lookup_filemap_idx(sp.lo);
if err_size < MAX_HIGHLIGHT_LINES && sp.expn_id == prev_expn && file == prev_file {
// `push` takes care of sorting, trimming, and merging
push(&mut groups.last_mut().unwrap(), sp);
if line != prev_line {
err_size += 1;
}
} else {
groups.push(sp.into());
err_size = 1;
}
prev_expn = sp.expn_id;
prev_file = file;
prev_line = line;
}
groups.extend(overflowing);
groups
}
/// Groups and sorts spans by lines into `MultiSpan`s, merging overlapping spans.
/// Spans overflowing a line are put into their own one-element-group.
pub fn group_spans(&self, spans: Vec<Span>) -> Vec<MultiSpan> {
self.custom_group_spans(spans, |msp, sp| msp.push_merge(sp))
}
/// Like `group_spans`, but trims overlapping spans instead of
/// merging them (for use with `end_highlight_lines`)
pub fn end_group_spans(&self, spans: Vec<Span>) -> Vec<MultiSpan> {
self.custom_group_spans(spans, |msp, sp| msp.push_trim(sp))
}
pub fn get_filemap(&self, filename: &str) -> Rc<FileMap> {
for fm in self.files.borrow().iter() {
if filename == fm.name {

View File

@ -0,0 +1,821 @@
// Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// Code for annotating snippets.
use codemap::{CharPos, CodeMap, FileMap, LineInfo, Span};
use std::cmp;
use std::rc::Rc;
use std::mem;
use std::ops::Range;
#[cfg(test)]
mod test;
pub struct SnippetData {
codemap: Rc<CodeMap>,
files: Vec<FileInfo>,
}
pub struct FileInfo {
file: Rc<FileMap>,
/// The "primary file", if any, gets a `-->` marker instead of
/// `>>>`, and has a line-number/column printed and not just a
/// filename. It appears first in the listing. It is known to
/// contain at least one primary span, though primary spans (which
/// are designated with `^^^`) may also occur in other files.
primary_span: Option<Span>,
lines: Vec<Line>,
}
struct Line {
line_index: usize,
annotations: Vec<Annotation>,
}
#[derive(Clone, PartialOrd, Ord, PartialEq, Eq)]
struct Annotation {
/// Start column, 0-based indexing -- counting *characters*, not
/// utf-8 bytes. Note that it is important that this field goes
/// first, so that when we sort, we sort orderings by start
/// column.
start_col: usize,
/// End column within the line.
end_col: usize,
/// Is this annotation derived from primary span
is_primary: bool,
/// Optional label to display adjacent to the annotation.
label: Option<String>,
}
#[derive(Debug)]
pub struct RenderedLine {
pub text: Vec<StyledString>,
pub kind: RenderedLineKind,
}
#[derive(Debug)]
pub struct StyledString {
pub text: String,
pub style: Style,
}
#[derive(Debug)]
pub struct StyledBuffer {
text: Vec<Vec<char>>,
styles: Vec<Vec<Style>>
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Style {
FileNameStyle,
LineAndColumn,
LineNumber,
Quotation,
UnderlinePrimary,
UnderlineSecondary,
LabelPrimary,
LabelSecondary,
NoStyle,
}
use self::Style::*;
#[derive(Debug, Clone)]
pub enum RenderedLineKind {
PrimaryFileName,
OtherFileName,
SourceText {
file: Rc<FileMap>,
line_index: usize,
},
Annotations,
Elision,
}
use self::RenderedLineKind::*;
impl SnippetData {
pub fn new(codemap: Rc<CodeMap>,
primary_span: Option<Span>) // (*)
-> Self {
// (*) The primary span indicates the file that must appear
// first, and which will have a line number etc in its
// name. Outside of tests, this is always `Some`, but for many
// tests it's not relevant to test this portion of the logic,
// and it's tedious to pick a primary span (read: tedious to
// port older tests that predate the existence of a primary
// span).
debug!("SnippetData::new(primary_span={:?})", primary_span);
let mut data = SnippetData {
codemap: codemap.clone(),
files: vec![]
};
if let Some(primary_span) = primary_span {
let lo = codemap.lookup_char_pos(primary_span.lo);
data.files.push(
FileInfo {
file: lo.file,
primary_span: Some(primary_span),
lines: vec![],
});
}
data
}
pub fn push(&mut self, span: Span, is_primary: bool, label: Option<String>) {
debug!("SnippetData::push(span={:?}, is_primary={}, label={:?})",
span, is_primary, label);
let file_lines = match self.codemap.span_to_lines(span) {
Ok(file_lines) => file_lines,
Err(_) => {
// ignore unprintable spans completely.
return;
}
};
self.file(&file_lines.file)
.push_lines(&file_lines.lines, is_primary, label);
}
fn file(&mut self, file_map: &Rc<FileMap>) -> &mut FileInfo {
let index = self.files.iter().position(|f| f.file.name == file_map.name);
if let Some(index) = index {
return &mut self.files[index];
}
self.files.push(
FileInfo {
file: file_map.clone(),
lines: vec![],
primary_span: None,
});
self.files.last_mut().unwrap()
}
pub fn render_lines(&self) -> Vec<RenderedLine> {
debug!("SnippetData::render_lines()");
let mut rendered_lines: Vec<_> =
self.files.iter()
.flat_map(|f| f.render_file_lines(&self.codemap))
.collect();
prepend_prefixes(&mut rendered_lines);
trim_lines(&mut rendered_lines);
rendered_lines
}
}
pub trait StringSource {
fn make_string(self) -> String;
}
impl StringSource for String {
fn make_string(self) -> String {
self
}
}
impl StringSource for Vec<char> {
fn make_string(self) -> String {
self.into_iter().collect()
}
}
impl<S> From<(S, Style, RenderedLineKind)> for RenderedLine
where S: StringSource
{
fn from((text, style, kind): (S, Style, RenderedLineKind)) -> Self {
RenderedLine {
text: vec![StyledString {
text: text.make_string(),
style: style,
}],
kind: kind,
}
}
}
impl<S1,S2> From<(S1, Style, S2, Style, RenderedLineKind)> for RenderedLine
where S1: StringSource, S2: StringSource
{
fn from(tuple: (S1, Style, S2, Style, RenderedLineKind))
-> Self {
let (text1, style1, text2, style2, kind) = tuple;
RenderedLine {
text: vec![
StyledString {
text: text1.make_string(),
style: style1,
},
StyledString {
text: text2.make_string(),
style: style2,
}
],
kind: kind,
}
}
}
impl RenderedLine {
fn trim_last(&mut self) {
if !self.text.is_empty() {
let last_text = &mut self.text.last_mut().unwrap().text;
let len = last_text.trim_right().len();
last_text.truncate(len);
}
}
}
impl RenderedLineKind {
fn prefix(&self) -> StyledString {
match *self {
SourceText { file: _, line_index } =>
StyledString {
text: format!("{}", line_index + 1),
style: LineNumber,
},
Elision =>
StyledString {
text: String::from("..."),
style: LineNumber,
},
PrimaryFileName |
OtherFileName |
Annotations =>
StyledString {
text: String::from(""),
style: LineNumber,
},
}
}
}
impl StyledBuffer {
fn new() -> StyledBuffer {
StyledBuffer { text: vec![], styles: vec![] }
}
fn render(&self, source_kind: RenderedLineKind) -> Vec<RenderedLine> {
let mut output: Vec<RenderedLine> = vec![];
let mut styled_vec: Vec<StyledString> = vec![];
for (row, row_style) in self.text.iter().zip(&self.styles) {
let mut current_style = NoStyle;
let mut current_text = String::new();
for (&c, &s) in row.iter().zip(row_style) {
if s != current_style {
if !current_text.is_empty() {
styled_vec.push(StyledString { text: current_text, style: current_style });
}
current_style = s;
current_text = String::new();
}
current_text.push(c);
}
if !current_text.is_empty() {
styled_vec.push(StyledString { text: current_text, style: current_style });
}
if output.is_empty() {
//We know our first output line is source and the rest are highlights and labels
output.push(RenderedLine { text: styled_vec, kind: source_kind.clone() });
} else {
output.push(RenderedLine { text: styled_vec, kind: Annotations });
}
styled_vec = vec![];
}
output
}
fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) {
while line >= self.text.len() {
self.text.push(vec![]);
self.styles.push(vec![]);
}
if col < self.text[line].len() {
self.text[line][col] = chr;
self.styles[line][col] = style;
} else {
while self.text[line].len() < col {
self.text[line].push(' ');
self.styles[line].push(NoStyle);
}
self.text[line].push(chr);
self.styles[line].push(style);
}
}
fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) {
let mut n = col;
for c in string.chars() {
self.putc(line, n, c, style);
n += 1;
}
}
fn set_style(&mut self, line: usize, col: usize, style: Style) {
if self.styles.len() > line && self.styles[line].len() > col {
self.styles[line][col] = style;
}
}
fn append(&mut self, line: usize, string: &str, style: Style) {
if line >= self.text.len() {
self.puts(line, 0, string, style);
} else {
let col = self.text[line].len();
self.puts(line, col, string, style);
}
}
}
impl FileInfo {
fn push_lines(&mut self,
lines: &[LineInfo],
is_primary: bool,
label: Option<String>) {
assert!(lines.len() > 0);
// If a span covers multiple lines, just put the label on the
// first one. This is a sort of arbitrary choice and not
// obviously correct.
let (line0, remaining_lines) = lines.split_first().unwrap();
let index = self.ensure_source_line(line0.line_index);
self.lines[index].push_annotation(line0.start_col,
line0.end_col,
is_primary,
label);
for line in remaining_lines {
if line.end_col > line.start_col {
let index = self.ensure_source_line(line.line_index);
self.lines[index].push_annotation(line.start_col,
line.end_col,
is_primary,
None);
}
}
}
/// Ensure that we have a `Line` struct corresponding to
/// `line_index` in the file. If we already have some other lines,
/// then this will add the intervening lines to ensure that we
/// have a complete snippet. (Note that when we finally display,
/// some of those lines may be elided.)
fn ensure_source_line(&mut self, line_index: usize) -> usize {
if self.lines.is_empty() {
self.lines.push(Line::new(line_index));
return 0;
}
// Find the range of lines we have thus far.
let first_line_index = self.lines.first().unwrap().line_index;
let last_line_index = self.lines.last().unwrap().line_index;
assert!(first_line_index <= last_line_index);
// If the new line is lower than all the lines we have thus
// far, then insert the new line and any intervening lines at
// the front. In a silly attempt at micro-optimization, we
// don't just call `insert` repeatedly, but instead make a new
// (empty) vector, pushing the new lines onto it, and then
// appending the old vector.
if line_index < first_line_index {
let lines = mem::replace(&mut self.lines, vec![]);
self.lines.extend(
(line_index .. first_line_index)
.map(|line| Line::new(line))
.chain(lines));
return 0;
}
// If the new line comes after the ones we have so far, insert
// lines for it.
if line_index > last_line_index {
self.lines.extend(
(last_line_index+1 .. line_index+1)
.map(|line| Line::new(line)));
return self.lines.len() - 1;
}
// Otherwise it should already exist.
return line_index - first_line_index;
}
fn render_file_lines(&self, codemap: &Rc<CodeMap>) -> Vec<RenderedLine> {
// Group our lines by those with annotations and those without
let mut lines_iter = self.lines.iter().peekable();
let mut line_groups = vec![];
loop {
match lines_iter.next() {
None => break,
Some(line) if line.annotations.is_empty() => {
// Collect unannotated group
let mut unannotated_group : Vec<&Line> = vec![];
unannotated_group.push(line);
loop {
let next_line =
match lines_iter.peek() {
None => break,
Some(x) if !x.annotations.is_empty() => break,
Some(x) => x.clone()
};
unannotated_group.push(next_line);
lines_iter.next();
}
line_groups.push((false, unannotated_group));
}
Some(line) => {
// Collect annotated group
let mut annotated_group : Vec<&Line> = vec![];
annotated_group.push(line);
loop {
let next_line =
match lines_iter.peek() {
None => break,
Some(x) if x.annotations.is_empty() => break,
Some(x) => x.clone()
};
annotated_group.push(next_line);
lines_iter.next();
}
line_groups.push((true, annotated_group));
}
}
}
let mut output = vec![];
// First insert the name of the file.
match self.primary_span {
Some(span) => {
let lo = codemap.lookup_char_pos(span.lo);
output.push(RenderedLine {
text: vec![StyledString {
text: lo.file.name.clone(),
style: FileNameStyle,
}, StyledString {
text: format!(":{}:{}", lo.line, lo.col.0 + 1),
style: LineAndColumn,
}],
kind: PrimaryFileName,
});
}
None => {
output.push(RenderedLine {
text: vec![StyledString {
text: self.file.name.clone(),
style: FileNameStyle,
}],
kind: OtherFileName,
});
}
}
for &(is_annotated, ref group) in line_groups.iter() {
if is_annotated {
let mut annotation_ends_at_eol = false;
let mut prev_ends_at_eol = false;
let mut elide_unlabeled_region = false;
for group_line in group.iter() {
let source_string_len =
self.file.get_line(group_line.line_index)
.map(|s| s.len())
.unwrap_or(0);
for annotation in &group_line.annotations {
if annotation.end_col == source_string_len {
annotation_ends_at_eol = true;
}
}
let is_single_unlabeled_annotated_line =
if group_line.annotations.len() == 1 {
if let Some(annotation) = group_line.annotations.first() {
match annotation.label {
Some(_) => false,
None => annotation.start_col == 0 &&
annotation.end_col == source_string_len
}
} else {
false
}
} else {
false
};
if prev_ends_at_eol && is_single_unlabeled_annotated_line {
if !elide_unlabeled_region {
output.push(RenderedLine::from((String::new(),
NoStyle, Elision)));
elide_unlabeled_region = true;
prev_ends_at_eol = true;
}
continue;
}
let mut v = self.render_line(group_line);
output.append(&mut v);
prev_ends_at_eol = annotation_ends_at_eol;
}
} else {
if group.len() > 1 {
output.push(RenderedLine::from((String::new(), NoStyle, Elision)));
} else {
let mut v: Vec<RenderedLine> =
group.iter().flat_map(|line| self.render_line(line)).collect();
output.append(&mut v);
}
}
}
output
}
fn render_line(&self, line: &Line) -> Vec<RenderedLine> {
let source_string = self.file.get_line(line.line_index)
.unwrap_or("");
let source_kind = SourceText {
file: self.file.clone(),
line_index: line.line_index,
};
let mut styled_buffer = StyledBuffer::new();
// First create the source line we will highlight.
styled_buffer.append(0, &source_string, Quotation);
if line.annotations.is_empty() {
return styled_buffer.render(source_kind);
}
// We want to display like this:
//
// vec.push(vec.pop().unwrap());
// --- ^^^ _ previous borrow ends here
// | |
// | error occurs here
// previous borrow of `vec` occurs here
//
// But there are some weird edge cases to be aware of:
//
// vec.push(vec.pop().unwrap());
// -------- - previous borrow ends here
// ||
// |this makes no sense
// previous borrow of `vec` occurs here
//
// For this reason, we group the lines into "highlight lines"
// and "annotations lines", where the highlight lines have the `~`.
//let mut highlight_line = Self::whitespace(&source_string);
// Sort the annotations by (start, end col)
let mut annotations = line.annotations.clone();
annotations.sort();
// Next, create the highlight line.
for annotation in &annotations {
for p in annotation.start_col .. annotation.end_col {
if annotation.is_primary {
styled_buffer.putc(1, p, '^', UnderlinePrimary);
styled_buffer.set_style(0, p, UnderlinePrimary);
} else {
styled_buffer.putc(1, p, '-', UnderlineSecondary);
}
}
}
// Now we are going to write labels in. To start, we'll exclude
// the annotations with no labels.
let (labeled_annotations, unlabeled_annotations): (Vec<_>, _) =
annotations.into_iter()
.partition(|a| a.label.is_some());
// If there are no annotations that need text, we're done.
if labeled_annotations.is_empty() {
return styled_buffer.render(source_kind);
}
// Now add the text labels. We try, when possible, to stick the rightmost
// annotation at the end of the highlight line:
//
// vec.push(vec.pop().unwrap());
// --- --- - previous borrow ends here
//
// But sometimes that's not possible because one of the other
// annotations overlaps it. For example, from the test
// `span_overlap_label`, we have the following annotations
// (written on distinct lines for clarity):
//
// fn foo(x: u32) {
// --------------
// -
//
// In this case, we can't stick the rightmost-most label on
// the highlight line, or we would get:
//
// fn foo(x: u32) {
// -------- x_span
// |
// fn_span
//
// which is totally weird. Instead we want:
//
// fn foo(x: u32) {
// --------------
// | |
// | x_span
// fn_span
//
// which is...less weird, at least. In fact, in general, if
// the rightmost span overlaps with any other span, we should
// use the "hang below" version, so we can at least make it
// clear where the span *starts*.
let mut labeled_annotations = &labeled_annotations[..];
match labeled_annotations.split_last().unwrap() {
(last, previous) => {
if previous.iter()
.chain(&unlabeled_annotations)
.all(|a| !overlaps(a, last))
{
// append the label afterwards; we keep it in a separate
// string
let highlight_label: String = format!(" {}", last.label.as_ref().unwrap());
if last.is_primary {
styled_buffer.append(1, &highlight_label, LabelPrimary);
} else {
styled_buffer.append(1, &highlight_label, LabelSecondary);
}
labeled_annotations = previous;
}
}
}
// If that's the last annotation, we're done
if labeled_annotations.is_empty() {
return styled_buffer.render(source_kind);
}
for (index, annotation) in labeled_annotations.iter().enumerate() {
// Leave:
// - 1 extra line
// - One line for each thing that comes after
let comes_after = labeled_annotations.len() - index - 1;
let blank_lines = 3 + comes_after;
// For each blank line, draw a `|` at our column. The
// text ought to be long enough for this.
for index in 2..blank_lines {
if annotation.is_primary {
styled_buffer.putc(index, annotation.start_col, '|', UnderlinePrimary);
} else {
styled_buffer.putc(index, annotation.start_col, '|', UnderlineSecondary);
}
}
if annotation.is_primary {
styled_buffer.puts(blank_lines, annotation.start_col,
annotation.label.as_ref().unwrap(), LabelPrimary);
} else {
styled_buffer.puts(blank_lines, annotation.start_col,
annotation.label.as_ref().unwrap(), LabelSecondary);
}
}
styled_buffer.render(source_kind)
}
}
fn prepend_prefixes(rendered_lines: &mut [RenderedLine]) {
let prefixes: Vec<_> =
rendered_lines.iter()
.map(|rl| rl.kind.prefix())
.collect();
// find the max amount of spacing we need; add 1 to
// p.text.len() to leave space between the prefix and the
// source text
let padding_len =
prefixes.iter()
.map(|p| if p.text.len() == 0 { 0 } else { p.text.len() + 1 })
.max()
.unwrap_or(0);
// Ensure we insert at least one character of padding, so that the
// `-->` arrows can fit etc.
let padding_len = cmp::max(padding_len, 1);
for (mut prefix, line) in prefixes.into_iter().zip(rendered_lines) {
let extra_spaces = (prefix.text.len() .. padding_len).map(|_| ' ');
prefix.text.extend(extra_spaces);
match line.kind {
RenderedLineKind::Elision => {
line.text.insert(0, prefix);
}
RenderedLineKind::PrimaryFileName => {
// --> filename
// 22 |>
// ^
// padding_len
let dashes = (0..padding_len - 1).map(|_| ' ')
.chain(Some('-'))
.chain(Some('-'))
.chain(Some('>'))
.chain(Some(' '));
line.text.insert(0, StyledString {text: dashes.collect(),
style: LineNumber})
}
RenderedLineKind::OtherFileName => {
// >>>>> filename
// 22 |>
// ^
// padding_len
let dashes = (0..padding_len + 2).map(|_| '>')
.chain(Some(' '));
line.text.insert(0, StyledString {text: dashes.collect(),
style: LineNumber})
}
_ => {
line.text.insert(0, prefix);
line.text.insert(1, StyledString {text: String::from("|> "),
style: LineNumber})
}
}
}
}
fn trim_lines(rendered_lines: &mut [RenderedLine]) {
for line in rendered_lines {
while !line.text.is_empty() {
line.trim_last();
if line.text.last().unwrap().text.is_empty() {
line.text.pop();
} else {
break;
}
}
}
}
impl Line {
fn new(line_index: usize) -> Line {
Line {
line_index: line_index,
annotations: vec![]
}
}
fn push_annotation(&mut self,
start: CharPos,
end: CharPos,
is_primary: bool,
label: Option<String>) {
self.annotations.push(Annotation {
start_col: start.0,
end_col: end.0,
is_primary: is_primary,
label: label,
});
}
}
fn overlaps(a1: &Annotation,
a2: &Annotation)
-> bool
{
between(a1.start_col, a2.start_col .. a2.end_col) ||
between(a2.start_col, a1.start_col .. a1.end_col)
}
fn between(v: usize, range: Range<usize>) -> bool {
v >= range.start && v < range.end
}

View File

@ -0,0 +1,524 @@
// Copyright 2012-2015 The Rust Project Developers. See the COPYRIGHT
// file at the top-level directory of this distribution and at
// http://rust-lang.org/COPYRIGHT.
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
// Code for testing annotated snippets.
use codemap::{BytePos, CodeMap, FileMap, NO_EXPANSION, Span};
use std::rc::Rc;
use super::{RenderedLine, SnippetData};
/// Returns the span corresponding to the `n`th occurrence of
/// `substring` in `source_text`.
trait CodeMapExtension {
fn span_substr(&self,
file: &Rc<FileMap>,
source_text: &str,
substring: &str,
n: usize)
-> Span;
}
impl CodeMapExtension for CodeMap {
fn span_substr(&self,
file: &Rc<FileMap>,
source_text: &str,
substring: &str,
n: usize)
-> Span
{
println!("span_substr(file={:?}/{:?}, substring={:?}, n={})",
file.name, file.start_pos, substring, n);
let mut i = 0;
let mut hi = 0;
loop {
let offset = source_text[hi..].find(substring).unwrap_or_else(|| {
panic!("source_text `{}` does not have {} occurrences of `{}`, only {}",
source_text, n, substring, i);
});
let lo = hi + offset;
hi = lo + substring.len();
if i == n {
let span = Span {
lo: BytePos(lo as u32 + file.start_pos.0),
hi: BytePos(hi as u32 + file.start_pos.0),
expn_id: NO_EXPANSION,
};
assert_eq!(&self.span_to_snippet(span).unwrap()[..],
substring);
return span;
}
i += 1;
}
}
}
fn splice(start: Span, end: Span) -> Span {
Span {
lo: start.lo,
hi: end.hi,
expn_id: NO_EXPANSION,
}
}
fn make_string(lines: &[RenderedLine]) -> String {
lines.iter()
.flat_map(|rl| {
rl.text.iter()
.map(|s| &s.text[..])
.chain(Some("\n"))
})
.collect()
}
#[test]
fn one_line() {
let file_text = r#"
fn foo() {
vec.push(vec.pop().unwrap());
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let span_vec0 = cm.span_substr(&foo, file_text, "vec", 0);
let span_vec1 = cm.span_substr(&foo, file_text, "vec", 1);
let span_semi = cm.span_substr(&foo, file_text, ";", 0);
let mut snippet = SnippetData::new(cm, None);
snippet.push(span_vec0, false, Some(format!("previous borrow of `vec` occurs here")));
snippet.push(span_vec1, false, Some(format!("error occurs here")));
snippet.push(span_semi, false, Some(format!("previous borrow ends here")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=\n{}", text);
assert_eq!(&text[..], &r#"
>>>> foo.rs
3 |> vec.push(vec.pop().unwrap());
|> --- --- - previous borrow ends here
|> | |
|> | error occurs here
|> previous borrow of `vec` occurs here
"#[1..]);
}
#[test]
fn two_files() {
let file_text_foo = r#"
fn foo() {
vec.push(vec.pop().unwrap());
}
"#;
let file_text_bar = r#"
fn bar() {
// these blank links here
// serve to ensure that the line numbers
// from bar.rs
// require more digits
vec.push();
// this line will get elided
vec.pop().unwrap());
}
"#;
let cm = Rc::new(CodeMap::new());
let foo_map = cm.new_filemap_and_lines("foo.rs", file_text_foo);
let span_foo_vec0 = cm.span_substr(&foo_map, file_text_foo, "vec", 0);
let span_foo_vec1 = cm.span_substr(&foo_map, file_text_foo, "vec", 1);
let span_foo_semi = cm.span_substr(&foo_map, file_text_foo, ";", 0);
let bar_map = cm.new_filemap_and_lines("bar.rs", file_text_bar);
let span_bar_vec0 = cm.span_substr(&bar_map, file_text_bar, "vec", 0);
let span_bar_vec1 = cm.span_substr(&bar_map, file_text_bar, "vec", 1);
let span_bar_semi = cm.span_substr(&bar_map, file_text_bar, ";", 0);
let mut snippet = SnippetData::new(cm, Some(span_foo_vec1));
snippet.push(span_foo_vec0, false, Some(format!("a")));
snippet.push(span_foo_vec1, true, Some(format!("b")));
snippet.push(span_foo_semi, false, Some(format!("c")));
snippet.push(span_bar_vec0, false, Some(format!("d")));
snippet.push(span_bar_vec1, false, Some(format!("e")));
snippet.push(span_bar_semi, false, Some(format!("f")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=\n{}", text);
// Note that the `|>` remain aligned across both files:
assert_eq!(&text[..], &r#"
--> foo.rs:3:14
3 |> vec.push(vec.pop().unwrap());
|> --- ^^^ - c
|> | |
|> | b
|> a
>>>>>> bar.rs
17 |> vec.push();
|> --- - f
|> |
|> d
...
21 |> vec.pop().unwrap());
|> --- e
"#[1..]);
}
#[test]
fn multi_line() {
let file_text = r#"
fn foo() {
let name = find_id(&data, 22).unwrap();
// Add one more item we forgot to the vector. Silly us.
data.push(Data { name: format!("Hera"), id: 66 });
// Print everything out.
println!("Name: {:?}", name);
println!("Data: {:?}", data);
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let span_data0 = cm.span_substr(&foo, file_text, "data", 0);
let span_data1 = cm.span_substr(&foo, file_text, "data", 1);
let span_rbrace = cm.span_substr(&foo, file_text, "}", 3);
let mut snippet = SnippetData::new(cm, None);
snippet.push(span_data0, false, Some(format!("immutable borrow begins here")));
snippet.push(span_data1, false, Some(format!("mutable borrow occurs here")));
snippet.push(span_rbrace, false, Some(format!("immutable borrow ends here")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=\n{}", text);
assert_eq!(&text[..], &r#"
>>>>>> foo.rs
3 |> let name = find_id(&data, 22).unwrap();
|> ---- immutable borrow begins here
...
6 |> data.push(Data { name: format!("Hera"), id: 66 });
|> ---- mutable borrow occurs here
...
11 |> }
|> - immutable borrow ends here
"#[1..]);
}
#[test]
fn overlapping() {
let file_text = r#"
fn foo() {
vec.push(vec.pop().unwrap());
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let span0 = cm.span_substr(&foo, file_text, "vec.push", 0);
let span1 = cm.span_substr(&foo, file_text, "vec", 0);
let span2 = cm.span_substr(&foo, file_text, "ec.push", 0);
let span3 = cm.span_substr(&foo, file_text, "unwrap", 0);
let mut snippet = SnippetData::new(cm, None);
snippet.push(span0, false, Some(format!("A")));
snippet.push(span1, false, Some(format!("B")));
snippet.push(span2, false, Some(format!("C")));
snippet.push(span3, false, Some(format!("D")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=r#\"\n{}\".trim_left()", text);
assert_eq!(&text[..], &r#"
>>>> foo.rs
3 |> vec.push(vec.pop().unwrap());
|> -------- ------ D
|> ||
|> |C
|> A
|> B
"#[1..]);
}
#[test]
fn one_line_out_of_order() {
let file_text = r#"
fn foo() {
vec.push(vec.pop().unwrap());
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let span_vec0 = cm.span_substr(&foo, file_text, "vec", 0);
let span_vec1 = cm.span_substr(&foo, file_text, "vec", 1);
let span_semi = cm.span_substr(&foo, file_text, ";", 0);
// intentionally don't push the snippets left to right
let mut snippet = SnippetData::new(cm, None);
snippet.push(span_vec1, false, Some(format!("error occurs here")));
snippet.push(span_vec0, false, Some(format!("previous borrow of `vec` occurs here")));
snippet.push(span_semi, false, Some(format!("previous borrow ends here")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=r#\"\n{}\".trim_left()", text);
assert_eq!(&text[..], &r#"
>>>> foo.rs
3 |> vec.push(vec.pop().unwrap());
|> --- --- - previous borrow ends here
|> | |
|> | error occurs here
|> previous borrow of `vec` occurs here
"#[1..]);
}
#[test]
fn elide_unnecessary_lines() {
let file_text = r#"
fn foo() {
let mut vec = vec![0, 1, 2];
let mut vec2 = vec;
vec2.push(3);
vec2.push(4);
vec2.push(5);
vec2.push(6);
vec.push(7);
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let span_vec0 = cm.span_substr(&foo, file_text, "vec", 3);
let span_vec1 = cm.span_substr(&foo, file_text, "vec", 8);
let mut snippet = SnippetData::new(cm, None);
snippet.push(span_vec0, false, Some(format!("`vec` moved here because it \
has type `collections::vec::Vec<i32>`")));
snippet.push(span_vec1, false, Some(format!("use of moved value: `vec`")));
let lines = snippet.render_lines();
println!("{:#?}", lines);
let text: String = make_string(&lines);
println!("text=r#\"\n{}\".trim_left()", text);
assert_eq!(&text[..], &r#"
>>>>>> foo.rs
4 |> let mut vec2 = vec;
|> --- `vec` moved here because it has type `collections::vec::Vec<i32>`
...
9 |> vec.push(7);
|> --- use of moved value: `vec`
"#[1..]);
}
#[test]
fn spans_without_labels() {
let file_text = r#"
fn foo() {
let mut vec = vec![0, 1, 2];
let mut vec2 = vec;
vec2.push(3);
vec2.push(4);
vec2.push(5);
vec2.push(6);
vec.push(7);
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let mut snippet = SnippetData::new(cm.clone(), None);
for i in 0..4 {
let span_veci = cm.span_substr(&foo, file_text, "vec", i);
snippet.push(span_veci, false, None);
}
let lines = snippet.render_lines();
let text: String = make_string(&lines);
println!("text=&r#\"\n{}\n\"#[1..]", text);
assert_eq!(text, &r#"
>>>> foo.rs
3 |> let mut vec = vec![0, 1, 2];
|> --- ---
4 |> let mut vec2 = vec;
|> --- ---
"#[1..]);
}
#[test]
fn span_long_selection() {
let file_text = r#"
impl SomeTrait for () {
fn foo(x: u32) {
// impl 1
// impl 2
// impl 3
}
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let mut snippet = SnippetData::new(cm.clone(), None);
let fn_span = cm.span_substr(&foo, file_text, "fn", 0);
let rbrace_span = cm.span_substr(&foo, file_text, "}", 0);
snippet.push(splice(fn_span, rbrace_span), false, None);
let lines = snippet.render_lines();
let text: String = make_string(&lines);
println!("r#\"\n{}\"", text);
assert_eq!(text, &r#"
>>>>>> foo.rs
3 |> fn foo(x: u32) {
|> ----------------
...
"#[1..]);
}
#[test]
fn span_overlap_label() {
// Test that we don't put `x_span` to the right of its highlight,
// since there is another highlight that overlaps it.
let file_text = r#"
fn foo(x: u32) {
}
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let mut snippet = SnippetData::new(cm.clone(), None);
let fn_span = cm.span_substr(&foo, file_text, "fn foo(x: u32)", 0);
let x_span = cm.span_substr(&foo, file_text, "x", 0);
snippet.push(fn_span, false, Some(format!("fn_span")));
snippet.push(x_span, false, Some(format!("x_span")));
let lines = snippet.render_lines();
let text: String = make_string(&lines);
println!("r#\"\n{}\"", text);
assert_eq!(text, &r#"
>>>> foo.rs
2 |> fn foo(x: u32) {
|> --------------
|> | |
|> | x_span
|> fn_span
"#[1..]);
}
#[test]
fn span_overlap_label2() {
// Test that we don't put `x_span` to the right of its highlight,
// since there is another highlight that overlaps it. In this
// case, the overlap is only at the beginning, but it's still
// better to show the beginning more clearly.
let file_text = r#"
fn foo(x: u32) {
}
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let mut snippet = SnippetData::new(cm.clone(), None);
let fn_span = cm.span_substr(&foo, file_text, "fn foo(x", 0);
let x_span = cm.span_substr(&foo, file_text, "x: u32)", 0);
snippet.push(fn_span, false, Some(format!("fn_span")));
snippet.push(x_span, false, Some(format!("x_span")));
let lines = snippet.render_lines();
let text: String = make_string(&lines);
println!("r#\"\n{}\"", text);
assert_eq!(text, &r#"
>>>> foo.rs
2 |> fn foo(x: u32) {
|> --------------
|> | |
|> | x_span
|> fn_span
"#[1..]);
}
#[test]
fn span_overlap_label3() {
// Test that we don't put `x_span` to the right of its highlight,
// since there is another highlight that overlaps it. In this
// case, the overlap is only at the beginning, but it's still
// better to show the beginning more clearly.
let file_text = r#"
fn foo() {
let closure = || {
inner
};
}
}
"#;
let cm = Rc::new(CodeMap::new());
let foo = cm.new_filemap_and_lines("foo.rs", file_text);
let mut snippet = SnippetData::new(cm.clone(), None);
let closure_span = {
let closure_start_span = cm.span_substr(&foo, file_text, "||", 0);
let closure_end_span = cm.span_substr(&foo, file_text, "}", 0);
splice(closure_start_span, closure_end_span)
};
let inner_span = cm.span_substr(&foo, file_text, "inner", 0);
snippet.push(closure_span, false, Some(format!("foo")));
snippet.push(inner_span, false, Some(format!("bar")));
let lines = snippet.render_lines();
let text: String = make_string(&lines);
println!("r#\"\n{}\"", text);
assert_eq!(text, &r#"
>>>> foo.rs
3 |> let closure = || {
|> ---- foo
4 |> inner
|> ----------------
|> |
|> bar
5 |> };
|> --------
"#[1..]);
}