Skip to content

Commit

Permalink
feat(highlighting): add syntax highlighting support with syntect crate (
Browse files Browse the repository at this point in the history
  • Loading branch information
erratic-pattern committed Feb 4, 2024
1 parent 1df3b1a commit e65d0a7
Show file tree
Hide file tree
Showing 11 changed files with 617 additions and 23 deletions.
11 changes: 7 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
5 changes: 4 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand All @@ -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"
Expand All @@ -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"]
Expand All @@ -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"]
Expand Down
66 changes: 65 additions & 1 deletion src/handler.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
50 changes: 44 additions & 6 deletions src/handlers/graphical.rs
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -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)]
Expand All @@ -59,6 +61,7 @@ impl GraphicalReportHandler {
break_words: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
}
}

Expand All @@ -76,6 +79,7 @@ impl GraphicalReportHandler {
break_words: true,
word_separator: None,
word_splitter: None,
highlighter: MietteHighlighter::default(),
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 36 additions & 0 deletions src/highlighters/blank.rs
Original file line number Diff line number Diff line change
@@ -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)]
}
}
Loading

0 comments on commit e65d0a7

Please sign in to comment.