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

Improve Rustdoc scrape-examples UI #105387

Merged
merged 10 commits into from
Dec 9, 2022
5 changes: 5 additions & 0 deletions src/librustdoc/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ pub(crate) struct Options {
pub(crate) input: PathBuf,
/// The name of the crate being documented.
pub(crate) crate_name: Option<String>,
/// Whether or not this is a bin crate
pub(crate) bin_crate: bool,
/// Whether or not this is a proc-macro crate
pub(crate) proc_macro_crate: bool,
/// How to format errors and warnings.
Expand Down Expand Up @@ -176,6 +178,7 @@ impl fmt::Debug for Options {
f.debug_struct("Options")
.field("input", &self.input)
.field("crate_name", &self.crate_name)
.field("bin_crate", &self.bin_crate)
.field("proc_macro_crate", &self.proc_macro_crate)
.field("error_format", &self.error_format)
.field("libs", &self.libs)
Expand Down Expand Up @@ -667,6 +670,7 @@ impl Options {
None => OutputFormat::default(),
};
let crate_name = matches.opt_str("crate-name");
let bin_crate = crate_types.contains(&CrateType::Executable);
let proc_macro_crate = crate_types.contains(&CrateType::ProcMacro);
let playground_url = matches.opt_str("playground-url");
let maybe_sysroot = matches.opt_str("sysroot").map(PathBuf::from);
Expand Down Expand Up @@ -718,6 +722,7 @@ impl Options {
rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref());
let options = Options {
input,
bin_crate,
proc_macro_crate,
error_format,
diagnostic_width,
Expand Down
19 changes: 14 additions & 5 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2957,14 +2957,23 @@ 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.
// 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
// understand at a glance.
// for determining relevance. We instead proxy relevance with the following heuristics:
// 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 prioritizing code that comes from
// a --crate-type bin.
// 2. Smaller examples are better than large examples. So we prioritize snippets that have
// the smallest number of lines in 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 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
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<_>>();
locs.sort_by_key(sort_criterion);
Expand Down
37 changes: 35 additions & 2 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -1815,6 +1815,22 @@ in storage.js
}
}

/* Should have min-width: (N + 1)px where N is the mobile breakpoint above. */
@media (min-width: 701px) {
/* Places file-link for a scraped example on top of the example to save space.
We only do this on large screens so the file-link doesn't overlap too much
with the example's content. */
.scraped-example-title {
position: absolute;
z-index: 10;
background: var(--main-background-color);
bottom: 8px;
right: 5px;
padding: 2px 4px;
box-shadow: 0 0 4px var(--main-background-color);
}
}

@media print {
nav.sidebar, nav.sub, .out-of-band, a.srclink, #copy-path,
details.rustdoc-toggle[open] > summary::before, details.rustdoc-toggle > summary::before,
Expand Down Expand Up @@ -1901,6 +1917,11 @@ in storage.js
border-radius: 50px;
}

.scraped-example {
/* So .scraped-example-title can be positioned absolutely */
position: relative;
}

.scraped-example .code-wrapper {
position: relative;
display: flex;
Expand All @@ -1910,18 +1931,30 @@ in storage.js
}

.scraped-example:not(.expanded) .code-wrapper {
max-height: 240px;
/* scrape-examples.js has a constant DEFAULT_MAX_LINES (call it N) for the number
* of lines shown in the un-expanded example code viewer. This pre needs to have
* a max-height equal to line-height * N. The line-height is currently 1.5em,
* and we include additional 10px for padding. */
max-height: calc(1.5em * 5 + 10px);
}

.scraped-example:not(.expanded) .code-wrapper pre {
overflow-y: hidden;
max-height: 240px;
padding-bottom: 0;
/* See above comment, should be the same max-height. */
max-height: calc(1.5em * 5 + 10px);
}

.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper,
.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper pre {
/* See above comment, except this height is based on HIDDEN_MAX_LINES. */
max-height: calc(1.5em * 10 + 10px);
}

.scraped-example .code-wrapper .next,
.scraped-example .code-wrapper .prev,
.scraped-example .code-wrapper .expand {
color: var(--main-color);
position: absolute;
top: 0.25em;
z-index: 1;
Expand Down
2 changes: 1 addition & 1 deletion src/librustdoc/html/static/js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ function loadCss(cssUrl) {
const innerToggle = document.getElementById(toggleAllDocsId);
removeClass(innerToggle, "will-expand");
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;
}
});
Expand Down
32 changes: 20 additions & 12 deletions src/librustdoc/html/static/js/scrape-examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,41 @@
"use strict";

(function() {
// Number of lines shown when code viewer is not expanded
const MAX_LINES = 10;
// Number of lines shown when code viewer is not expanded.
// DEFAULT is the first example shown by default, while HIDDEN is
// the examples hidden beneath the "More examples" toggle.
//
// NOTE: these values MUST be synchronized with certain rules in rustdoc.css!
const DEFAULT_MAX_LINES = 5;
const HIDDEN_MAX_LINES = 10;

// Scroll code block to the given code location
function scrollToLoc(elt, loc) {
function scrollToLoc(elt, loc, isHidden) {
const lines = elt.querySelector(".src-line-numbers");
let scrollOffset;

// If the block is greater than the size of the viewer,
// then scroll to the top of the block. Otherwise scroll
// to the middle of the block.
if (loc[1] - loc[0] > MAX_LINES) {
const maxLines = isHidden ? HIDDEN_MAX_LINES : DEFAULT_MAX_LINES;
if (loc[1] - loc[0] > maxLines) {
const line = Math.max(0, loc[0] - 1);
scrollOffset = lines.children[line].offsetTop;
} else {
const wrapper = elt.querySelector(".code-wrapper");
const halfHeight = wrapper.offsetHeight / 2;
const offsetMid = (lines.children[loc[0]].offsetTop
+ lines.children[loc[1]].offsetTop) / 2;
const offsetTop = lines.children[loc[0]].offsetTop;
const lastLine = lines.children[loc[1]];
const offsetBot = lastLine.offsetTop + lastLine.offsetHeight;
const offsetMid = (offsetTop + offsetBot) / 2;
scrollOffset = offsetMid - halfHeight;
}

lines.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);
let locIndex = 0;
const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight"));
Expand All @@ -40,7 +48,7 @@
const onChangeLoc = changeIndex => {
removeClass(highlights[locIndex], "focus");
changeIndex();
scrollToLoc(example, locs[locIndex][0]);
scrollToLoc(example, locs[locIndex][0], isHidden);
addClass(highlights[locIndex], "focus");

const url = locs[locIndex][1];
Expand Down Expand Up @@ -70,19 +78,19 @@
expandButton.addEventListener("click", () => {
if (hasClass(example, "expanded")) {
removeClass(example, "expanded");
scrollToLoc(example, locs[0][0]);
scrollToLoc(example, locs[0][0], isHidden);
} else {
addClass(example, "expanded");
}
});
}

// 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");
onEachLazy(firstExamples, updateScrapedExample);
onEachLazy(firstExamples, el => updateScrapedExample(el, false));
onEachLazy(document.querySelectorAll(".more-examples-toggle"), toggle => {
// 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.
Expand All @@ -99,7 +107,7 @@
// depends on offsetHeight, a property that requires an element to be visible to
// compute correctly.
setTimeout(() => {
onEachLazy(moreExamples, updateScrapedExample);
onEachLazy(moreExamples, el => updateScrapedExample(el, true));
});
}, {once: true});
});
Expand Down
5 changes: 3 additions & 2 deletions src/librustdoc/html/static/scrape-examples-help.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Rustdoc will automatically scrape examples of documented items from the `examples/` directory of a project. These examples will be included within the generated documentation for that item. For example, if your library contains a public function:
Rustdoc will automatically scrape examples of documented items from a project's source code. These examples will be included within the generated documentation for that item. For example, if your library contains a public function:

```rust
// src/lib.rs
Expand All @@ -16,6 +16,7 @@ fn main() {

Then this code snippet will be included in the documentation for `a_func`.


## How to read scraped examples

Scraped examples are shown as blocks of code from a given file. The relevant item will be highlighted. If the file is larger than a couple lines, only a small window will be shown which you can expand by clicking &varr; in the top-right. If a file contains multiple instances of an item, you can use the &pr; and &sc; buttons to toggle through each instance.
Expand All @@ -25,7 +26,7 @@ If there is more than one file that contains examples, then you should click "Mo

## How Rustdoc scrapes examples

When you run `cargo doc`, Rustdoc will analyze all the crates that match Cargo's `--examples` filter for instances of items that occur in the crates being documented. Then Rustdoc will include the source code of these instances in the generated documentation.
When you run `cargo doc -Zunstable-options -Zrustdoc-scrape-examples`, Rustdoc will analyze all the documented crates for uses of documented items. Then Rustdoc will include the source code of these instances in the generated documentation.

Rustdoc has a few techniques to ensure this doesn't overwhelm documentation readers, and that it doesn't blow up the page size:

Expand Down
10 changes: 9 additions & 1 deletion src/librustdoc/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -774,6 +774,7 @@ fn main_args(at_args: &[String]) -> MainResult {
let output_format = options.output_format;
let externs = options.externs.clone();
let scrape_examples_options = options.scrape_examples_options.clone();
let bin_crate = options.bin_crate;

let config = core::create_config(options);

Expand Down Expand Up @@ -832,7 +833,14 @@ fn main_args(at_args: &[String]) -> MainResult {
info!("finished with rustc");

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,
bin_crate,
);
}

cache.crate_version = crate_version;
Expand Down
10 changes: 8 additions & 2 deletions src/librustdoc/scrape_examples.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ pub(crate) struct CallData {
pub(crate) url: String,
pub(crate) display_name: String,
pub(crate) edition: Edition,
pub(crate) is_bin: bool,
}

pub(crate) type FnCallLocations = FxHashMap<PathBuf, CallData>;
Expand All @@ -122,6 +123,7 @@ struct FindCalls<'a, 'tcx> {
cx: Context<'tcx>,
target_crates: Vec<CrateNum>,
calls: &'a mut AllCallLocations,
bin_crate: bool,
}

impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx>
Expand Down Expand Up @@ -245,7 +247,9 @@ where
let mk_call_data = || {
let display_name = file_path.display().to_string();
let edition = call_span.edition();
CallData { locations: Vec::new(), url, display_name, edition }
let is_bin = self.bin_crate;

CallData { locations: Vec::new(), url, display_name, edition, is_bin }
};

let fn_key = tcx.def_path_hash(*def_id);
Expand Down Expand Up @@ -274,6 +278,7 @@ pub(crate) fn run(
cache: formats::cache::Cache,
tcx: TyCtxt<'_>,
options: ScrapeExamplesOptions,
bin_crate: bool,
) -> interface::Result<()> {
let inner = move || -> Result<(), String> {
// Generates source files for examples
Expand All @@ -300,7 +305,8 @@ pub(crate) fn run(

// Run call-finder on all items
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, bin_crate };
tcx.hir().visit_all_item_likes_in_crate(&mut finder);

// The visitor might have found a type error, which we need to
Expand Down
2 changes: 2 additions & 0 deletions src/test/rustdoc-gui/scrape-examples-button-focus.goml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
goto: "file://" + |DOC_PATH| + "/scrape_examples/fn.test.html"

// The next/prev buttons vertically scroll the code viewport between examples
store-property: (initialScrollTop, ".scraped-example-list > .scraped-example pre", "scrollTop")
focus: ".scraped-example-list > .scraped-example .next"
press-key: "Enter"
Expand All @@ -12,6 +13,7 @@ assert-property: (".scraped-example-list > .scraped-example pre", {
"scrollTop": |initialScrollTop|
})

// The expand button increases the scrollHeight of the minimized code viewport
store-property: (smallOffsetHeight, ".scraped-example-list > .scraped-example pre", "offsetHeight")
assert-property-false: (".scraped-example-list > .scraped-example pre", {
"scrollHeight": |smallOffsetHeight|
Expand Down
14 changes: 14 additions & 0 deletions src/test/rustdoc-gui/scrape-examples-toggle.goml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
goto: "file://" + |DOC_PATH| + "/scrape_examples/fn.test_many.html"

// Clicking "More examples..." will open additional examples
assert-attribute-false: (".more-examples-toggle", {"open": ""})
click: ".more-examples-toggle"
assert-attribute: (".more-examples-toggle", {"open": ""})

// Toggling all docs will close additional examples
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": ""})