Skip to content

Commit

Permalink
Add global IDs to Sources, refactor SourceRef, continued WIP on diagn…
Browse files Browse the repository at this point in the history
…ostic rendering.
  • Loading branch information
vcfxb committed Jul 2, 2024
1 parent 6be8599 commit d43d43a
Show file tree
Hide file tree
Showing 8 changed files with 293 additions and 73 deletions.
3 changes: 1 addition & 2 deletions wright/src/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@
use self::{owned_string::OwnedString, severity::Severity, style::Style};
use crate::source_tracking::fragment::Fragment;
use std::io;
use render::Renderer;
use supports_unicode::Stream;
use termcolor::{ColorChoice, StandardStream, StandardStreamLock, WriteColor};
use termcolor::ColorChoice;

pub mod render;
pub mod style;
Expand Down
2 changes: 2 additions & 0 deletions wright/src/reporting/box_drawing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#[allow(missing_docs)]
pub mod light {
pub const HORIZONTAL: char = '─';
pub const HORIZONTAL_DASHED: char = '\u{254C}';
pub const VERTICAL: char = '│';
pub const DOWN_RIGHT: char = '┌';
pub const DOWN_LEFT: char = '┐';
Expand All @@ -25,6 +26,7 @@ pub mod light {
#[allow(missing_docs)]
pub mod heavy {
pub const HORIZONTAL: char = '━';
pub const HORIZONTAL_DASHED: char = '\u{254D}';
pub const VERTICAL: char = '┃';
pub const DOWN_RIGHT: char = '┏';
pub const DOWN_LEFT: char = '┓';
Expand Down
182 changes: 148 additions & 34 deletions wright/src/reporting/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
//!
//! [Diagnostic]: super::Diagnostic
use crate::source_tracking::{filename::FileName, fragment::Fragment, SourceRef};
use super::{box_drawing, owned_string::OwnedString, style::{self, Style}, Diagnostic, Highlight};
use std::{borrow::Cow, io, sync::Arc};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, StandardStreamLock, WriteColor};
use crate::source_tracking::filename::FileName;
use super::{owned_string::OwnedString, style::Style, Diagnostic, Highlight};
use std::{collections::HashMap, io, ops::Range};
use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
use terminal_link::Link;
use terminal_size::Width;

Expand Down Expand Up @@ -54,6 +54,36 @@ pub fn for_stderr(color_choice: ColorChoice, style: Style) -> Renderer<StandardS
impl<W: WriteColor> Renderer<W> {
/// Draw a [Diagnostic].
pub fn draw_diagnostic(&mut self, diagnostic: &Diagnostic) -> io::Result<()> {
// Draw the header line at the top of the diagnostic.
self.draw_diagnostic_header(diagnostic)?;

// Draw the section with all the highlights.
if diagnostic.primary_highlight.is_some() {
self.draw_code_section(diagnostic)?;
}

// Draw the note. Add an extra blank line between the fragment/title and the note.
if let Some(note) = diagnostic.note.as_ref() {
// Draw a single blank (just vertical char) separating the highlight section from the note section.
writeln!(self.writer, "{}", self.style.vertical_char())?;
self.draw_note(note)?;
}

// Write an extra newline at the end to provide space between diagnostics.
writeln!(self.writer)?;

// If there was no error writing the diagnostic, return Ok.
Ok(())
}


/// Draw the header a the top of a [Diagnostic].
///
/// i.e.
/// ```text
/// | error[code]: Message goes here.\n
/// ```
fn draw_diagnostic_header(&mut self, diagnostic: &Diagnostic) -> io::Result<()> {
// Create a color spec to use as we write to the writer.
let mut spec: ColorSpec = ColorSpec::new();

Expand All @@ -78,30 +108,114 @@ impl<W: WriteColor> Renderer<W> {
self.writer.set_color(&spec)?;

// Write the message and a new line.
writeln!(self.writer, ": {}", diagnostic.message)?;
writeln!(self.writer, ": {}", diagnostic.message)
}

// Draw the primary highlight if there is one.
if let Some(highlight) = diagnostic.primary_highlight.as_ref() {
self.draw_highlight(highlight, diagnostic.severity.color())?;
/// Draw the code section of a [Diagnostic] -- the section that displays highlighted fragments.
///
/// This is pretty complex, and contains most of the core logic for rendering code fragments.
///
/// Assumes that [Diagnostic::primary_highlight] is [Some].
fn draw_code_section(&mut self, diagnostic: &Diagnostic) -> io::Result<()> {
// Get a reference to the primary highlight.
let primary_highlight: &Highlight = diagnostic.primary_highlight
.as_ref()
.expect("diagnostic has primary highlight");

// Get the vertical char for the style.
let vertical = self.style.vertical_char();
// Get the line number and column number to print.
let primary_line_range = primary_highlight.fragment.line_indices();
let primary_line_num = primary_line_range.start + 1;
// Get the column on that line that the fragment starts on. Add 1 to make this 1-indexed.
let primary_col_num = primary_highlight.fragment.starting_col_index() + 1;

// Write the file name where this diagnostic originated -- this is considered to be the file
// that the primary highlight is from.

// Create a string that represents the location.
let primary_location = match (primary_highlight.fragment.source.name(), self.supports_hyperlinks) {
// In cases of real files printing to terminals that support hyperlinks, create a hyperlink.
(name @ FileName::Real(path), true) => {
let link_text = format!("{name}:{primary_line_num}:{primary_col_num}");
let link_url = format!("file://localhost{}", path.canonicalize()?.display());
Link::new(&link_text, &link_url).to_string()
}

(name , _) => format!("{name}:{primary_line_num}:{primary_col_num}"),
};

// Use '.' for a horizontal dashed char when using ascii.
let horizontal_dashed = self.style.horizontal_dashed_char().unwrap_or('.');
// Get a vertical-right branch character, or just a vertical character on ascii.
let vertical_right_branch = self.style.vertical_right_char().unwrap_or(self.style.vertical_char());
// Get a horizontal char with a down branch for above the start of the code colunm (after the line numbers column).
// Use '.' on ascii once again.
let horizontal_down_branch = self.style.down_horizontal_char().unwrap_or('.');

// We need to know the maximum line index and minimum line index.
// By default these will be the ones for the primary highlight.
let mut max_line_index: usize = primary_highlight.fragment.line_indices().end;
let mut min_line_index: usize = primary_highlight.fragment.line_indices().start;

// Iterate through all the secondary highlights to determine if there are any lower or higher than
// the indices for the primary highlight.
for secondary_highlight in diagnostic.secondary_highlights.iter() {
let Range { start, end } = secondary_highlight.fragment.line_indices();

max_line_index = std::cmp::max(max_line_index, end);
min_line_index = std::cmp::min(min_line_index, start);
}

// Draw all secondary highlights. Use green as the color for secondary highlights.
// Get the width of the highest line number we'll need to write.
let line_num_width = f64::log10((max_line_index + 1) as f64).ceil() as usize;
// Use this to write spaces in the line number column for lines that get skipped.
let skip_line_nums = " ".repeat(line_num_width + 2);

// Reset the colorspec.
self.writer.set_color(&ColorSpec::new())?;

// Write our dashed divider.
writeln!(&mut self.writer, "{vertical_right_branch}{a}{horizontal_down_branch}{b}",
// Add two for spaces.
a = horizontal_dashed.to_string().repeat(line_num_width + 2),
// Default to 60 char term width if unknown.
// Do subtraction to make sure we get the right length.
b = horizontal_dashed.to_string().repeat(self.width.unwrap_or(60) as usize - (line_num_width + 4))
)?;

// Write our location at the top of the code colum under the horizontal divider.
write!(&mut self.writer, "{vertical}{skip_line_nums}{vertical} ")?;
// Write the location in bold.
self.writer.set_color(&ColorSpec::new().set_bold(true))?;
writeln!(self.writer, "[{primary_location}]:")?;

// Categorize highlights by source file -- use gids to identify sources.
let mut highlights_by_source: HashMap<u64, Vec<&Highlight>> = HashMap::with_capacity(diagnostic.secondary_highlights.len() + 1);

// Start with the primary highlight.
highlights_by_source
.entry(primary_highlight.fragment.source.gid())
.or_default()
.push(primary_highlight);

// Go through all of the secondary highlights.
for highlight in diagnostic.secondary_highlights.iter() {
self.draw_highlight(highlight, Color::Green)?;
highlights_by_source
.entry(highlight.fragment.source.gid())
.or_default()
.push(highlight);
}

// Draw the note. Add an extra blank line between the fragment/title and the note.
if let Some(note) = diagnostic.note.as_ref() {
writeln!(self.writer, "{}", self.style.vertical_char())?;
self.draw_note(note)?;
// Now that we've categorized all the highlights by source (using source gids) we can handle the sources one
// at a time starting with the one for the primary highlight.
// TODO:

for line_indice in min_line_index..max_line_index {
writeln!(&mut self.writer, "{vertical} {line_num:>line_num_width$} {vertical}", line_num = line_indice + 1)?;
}

// Write an extra newline at the end to provide space between diagnostics.
spec.clear();
self.writer.set_color(&spec)?;
writeln!(self.writer)?;

// If there was no error writing the diagnostic, return Ok.
Ok(())
}

Expand Down Expand Up @@ -217,7 +331,7 @@ impl<W: WriteColor> Renderer<W> {
mod tests {
use std::{io, sync::Arc};

use crate::{reporting::{style::Style, Diagnostic, Highlight}, source_tracking::{filename::FileName, fragment::Fragment, source::Source, SourceRef}};
use crate::{reporting::{style::Style, Diagnostic, Highlight}, source_tracking::{filename::FileName, fragment::Fragment, source::Source, source_ref::SourceRef}};
use indoc::indoc;
use termcolor::{ColorChoice, NoColor};

Expand Down Expand Up @@ -270,22 +384,22 @@ mod tests {
// }


// #[test]
// fn print_with_highlight_and_note() -> io::Result<()> {
#[test]
fn print_with_highlight_and_note() -> io::Result<()> {

// let source = Source::new_from_static_str(FileName::Test("test.wr"), indoc! {"
// func main() {
// wright::println(\"Hello World!\");
// }
// "});
let source = Source::new_from_static_str(FileName::Test("test.wr"), indoc! {"
func main() {
wright::println(\"Hello World!\");
}
"});

// let frag = Fragment { source: SourceRef(Arc::new(source)), range: 0..12 };
let frag = Fragment { source: SourceRef(Arc::new(source)), range: 0..12 };

// let d = Diagnostic::error("test")
// .with_code("TEST001")
// .with_primary_highlight(Highlight::new(frag, "main() defined here"))
// .with_note("This is a sample note.");
let d = Diagnostic::error("test")
.with_code("TEST001")
.with_primary_highlight(Highlight::new(frag, "main() defined here"))
.with_note("This is a sample note.");

// d.print(ColorChoice::Auto)
// }
d.print(ColorChoice::Auto)
}
}
18 changes: 18 additions & 0 deletions wright/src/reporting/style.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,24 @@ impl Style {
}
}

/// Get a character to use while drawing dashed horizontal lines.
pub const fn horizontal_dashed_char(self) -> Option<char> {
match self {
Style::UnicodeHeavy => Some(box_drawing::heavy::HORIZONTAL_DASHED),
Style::UnicodeLight => Some(box_drawing::light::HORIZONTAL_DASHED),
Style::Ascii => None,
}
}

/// Get a horizontal character with a downward branch if available.
pub const fn down_horizontal_char(self) -> Option<char> {
match self {
Style::UnicodeHeavy => Some(box_drawing::heavy::DOWN_HORIZONTAL),
Style::UnicodeLight => Some(box_drawing::light::DOWN_HORIZONTAL),
Style::Ascii => None,
}
}

/// Check if this style is a unicode style. This includes [Style::UnicodeHeavy] and [Style::UnicodeLight].
pub const fn is_unicode(self) -> bool {
// Use usize cast to make this possible in const contexts.
Expand Down
36 changes: 4 additions & 32 deletions wright/src/source_tracking.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
//! Types and traits for tracking source code fed to the wright compiler.
use source_ref::SourceRef;

use self::source::Source;
use std::{ops::Deref, sync::{Arc, RwLock}};
use std::sync::{Arc, RwLock};

pub mod filename;
pub mod fragment;
pub mod immutable_string;
pub mod source;
pub mod source_ref;

/// Storage for a list of [Source]s used and reference in compiling a wright project.
#[derive(Debug)]
Expand All @@ -17,15 +20,6 @@ pub struct SourceMap {
inner: Arc<RwLock<Vec<Arc<Source>>>>,
}

/// A reference to a [Source] in a [SourceMap].
///
/// This is cheap to [Clone] since it uses an [Arc] internally.
///
/// Equality on this struct is checked using [Arc::ptr_eq] -- this cannot be used for checking if
/// two [Source]s contain identical content.
#[derive(Debug)]
pub struct SourceRef(pub(crate) Arc<Source>);

impl SourceMap {
/// Construct a new empty [SourceMap].
pub fn new() -> Self {
Expand Down Expand Up @@ -59,25 +53,3 @@ impl Default for SourceMap {
SourceMap::new()
}
}

impl Clone for SourceRef {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}

impl Deref for SourceRef {
type Target = Source;

fn deref(&self) -> &Self::Target {
&*self.0
}
}

impl PartialEq for SourceRef {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}

impl Eq for SourceRef {}
8 changes: 6 additions & 2 deletions wright/src/source_tracking/fragment.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! [Fragment] struct and implementation for dealing with fragments of source code.
use super::SourceRef;
use std::{ops::Range, str::Chars, sync::Arc};
use std::{ops::Range, str::Chars};

#[cfg(doc)]
use super::Source;
Expand Down Expand Up @@ -237,7 +237,11 @@ impl Fragment {
self.line_indices().start + 1
}


/// Get the number of bytes between the start of the line that this [Fragment] starts on and the start of this
/// [Fragment]
pub fn starting_col_index(&self) -> usize {
self.range.start - self.source.get_line(self.line_indices().start).range.start
}
}

impl PartialEq for Fragment {
Expand Down
Loading

0 comments on commit d43d43a

Please sign in to comment.