Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 468a7a1

Browse files
committedNov 3, 2023
add syntax highlighting support with syntect crate
1 parent d37ada8 commit 468a7a1

File tree

8 files changed

+292
-4
lines changed

8 files changed

+292
-4
lines changed
 

‎.github/workflows/ci.yml

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ jobs:
2626
runs-on: ${{ matrix.os }}
2727
strategy:
2828
matrix:
29+
features: [fancy, syntect]
2930
rust: [1.56.0, stable]
3031
os: [ubuntu-latest, macOS-latest, windows-latest]
3132

@@ -43,10 +44,10 @@ jobs:
4344
run: cargo clippy --all -- -D warnings
4445
- name: Run tests
4546
if: matrix.rust == 'stable'
46-
run: cargo test --all --verbose --features fancy
47+
run: cargo test --all --verbose --features ${{matrix.features}}
4748
- name: Run tests
4849
if: matrix.rust == '1.56.0'
49-
run: cargo test --all --verbose --features fancy no-format-args-capture
50+
run: cargo test --all --verbose --features ${{matrix.features}} no-format-args-capture
5051

5152
miri:
5253
name: Miri

‎Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ backtrace = { version = "0.3.61", optional = true }
2828
terminal_size = { version = "0.1.17", optional = true }
2929
backtrace-ext = { version = "0.2.1", optional = true }
3030
serde = { version = "1.0.162", features = ["derive"], optional = true }
31+
syntect = { version = "5.1.0", optional = true }
3132

3233
[dev-dependencies]
3334
semver = "1.0.4"
@@ -56,6 +57,7 @@ fancy-no-backtrace = [
5657
"supports-unicode",
5758
]
5859
fancy = ["fancy-no-backtrace", "backtrace", "backtrace-ext"]
60+
syntect = ["fancy", "dep:syntect"]
5961

6062
[workspace]
6163
members = ["miette-derive"]

‎src/handlers/graphical.rs

+35-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
use std::fmt::{self, Write};
22

3-
use owo_colors::{OwoColorize, Style};
3+
use owo_colors::{OwoColorize, Style, Styled};
44
use unicode_width::UnicodeWidthChar;
55

66
use crate::diagnostic_chain::{DiagnosticChain, ErrorKind};
77
use crate::handlers::theme::*;
8+
use crate::highlighters::get_highlighter;
89
use crate::protocol::{Diagnostic, Severity};
910
use crate::{LabeledSpan, MietteError, ReportHandler, SourceCode, SourceSpan, SpanContents};
1011

@@ -403,6 +404,9 @@ impl GraphicalReportHandler {
403404
.map(|(label, st)| FancySpan::new(label.label().map(String::from), *label.inner(), st))
404405
.collect::<Vec<_>>();
405406

407+
let mut highlighter_state =
408+
get_highlighter().and_then(|h| h.start_highlighter_state(source));
409+
406410
// The max number of gutter-lines that will be active at any given
407411
// point. We need this to figure out indentation, so we do one loop
408412
// over the lines to see what the damage is gonna be.
@@ -476,7 +480,12 @@ impl GraphicalReportHandler {
476480
self.render_line_gutter(f, max_gutter, line, &labels)?;
477481

478482
// And _now_ we can print out the line text itself!
479-
self.render_line_text(f, &line.text)?;
483+
if let Some(ref mut highlighter_state) = highlighter_state {
484+
let styled_text = highlighter_state.highlight_line(&line.text);
485+
self.render_highlighted_line_text(f, &styled_text)?;
486+
} else {
487+
self.render_line_text(f, &line.text)?;
488+
}
480489

481490
// Next, we write all the highlights that apply to this particular line.
482491
let (single_line, multi_line): (Vec<_>, Vec<_>) = labels
@@ -688,6 +697,30 @@ impl GraphicalReportHandler {
688697
Ok(())
689698
}
690699

700+
fn render_highlighted_line_text(
701+
&self,
702+
f: &mut impl fmt::Write,
703+
styled_text: &[Styled<&str>],
704+
) -> fmt::Result {
705+
for styled in styled_text {
706+
let styled_text = styled.to_string();
707+
for (c, width) in styled_text
708+
.chars()
709+
.zip(self.line_visual_char_width(&styled_text))
710+
{
711+
if c == '\t' {
712+
for _ in 0..width {
713+
f.write_char(' ')?;
714+
}
715+
} else {
716+
f.write_char(c)?;
717+
}
718+
}
719+
}
720+
f.write_char('\n')?;
721+
Ok(())
722+
}
723+
691724
fn render_single_line_highlights(
692725
&self,
693726
f: &mut impl fmt::Write,

‎src/highlighters/mod.rs

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//! This module exposes traits for creating syntax highlighters to highlight source code with
2+
//! ANSI terminal codes when rendering with the [crate::GraphicalReportHandler].
3+
//!
4+
//! By default, there are no syntax highlighters exported by miette. To enable support for
5+
//! specific highlighters, you should enable their associated feature flag.
6+
//!
7+
//! Currently supported syntax highlighters and their feature flags:
8+
//! * `syntect` - Enables [syntect] syntax highlighting support via the [crate::highlighters::SyntectHighlighter]
9+
//!
10+
//! Docs for these highlighters can be found in the [crate::highlighters] module.
11+
//!
12+
13+
use std::error::Error as StdError;
14+
15+
use crate::{Diagnostic, SourceCode};
16+
use once_cell::sync::OnceCell;
17+
use owo_colors::Styled;
18+
19+
#[cfg(feature = "syntect")]
20+
pub use syntect::*;
21+
22+
#[cfg(feature = "syntect")]
23+
mod syntect;
24+
25+
/// A syntax highlighter for highlighting miette [SourceCode] snippets.
26+
pub trait Highlighter {
27+
/// Creates a new [HighlighterState] to begin parsing and highlighting a [SourceCode] snippets.
28+
///
29+
/// The [crate::GraphicalReportHandler] will call this function at the start of rendering
30+
/// to highlight lines of source code.
31+
fn start_highlighter_state<'h>(
32+
&'h self,
33+
source: &dyn SourceCode,
34+
) -> Option<Box<dyn HighlighterState + 'h>>;
35+
}
36+
37+
/// A stateful highlighting iterator over lines of source code.
38+
///
39+
/// Use [Highlighter::start_highlighter_state] to create a HighlighterState, which
40+
/// can then be used to iteratively produce highlighted regions of text with
41+
/// the [HighlighterState::highlight_line] method.
42+
pub trait HighlighterState {
43+
/// Highlight an individual line from the source code by returning a vector of [Styled]
44+
/// regions.
45+
fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>>;
46+
}
47+
48+
static HIGHLIGHTER: OnceCell<Option<Box<dyn Highlighter + Send + Sync>>> = OnceCell::new();
49+
50+
/// Error indicating that [`set_highlighter()`] was unable to install the provided
51+
/// [`Highlighter`]
52+
#[derive(Debug)]
53+
pub struct HighlighterInstallError;
54+
55+
impl core::fmt::Display for HighlighterInstallError {
56+
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
57+
f.write_str("cannot install provided Highlighter, a hook has already been installed")
58+
}
59+
}
60+
61+
impl StdError for HighlighterInstallError {}
62+
impl Diagnostic for HighlighterInstallError {}
63+
64+
/// Set the [`Highlighter`] to use with [`crate::GraphicalReportHandler`]
65+
///
66+
/// If compiling with the `syntect` feature, it will automatically use the default
67+
/// [`SyntectHighlighter`] and you will only want to call this if you're overriding
68+
/// the default highlighting theme and settings.
69+
pub fn set_highlighter(
70+
highlighter: impl Highlighter + Send + Sync + 'static,
71+
) -> Result<(), HighlighterInstallError> {
72+
HIGHLIGHTER
73+
.set(Some(Box::new(highlighter)))
74+
.map_err(|_| HighlighterInstallError)
75+
}
76+
77+
/// Retrieves the current global [`Highlighter`]
78+
pub(crate) fn get_highlighter() -> Option<&'static (dyn Highlighter + Send + Sync)> {
79+
HIGHLIGHTER.get_or_init(|| default_highlighter()).as_deref()
80+
}
81+
82+
/// Default [`Highlighter`]. Uses feature flags to determine
83+
///
84+
/// NOTE: if/when multiple highlighters are supported, this should panic with
85+
/// a helpful message if multiple are enabled.
86+
fn default_highlighter() -> Option<Box<dyn Highlighter + Send + Sync>> {
87+
#[cfg(feature = "syntect")]
88+
return Some(Box::new(SyntectHighlighter::default()));
89+
#[cfg(not(feature = "syntect"))]
90+
return None;
91+
}

‎src/highlighters/syntect.rs

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use std::path::Path;
2+
3+
// all syntect imports are explicitly qualified, but their paths are shortened for convenience
4+
mod syntect {
5+
pub(super) use syntect::{
6+
highlighting::{
7+
Color, HighlightIterator, HighlightState, Highlighter, Style, Theme, ThemeSet,
8+
},
9+
parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet},
10+
};
11+
}
12+
13+
use owo_colors::{Rgb, Style, Styled};
14+
15+
use crate::{
16+
highlighters::{Highlighter, HighlighterState},
17+
SourceCode,
18+
};
19+
20+
/// Highlights miette [SourceCode] with the [syntect] highlighting crate.
21+
#[derive(Debug, Clone)]
22+
pub struct SyntectHighlighter {
23+
theme: syntect::Theme,
24+
syntax_set: syntect::SyntaxSet,
25+
}
26+
27+
impl Default for SyntectHighlighter {
28+
fn default() -> Self {
29+
let syntax_set = syntect::SyntaxSet::load_defaults_nonewlines();
30+
let theme_set = syntect::ThemeSet::load_defaults();
31+
let theme = theme_set.themes.get("base16-ocean.dark").unwrap().clone();
32+
Self::new(theme, syntax_set)
33+
}
34+
}
35+
36+
impl Highlighter for SyntectHighlighter {
37+
fn start_highlighter_state<'h>(
38+
&'h self,
39+
source: &dyn SourceCode,
40+
) -> Option<Box<dyn HighlighterState + 'h>> {
41+
let syntax = self.get_syntax_from_source(source)?;
42+
let highlighter = syntect::Highlighter::new(&self.theme);
43+
let parse_state = syntect::ParseState::new(syntax);
44+
let highlight_state =
45+
syntect::HighlightState::new(&highlighter, syntect::ScopeStack::new());
46+
Some(Box::new(SyntectHighlighterState {
47+
syntax_set: &self.syntax_set,
48+
highlighter,
49+
parse_state,
50+
highlight_state,
51+
}))
52+
}
53+
}
54+
55+
impl SyntectHighlighter {
56+
/// Create a SyntectHighlighter
57+
pub fn new(theme: syntect::Theme, syntax_set: syntect::SyntaxSet) -> Self {
58+
Self { theme, syntax_set }
59+
}
60+
61+
/// Determine syntect SyntaxReference to use for given SourceCode
62+
fn get_syntax_from_source(&self, source: &dyn SourceCode) -> Option<&syntect::SyntaxReference> {
63+
if let Some(language) = source.language() {
64+
self.syntax_set.find_syntax_by_name(language)
65+
} else if let Some(name) = source.name() {
66+
if let Some(ext) = Path::new(name).extension() {
67+
self.syntax_set
68+
.find_syntax_by_extension(ext.to_string_lossy().as_ref())
69+
} else {
70+
None
71+
}
72+
} else {
73+
None
74+
}
75+
}
76+
}
77+
78+
/// Stateful highlighting iterator for [SyntectHighlighter]
79+
#[derive(Debug)]
80+
pub struct SyntectHighlighterState<'h> {
81+
syntax_set: &'h syntect::SyntaxSet,
82+
highlighter: syntect::Highlighter<'h>,
83+
parse_state: syntect::ParseState,
84+
highlight_state: syntect::HighlightState,
85+
}
86+
87+
impl<'h> HighlighterState for SyntectHighlighterState<'h> {
88+
fn highlight_line<'s>(&mut self, line: &'s str) -> Vec<Styled<&'s str>> {
89+
let use_bg_color = false;
90+
let ops = self.parse_state.parse_line(line, &self.syntax_set).unwrap();
91+
syntect::HighlightIterator::new(
92+
&mut self.highlight_state,
93+
&ops,
94+
line,
95+
&mut self.highlighter,
96+
)
97+
.map(|(style, str)| (convert_style(style, use_bg_color).style(str)))
98+
.collect()
99+
}
100+
}
101+
102+
/* Convert syntect Style into owo_colors Style */
103+
#[inline]
104+
fn convert_style(syntect_style: syntect::Style, use_bg_color: bool) -> Style {
105+
if use_bg_color {
106+
let fg = blend_fg_color(syntect_style);
107+
let bg = convert_color(syntect_style.background);
108+
Style::new().color(fg).on_color(bg)
109+
} else {
110+
let fg = convert_color(syntect_style.foreground);
111+
Style::new().color(fg)
112+
}
113+
}
114+
115+
/* Blend foreground RGB into background RGB according to alpha channel */
116+
#[inline]
117+
fn blend_fg_color(syntect_style: syntect::Style) -> Rgb {
118+
let fg = syntect_style.foreground;
119+
if fg.a == 0xff {
120+
return convert_color(fg);
121+
}
122+
let bg = syntect_style.background;
123+
let ratio = fg.a as u32;
124+
let r = (fg.r as u32 * ratio + bg.r as u32 * (255 - ratio)) / 255;
125+
let g = (fg.g as u32 * ratio + bg.g as u32 * (255 - ratio)) / 255;
126+
let b = (fg.b as u32 * ratio + bg.b as u32 * (255 - ratio)) / 255;
127+
Rgb(r as u8, g as u8, b as u8)
128+
}
129+
130+
/** Convert syntect color into owo color.
131+
*
132+
* Note: ignores alpha channel. use blend_fg_color if you need that
133+
*/
134+
#[inline]
135+
fn convert_color(color: syntect::Color) -> Rgb {
136+
Rgb(color.r, color.g, color.b)
137+
}

‎src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -672,6 +672,8 @@ mod eyreish;
672672
#[cfg(feature = "fancy-no-backtrace")]
673673
mod handler;
674674
mod handlers;
675+
#[cfg(feature = "fancy-no-backtrace")]
676+
pub mod highlighters;
675677
#[doc(hidden)]
676678
pub mod macro_helpers;
677679
mod miette_diagnostic;

‎src/named_source.rs

+8
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,12 @@ impl SourceCode for NamedSource {
5858
contents.line_count(),
5959
)))
6060
}
61+
62+
fn name(&self) -> Option<&str> {
63+
Some(&self.name)
64+
}
65+
66+
fn language(&self) -> Option<&str> {
67+
self.source.language()
68+
}
6169
}

‎src/protocol.rs

+14
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,20 @@ pub trait SourceCode: Send + Sync {
240240
context_lines_before: usize,
241241
context_lines_after: usize,
242242
) -> Result<Box<dyn SpanContents<'a> + 'a>, MietteError>;
243+
244+
/// Optional method. The name of this source, if any
245+
fn name(&self) -> Option<&str> {
246+
None
247+
}
248+
249+
/// Optional method. The language name for this source code, if any.
250+
/// This is used to drive syntax highlighting.
251+
///
252+
/// Examples: Rust, TOML, C
253+
///
254+
fn language(&self) -> Option<&str> {
255+
None
256+
}
243257
}
244258

245259
/// A labeled [`SourceSpan`].

0 commit comments

Comments
 (0)
Please sign in to comment.