Skip to content

Commit 6dab6dc

Browse files
committed
Auto merge of rust-lang#112697 - tgross35:explain-markdown, r=oli-obk
Add simple markdown formatting to `rustc --explain` output This is a second attempt at rust-lang#104540, which is rust-lang#63128 without dependencies. This PR adds basic markdown formatting to `rustc --explain` output when available. Currently, the output just displays raw markdown: this works of course, but it really doesn't look very elegant. (output is `rustc --explain E0038`) <img width="583" alt="image" src="https://github.com/rust-lang/rust/assets/13724985/ea418117-47af-455b-83c0-6fc59276efee"> After this patch, sample output from the same file: <img width="693" alt="image" src="https://github.com/rust-lang/rust/assets/13724985/12f7bf9b-a3fe-4104-b74b-c3e5227f3de9"> This also obeys the `--color always/auto/never` command option. Behavior: - If pager is available and supports color, print with formatting to the pager - If pager is not available or fails print with formatting to stdout - otherwise without formatting - Follow `--color always/never` if suppied - If everything fails, just print plain text to stdout r? `@oli-obk` cc `@estebank` (since the two of you were involved in the previous discussion)
2 parents 9227ff2 + 6a1c10b commit 6dab6dc

File tree

14 files changed

+1406
-17
lines changed

14 files changed

+1406
-17
lines changed

compiler/rustc_driver_impl/src/lib.rs

+54-15
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

+1-1
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

+1-1
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

+1
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;
+76
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)