Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add syntax highlighting support with syntect crate #313

Merged
merged 11 commits into from
Feb 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.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"
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
Loading