crates: vendor annotate-snippets crate

This merely adds the crate to our repository. Some cosmetic changes are
made to make it work in our repo and follow our conventions, such as
changing the name to `ruff_annotate_snippets`. We retain the original
license information. We do drop some things, such as benchmarks, but
keep tests and examples.
This commit is contained in:
Andrew Gallant 2024-12-20 12:55:16 -05:00 committed by Andrew Gallant
parent 4f3209a3ec
commit 9c27c57b5b
58 changed files with 6171 additions and 8 deletions

View file

@ -0,0 +1,69 @@
//! A library for formatting of text or programming code snippets.
//!
//! It's primary purpose is to build an ASCII-graphical representation of the snippet
//! with annotations.
//!
//! # Example
//!
//! ```rust
#![doc = include_str!("../examples/expected_type.rs")]
//! ```
//!
#![doc = include_str!("../examples/expected_type.svg")]
//!
//! The crate uses a three stage process with two conversions between states:
//!
//! ```text
//! Message --> Renderer --> impl Display
//! ```
//!
//! The input type - [Message] is a structure designed
//! to align with likely output from any parser whose code snippet is to be
//! annotated.
//!
//! The middle structure - [Renderer] is a structure designed
//! to convert a snippet into an internal structure that is designed to store
//! the snippet data in a way that is easy to format.
//! [Renderer] also handles the user-configurable formatting
//! options, such as color, or margins.
//!
//! Finally, `impl Display` into a final `String` output.
//!
//! # features
//! - `testing-colors` - Makes [Renderer::styled] colors OS independent, which
//! allows for easier testing when testing colored output. It should be added as
//! a feature in `[dev-dependencies]`, which can be done with the following command:
//! ```text
//! cargo add annotate-snippets --dev --feature testing-colors
//! ```
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
#![warn(missing_debug_implementations)]
// Since this is a vendored copy of `annotate-snippets`, we squash Clippy
// warnings from upstream in order to the reduce the diff. If our copy drifts
// far from upstream such that patches become impractical to apply in both
// places, then we can get rid of these suppressions and fix the lints.
#![allow(
clippy::return_self_not_must_use,
clippy::cast_possible_truncation,
clippy::cast_precision_loss,
clippy::explicit_iter_loop,
clippy::unused_self,
clippy::unnecessary_wraps,
clippy::range_plus_one,
clippy::redundant_closure_for_method_calls,
clippy::struct_field_names,
clippy::cloned_instead_of_copied,
clippy::cast_sign_loss,
clippy::needless_as_bytes,
clippy::unnecessary_map_or
)]
pub mod renderer;
mod snippet;
#[doc(inline)]
pub use renderer::Renderer;
pub use snippet::*;

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,119 @@
use std::cmp::{max, min};
const ELLIPSIS_PASSING: usize = 6;
const LONG_WHITESPACE: usize = 20;
const LONG_WHITESPACE_PADDING: usize = 4;
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Margin {
/// The available whitespace in the left that can be consumed when centering.
whitespace_left: usize,
/// The column of the beginning of left-most span.
span_left: usize,
/// The column of the end of right-most span.
span_right: usize,
/// The beginning of the line to be displayed.
computed_left: usize,
/// The end of the line to be displayed.
computed_right: usize,
/// The current width of the terminal. 140 by default and in tests.
term_width: usize,
/// The end column of a span label, including the span. Doesn't account for labels not in the
/// same line as the span.
label_right: usize,
}
impl Margin {
pub(crate) fn new(
whitespace_left: usize,
span_left: usize,
span_right: usize,
label_right: usize,
term_width: usize,
max_line_len: usize,
) -> Self {
// The 6 is padding to give a bit of room for `...` when displaying:
// ```
// error: message
// --> file.rs:16:58
// |
// 16 | ... fn foo(self) -> Self::Bar {
// | ^^^^^^^^^
// ```
let mut m = Margin {
whitespace_left: whitespace_left.saturating_sub(ELLIPSIS_PASSING),
span_left: span_left.saturating_sub(ELLIPSIS_PASSING),
span_right: span_right + ELLIPSIS_PASSING,
computed_left: 0,
computed_right: 0,
term_width,
label_right: label_right + ELLIPSIS_PASSING,
};
m.compute(max_line_len);
m
}
pub(crate) fn was_cut_left(&self) -> bool {
self.computed_left > 0
}
pub(crate) fn was_cut_right(&self, line_len: usize) -> bool {
let right =
if self.computed_right == self.span_right || self.computed_right == self.label_right {
// Account for the "..." padding given above. Otherwise we end up with code lines that
// do fit but end in "..." as if they were trimmed.
self.computed_right - ELLIPSIS_PASSING
} else {
self.computed_right
};
right < line_len && self.computed_left + self.term_width < line_len
}
fn compute(&mut self, max_line_len: usize) {
// When there's a lot of whitespace (>20), we want to trim it as it is useless.
self.computed_left = if self.whitespace_left > LONG_WHITESPACE {
self.whitespace_left - (LONG_WHITESPACE - LONG_WHITESPACE_PADDING) // We want some padding.
} else {
0
};
// We want to show as much as possible, max_line_len is the right-most boundary for the
// relevant code.
self.computed_right = max(max_line_len, self.computed_left);
if self.computed_right - self.computed_left > self.term_width {
// Trimming only whitespace isn't enough, let's get craftier.
if self.label_right - self.whitespace_left <= self.term_width {
// Attempt to fit the code window only trimming whitespace.
self.computed_left = self.whitespace_left;
self.computed_right = self.computed_left + self.term_width;
} else if self.label_right - self.span_left <= self.term_width {
// Attempt to fit the code window considering only the spans and labels.
let padding_left = (self.term_width - (self.label_right - self.span_left)) / 2;
self.computed_left = self.span_left.saturating_sub(padding_left);
self.computed_right = self.computed_left + self.term_width;
} else if self.span_right - self.span_left <= self.term_width {
// Attempt to fit the code window considering the spans and labels plus padding.
let padding_left = (self.term_width - (self.span_right - self.span_left)) / 5 * 2;
self.computed_left = self.span_left.saturating_sub(padding_left);
self.computed_right = self.computed_left + self.term_width;
} else {
// Mostly give up but still don't show the full line.
self.computed_left = self.span_left;
self.computed_right = self.span_right;
}
}
}
pub(crate) fn left(&self, line_len: usize) -> usize {
min(self.computed_left, line_len)
}
pub(crate) fn right(&self, line_len: usize) -> usize {
if line_len.saturating_sub(self.computed_left) <= self.term_width {
line_len
} else {
min(line_len, self.computed_right)
}
}
}

View file

@ -0,0 +1,163 @@
//! The renderer for [`Message`]s
//!
//! # Example
//! ```
//! use ruff_annotate_snippets::{Renderer, Snippet, Level};
//! let snippet = Level::Error.title("mismatched types")
//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs"))
//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs"));
//!
//! let renderer = Renderer::styled();
//! println!("{}", renderer.render(snippet));
mod display_list;
mod margin;
mod styled_buffer;
pub(crate) mod stylesheet;
use crate::snippet::Message;
pub use anstyle::*;
use display_list::DisplayList;
use margin::Margin;
use std::fmt::Display;
use stylesheet::Stylesheet;
pub const DEFAULT_TERM_WIDTH: usize = 140;
/// A renderer for [`Message`]s
#[derive(Clone, Debug)]
pub struct Renderer {
anonymized_line_numbers: bool,
term_width: usize,
stylesheet: Stylesheet,
}
impl Renderer {
/// No terminal styling
pub const fn plain() -> Self {
Self {
anonymized_line_numbers: false,
term_width: DEFAULT_TERM_WIDTH,
stylesheet: Stylesheet::plain(),
}
}
/// Default terminal styling
///
/// # Note
/// When testing styled terminal output, see the [`testing-colors` feature](crate#features)
pub const fn styled() -> Self {
const USE_WINDOWS_COLORS: bool = cfg!(windows) && !cfg!(feature = "testing-colors");
const BRIGHT_BLUE: Style = if USE_WINDOWS_COLORS {
AnsiColor::BrightCyan.on_default()
} else {
AnsiColor::BrightBlue.on_default()
};
Self {
stylesheet: Stylesheet {
error: AnsiColor::BrightRed.on_default().effects(Effects::BOLD),
warning: if USE_WINDOWS_COLORS {
AnsiColor::BrightYellow.on_default()
} else {
AnsiColor::Yellow.on_default()
}
.effects(Effects::BOLD),
info: BRIGHT_BLUE.effects(Effects::BOLD),
note: AnsiColor::BrightGreen.on_default().effects(Effects::BOLD),
help: AnsiColor::BrightCyan.on_default().effects(Effects::BOLD),
line_no: BRIGHT_BLUE.effects(Effects::BOLD),
emphasis: if USE_WINDOWS_COLORS {
AnsiColor::BrightWhite.on_default()
} else {
Style::new()
}
.effects(Effects::BOLD),
none: Style::new(),
},
..Self::plain()
}
}
/// Anonymize line numbers
///
/// This enables (or disables) line number anonymization. When enabled, line numbers are replaced
/// with `LL`.
///
/// # Example
///
/// ```text
/// --> $DIR/whitespace-trimming.rs:4:193
/// |
/// LL | ... let _: () = 42;
/// | ^^ expected (), found integer
/// |
/// ```
pub const fn anonymized_line_numbers(mut self, anonymized_line_numbers: bool) -> Self {
self.anonymized_line_numbers = anonymized_line_numbers;
self
}
// Set the terminal width
pub const fn term_width(mut self, term_width: usize) -> Self {
self.term_width = term_width;
self
}
/// Set the output style for `error`
pub const fn error(mut self, style: Style) -> Self {
self.stylesheet.error = style;
self
}
/// Set the output style for `warning`
pub const fn warning(mut self, style: Style) -> Self {
self.stylesheet.warning = style;
self
}
/// Set the output style for `info`
pub const fn info(mut self, style: Style) -> Self {
self.stylesheet.info = style;
self
}
/// Set the output style for `note`
pub const fn note(mut self, style: Style) -> Self {
self.stylesheet.note = style;
self
}
/// Set the output style for `help`
pub const fn help(mut self, style: Style) -> Self {
self.stylesheet.help = style;
self
}
/// Set the output style for line numbers
pub const fn line_no(mut self, style: Style) -> Self {
self.stylesheet.line_no = style;
self
}
/// Set the output style for emphasis
pub const fn emphasis(mut self, style: Style) -> Self {
self.stylesheet.emphasis = style;
self
}
/// Set the output style for none
pub const fn none(mut self, style: Style) -> Self {
self.stylesheet.none = style;
self
}
/// Render a snippet into a `Display`able object
pub fn render<'a>(&'a self, msg: Message<'a>) -> impl Display + 'a {
DisplayList::new(
msg,
&self.stylesheet,
self.anonymized_line_numbers,
self.term_width,
)
}
}

View file

@ -0,0 +1,97 @@
//! Adapted from [styled_buffer]
//!
//! [styled_buffer]: https://github.com/rust-lang/rust/blob/894f7a4ba6554d3797404bbf550d9919df060b97/compiler/rustc_errors/src/styled_buffer.rs
use crate::renderer::stylesheet::Stylesheet;
use anstyle::Style;
use std::fmt;
use std::fmt::Write;
#[derive(Debug)]
pub(crate) struct StyledBuffer {
lines: Vec<Vec<StyledChar>>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct StyledChar {
ch: char,
style: Style,
}
impl StyledChar {
pub(crate) const SPACE: Self = StyledChar::new(' ', Style::new());
pub(crate) const fn new(ch: char, style: Style) -> StyledChar {
StyledChar { ch, style }
}
}
impl StyledBuffer {
pub(crate) fn new() -> StyledBuffer {
StyledBuffer { lines: vec![] }
}
fn ensure_lines(&mut self, line: usize) {
if line >= self.lines.len() {
self.lines.resize(line + 1, Vec::new());
}
}
pub(crate) fn render(&self, stylesheet: &Stylesheet) -> Result<String, fmt::Error> {
let mut str = String::new();
for (i, line) in self.lines.iter().enumerate() {
let mut current_style = stylesheet.none;
for ch in line {
if ch.style != current_style {
if !line.is_empty() {
write!(str, "{}", current_style.render_reset())?;
}
current_style = ch.style;
write!(str, "{}", current_style.render())?;
}
write!(str, "{}", ch.ch)?;
}
write!(str, "{}", current_style.render_reset())?;
if i != self.lines.len() - 1 {
writeln!(str)?;
}
}
Ok(str)
}
/// Sets `chr` with `style` for given `line`, `col`.
/// If `line` does not exist in our buffer, adds empty lines up to the given
/// and fills the last line with unstyled whitespace.
pub(crate) fn putc(&mut self, line: usize, col: usize, chr: char, style: Style) {
self.ensure_lines(line);
if col >= self.lines[line].len() {
self.lines[line].resize(col + 1, StyledChar::SPACE);
}
self.lines[line][col] = StyledChar::new(chr, style);
}
/// Sets `string` with `style` for given `line`, starting from `col`.
/// If `line` does not exist in our buffer, adds empty lines up to the given
/// and fills the last line with unstyled whitespace.
pub(crate) fn puts(&mut self, line: usize, col: usize, string: &str, style: Style) {
let mut n = col;
for c in string.chars() {
self.putc(line, n, c, style);
n += 1;
}
}
/// For given `line` inserts `string` with `style` after old content of that line,
/// adding lines if needed
pub(crate) fn append(&mut self, line: usize, string: &str, style: Style) {
if line >= self.lines.len() {
self.puts(line, 0, string, style);
} else {
let col = self.lines[line].len();
self.puts(line, col, string, style);
}
}
pub(crate) fn num_lines(&self) -> usize {
self.lines.len()
}
}

View file

@ -0,0 +1,68 @@
use anstyle::Style;
#[derive(Clone, Copy, Debug)]
pub(crate) struct Stylesheet {
pub(crate) error: Style,
pub(crate) warning: Style,
pub(crate) info: Style,
pub(crate) note: Style,
pub(crate) help: Style,
pub(crate) line_no: Style,
pub(crate) emphasis: Style,
pub(crate) none: Style,
}
impl Default for Stylesheet {
fn default() -> Self {
Self::plain()
}
}
impl Stylesheet {
pub(crate) const fn plain() -> Self {
Self {
error: Style::new(),
warning: Style::new(),
info: Style::new(),
note: Style::new(),
help: Style::new(),
line_no: Style::new(),
emphasis: Style::new(),
none: Style::new(),
}
}
}
impl Stylesheet {
pub(crate) fn error(&self) -> &Style {
&self.error
}
pub(crate) fn warning(&self) -> &Style {
&self.warning
}
pub(crate) fn info(&self) -> &Style {
&self.info
}
pub(crate) fn note(&self) -> &Style {
&self.note
}
pub(crate) fn help(&self) -> &Style {
&self.help
}
pub(crate) fn line_no(&self) -> &Style {
&self.line_no
}
pub(crate) fn emphasis(&self) -> &Style {
&self.emphasis
}
pub(crate) fn none(&self) -> &Style {
&self.none
}
}

View file

@ -0,0 +1,157 @@
//! Structures used as an input for the library.
//!
//! Example:
//!
//! ```
//! use ruff_annotate_snippets::*;
//!
//! Level::Error.title("mismatched types")
//! .snippet(Snippet::source("Foo").line_start(51).origin("src/format.rs"))
//! .snippet(Snippet::source("Faa").line_start(129).origin("src/display.rs"));
//! ```
use std::ops::Range;
/// Primary structure provided for formatting
///
/// See [`Level::title`] to create a [`Message`]
#[derive(Debug)]
pub struct Message<'a> {
pub(crate) level: Level,
pub(crate) id: Option<&'a str>,
pub(crate) title: &'a str,
pub(crate) snippets: Vec<Snippet<'a>>,
pub(crate) footer: Vec<Message<'a>>,
}
impl<'a> Message<'a> {
pub fn id(mut self, id: &'a str) -> Self {
self.id = Some(id);
self
}
pub fn snippet(mut self, slice: Snippet<'a>) -> Self {
self.snippets.push(slice);
self
}
pub fn snippets(mut self, slice: impl IntoIterator<Item = Snippet<'a>>) -> Self {
self.snippets.extend(slice);
self
}
pub fn footer(mut self, footer: Message<'a>) -> Self {
self.footer.push(footer);
self
}
pub fn footers(mut self, footer: impl IntoIterator<Item = Message<'a>>) -> Self {
self.footer.extend(footer);
self
}
}
/// Structure containing the slice of text to be annotated and
/// basic information about the location of the slice.
///
/// One `Snippet` is meant to represent a single, continuous,
/// slice of source code that you want to annotate.
#[derive(Debug)]
pub struct Snippet<'a> {
pub(crate) origin: Option<&'a str>,
pub(crate) line_start: usize,
pub(crate) source: &'a str,
pub(crate) annotations: Vec<Annotation<'a>>,
pub(crate) fold: bool,
}
impl<'a> Snippet<'a> {
pub fn source(source: &'a str) -> Self {
Self {
origin: None,
line_start: 1,
source,
annotations: vec![],
fold: false,
}
}
pub fn line_start(mut self, line_start: usize) -> Self {
self.line_start = line_start;
self
}
pub fn origin(mut self, origin: &'a str) -> Self {
self.origin = Some(origin);
self
}
pub fn annotation(mut self, annotation: Annotation<'a>) -> Self {
self.annotations.push(annotation);
self
}
pub fn annotations(mut self, annotation: impl IntoIterator<Item = Annotation<'a>>) -> Self {
self.annotations.extend(annotation);
self
}
/// Hide lines without [`Annotation`]s
pub fn fold(mut self, fold: bool) -> Self {
self.fold = fold;
self
}
}
/// An annotation for a [`Snippet`].
///
/// See [`Level::span`] to create a [`Annotation`]
#[derive(Debug)]
pub struct Annotation<'a> {
/// The byte range of the annotation in the `source` string
pub(crate) range: Range<usize>,
pub(crate) label: Option<&'a str>,
pub(crate) level: Level,
}
impl<'a> Annotation<'a> {
pub fn label(mut self, label: &'a str) -> Self {
self.label = Some(label);
self
}
}
/// Types of annotations.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Level {
/// Error annotations are displayed using red color and "^" character.
Error,
/// Warning annotations are displayed using blue color and "-" character.
Warning,
Info,
Note,
Help,
}
impl Level {
pub fn title(self, title: &str) -> Message<'_> {
Message {
level: self,
id: None,
title,
snippets: vec![],
footer: vec![],
}
}
/// Create a [`Annotation`] with the given span for a [`Snippet`]
pub fn span<'a>(self, span: Range<usize>) -> Annotation<'a> {
Annotation {
range: span,
label: None,
level: self,
}
}
}