Skip to content

Commit 6a1c10b

Browse files
committed
Add a simple markdown parser for formatting rustc --explain
Currently, the output of `rustc --explain foo` displays the raw markdown in a pager. This is acceptable, but using actual formatting makes it easier to understand. This patch consists of three major components: 1. A markdown parser. This is an extremely simple non-backtracking recursive implementation that requires normalization of the final token stream 2. A utility to write the token stream to an output buffer 3. Configuration within rustc_driver_impl to invoke this combination for `--explain`. Like the current implementation, it first attempts to print to a pager with a fallback colorized terminal, and standard print as a last resort. If color is disabled, or if the output does not support it, or if printing with color fails, it will write the raw markdown (which matches current behavior). Pagers known to support color are: `less` (with `-r`), `bat` (aka `catbat`), and `delta`. The markdown parser does not support the entire markdown specification, but should support the following with reasonable accuracy: - Headings, including formatting - Comments - Code, inline and fenced block (no indented block) - Strong, emphasis, and strikethrough formatted text - Links, anchor, inline, and reference-style - Horizontal rules - Unordered and ordered list items, including formatting This parser and writer should be reusable by other systems if ever needed.
1 parent 8aed93d commit 6a1c10b

File tree

15 files changed

+1408
-19
lines changed

15 files changed

+1408
-19
lines changed

Cargo.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4746,9 +4746,9 @@ dependencies = [
47464746

47474747
[[package]]
47484748
name = "termcolor"
4749-
version = "1.1.3"
4749+
version = "1.2.0"
47504750
source = "registry+https://github.com/rust-lang/crates.io-index"
4751-
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
4751+
checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
47524752
dependencies = [
47534753
"winapi-util",
47544754
]

compiler/rustc_driver_impl/src/lib.rs

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use rustc_data_structures::profiling::{
2424
};
2525
use rustc_data_structures::sync::SeqCst;
2626
use rustc_errors::registry::{InvalidErrorCode, Registry};
27+
use rustc_errors::{markdown, ColorConfig};
2728
use rustc_errors::{
2829
DiagnosticMessage, ErrorGuaranteed, Handler, PResult, SubdiagnosticMessage, TerminalUrl,
2930
};
@@ -282,7 +283,7 @@ fn run_compiler(
282283
interface::set_thread_safe_mode(&sopts.unstable_opts);
283284

284285
if let Some(ref code) = matches.opt_str("explain") {
285-
handle_explain(&early_error_handler, diagnostics_registry(), code);
286+
handle_explain(&early_error_handler, diagnostics_registry(), code, sopts.color);
286287
return Ok(());
287288
}
288289

@@ -540,7 +541,7 @@ impl Compilation {
540541
}
541542
}
542543

543-
fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
544+
fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str, color: ColorConfig) {
544545
let upper_cased_code = code.to_ascii_uppercase();
545546
let normalised =
546547
if upper_cased_code.starts_with('E') { upper_cased_code } else { format!("E{code:0>4}") };
@@ -564,7 +565,7 @@ fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
564565
text.push('\n');
565566
}
566567
if io::stdout().is_terminal() {
567-
show_content_with_pager(&text);
568+
show_md_content_with_pager(&text, color);
568569
} else {
569570
safe_print!("{text}");
570571
}
@@ -575,34 +576,72 @@ fn handle_explain(handler: &EarlyErrorHandler, registry: Registry, code: &str) {
575576
}
576577
}
577578

578-
fn show_content_with_pager(content: &str) {
579+
/// If color is always or auto, print formatted & colorized markdown. If color is never or
580+
/// if formatted printing fails, print the raw text.
581+
///
582+
/// Prefers a pager, falls back standard print
583+
fn show_md_content_with_pager(content: &str, color: ColorConfig) {
584+
let mut fallback_to_println = false;
579585
let pager_name = env::var_os("PAGER").unwrap_or_else(|| {
580586
if cfg!(windows) { OsString::from("more.com") } else { OsString::from("less") }
581587
});
582588

583-
let mut fallback_to_println = false;
589+
let mut cmd = Command::new(&pager_name);
590+
// FIXME: find if other pagers accept color options
591+
let mut print_formatted = if pager_name == "less" {
592+
cmd.arg("-r");
593+
true
594+
} else if ["bat", "catbat", "delta"].iter().any(|v| *v == pager_name) {
595+
true
596+
} else {
597+
false
598+
};
584599

585-
match Command::new(pager_name).stdin(Stdio::piped()).spawn() {
586-
Ok(mut pager) => {
587-
if let Some(pipe) = pager.stdin.as_mut() {
588-
if pipe.write_all(content.as_bytes()).is_err() {
589-
fallback_to_println = true;
590-
}
591-
}
600+
if color == ColorConfig::Never {
601+
print_formatted = false;
602+
} else if color == ColorConfig::Always {
603+
print_formatted = true;
604+
}
605+
606+
let mdstream = markdown::MdStream::parse_str(content);
607+
let bufwtr = markdown::create_stdout_bufwtr();
608+
let mut mdbuf = bufwtr.buffer();
609+
if mdstream.write_termcolor_buf(&mut mdbuf).is_err() {
610+
print_formatted = false;
611+
}
592612

593-
if pager.wait().is_err() {
613+
if let Ok(mut pager) = cmd.stdin(Stdio::piped()).spawn() {
614+
if let Some(pipe) = pager.stdin.as_mut() {
615+
let res = if print_formatted {
616+
pipe.write_all(mdbuf.as_slice())
617+
} else {
618+
pipe.write_all(content.as_bytes())
619+
};
620+
621+
if res.is_err() {
594622
fallback_to_println = true;
595623
}
596624
}
597-
Err(_) => {
625+
626+
if pager.wait().is_err() {
598627
fallback_to_println = true;
599628
}
629+
} else {
630+
fallback_to_println = true;
600631
}
601632

602633
// If pager fails for whatever reason, we should still print the content
603634
// to standard output
604635
if fallback_to_println {
605-
safe_print!("{content}");
636+
let fmt_success = match color {
637+
ColorConfig::Auto => io::stdout().is_terminal() && bufwtr.print(&mdbuf).is_ok(),
638+
ColorConfig::Always => bufwtr.print(&mdbuf).is_ok(),
639+
ColorConfig::Never => false,
640+
};
641+
642+
if !fmt_success {
643+
safe_print!("{content}");
644+
}
606645
}
607646
}
608647

compiler/rustc_errors/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ rustc_hir = { path = "../rustc_hir" }
2020
rustc_lint_defs = { path = "../rustc_lint_defs" }
2121
rustc_type_ir = { path = "../rustc_type_ir" }
2222
unicode-width = "0.1.4"
23-
termcolor = "1.0"
23+
termcolor = "1.2.0"
2424
annotate-snippets = "0.9"
2525
termize = "0.1.1"
2626
serde = { version = "1.0.125", features = [ "derive" ] }

compiler/rustc_errors/src/emitter.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ pub enum ColorConfig {
616616
}
617617

618618
impl ColorConfig {
619-
fn to_color_choice(self) -> ColorChoice {
619+
pub fn to_color_choice(self) -> ColorChoice {
620620
match self {
621621
ColorConfig::Always => {
622622
if io::stderr().is_terminal() {

compiler/rustc_errors/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ pub mod emitter;
6161
pub mod error;
6262
pub mod json;
6363
mod lock;
64+
pub mod markdown;
6465
pub mod registry;
6566
mod snippet;
6667
mod styled_buffer;
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
//! A simple markdown parser that can write formatted text to the terminal
2+
//!
3+
//! Entrypoint is `MdStream::parse_str(...)`
4+
use std::io;
5+
6+
use termcolor::{Buffer, BufferWriter, ColorChoice};
7+
mod parse;
8+
mod term;
9+
10+
/// An AST representation of a Markdown document
11+
#[derive(Clone, Debug, Default, PartialEq)]
12+
pub struct MdStream<'a>(Vec<MdTree<'a>>);
13+
14+
impl<'a> MdStream<'a> {
15+
/// Parse a markdown string to a tokenstream
16+
#[must_use]
17+
pub fn parse_str(s: &str) -> MdStream<'_> {
18+
parse::entrypoint(s)
19+
}
20+
21+
/// Write formatted output to a termcolor buffer
22+
pub fn write_termcolor_buf(&self, buf: &mut Buffer) -> io::Result<()> {
23+
term::entrypoint(self, buf)
24+
}
25+
}
26+
27+
/// Create a termcolor buffer with the `Always` color choice
28+
pub fn create_stdout_bufwtr() -> BufferWriter {
29+
BufferWriter::stdout(ColorChoice::Always)
30+
}
31+
32+
/// A single tokentree within a Markdown document
33+
#[derive(Clone, Debug, PartialEq)]
34+
pub enum MdTree<'a> {
35+
/// Leaf types
36+
Comment(&'a str),
37+
CodeBlock {
38+
txt: &'a str,
39+
lang: Option<&'a str>,
40+
},
41+
CodeInline(&'a str),
42+
Strong(&'a str),
43+
Emphasis(&'a str),
44+
Strikethrough(&'a str),
45+
PlainText(&'a str),
46+
/// [Foo](www.foo.com) or simple anchor <www.foo.com>
47+
Link {
48+
disp: &'a str,
49+
link: &'a str,
50+
},
51+
/// `[Foo link][ref]`
52+
RefLink {
53+
disp: &'a str,
54+
id: Option<&'a str>,
55+
},
56+
/// [ref]: www.foo.com
57+
LinkDef {
58+
id: &'a str,
59+
link: &'a str,
60+
},
61+
/// Break bewtween two paragraphs (double `\n`), not directly parsed but
62+
/// added later
63+
ParagraphBreak,
64+
/// Break bewtween two lines (single `\n`)
65+
LineBreak,
66+
HorizontalRule,
67+
Heading(u8, MdStream<'a>),
68+
OrderedListItem(u16, MdStream<'a>),
69+
UnorderedListItem(MdStream<'a>),
70+
}
71+
72+
impl<'a> From<Vec<MdTree<'a>>> for MdStream<'a> {
73+
fn from(value: Vec<MdTree<'a>>) -> Self {
74+
Self(value)
75+
}
76+
}

0 commit comments

Comments
 (0)