diff --git a/src/handlers/graphical.rs b/src/handlers/graphical.rs index 2b385bd..767675a 100644 --- a/src/handlers/graphical.rs +++ b/src/handlers/graphical.rs @@ -482,7 +482,7 @@ impl GraphicalReportHandler { .map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st)) .collect::>(); - let mut highlighter_state = self.highlighter.start_highlighter_state(source); + let mut highlighter_state = self.highlighter.start_highlighter_state(&*contents); // The max number of gutter-lines that will be active at any given // point. We need this to figure out indentation, so we do one loop diff --git a/src/highlighters/blank.rs b/src/highlighters/blank.rs index 681d401..50a9c65 100644 --- a/src/highlighters/blank.rs +++ b/src/highlighters/blank.rs @@ -1,5 +1,7 @@ use owo_colors::Style; +use crate::SpanContents; + use super::{Highlighter, HighlighterState}; /// The default syntax highlighter. It applies `Style::default()` to input text. @@ -10,7 +12,7 @@ pub struct BlankHighlighter; impl Highlighter for BlankHighlighter { fn start_highlighter_state<'h>( &'h self, - _source: &dyn crate::SourceCode, + _source: &dyn SpanContents<'_>, ) -> Box { Box::new(BlankHighlighterState) } diff --git a/src/highlighters/mod.rs b/src/highlighters/mod.rs index 2ffe693..47e7f7c 100644 --- a/src/highlighters/mod.rs +++ b/src/highlighters/mod.rs @@ -13,7 +13,7 @@ use std::{ops::Deref, sync::Arc}; -use crate::SourceCode; +use crate::SpanContents; use owo_colors::Styled; #[cfg(feature = "syntect-highlighter")] @@ -24,22 +24,22 @@ mod blank; #[cfg(feature = "syntect-highlighter")] mod syntect; -/// A syntax highlighter for highlighting miette [SourceCode] snippets. +/// A syntax highlighter for highlighting miette [`SourceCode`](crate::SourceCode) snippets. pub trait Highlighter { - /// Creates a new [HighlighterState] to begin parsing and highlighting - /// a [SourceCode] snippet. + /// Creates a new [HighlighterState] to begin parsing and highlighting + /// a [SpanContents]. /// /// The [GraphicalReportHandler](crate::GraphicalReportHandler) will call - /// this method at the start of rendering a [Diagnostic](crate::Diagnostic). + /// this method at the start of rendering a [SpanContents]. /// - /// The source is provided as input only so that the Highlighter can detect - /// language syntax and make other initialization decisions prior + /// The [SpanContents] is provided as input only so that the [Highlighter] + /// can detect language syntax and make other initialization decisions prior /// to highlighting, but it is not intended that the Highlighter begin /// highlighting at this point. The returned [HighlighterState] is /// responsible for the actual rendering. fn start_highlighter_state<'h>( &'h self, - source: &dyn SourceCode, + source: &dyn SpanContents<'_>, ) -> Box; } diff --git a/src/highlighters/syntect.rs b/src/highlighters/syntect.rs index 0f76f22..57ebadf 100644 --- a/src/highlighters/syntect.rs +++ b/src/highlighters/syntect.rs @@ -14,7 +14,7 @@ use owo_colors::{Rgb, Style, Styled}; use crate::{ highlighters::{Highlighter, HighlighterState}, - SourceCode, + SpanContents, }; use super::BlankHighlighterState; @@ -41,9 +41,9 @@ impl Default for SyntectHighlighter { impl Highlighter for SyntectHighlighter { fn start_highlighter_state<'h>( &'h self, - source: &dyn SourceCode, + source: &dyn SpanContents<'_>, ) -> Box { - if let Some(syntax) = self.get_syntax_from_source(source) { + if let Some(syntax) = self.detect_syntax(source) { let highlighter = syntect::Highlighter::new(&self.theme); let parse_state = syntect::ParseState::new(syntax); let highlight_state = @@ -81,19 +81,26 @@ impl SyntectHighlighter { } /// Determine syntect SyntaxReference to use for given SourceCode - fn get_syntax_from_source(&self, source: &dyn SourceCode) -> Option<&syntect::SyntaxReference> { - if let Some(language) = source.language() { - self.syntax_set.find_syntax_by_name(language) - } else if let Some(name) = source.name() { - if let Some(ext) = Path::new(name).extension() { - self.syntax_set - .find_syntax_by_extension(ext.to_string_lossy().as_ref()) - } else { - None - } - } else { - None + fn detect_syntax(&self, contents: &dyn SpanContents<'_>) -> Option<&syntect::SyntaxReference> { + // use language if given + if let Some(language) = contents.language() { + return self.syntax_set.find_syntax_by_name(language); } + // otherwise try to use any file extension provided in the name + if let Some(name) = contents.name() { + if let Some(ext) = Path::new(name).extension() { + return self + .syntax_set + .find_syntax_by_extension(ext.to_string_lossy().as_ref()); + } + } + // finally, attempt to guess syntax based on first line + return self.syntax_set.find_syntax_by_first_line( + &std::str::from_utf8(contents.data()) + .ok()? + .split('\n') + .next()?, + ); } } diff --git a/src/lib.rs b/src/lib.rs index aaf4037..aec57c1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -655,13 +655,14 @@ //! automatically use the [`syntect`] crate to highlight the `#[source_code]` //! field of your [`Diagnostic`]. //! -//! Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SourceCode`] trait, in order: -//! * [language()](SourceCode::language) - Provides the name of the language +//! Syntax detection with [`syntect`] is handled by checking 2 methods on the [`SpanContents`] trait, in order: +//! * [language()](SpanContents::language) - Provides the name of the language //! as a string. For example `"Rust"` will indicate Rust syntax highlighting. -//! You can set the language of a [`NamedSource`] via the -//! [`with_language`](NamedSource::with_language) method. -//! * [name()](SourceCode::name) - In the absence of an explicitly set -//! language, the source code's name is assumed to contain a file name or file path. +//! You can set the language of the [`SpanContents`] produced by a +//! [`NamedSource`] via the [`with_language`](NamedSource::with_language) +//! method. +//! * [name()](SpanContents::name) - In the absence of an explicitly set +//! language, the name is assumed to contain a file name or file path. //! The highlighter will check for a file extension at the end of the name and //! try to guess the syntax from that. //! diff --git a/src/named_source.rs b/src/named_source.rs index 463828e..d13870a 100644 --- a/src/named_source.rs +++ b/src/named_source.rs @@ -36,7 +36,7 @@ impl NamedSource { &*self.source } - /// Sets the [`language`](SourceCode::language) for this source code. + /// Sets the [`language`](SpanContents::language) for this source code. pub fn with_language(mut self, language: impl Into) -> Self { self.language = Some(language.into()); self @@ -50,27 +50,20 @@ impl SourceCode for NamedSource { context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { - let contents = self - .inner() - .read_span(span, context_lines_before, context_lines_after)?; - Ok(Box::new(MietteSpanContents::new_named( + let inner_contents = + self.inner() + .read_span(span, context_lines_before, context_lines_after)?; + let mut contents = MietteSpanContents::new_named( self.name.clone(), - contents.data(), - *contents.span(), - contents.line(), - contents.column(), - contents.line_count(), - ))) - } - - fn name(&self) -> Option<&str> { - Some(&self.name) - } - - fn language(&self) -> Option<&str> { - match &self.language { - Some(language) => Some(language), - None => self.source.language(), + inner_contents.data(), + *inner_contents.span(), + inner_contents.line(), + inner_contents.column(), + inner_contents.line_count(), + ); + if let Some(language) = &self.language { + contents = contents.with_language(language); } + Ok(Box::new(contents)) } } diff --git a/src/protocol.rs b/src/protocol.rs index fdaa4a4..a02b227 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -240,20 +240,6 @@ pub trait SourceCode: Send + Sync { context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError>; - - /// Optional method. The name of this source, if any - fn name(&self) -> Option<&str> { - None - } - - /// Optional method. The language name for this source code, if any. - /// This is used to drive syntax highlighting. - /// - /// Examples: Rust, TOML, C - /// - fn language(&self) -> Option<&str> { - None - } } /// A labeled [`SourceSpan`]. @@ -447,6 +433,15 @@ pub trait SpanContents<'a> { fn column(&self) -> usize; /// Total number of lines covered by this `SpanContents`. fn line_count(&self) -> usize; + + /// Optional method. The language name for this source code, if any. + /// This is used to drive syntax highlighting. + /// + /// Examples: Rust, TOML, C + /// + fn language(&self) -> Option<&str> { + None + } } /** @@ -466,6 +461,8 @@ pub struct MietteSpanContents<'a> { line_count: usize, // Optional filename name: Option, + // Optional language + language: Option, } impl<'a> MietteSpanContents<'a> { @@ -484,6 +481,7 @@ impl<'a> MietteSpanContents<'a> { column, line_count, name: None, + language: None, } } @@ -503,8 +501,15 @@ impl<'a> MietteSpanContents<'a> { column, line_count, name: Some(name), + language: None, } } + + /// Sets the [`language`](SourceCode::language) for syntax highlighting. + pub fn with_language(mut self, language: impl Into) -> Self { + self.language = Some(language.into()); + self + } } impl<'a> SpanContents<'a> for MietteSpanContents<'a> { @@ -526,6 +531,9 @@ impl<'a> SpanContents<'a> for MietteSpanContents<'a> { fn name(&self) -> Option<&str> { self.name.as_deref() } + fn language(&self) -> Option<&str> { + self.language.as_deref() + } } /// Span within a [`SourceCode`]