mirror of https://github.com/zkat/miette.git
feat(highlighting): add syntax highlighting support with syntect crate (#313)
This commit is contained in:
parent
1df3b1a537
commit
e65d0a78cc
|
|
@ -26,8 +26,12 @@ jobs:
|
|||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
features: [fancy, syntect-highlighter]
|
||||
rust: [1.56.0, stable]
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
exclude:
|
||||
- features: syntect-highlighter
|
||||
rust: 1.56.0
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
|
@ -43,10 +47,10 @@ jobs:
|
|||
run: cargo clippy --all -- -D warnings
|
||||
- name: Run tests
|
||||
if: matrix.rust == 'stable'
|
||||
run: cargo test --all --verbose --features fancy
|
||||
run: cargo test --all --verbose --features ${{matrix.features}}
|
||||
- name: Run tests
|
||||
if: matrix.rust == '1.56.0'
|
||||
run: cargo test --all --verbose --features fancy no-format-args-capture
|
||||
run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture
|
||||
|
||||
miri:
|
||||
name: Miri
|
||||
|
|
@ -78,5 +82,4 @@ jobs:
|
|||
with:
|
||||
toolchain: nightly
|
||||
- name: Run minimal version build
|
||||
run: cargo build -Z minimal-versions --all-features
|
||||
|
||||
run: cargo build -Z minimal-versions --features fancy,no-format-args-capture
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ miette-derive = { path = "miette-derive", version = "=5.10.0", optional = true }
|
|||
once_cell = "1.8.0"
|
||||
unicode-width = "0.1.9"
|
||||
|
||||
owo-colors = { version = "3.0.0", optional = true }
|
||||
owo-colors = { version = "3.4.0", optional = true }
|
||||
is-terminal = { version = "0.4.0", optional = true }
|
||||
textwrap = { version = "0.15.0", optional = true }
|
||||
supports-hyperlinks = { version = "2.0.0", optional = true }
|
||||
|
|
@ -28,6 +28,7 @@ backtrace = { version = "0.3.61", optional = true }
|
|||
terminal_size = { version = "0.3.0", optional = true }
|
||||
backtrace-ext = { version = "0.2.1", optional = true }
|
||||
serde = { version = "1.0.162", features = ["derive"], optional = true }
|
||||
syntect = { version = "5.1.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
semver = "1.0.4"
|
||||
|
|
@ -42,6 +43,7 @@ regex = "1.5"
|
|||
lazy_static = "1.4"
|
||||
|
||||
serde_json = "1.0.64"
|
||||
strip-ansi-escapes = "0.2.0"
|
||||
|
||||
[features]
|
||||
default = ["derive"]
|
||||
|
|
@ -57,6 +59,7 @@ fancy-no-backtrace = [
|
|||
"supports-unicode",
|
||||
]
|
||||
fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
|
||||
syntect-highlighter = ["fancy-no-backtrace", "syntect"]
|
||||
|
||||
[workspace]
|
||||
members = ["miette-derive"]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
use std::fmt;
|
||||
|
||||
use crate::highlighters::Highlighter;
|
||||
use crate::highlighters::MietteHighlighter;
|
||||
use crate::protocol::Diagnostic;
|
||||
use crate::GraphicalReportHandler;
|
||||
use crate::GraphicalTheme;
|
||||
|
|
@ -59,6 +61,7 @@ pub struct MietteHandlerOpts {
|
|||
pub(crate) wrap_lines: Option<bool>,
|
||||
pub(crate) word_separator: Option<textwrap::WordSeparator>,
|
||||
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
|
||||
pub(crate) highlighter: Option<MietteHighlighter>,
|
||||
}
|
||||
|
||||
impl MietteHandlerOpts {
|
||||
|
|
@ -84,6 +87,43 @@ impl MietteHandlerOpts {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set a syntax highlighter when rendering in graphical mode.
|
||||
/// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
|
||||
/// force graphical mode.
|
||||
///
|
||||
/// Syntax highlighting is disabled by default unless the
|
||||
/// `syntect-highlighter` feature is enabled. Call this method
|
||||
/// to override the default and use a custom highlighter
|
||||
/// implmentation instead.
|
||||
///
|
||||
/// Use
|
||||
/// [`without_syntax_highlighting()`](MietteHandlerOpts::without_syntax_highlighting())
|
||||
/// To disable highlighting completely.
|
||||
///
|
||||
/// Setting this option will not force color output. In all cases, the
|
||||
/// current color configuration via
|
||||
/// [`color()`](MietteHandlerOpts::color()) takes precedence over
|
||||
/// highlighter configuration.
|
||||
pub fn with_syntax_highlighting(
|
||||
mut self,
|
||||
highlighter: impl Highlighter + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.highlighter = Some(MietteHighlighter::from(highlighter));
|
||||
self
|
||||
}
|
||||
|
||||
/// Disables syntax highlighting when rendering in graphical mode.
|
||||
/// Use [`force_graphical()`](MietteHandlerOpts::force_graphical()) to
|
||||
/// force graphical mode.
|
||||
///
|
||||
/// Syntax highlighting is disabled by default unless the
|
||||
/// `syntect-highlighter` feature is enabled. Call this method if you want
|
||||
/// to disable highlighting when building with this feature.
|
||||
pub fn without_syntax_highlighting(mut self) -> Self {
|
||||
self.highlighter = Some(MietteHighlighter::nocolor());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the width to wrap the report at. Defaults to 80.
|
||||
pub fn width(mut self, width: usize) -> Self {
|
||||
self.width = Some(width);
|
||||
|
|
@ -246,10 +286,34 @@ impl MietteHandlerOpts {
|
|||
} else {
|
||||
ThemeStyles::none()
|
||||
};
|
||||
#[cfg(not(feature = "syntect-highlighter"))]
|
||||
let highlighter = self.highlighter.unwrap_or_else(MietteHighlighter::nocolor);
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
let highlighter = if self.color == Some(false) {
|
||||
MietteHighlighter::nocolor()
|
||||
} else if self.color == Some(true)
|
||||
|| supports_color::on(supports_color::Stream::Stderr).is_some()
|
||||
{
|
||||
match self.highlighter {
|
||||
Some(highlighter) => highlighter,
|
||||
None => match self.rgb_colors {
|
||||
// Because the syntect highlighter currently only supports 24-bit truecolor,
|
||||
// respect RgbColor::Never by disabling the highlighter.
|
||||
// TODO: In the future, find a way to convert the RGB syntect theme
|
||||
// into an ANSI color theme.
|
||||
RgbColors::Never => MietteHighlighter::nocolor(),
|
||||
_ => MietteHighlighter::syntect_truecolor(),
|
||||
},
|
||||
}
|
||||
} else {
|
||||
MietteHighlighter::nocolor()
|
||||
};
|
||||
let theme = self.theme.unwrap_or(GraphicalTheme { characters, styles });
|
||||
let mut handler = GraphicalReportHandler::new_themed(theme)
|
||||
.with_width(width)
|
||||
.with_links(linkify);
|
||||
.with_links(linkify)
|
||||
.with_theme(theme);
|
||||
handler.highlighter = highlighter;
|
||||
if let Some(with_cause_chain) = self.with_cause_chain {
|
||||
if with_cause_chain {
|
||||
handler = handler.with_cause_chain();
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
use std::fmt::{self, Write};
|
||||
|
||||
use owo_colors::{OwoColorize, Style};
|
||||
use owo_colors::{OwoColorize, Style, StyledList};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
|
||||
use crate::handlers::theme::*;
|
||||
use crate::highlighters::{Highlighter, MietteHighlighter};
|
||||
use crate::protocol::{Diagnostic, Severity};
|
||||
use crate::{LabeledSpan, ReportHandler, SourceCode, SourceSpan, SpanContents};
|
||||
|
||||
|
|
@ -34,6 +35,7 @@ pub struct GraphicalReportHandler {
|
|||
pub(crate) break_words: bool,
|
||||
pub(crate) word_separator: Option<textwrap::WordSeparator>,
|
||||
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
|
||||
pub(crate) highlighter: MietteHighlighter,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
|
@ -59,6 +61,7 @@ impl GraphicalReportHandler {
|
|||
break_words: true,
|
||||
word_separator: None,
|
||||
word_splitter: None,
|
||||
highlighter: MietteHighlighter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,6 +79,7 @@ impl GraphicalReportHandler {
|
|||
break_words: true,
|
||||
word_separator: None,
|
||||
word_splitter: None,
|
||||
highlighter: MietteHighlighter::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -169,6 +173,23 @@ impl GraphicalReportHandler {
|
|||
self.context_lines = lines;
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable syntax highlighting for source code snippets, using the given
|
||||
/// [`Highlighter`]. See the [crate::highlighters] crate for more details.
|
||||
pub fn with_syntax_highlighting(
|
||||
mut self,
|
||||
highlighter: impl Highlighter + Send + Sync + 'static,
|
||||
) -> Self {
|
||||
self.highlighter = MietteHighlighter::from(highlighter);
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable syntax highlighting. This uses the
|
||||
/// [`crate::highlighters::BlankHighlighter`] as a no-op highlighter.
|
||||
pub fn without_syntax_highlighting(mut self) -> Self {
|
||||
self.highlighter = MietteHighlighter::nocolor();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for GraphicalReportHandler {
|
||||
|
|
@ -472,6 +493,8 @@ impl GraphicalReportHandler {
|
|||
.map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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
|
||||
// over the lines to see what the damage is gonna be.
|
||||
|
|
@ -545,7 +568,9 @@ impl GraphicalReportHandler {
|
|||
self.render_line_gutter(f, max_gutter, line, &labels)?;
|
||||
|
||||
// And _now_ we can print out the line text itself!
|
||||
self.render_line_text(f, &line.text)?;
|
||||
let styled_text =
|
||||
StyledList::from(highlighter_state.highlight_line(&line.text)).to_string();
|
||||
self.render_line_text(f, &styled_text)?;
|
||||
|
||||
// Next, we write all the highlights that apply to this particular line.
|
||||
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
|
||||
|
|
@ -881,13 +906,26 @@ impl GraphicalReportHandler {
|
|||
/// Returns an iterator over the visual width of each character in a line.
|
||||
fn line_visual_char_width<'a>(&self, text: &'a str) -> impl Iterator<Item = usize> + 'a {
|
||||
let mut column = 0;
|
||||
let mut escaped = false;
|
||||
let tab_width = self.tab_width;
|
||||
text.chars().map(move |c| {
|
||||
let width = if c == '\t' {
|
||||
let width = match (escaped, c) {
|
||||
// Round up to the next multiple of tab_width
|
||||
tab_width - column % tab_width
|
||||
} else {
|
||||
c.width().unwrap_or(0)
|
||||
(false, '\t') => tab_width - column % tab_width,
|
||||
// start of ANSI escape
|
||||
(false, '\x1b') => {
|
||||
escaped = true;
|
||||
0
|
||||
}
|
||||
// use Unicode width for all other characters
|
||||
(false, c) => c.width().unwrap_or(0),
|
||||
// end of ANSI escape
|
||||
(true, 'm') => {
|
||||
escaped = false;
|
||||
0
|
||||
}
|
||||
// characters are zero width within escape sequence
|
||||
(true, _) => 0,
|
||||
};
|
||||
column += width;
|
||||
width
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
use owo_colors::Style;
|
||||
|
||||
use crate::SpanContents;
|
||||
|
||||
use super::{Highlighter, HighlighterState};
|
||||
|
||||
/// The default syntax highlighter. It applies `Style::default()` to input text.
|
||||
/// This is used by default when no syntax highlighting features are enabled.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlankHighlighter;
|
||||
|
||||
impl Highlighter for BlankHighlighter {
|
||||
fn start_highlighter_state<'h>(
|
||||
&'h self,
|
||||
_source: &dyn SpanContents<'_>,
|
||||
) -> Box<dyn super::HighlighterState + 'h> {
|
||||
Box::new(BlankHighlighterState)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlankHighlighter {
|
||||
fn default() -> Self {
|
||||
BlankHighlighter
|
||||
}
|
||||
}
|
||||
|
||||
/// The default highlighter state. It applies `Style::default()` to input text.
|
||||
/// This is used by default when no syntax highlighting features are enabled.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BlankHighlighterState;
|
||||
|
||||
impl HighlighterState for BlankHighlighterState {
|
||||
fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<owo_colors::Styled<&'s str>> {
|
||||
vec![Style::default().style(line)]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
//! This module provides a trait for creating custom syntax highlighters that
|
||||
//! highlight [`Diagnostic`](crate::Diagnostic) source code with ANSI escape
|
||||
//! sequences when rendering with the [`GraphicalReportHighlighter`](crate::GraphicalReportHandler).
|
||||
//!
|
||||
//! It also provides built-in highlighter implementations that you can use out of the box.
|
||||
//! By default, there are no syntax highlighters exported by miette
|
||||
//! (except for the no-op [`BlankHighlighter`]).
|
||||
//! To enable support for specific highlighters, you should enable their associated feature flag.
|
||||
//!
|
||||
//! Currently supported syntax highlighters and their feature flags:
|
||||
//! * `syntect-highlighter` - Enables [`syntect`](https://docs.rs/syntect/latest/syntect/) syntax highlighting support via the [`SyntectHighlighter`]
|
||||
//!
|
||||
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use crate::SpanContents;
|
||||
use owo_colors::Styled;
|
||||
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
pub use self::syntect::*;
|
||||
pub use blank::*;
|
||||
|
||||
mod blank;
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
mod syntect;
|
||||
|
||||
/// A syntax highlighter for highlighting miette [`SourceCode`](crate::SourceCode) snippets.
|
||||
pub trait Highlighter {
|
||||
/// 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 [SpanContents].
|
||||
///
|
||||
/// 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 SpanContents<'_>,
|
||||
) -> Box<dyn HighlighterState + 'h>;
|
||||
}
|
||||
|
||||
/// A stateful highlighter that incrementally highlights lines of a particular
|
||||
/// source code.
|
||||
///
|
||||
/// The [GraphicalReportHandler](crate::GraphicalReportHandler)
|
||||
/// will create a highlighter state by calling
|
||||
/// [start_highlighter_state](Highlighter::start_highlighter_state) at the
|
||||
/// start of rendering, then it will iteratively call
|
||||
/// [highlight_line](HighlighterState::highlight_line) to render individual
|
||||
/// highlighted lines. This allows [Highlighter] implementations to maintain
|
||||
/// mutable parsing and highlighting state.
|
||||
pub trait HighlighterState {
|
||||
/// Highlight an individual line from the source code by returning a vector of [Styled]
|
||||
/// regions.
|
||||
fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>>;
|
||||
}
|
||||
|
||||
/// Arcified trait object for Highlighter. Used internally by [GraphicalReportHandler]
|
||||
///
|
||||
/// Wrapping the trait object in this way allows us to implement Debug and Clone.
|
||||
#[derive(Clone)]
|
||||
#[repr(transparent)]
|
||||
pub(crate) struct MietteHighlighter(Arc<dyn Highlighter + Send + Sync>);
|
||||
|
||||
impl MietteHighlighter {
|
||||
pub(crate) fn nocolor() -> Self {
|
||||
Self::from(BlankHighlighter)
|
||||
}
|
||||
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
pub(crate) fn syntect_truecolor() -> Self {
|
||||
Self::from(SyntectHighlighter::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for MietteHighlighter {
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
fn default() -> Self {
|
||||
use is_terminal::IsTerminal;
|
||||
match std::env::var("NO_COLOR") {
|
||||
_ if !std::io::stdout().is_terminal() || !std::io::stderr().is_terminal() => {
|
||||
//TODO: should use ANSI styling instead of 24-bit truecolor here
|
||||
Self(Arc::new(SyntectHighlighter::default()))
|
||||
}
|
||||
Ok(string) if string != "0" => MietteHighlighter::nocolor(),
|
||||
_ => Self(Arc::new(SyntectHighlighter::default())),
|
||||
}
|
||||
}
|
||||
#[cfg(not(feature = "syntect-highlighter"))]
|
||||
fn default() -> Self {
|
||||
return MietteHighlighter::nocolor();
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Highlighter + Send + Sync + 'static> From<T> for MietteHighlighter {
|
||||
fn from(value: T) -> Self {
|
||||
Self(Arc::new(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for MietteHighlighter {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "MietteHighlighter(...)")
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for MietteHighlighter {
|
||||
type Target = dyn Highlighter + Send + Sync;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,170 @@
|
|||
use std::path::Path;
|
||||
|
||||
// all syntect imports are explicitly qualified, but their paths are shortened for convenience
|
||||
mod syntect {
|
||||
pub(super) use syntect::{
|
||||
highlighting::{
|
||||
Color, HighlightIterator, HighlightState, Highlighter, Style, Theme, ThemeSet,
|
||||
},
|
||||
parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
|
||||
};
|
||||
}
|
||||
|
||||
use owo_colors::{Rgb, Style, Styled};
|
||||
|
||||
use crate::{
|
||||
highlighters::{Highlighter, HighlighterState},
|
||||
SpanContents,
|
||||
};
|
||||
|
||||
use super::BlankHighlighterState;
|
||||
|
||||
/// Highlights miette [SourceCode] with the [syntect](https://docs.rs/syntect/latest/syntect/) highlighting crate.
|
||||
///
|
||||
/// Currently only 24-bit truecolor output is supported due to syntect themes
|
||||
/// representing color as RGBA.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SyntectHighlighter {
|
||||
theme: syntect::Theme,
|
||||
syntax_set: syntect::SyntaxSet,
|
||||
use_bg_color: bool,
|
||||
}
|
||||
|
||||
impl Default for SyntectHighlighter {
|
||||
fn default() -> Self {
|
||||
let theme_set = syntect::ThemeSet::load_defaults();
|
||||
let theme = theme_set.themes["base16-ocean.dark"].clone();
|
||||
Self::new_themed(theme, false)
|
||||
}
|
||||
}
|
||||
|
||||
impl Highlighter for SyntectHighlighter {
|
||||
fn start_highlighter_state<'h>(
|
||||
&'h self,
|
||||
source: &dyn SpanContents<'_>,
|
||||
) -> Box<dyn HighlighterState + 'h> {
|
||||
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 =
|
||||
syntect::HighlightState::new(&highlighter, syntect::ScopeStack::new());
|
||||
Box::new(SyntectHighlighterState {
|
||||
syntax_set: &self.syntax_set,
|
||||
highlighter,
|
||||
parse_state,
|
||||
highlight_state,
|
||||
use_bg_color: self.use_bg_color,
|
||||
})
|
||||
} else {
|
||||
Box::new(BlankHighlighterState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SyntectHighlighter {
|
||||
/// Create a syntect highlighter with the given theme and syntax set.
|
||||
pub fn new(syntax_set: syntect::SyntaxSet, theme: syntect::Theme, use_bg_color: bool) -> Self {
|
||||
Self {
|
||||
theme,
|
||||
syntax_set,
|
||||
use_bg_color,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a syntect highlighter with the given theme and the default syntax set.
|
||||
pub fn new_themed(theme: syntect::Theme, use_bg_color: bool) -> Self {
|
||||
Self::new(
|
||||
syntect::SyntaxSet::load_defaults_nonewlines(),
|
||||
theme,
|
||||
use_bg_color,
|
||||
)
|
||||
}
|
||||
|
||||
/// Determine syntect SyntaxReference to use for given SourceCode
|
||||
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()?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Stateful highlighting iterator for [SyntectHighlighter]
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct SyntectHighlighterState<'h> {
|
||||
syntax_set: &'h syntect::SyntaxSet,
|
||||
highlighter: syntect::Highlighter<'h>,
|
||||
parse_state: syntect::ParseState,
|
||||
highlight_state: syntect::HighlightState,
|
||||
use_bg_color: bool,
|
||||
}
|
||||
|
||||
impl<'h> HighlighterState for SyntectHighlighterState<'h> {
|
||||
fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>> {
|
||||
if let Ok(ops) = self.parse_state.parse_line(line, &self.syntax_set) {
|
||||
let use_bg_color = self.use_bg_color;
|
||||
syntect::HighlightIterator::new(
|
||||
&mut self.highlight_state,
|
||||
&ops,
|
||||
line,
|
||||
&mut self.highlighter,
|
||||
)
|
||||
.map(|(style, str)| (convert_style(style, use_bg_color).style(str)))
|
||||
.collect()
|
||||
} else {
|
||||
vec![Style::default().style(line)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert syntect [syntect::Style] into owo_colors [Style] */
|
||||
#[inline]
|
||||
fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
|
||||
if use_bg_color {
|
||||
let fg = blend_fg_color(syntect_style);
|
||||
let bg = convert_color(syntect_style.background);
|
||||
Style::new().color(fg).on_color(bg)
|
||||
} else {
|
||||
let fg = convert_color(syntect_style.foreground);
|
||||
Style::new().color(fg)
|
||||
}
|
||||
}
|
||||
|
||||
/// Blend foreground RGB into background RGB according to alpha channel
|
||||
#[inline]
|
||||
fn blend_fg_color(syntect_style: syntect::Style) -> Rgb {
|
||||
let fg = syntect_style.foreground;
|
||||
if fg.a == 0xff {
|
||||
return convert_color(fg);
|
||||
}
|
||||
let bg = syntect_style.background;
|
||||
let ratio = fg.a as u32;
|
||||
let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
|
||||
let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
|
||||
let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
|
||||
Rgb(r as u8, g as u8, b as u8)
|
||||
}
|
||||
|
||||
/// Convert syntect color into owo color.
|
||||
///
|
||||
/// Note: ignores alpha channel. use [`blend_fg_color`] if you need that
|
||||
///
|
||||
#[inline]
|
||||
fn convert_color(color: syntect::Color) -> Rgb {
|
||||
Rgb(color.r, color.g, color.b)
|
||||
}
|
||||
31
src/lib.rs
31
src/lib.rs
|
|
@ -47,6 +47,7 @@
|
|||
//! - [... delayed source code](#-delayed-source-code)
|
||||
//! - [... handler options](#-handler-options)
|
||||
//! - [... dynamic diagnostics](#-dynamic-diagnostics)
|
||||
//! - [... syntax highlighting](#-syntax-highlighting)
|
||||
//! - [Acknowledgements](#acknowledgements)
|
||||
//! - [License](#license)
|
||||
//!
|
||||
|
|
@ -643,6 +644,34 @@
|
|||
//! println!("{:?}", report)
|
||||
//! ```
|
||||
//!
|
||||
//! ### ... syntax highlighting
|
||||
//!
|
||||
//! `miette` can be configured to highlight syntax in source code snippets.
|
||||
//!
|
||||
//! <!-- TODO: screenshot goes here once default Theme is decided -->
|
||||
//!
|
||||
//! To use the built-in highlighting functionality, you must enable the
|
||||
//! `syntect-highlighter` crate feature. When this feature is enabled, `miette` will
|
||||
//! 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 [`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 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.
|
||||
//!
|
||||
//! If you want to use a custom highlighter, you can provide a custom
|
||||
//! implementation of the [`Highlighter`](highlighters::Highlighter)
|
||||
//! trait to [`MietteHandlerOpts`] by calling the
|
||||
//! [`with_syntax_highlighting`](MietteHandlerOpts::with_syntax_highlighting)
|
||||
//! method. See the [`highlighters`] module docs for more details.
|
||||
//!
|
||||
//! ## Acknowledgements
|
||||
//!
|
||||
//! `miette` was not developed in a void. It owes enormous credit to various
|
||||
|
|
@ -691,6 +720,8 @@ mod eyreish;
|
|||
#[cfg(feature = "fancy-no-backtrace")]
|
||||
mod handler;
|
||||
mod handlers;
|
||||
#[cfg(feature = "fancy-no-backtrace")]
|
||||
pub mod highlighters;
|
||||
#[doc(hidden)]
|
||||
pub mod macro_helpers;
|
||||
mod miette_diagnostic;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,15 @@ use crate::{MietteError, MietteSpanContents, SourceCode, SpanContents};
|
|||
pub struct NamedSource<S: SourceCode + 'static> {
|
||||
source: S,
|
||||
name: String,
|
||||
language: Option<String>,
|
||||
}
|
||||
|
||||
impl<S: SourceCode> std::fmt::Debug for NamedSource<S> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("NamedSource")
|
||||
.field("name", &self.name)
|
||||
.field("source", &"<redacted>");
|
||||
.field("source", &"<redacted>")
|
||||
.field("language", &self.language);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +29,7 @@ impl<S: SourceCode + 'static> NamedSource<S> {
|
|||
Self {
|
||||
source,
|
||||
name: name.as_ref().to_string(),
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -40,6 +43,12 @@ impl<S: SourceCode + 'static> NamedSource<S> {
|
|||
pub fn inner(&self) -> &S {
|
||||
&self.source
|
||||
}
|
||||
|
||||
/// Sets the [`language`](SpanContents::language) for this source code.
|
||||
pub fn with_language(mut self, language: impl Into<String>) -> Self {
|
||||
self.language = Some(language.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: SourceCode + 'static> SourceCode for NamedSource<S> {
|
||||
|
|
@ -49,16 +58,20 @@ impl<S: SourceCode + 'static> SourceCode for NamedSource<S> {
|
|||
context_lines_before: usize,
|
||||
context_lines_after: usize,
|
||||
) -> Result<Box<dyn SpanContents<'a> + '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(),
|
||||
)))
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -433,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
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -452,6 +461,8 @@ pub struct MietteSpanContents<'a> {
|
|||
line_count: usize,
|
||||
// Optional filename
|
||||
name: Option<String>,
|
||||
// Optional language
|
||||
language: Option<String>,
|
||||
}
|
||||
|
||||
impl<'a> MietteSpanContents<'a> {
|
||||
|
|
@ -470,6 +481,7 @@ impl<'a> MietteSpanContents<'a> {
|
|||
column,
|
||||
line_count,
|
||||
name: None,
|
||||
language: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -489,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<String>) -> Self {
|
||||
self.language = Some(language.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SpanContents<'a> for MietteSpanContents<'a> {
|
||||
|
|
@ -512,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`]
|
||||
|
|
|
|||
|
|
@ -21,12 +21,14 @@ fn fmt_report(diag: Report) -> String {
|
|||
.unwrap();
|
||||
} else if let Ok(w) = std::env::var("REPLACE_TABS") {
|
||||
GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
|
||||
.without_syntax_highlighting()
|
||||
.with_width(80)
|
||||
.tab_width(w.parse().expect("Invalid tab width."))
|
||||
.render_report(&mut out, diag.as_ref())
|
||||
.unwrap();
|
||||
} else {
|
||||
GraphicalReportHandler::new_themed(GraphicalTheme::unicode_nocolor())
|
||||
.without_syntax_highlighting()
|
||||
.with_width(80)
|
||||
.render_report(&mut out, diag.as_ref())
|
||||
.unwrap();
|
||||
|
|
@ -1758,6 +1760,102 @@ fn single_line_with_wide_char_unaligned_span_empty() -> Result<(), MietteError>
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
fn syntax_highlighter() {
|
||||
std::env::set_var("REPLACE_TABS", "4");
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("This is an error")]
|
||||
#[diagnostic()]
|
||||
pub struct Test {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this is a label")]
|
||||
src_span: SourceSpan,
|
||||
}
|
||||
let src = NamedSource::new(
|
||||
"hello_world", //NOTE: intentionally missing file extension
|
||||
"fn main() {\n println!(\"Hello, World!\");\n}\n".to_string(),
|
||||
)
|
||||
.with_language("Rust");
|
||||
let err = Test {
|
||||
src,
|
||||
src_span: (16, 26).into(),
|
||||
};
|
||||
let mut out = String::new();
|
||||
GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
|
||||
.render_report(&mut out, &err)
|
||||
.unwrap();
|
||||
let expected = r#" × This is an error
|
||||
╭─[hello_world:2:5]
|
||||
1 │ fn main() {
|
||||
2 │ println!("Hello, World!");
|
||||
· ─────────────┬────────────
|
||||
· ╰── this is a label
|
||||
3 │ }
|
||||
╰────
|
||||
"#;
|
||||
assert!(out.contains("\u{1b}[38;2;180;142;173m"));
|
||||
assert_eq!(expected, strip_ansi_escapes::strip_str(out))
|
||||
}
|
||||
|
||||
// This test reads a line from the current source file and renders it with Rust
|
||||
// syntax highlighting. The goal is to test syntax highlighting on a non-trivial
|
||||
// source code example. However, if tests are running in an environment where
|
||||
// source files are missing, this will cause problems. In that case, it would
|
||||
// be better to use include_str!() on a sufficiently complex example file.
|
||||
#[test]
|
||||
#[cfg(feature = "syntect-highlighter")]
|
||||
fn syntax_highlighter_on_real_file() {
|
||||
std::env::set_var("REPLACE_TABS", "4");
|
||||
|
||||
#[derive(Debug, Error, Diagnostic)]
|
||||
#[error("This is an error")]
|
||||
#[diagnostic()]
|
||||
pub struct Test {
|
||||
#[source_code]
|
||||
src: NamedSource,
|
||||
#[label("this is a label")]
|
||||
src_span: SourceSpan,
|
||||
}
|
||||
// BEGIN SOURCE SNIPPET
|
||||
|
||||
let (filename, line) = (file!(), line!() as usize);
|
||||
|
||||
// END SOURCE SNIPPET
|
||||
// SourceSpan constants for column and length
|
||||
const CO: usize = 28;
|
||||
const LEN: usize = 27;
|
||||
let file_src = std::fs::read_to_string(&filename).unwrap();
|
||||
let offset = miette::SourceOffset::from_location(&file_src, line, CO);
|
||||
let err = Test {
|
||||
src: NamedSource::new(&filename, file_src.clone()),
|
||||
src_span: SourceSpan::new(offset, LEN.into()),
|
||||
};
|
||||
|
||||
let mut out = String::new();
|
||||
GraphicalReportHandler::new_themed(GraphicalTheme::unicode())
|
||||
.with_context_lines(1)
|
||||
.render_report(&mut out, &err)
|
||||
.unwrap();
|
||||
|
||||
let expected = format!(
|
||||
r#" × This is an error
|
||||
╭─[{filename}:{l2}:{CO}]
|
||||
{l1} │
|
||||
{l2} │ let (filename, line) = (file!(), line!() as usize);
|
||||
· ─────────────┬─────────────
|
||||
· ╰── this is a label
|
||||
{l3} │
|
||||
╰────
|
||||
"#,
|
||||
l1 = line - 1,
|
||||
l2 = line,
|
||||
l3 = line + 1
|
||||
);
|
||||
assert!(out.contains("\u{1b}[38;2;180;142;173m"));
|
||||
assert_eq!(expected, strip_ansi_escapes::strip_str(out));
|
||||
|
||||
#[test]
|
||||
fn triple_adjacent_highlight() -> Result<(), MietteError> {
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
|
|
|
|||
Loading…
Reference in New Issue