Improve several aspects of the Rustdoc scrape-examples UI.

* Examples take up less screen height.
* Snippets from binary crates are prioritized.
* toggle-all-docs does not expand "More examples" sections.
This commit is contained in:
Will Crichton 2022-11-27 13:11:21 -06:00
parent 01fbc5ae78
commit 6ccd14a782
11 changed files with 106 additions and 33 deletions

View File

@ -69,8 +69,8 @@ pub(crate) struct Options {
pub(crate) input: PathBuf, pub(crate) input: PathBuf,
/// The name of the crate being documented. /// The name of the crate being documented.
pub(crate) crate_name: Option<String>, pub(crate) crate_name: Option<String>,
/// Whether or not this is a proc-macro crate /// The types of the crate being documented.
pub(crate) proc_macro_crate: bool, pub(crate) crate_types: Vec<CrateType>,
/// How to format errors and warnings. /// How to format errors and warnings.
pub(crate) error_format: ErrorOutputType, pub(crate) error_format: ErrorOutputType,
/// Width of output buffer to truncate errors appropriately. /// Width of output buffer to truncate errors appropriately.
@ -176,7 +176,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Options") f.debug_struct("Options")
.field("input", &self.input) .field("input", &self.input)
.field("crate_name", &self.crate_name) .field("crate_name", &self.crate_name)
.field("proc_macro_crate", &self.proc_macro_crate) .field("crate_types", &self.crate_types)
.field("error_format", &self.error_format) .field("error_format", &self.error_format)
.field("libs", &self.libs) .field("libs", &self.libs)
.field("externs", &FmtExterns(&self.externs)) .field("externs", &FmtExterns(&self.externs))
@ -667,7 +667,6 @@ fn println_condition(condition: Condition) {
None => OutputFormat::default(), None => OutputFormat::default(),
}; };
let crate_name = matches.opt_str("crate-name"); let crate_name = matches.opt_str("crate-name");
let proc_macro_crate = crate_types.contains(&CrateType::ProcMacro);
let playground_url = matches.opt_str("playground-url"); let playground_url = matches.opt_str("playground-url");
let maybe_sysroot = matches.opt_str("sysroot").map(PathBuf::from); let maybe_sysroot = matches.opt_str("sysroot").map(PathBuf::from);
let module_sorting = if matches.opt_present("sort-modules-by-appearance") { let module_sorting = if matches.opt_present("sort-modules-by-appearance") {
@ -718,7 +717,7 @@ fn println_condition(condition: Condition) {
rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref()); rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref());
let options = Options { let options = Options {
input, input,
proc_macro_crate, crate_types,
error_format, error_format,
diagnostic_width, diagnostic_width,
libs, libs,

View File

@ -13,7 +13,7 @@
use rustc_middle::hir::nested_filter; use rustc_middle::hir::nested_filter;
use rustc_middle::ty::{ParamEnv, Ty, TyCtxt}; use rustc_middle::ty::{ParamEnv, Ty, TyCtxt};
use rustc_resolve as resolve; use rustc_resolve as resolve;
use rustc_session::config::{self, CrateType, ErrorOutputType}; use rustc_session::config::{self, ErrorOutputType};
use rustc_session::lint; use rustc_session::lint;
use rustc_session::Session; use rustc_session::Session;
use rustc_span::symbol::sym; use rustc_span::symbol::sym;
@ -203,7 +203,7 @@ pub(crate) fn create_config(
RustdocOptions { RustdocOptions {
input, input,
crate_name, crate_name,
proc_macro_crate, crate_types,
error_format, error_format,
diagnostic_width, diagnostic_width,
libs, libs,
@ -247,8 +247,6 @@ pub(crate) fn create_config(
Some((lint.name_lower(), lint::Allow)) Some((lint.name_lower(), lint::Allow))
}); });
let crate_types =
if proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
let test = scrape_examples_options.map(|opts| opts.scrape_tests).unwrap_or(false); let test = scrape_examples_options.map(|opts| opts.scrape_tests).unwrap_or(false);
// plays with error output here! // plays with error output here!
let sessopts = config::Options { let sessopts = config::Options {

View File

@ -12,7 +12,7 @@
use rustc_middle::ty::TyCtxt; use rustc_middle::ty::TyCtxt;
use rustc_parse::maybe_new_parser_from_source_str; use rustc_parse::maybe_new_parser_from_source_str;
use rustc_parse::parser::attr::InnerAttrPolicy; use rustc_parse::parser::attr::InnerAttrPolicy;
use rustc_session::config::{self, CrateType, ErrorOutputType}; use rustc_session::config::{self, ErrorOutputType};
use rustc_session::parse::ParseSess; use rustc_session::parse::ParseSess;
use rustc_session::{lint, Session}; use rustc_session::{lint, Session};
use rustc_span::edition::Edition; use rustc_span::edition::Edition;
@ -68,8 +68,7 @@ pub(crate) fn run(options: RustdocOptions) -> Result<(), ErrorGuaranteed> {
debug!(?lint_opts); debug!(?lint_opts);
let crate_types = let crate_types = options.crate_types.clone();
if options.proc_macro_crate { vec![CrateType::ProcMacro] } else { vec![CrateType::Rlib] };
let sessopts = config::Options { let sessopts = config::Options {
maybe_sysroot: options.maybe_sysroot.clone(), maybe_sysroot: options.maybe_sysroot.clone(),

View File

@ -2957,14 +2957,22 @@ fn render_call_locations(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Ite
// The call locations are output in sequence, so that sequence needs to be determined. // The call locations are output in sequence, so that sequence needs to be determined.
// Ideally the most "relevant" examples would be shown first, but there's no general algorithm // Ideally the most "relevant" examples would be shown first, but there's no general algorithm
// for determining relevance. Instead, we prefer the smallest examples being likely the easiest to // for determining relevance. We instead make a proxy for relevance with the following heuristics:
// understand at a glance. // 1. Code written to be an example is better than code not written to be an example, e.g.
// a snippet from examples/foo.rs is better than src/lib.rs. We don't know the Cargo directory
// structure in Rustdoc, so we proxy this by prioriting code that comes from a --crate-type bin.
// 2. Smaller examples are better than large examples. So we prioritize snippets that have the
// smallest line span for their enclosing item.
// 3. Finally we sort by the displayed file name, which is arbitrary but prevents the ordering
// of examples from randomly changing between Rustdoc invocations.
let ordered_locations = { let ordered_locations = {
let sort_criterion = |(_, call_data): &(_, &CallData)| { fn sort_criterion<'a>(
(_, call_data): &(&PathBuf, &'a CallData),
) -> (bool, u32, &'a String) {
// Use the first location because that's what the user will see initially // Use the first location because that's what the user will see initially
let (lo, hi) = call_data.locations[0].enclosing_item.byte_span; let (lo, hi) = call_data.locations[0].enclosing_item.byte_span;
hi - lo (!call_data.is_bin, hi - lo, &call_data.display_name)
}; }
let mut locs = call_locations.iter().collect::<Vec<_>>(); let mut locs = call_locations.iter().collect::<Vec<_>>();
locs.sort_by_key(sort_criterion); locs.sort_by_key(sort_criterion);

View File

@ -1901,6 +1901,10 @@ in storage.js
border-radius: 50px; border-radius: 50px;
} }
.scraped-example {
position: relative;
}
.scraped-example .code-wrapper { .scraped-example .code-wrapper {
position: relative; position: relative;
display: flex; display: flex;
@ -1909,16 +1913,31 @@ in storage.js
width: 100%; width: 100%;
} }
.scraped-example-title {
position: absolute;
z-index: 1000;
background: white;
bottom: 8px;
right: 5px;
padding: 2px 4px;
box-shadow: 0 0 4px white;
}
.scraped-example:not(.expanded) .code-wrapper { .scraped-example:not(.expanded) .code-wrapper {
max-height: 240px; max-height: 120px;
} }
.scraped-example:not(.expanded) .code-wrapper pre { .scraped-example:not(.expanded) .code-wrapper pre {
overflow-y: hidden; overflow-y: hidden;
max-height: 240px; max-height: 120px;
padding-bottom: 0; padding-bottom: 0;
} }
.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper,
.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper pre {
max-height: 240px;
}
.scraped-example .code-wrapper .next, .scraped-example .code-wrapper .next,
.scraped-example .code-wrapper .prev, .scraped-example .code-wrapper .prev,
.scraped-example .code-wrapper .expand { .scraped-example .code-wrapper .expand {

View File

@ -622,7 +622,7 @@ function loadCss(cssUrl) {
const innerToggle = document.getElementById(toggleAllDocsId); const innerToggle = document.getElementById(toggleAllDocsId);
removeClass(innerToggle, "will-expand"); removeClass(innerToggle, "will-expand");
onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => { onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
if (!hasClass(e, "type-contents-toggle")) { if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) {
e.open = true; e.open = true;
} }
}); });

View File

@ -4,17 +4,19 @@
(function() { (function() {
// Number of lines shown when code viewer is not expanded // Number of lines shown when code viewer is not expanded
const MAX_LINES = 10; const DEFAULT_MAX_LINES = 5;
const HIDDEN_MAX_LINES = 10;
// Scroll code block to the given code location // Scroll code block to the given code location
function scrollToLoc(elt, loc) { function scrollToLoc(elt, loc, isHidden) {
const lines = elt.querySelector(".src-line-numbers"); const lines = elt.querySelector(".src-line-numbers");
let scrollOffset; let scrollOffset;
// If the block is greater than the size of the viewer, // If the block is greater than the size of the viewer,
// then scroll to the top of the block. Otherwise scroll // then scroll to the top of the block. Otherwise scroll
// to the middle of the block. // to the middle of the block.
if (loc[1] - loc[0] > MAX_LINES) { let maxLines = isHidden ? HIDDEN_MAX_LINES : DEFAULT_MAX_LINES;
if (loc[1] - loc[0] > maxLines) {
const line = Math.max(0, loc[0] - 1); const line = Math.max(0, loc[0] - 1);
scrollOffset = lines.children[line].offsetTop; scrollOffset = lines.children[line].offsetTop;
} else { } else {
@ -29,7 +31,7 @@
elt.querySelector(".rust").scrollTo(0, scrollOffset); elt.querySelector(".rust").scrollTo(0, scrollOffset);
} }
function updateScrapedExample(example) { function updateScrapedExample(example, isHidden) {
const locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent); const locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent);
let locIndex = 0; let locIndex = 0;
const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight")); const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight"));
@ -40,7 +42,7 @@
const onChangeLoc = changeIndex => { const onChangeLoc = changeIndex => {
removeClass(highlights[locIndex], "focus"); removeClass(highlights[locIndex], "focus");
changeIndex(); changeIndex();
scrollToLoc(example, locs[locIndex][0]); scrollToLoc(example, locs[locIndex][0], isHidden);
addClass(highlights[locIndex], "focus"); addClass(highlights[locIndex], "focus");
const url = locs[locIndex][1]; const url = locs[locIndex][1];
@ -70,7 +72,7 @@
expandButton.addEventListener("click", () => { expandButton.addEventListener("click", () => {
if (hasClass(example, "expanded")) { if (hasClass(example, "expanded")) {
removeClass(example, "expanded"); removeClass(example, "expanded");
scrollToLoc(example, locs[0][0]); scrollToLoc(example, locs[0][0], isHidden);
} else { } else {
addClass(example, "expanded"); addClass(example, "expanded");
} }
@ -78,11 +80,11 @@
} }
// Start with the first example in view // Start with the first example in view
scrollToLoc(example, locs[0][0]); scrollToLoc(example, locs[0][0], isHidden);
} }
const firstExamples = document.querySelectorAll(".scraped-example-list > .scraped-example"); const firstExamples = document.querySelectorAll(".scraped-example-list > .scraped-example");
onEachLazy(firstExamples, updateScrapedExample); onEachLazy(firstExamples, el => updateScrapedExample(el, false));
onEachLazy(document.querySelectorAll(".more-examples-toggle"), toggle => { onEachLazy(document.querySelectorAll(".more-examples-toggle"), toggle => {
// Allow users to click the left border of the <details> section to close it, // Allow users to click the left border of the <details> section to close it,
// since the section can be large and finding the [+] button is annoying. // since the section can be large and finding the [+] button is annoying.
@ -99,7 +101,7 @@
// depends on offsetHeight, a property that requires an element to be visible to // depends on offsetHeight, a property that requires an element to be visible to
// compute correctly. // compute correctly.
setTimeout(() => { setTimeout(() => {
onEachLazy(moreExamples, updateScrapedExample); onEachLazy(moreExamples, el => updateScrapedExample(el, true));
}); });
}, {once: true}); }, {once: true});
}); });

View File

@ -774,6 +774,7 @@ fn main_args(at_args: &[String]) -> MainResult {
let output_format = options.output_format; let output_format = options.output_format;
let externs = options.externs.clone(); let externs = options.externs.clone();
let scrape_examples_options = options.scrape_examples_options.clone(); let scrape_examples_options = options.scrape_examples_options.clone();
let crate_types = options.crate_types.clone();
let config = core::create_config(options); let config = core::create_config(options);
@ -832,7 +833,14 @@ fn main_args(at_args: &[String]) -> MainResult {
info!("finished with rustc"); info!("finished with rustc");
if let Some(options) = scrape_examples_options { if let Some(options) = scrape_examples_options {
return scrape_examples::run(krate, render_opts, cache, tcx, options); return scrape_examples::run(
krate,
render_opts,
cache,
tcx,
options,
crate_types,
);
} }
cache.crate_version = crate_version; cache.crate_version = crate_version;

View File

@ -20,7 +20,7 @@
opaque::{FileEncoder, MemDecoder}, opaque::{FileEncoder, MemDecoder},
Decodable, Encodable, Decodable, Encodable,
}; };
use rustc_session::getopts; use rustc_session::{config::CrateType, getopts};
use rustc_span::{ use rustc_span::{
def_id::{CrateNum, DefPathHash, LOCAL_CRATE}, def_id::{CrateNum, DefPathHash, LOCAL_CRATE},
edition::Edition, edition::Edition,
@ -110,6 +110,7 @@ pub(crate) struct CallData {
pub(crate) url: String, pub(crate) url: String,
pub(crate) display_name: String, pub(crate) display_name: String,
pub(crate) edition: Edition, pub(crate) edition: Edition,
pub(crate) is_bin: bool,
} }
pub(crate) type FnCallLocations = FxHashMap<PathBuf, CallData>; pub(crate) type FnCallLocations = FxHashMap<PathBuf, CallData>;
@ -122,6 +123,7 @@ struct FindCalls<'a, 'tcx> {
cx: Context<'tcx>, cx: Context<'tcx>,
target_crates: Vec<CrateNum>, target_crates: Vec<CrateNum>,
calls: &'a mut AllCallLocations, calls: &'a mut AllCallLocations,
crate_types: Vec<CrateType>,
} }
impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx> impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx>
@ -245,7 +247,9 @@ fn visit_expr(&mut self, ex: &'tcx hir::Expr<'tcx>) {
let mk_call_data = || { let mk_call_data = || {
let display_name = file_path.display().to_string(); let display_name = file_path.display().to_string();
let edition = call_span.edition(); let edition = call_span.edition();
CallData { locations: Vec::new(), url, display_name, edition } let is_bin = self.crate_types.contains(&CrateType::Executable);
CallData { locations: Vec::new(), url, display_name, edition, is_bin }
}; };
let fn_key = tcx.def_path_hash(*def_id); let fn_key = tcx.def_path_hash(*def_id);
@ -274,6 +278,7 @@ pub(crate) fn run(
cache: formats::cache::Cache, cache: formats::cache::Cache,
tcx: TyCtxt<'_>, tcx: TyCtxt<'_>,
options: ScrapeExamplesOptions, options: ScrapeExamplesOptions,
crate_types: Vec<CrateType>,
) -> interface::Result<()> { ) -> interface::Result<()> {
let inner = move || -> Result<(), String> { let inner = move || -> Result<(), String> {
// Generates source files for examples // Generates source files for examples
@ -300,7 +305,8 @@ pub(crate) fn run(
// Run call-finder on all items // Run call-finder on all items
let mut calls = FxHashMap::default(); let mut calls = FxHashMap::default();
let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates }; let mut finder =
FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates, crate_types };
tcx.hir().visit_all_item_likes_in_crate(&mut finder); tcx.hir().visit_all_item_likes_in_crate(&mut finder);
// The visitor might have found a type error, which we need to // The visitor might have found a type error, which we need to

View File

@ -25,3 +25,12 @@ store-property: (fullOffsetHeight, ".scraped-example-list > .scraped-example pre
assert-property: (".scraped-example-list > .scraped-example pre", { assert-property: (".scraped-example-list > .scraped-example pre", {
"scrollHeight": |fullOffsetHeight| "scrollHeight": |fullOffsetHeight|
}) })
assert-attribute-false: (".more-examples-toggle", {"open": ""})
click: ".more-examples-toggle"
assert-attribute: (".more-examples-toggle", {"open": ""})
click: "#toggle-all-docs"
assert-attribute-false: (".more-examples-toggle", {"open": ""})
// After re-opening the docs, the additional examples should stay closed
click: "#toggle-all-docs"
assert-attribute-false: (".more-examples-toggle", {"open": ""})

View File

@ -0,0 +1,25 @@
fn main() {
for i in 0..9 {
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
}
scrape_examples::test();
for i in 0..9 {
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
println!("hello world!");
}
}