add syntax highlighting support with syntect crate

This commit is contained in:
Adam Curtis 2023-11-03 17:12:55 -04:00
parent 7ff4f874d6
commit 6bf9ff4775
11 changed files with 420 additions and 5 deletions

View File

@ -26,6 +26,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
features: [fancy, syntect-highlighter]
rust: [1.56.0, stable]
os: [ubuntu-latest, macOS-latest, windows-latest]
@ -43,10 +44,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

View File

@ -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.1.17", 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"
@ -57,6 +58,7 @@ fancy-no-backtrace = [
"supports-unicode",
]
fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
syntect-highlighter = ["fancy-no-backtrace", "syntect"]
[workspace]
members = ["miette-derive"]

View File

@ -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;
@ -58,6 +60,7 @@ pub struct MietteHandlerOpts {
pub(crate) break_words: Option<bool>,
pub(crate) word_separator: Option<textwrap::WordSeparator>,
pub(crate) word_splitter: Option<textwrap::WordSplitter>,
pub(crate) highlighter: Option<MietteHighlighter>,
}
impl MietteHandlerOpts {
@ -83,6 +86,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);
@ -236,11 +276,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()
.with_width(width)
.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();

View File

@ -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, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
@ -33,6 +34,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)]
@ -57,6 +59,7 @@ impl GraphicalReportHandler {
break_words: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
}
}
@ -73,6 +76,7 @@ impl GraphicalReportHandler {
break_words: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
}
}
@ -160,6 +164,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 {
@ -461,6 +482,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(source);
// 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.
@ -534,7 +557,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

34
src/highlighters/blank.rs Normal file
View File

@ -0,0 +1,34 @@
use owo_colors::Style;
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 crate::SourceCode,
) -> 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)]
}
}

116
src/highlighters/mod.rs Normal file
View File

@ -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::SourceCode;
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] snippets.
pub trait Highlighter {
/// Creates a new [HighlighterState] to begin parsing and highlighting
/// a [SourceCode] snippet.
///
/// The [GraphicalReportHandler](crate::GraphicalReportHandler) will call
/// this method at the start of rendering a [Diagnostic](crate::Diagnostic).
///
/// The source 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,
) -> 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
}
}

161
src/highlighters/syntect.rs Normal file
View File

@ -0,0 +1,161 @@
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},
SourceCode,
};
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 SourceCode,
) -> Box<dyn HighlighterState + 'h> {
if let Some(syntax) = self.get_syntax_from_source(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 get_syntax_from_source(&self, source: &dyn SourceCode) -> Option<&syntect::SyntaxReference> {
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
}
}
}
/// 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)
}

View File

@ -691,6 +691,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;

View File

@ -58,4 +58,8 @@ impl SourceCode for NamedSource {
contents.line_count(),
)))
}
fn name(&self) -> Option<&str> {
Some(&self.name)
}
}

View File

@ -240,6 +240,11 @@ pub trait SourceCode: Send + Sync {
context_lines_before: usize,
context_lines_after: usize,
) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError>;
/// Optional method. The name of this source, if any
fn name(&self) -> Option<&str> {
None
}
}
/// A labeled [`SourceSpan`].

View File

@ -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();