use std::collections::HashMap;
use std::ops::RangeBounds;
use std::ops::Bound;
use std::ops::Range;
use std::rc::Rc;
use std::fmt::Display;
use std::path::{Path, PathBuf};
pub trait SourceErrorBounds {
fn bounds(&self) -> Range<usize>;
}
#[derive(Clone, Debug, PartialEq)]
pub enum SourceError {
Range(Range<usize>),
TaggedRange(String, Range<usize>),
Ranges(Vec<SourceError>),
TaggedRanges(String, Vec<SourceError>),
}
impl SourceError {
#[inline]
pub fn tag<S>(self, tag: S) -> Self
where
S: AsRef<str>,
{
match self {
SourceError::Range(range) => SourceError::TaggedRange(tag.as_ref().into(), range),
_ => SourceError::TaggedRanges(tag.as_ref().into(), vec![self]),
}
}
}
impl SourceErrorBounds for SourceError {
fn bounds(&self) -> Range<usize> {
match self {
SourceError::Range(bounds) | SourceError::TaggedRange(_, bounds) => bounds.clone(),
SourceError::Ranges(errors) | SourceError::TaggedRanges(_, errors) => errors.bounds(),
}
}
}
impl SourceErrorBounds for Vec<SourceError> {
fn bounds(&self) -> Range<usize> {
let mut start = std::usize::MAX;
let mut end = 0;
for error in self {
let bounds = error.bounds();
if start > bounds.start {
start = bounds.start;
}
if end < bounds.end {
end = bounds.end;
}
}
start..end
}
}
pub type RcMessage = Rc<Message>;
#[derive(Clone, Debug, PartialEq)]
pub struct Message {
bounds: Range<usize>,
message: String,
}
#[derive(Clone, Debug, PartialEq)]
pub enum SourcePosition {
LineCol(usize, usize),
Unknown,
}
impl Display for SourcePosition {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
SourcePosition::LineCol(line, col) => write!(f, "{}:{}", line, col),
SourcePosition::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct LineMessage {
message: RcMessage,
bounds: Range<usize>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Line<'a> {
line_no: usize,
bounds: Range<usize>,
messages: Vec<LineMessage>,
source: &'a str,
}
impl<'a> Line<'a> {
pub fn add_message(&mut self, message: RcMessage) {
if self.bounds.contains(&message.bounds.start) {
if self.bounds.contains(&message.bounds.end) {
self.messages.push(LineMessage {
bounds: message.bounds.start - self.bounds.start
..message.bounds.end - self.bounds.start,
message,
});
} else {
self.messages.push(LineMessage {
bounds: message.bounds.start - self.bounds.start..self.source.len(),
message,
});
}
} else if self.bounds.contains(&message.bounds.end) {
self.messages.push(LineMessage {
bounds: 0..message.bounds.end - self.bounds.start,
message,
});
} else if message.bounds.start < self.bounds.start && message.bounds.end > self.bounds.end {
self.messages.push(LineMessage {
bounds: 0..self.source.len(),
message,
});
}
}
pub fn has_messages(&self) -> bool {
!self.messages.is_empty()
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Source<'a> {
pub filename: PathBuf,
pub lines: Vec<Line<'a>>,
pub messages: Vec<RcMessage>,
}
impl<'a> Source<'a> {
pub fn new<P>(filename: P, source: &'a str) -> Self
where
P: AsRef<Path> + 'a,
{
let filename = filename.as_ref().to_path_buf();
let mut lines = Vec::new();
let messages = Vec::new();
for (mut cur_line_no, cur_line) in source.lines().enumerate() {
let cur_offset = (cur_line.as_ptr() as usize) - (source.as_ptr() as usize);
cur_line_no += 1;
lines.push(Line {
line_no: cur_line_no,
bounds: cur_offset..cur_offset + cur_line.len(),
messages: Vec::new(),
source: cur_line,
});
}
Source {
filename,
lines,
messages,
}
}
pub fn add_error(&mut self, error: SourceError) {
match error {
SourceError::Range(_) => {}
SourceError::TaggedRange(message, bounds) => {
self.add_message(bounds, message);
}
SourceError::Ranges(errors) => for error in errors {
self.add_error(error);
},
SourceError::TaggedRanges(message, errors) => {
self.add_message(errors.bounds(), message);
for error in errors {
self.add_error(error);
}
}
}
}
pub fn add_message<B, S>(&mut self, bounds: B, message: S)
where
B: RangeBounds<usize>,
S: AsRef<str>,
{
let start_bound: usize = match bounds.start_bound() {
Bound::Excluded(val) => *val,
Bound::Included(val) => *val,
Bound::Unbounded => 0,
};
let end_bound: usize = match bounds.end_bound() {
Bound::Excluded(val) => *val,
Bound::Included(val) => *val + 1,
Bound::Unbounded => 0,
};
let bounds = start_bound..end_bound;
let message = message.as_ref().to_string();
let message = Rc::new(Message { bounds, message });
self.messages.push(message.clone());
for line in &mut self.lines {
if line.bounds.contains(&message.bounds.start)
|| line.bounds.contains(&message.bounds.end)
|| (message.bounds.start < line.bounds.start
&& message.bounds.end > line.bounds.end)
{
line.add_message(message.clone());
}
}
}
pub fn position<B>(&self, bounds: B) -> SourcePosition
where
B: RangeBounds<usize>,
{
let start_bound: usize = match bounds.start_bound() {
Bound::Excluded(val) => *val,
Bound::Included(val) => *val,
Bound::Unbounded => 0,
};
for line in &self.lines {
if line.bounds.contains(&start_bound) {
return SourcePosition::LineCol(line.line_no, 1 + (start_bound - line.bounds.start));
}
}
SourcePosition::Unknown
}
}
impl<'a> Display for Source<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
let line_nos = self.lines.len();
let line_no_width = 2 + format!("{}", self.lines.len()).len();
let mut groups = Vec::new();
let mut group = Vec::new();
for line in &self.lines {
if line.has_messages() {
group.push(line);
} else if !group.is_empty() {
groups.push(group);
group = Vec::new();
}
}
if !group.is_empty() {
groups.push(group);
}
let mut write_line = |in_source: bool, line_no: usize, line: &str| {
if in_source {
writeln!(
f,
"{line_no:>line_no_width$} │ {line}",
line = line,
line_no = if line_no > 0 {
format!("{}", line_no)
} else {
"".into()
},
line_no_width = line_no_width,
)
} else {
writeln!(f, "{}", line)
}
};
for group in groups {
let mut message_id = 1;
let messages = group
.clone()
.into_iter()
.fold(Vec::new(), |mut messages, line| {
for message in &line.messages {
if !messages.contains(&message.message) {
messages.push(message.message.clone());
}
}
messages
});
write_line(false, 0, "")?;
for message in &messages {
let cur_id = format!("#{} - ", message_id);
write_line(false, 0, &format!(
"{}{}\n{}{}:{}\n",
cur_id,
message.message,
" ".repeat(cur_id.len()),
self.filename.display(),
self.position(message.bounds.clone())
))?;
message_id += 1;
}
if let Some(first) = group.first() {
let first_line_no = if first.line_no > 4 {
first.line_no - 4
} else {
1
};
let last_line_no = if first.line_no > 2 {
first.line_no - 1
} else {
1
};
if first_line_no > 1 {
write_line(true, 0, "···")?;
}
if let Some(lines) = self.lines.get(first_line_no..last_line_no) {
for line in lines {
write_line(true, line.line_no, line.source)?;
}
}
}
for line in &group {
write_line(true, line.line_no, line.source)?;
for (index, message) in line.messages.clone().into_iter().enumerate() {
let mut intersections = Vec::new();
let mut end = message.bounds.end;
for following in line.messages.get(index + 1..).unwrap() {
intersections.push(following.bounds.start);
intersections.push(following.bounds.end);
if end < following.bounds.end {
end = following.bounds.end;
}
}
for preceding in line.messages.get(..index).unwrap() {
intersections.push(preceding.bounds.start);
if end < preceding.bounds.end {
end = preceding.bounds.end;
}
}
if message.bounds.start == message.bounds.end && !intersections.is_empty() {
continue;
}
let mut diagram = String::new();
for position in 0..=end {
let is_intersected = intersections.contains(&position);
let is_start_bound = position == message.bounds.start;
let is_end_bound = position == message.bounds.end;
let is_in_bounds =
position < message.bounds.end && position > message.bounds.start;
if is_end_bound && is_start_bound {
diagram.push('│');
} else if is_end_bound {
if is_intersected {
diagram.push('┤');
} else {
diagram.push('┘');
}
} else if is_start_bound {
diagram.push('├');
} else if is_intersected {
diagram.push('│');
} else if is_in_bounds {
diagram.push('─');
} else {
diagram.push(' ');
}
}
write_line(true, 0, &diagram)?;
}
let mut message_map: HashMap<usize, Vec<String>> = HashMap::new();
let mut depth = 0;
let mut end = 0;
for line_message in &line.messages {
let entry = message_map
.entry(line_message.bounds.start)
.or_insert(Vec::new());
entry.push(format!(
"{}",
1 + messages
.clone()
.iter()
.position(|r| r == &line_message.message)
.unwrap()
));
if end < line_message.bounds.start {
end = line_message.bounds.start;
}
if depth < entry.len() {
depth = entry.len()
}
}
for depth in 0..depth {
let mut key = String::new();
for col in 0..=end {
if let Some(entry) = message_map.get(&col) {
if let Some(id) = entry.get(depth) {
key.push_str(&format!("{}", id));
} else {
key.push(' ');
}
} else {
key.push(' ');
}
}
write_line(true, 0, &key)?;
}
write_line(true, 0, "")?;
}
if let Some(last) = group.last() {
let last_line_no = if last.line_no > 4 && last.line_no < line_nos - 4 {
last.line_no + 4
} else {
line_nos
};
let first_line_no = if last.line_no > 2 && last.line_no < line_nos - 2 {
last.line_no
} else {
line_nos
};
if let Some(lines) = self.lines.get(first_line_no..last_line_no) {
for line in lines {
write_line(true, line.line_no, line.source)?;
}
}
if last.line_no < line_nos {
write_line(true, 0, "···")?;
}
}
}
Ok(())
}
}
#[test]
fn test_print_single_line() {
let mut src = Source::new("my-file.txt", "The quick brown fox...");
src.add_message(0..0, "All of my problems start here.");
src.add_message(4..8, "Turns out this isn't quick.");
src.add_message(16..18, "And it's not a fox!");
println!("{}", src);
}