diff --git a/wright/src/reporting.rs b/wright/src/reporting.rs index 828d13e4..e10350a1 100644 --- a/wright/src/reporting.rs +++ b/wright/src/reporting.rs @@ -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; diff --git a/wright/src/reporting/box_drawing.rs b/wright/src/reporting/box_drawing.rs index dcbc31ab..1935c02b 100644 --- a/wright/src/reporting/box_drawing.rs +++ b/wright/src/reporting/box_drawing.rs @@ -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 = '┐'; @@ -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 = '┓'; diff --git a/wright/src/reporting/render.rs b/wright/src/reporting/render.rs index 8b453986..d63536d4 100644 --- a/wright/src/reporting/render.rs +++ b/wright/src/reporting/render.rs @@ -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; @@ -54,6 +54,36 @@ pub fn for_stderr(color_choice: ColorChoice, style: Style) -> Renderer Renderer { /// 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(); @@ -78,30 +108,114 @@ impl Renderer { 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> = 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(()) } @@ -217,7 +331,7 @@ impl Renderer { 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}; @@ -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) + } } diff --git a/wright/src/reporting/style.rs b/wright/src/reporting/style.rs index 39fc5f40..af070789 100644 --- a/wright/src/reporting/style.rs +++ b/wright/src/reporting/style.rs @@ -61,6 +61,24 @@ impl Style { } } + /// Get a character to use while drawing dashed horizontal lines. + pub const fn horizontal_dashed_char(self) -> Option { + 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 { + 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. diff --git a/wright/src/source_tracking.rs b/wright/src/source_tracking.rs index 62e7b643..ee8b8890 100644 --- a/wright/src/source_tracking.rs +++ b/wright/src/source_tracking.rs @@ -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)] @@ -17,15 +20,6 @@ pub struct SourceMap { inner: Arc>>>, } -/// 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); - impl SourceMap { /// Construct a new empty [SourceMap]. pub fn new() -> Self { @@ -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 {} diff --git a/wright/src/source_tracking/fragment.rs b/wright/src/source_tracking/fragment.rs index ee662690..a5b84805 100644 --- a/wright/src/source_tracking/fragment.rs +++ b/wright/src/source_tracking/fragment.rs @@ -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; @@ -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 { diff --git a/wright/src/source_tracking/source.rs b/wright/src/source_tracking/source.rs index 789be1a5..2d5a84c4 100644 --- a/wright/src/source_tracking/source.rs +++ b/wright/src/source_tracking/source.rs @@ -4,6 +4,7 @@ use super::SourceRef; use super::{filename::FileName, fragment::Fragment, immutable_string::ImmutableString}; +use std::sync::atomic::{AtomicU64, Ordering}; use std::{fs::File, sync::Arc}; use std::io; use std::path::PathBuf; @@ -27,9 +28,24 @@ use termcolor::ColorChoice; #[cfg(feature = "file_memmap")] pub const FILE_LOCK_WARNING_TIME: Duration = Duration::from_secs(5); +/// The global [Source::gid] generator. +/// +/// This is just a global [u64] that gets incremented everytime a new source is instantiated. +static SOURCE_GID_GENERATOR: AtomicU64 = AtomicU64::new(1); + /// A full source. This is usually a file, but may also be passed in the form of a string for testing. #[derive(Debug)] pub struct Source { + /// Globally unique [Source] ID. + /// + /// It is fequently useful to have a consistient way to sort sources and check for equality between sources. + /// This cannot be done with the [Source::name] since that can be [FileName::None], and checking for equality + /// of content can be an expensive process. + /// + /// The gid of a source is an identifier that's globally unique for the runtime of the program, and is assigned to + /// the [Source] when it is instantiated. + gid: u64, + /// The name of this source file. name: FileName, @@ -44,6 +60,10 @@ impl Source { /// Construct a new [Source]. fn new(name: FileName, source: ImmutableString) -> Self { Source { + // I believe we can use relaxed ordering here, since as long as all operations are atomic, + // we're not really worried about another thread's `fetch_add` being re-ordered before this one, since + // neither will get the same number. + gid: SOURCE_GID_GENERATOR.fetch_add(1, Ordering::Relaxed), name, line_starts: source.line_starts(), source, @@ -201,7 +221,7 @@ impl Source { /// # Panics /// - This will panic if you ask for a line index that's higher than or equal to the number returned /// by [`Self::count_lines`]. - pub fn get_line(self: &Arc, line_index: usize) -> Fragment { + pub fn get_line(self: Arc, line_index: usize) -> Fragment { if line_index >= self.count_lines() { panic!("{} is greater than the number of lines in {}", line_index, self.name); } @@ -217,7 +237,7 @@ impl Source { }; // Construct the resultant fragment. - let frag = Fragment { source: SourceRef(Arc::clone(self)), range: start_byte_index..end_byte_index }; + let frag = Fragment { source: SourceRef(Arc::clone(&self)), range: start_byte_index..end_byte_index }; // Debug assert that the fragment is valid. This should always be true but might be useful for testing. debug_assert!(frag.is_valid()); // Return constructed fragment. @@ -232,7 +252,7 @@ impl Source { pub fn lines(self: Arc) -> impl Iterator { (0..self.count_lines()) .into_iter() - .map(move |line_index| self.get_line(line_index)) + .map(move |line_index| self.clone().get_line(line_index)) } /// Get the the source code stored. @@ -244,4 +264,50 @@ impl Source { pub const fn name(&self) -> &FileName { &self.name } + + /// Globally unique [Source] ID. + /// + /// It is fequently useful to have a consistient way to sort sources and check for equality between sources. + /// This cannot be done with the [Source::name] since that can be [FileName::None], and checking for equality + /// of content can be an expensive process. + /// + /// The gid of a source is an identifier that's globally unique for the runtime of the program, and is assigned to + /// the [Source] when it is instantiated. + pub const fn gid(&self) -> u64 { + self.gid + } +} + +#[cfg(test)] +mod tests { + use std::{sync::mpsc, thread}; + + use crate::source_tracking::filename::FileName; + + use super::Source; + + #[test] + fn dozen_threads_dont_share_gids() { + let (tx, rx) = mpsc::channel(); + + for i in 0..12 { + let tx = tx.clone(); + thread::spawn(move || { + let source = Source::new_from_string(FileName::None, format!("{i}")); + tx.send(source.gid()).unwrap(); + }); + } + + let mut gids = (0..12) + .map(|_| rx.recv().unwrap()) + .collect::>(); + + let original_len = gids.len(); + gids.sort(); + println!("{gids:?}"); + gids.dedup(); + let dedup_len = gids.len(); + + assert_eq!(original_len, dedup_len, "global ids are not duplicated"); + } } diff --git a/wright/src/source_tracking/source_ref.rs b/wright/src/source_tracking/source_ref.rs new file mode 100644 index 00000000..bec7990b --- /dev/null +++ b/wright/src/source_tracking/source_ref.rs @@ -0,0 +1,45 @@ +//! Types and functions for handling references to [Source]s -- this is done frequently, since [Source]s themselves +//! can be expensive to clone and pass around. + +use std::{ops::Deref, sync::Arc}; +use super::{fragment::Fragment, source::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 Arc); + +impl SourceRef { + /// See [Source::get_line]. + /// + /// This is a convenience function to unwrap/pass through the reciever type where [Deref] might not automatically. + pub fn get_line(&self, line_index: usize) -> Fragment { + Source::get_line(self.0.clone(), line_index) + } +} + +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 {}