Skip to content

Commit 5fd540b

Browse files
authored
Rollup merge of rust-lang#105387 - willcrichton:scrape-examples-ui-improvements, r=notriddle
Improve Rustdoc scrape-examples UI This PR combines a few different improvements to the scrape-examples UI. See a live demo here: https://willcrichton.net/misc/scrape-examples/small-first-example/clap/struct.Arg.html ### 1. The first scraped example now takes up significantly less screen height. Inserting the first scraped example takes up a lot of vertical screen space. I don't want this addition to overwhelm users, so I decided to reduce the height of the initial example in two ways: (A) the default un-expanded height is reduced from 240px (10 LOC) to 120px (5 LOC), and (B) the link to the example is now positioned *over* the example instead of *atop* the example (only on desktop though, not mobile). The changes to `scrape-examples.js` and `rustdoc.css` implement this fix. Here is what an example docblock now looks like: ![Screen Shot 2022-12-06 at 10 02 21 AM](https://user-images.githubusercontent.com/663326/205987450-3940063c-5973-4a34-8579-baff6a43aa9b.png) ### 2. Expanding all docblocks will not expand "More examples". The "More examples blocks" are huge, so fully expanding everything on the page would take up too much vertical space. The changes to `main.js` implement this fix. This is tested in `scrape-examples-toggle.goml`. ### 3. Examples from binary crates are sorted higher than examples from library crates. Code that is written as an example of an API is probably better for learning than code that happens to use an API, but isn't intended for pedagogic purposes. Unfortunately Rustc doesn't know whether a particular crate comes from an example target (only Cargo knows this). But we can at least create a proxy that prefers examples from binary crates over library crates, which we know from `--crate-type`. This change is implemented by adding a new field `bin_crate` in `Options` (see `config.rs`). An `is_bin` field has been added to the scraped examples metadata (see `scrape_examples.rs`). Then the example sorting metric uses `is_bin` as the first entry of a lexicographic sort on `(is_bin, example_size, display_name)` (see `render/mod.rs`). Note that in the future we can consider adding another flag like `--scrape-examples-cargo-target` that would pass target information from Cargo into the example metadata. But I'm proposing a less intrusive change for now. ### 4. The scrape-examples help page has been updated to reflect the latest Cargo interface. See `scrape-examples-help.md`. r? `@notriddle` P.S. once this PR and rust-lang/cargo#11450 are merged, then I think the scrape-examples feature is officially ready for deployment on docs.rs!
2 parents 0b4d57b + 9499d2c commit 5fd540b

File tree

10 files changed

+111
-25
lines changed

10 files changed

+111
-25
lines changed

src/librustdoc/config.rs

+5
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ pub(crate) struct Options {
6969
pub(crate) input: PathBuf,
7070
/// The name of the crate being documented.
7171
pub(crate) crate_name: Option<String>,
72+
/// Whether or not this is a bin crate
73+
pub(crate) bin_crate: bool,
7274
/// Whether or not this is a proc-macro crate
7375
pub(crate) proc_macro_crate: bool,
7476
/// How to format errors and warnings.
@@ -176,6 +178,7 @@ impl fmt::Debug for Options {
176178
f.debug_struct("Options")
177179
.field("input", &self.input)
178180
.field("crate_name", &self.crate_name)
181+
.field("bin_crate", &self.bin_crate)
179182
.field("proc_macro_crate", &self.proc_macro_crate)
180183
.field("error_format", &self.error_format)
181184
.field("libs", &self.libs)
@@ -667,6 +670,7 @@ impl Options {
667670
None => OutputFormat::default(),
668671
};
669672
let crate_name = matches.opt_str("crate-name");
673+
let bin_crate = crate_types.contains(&CrateType::Executable);
670674
let proc_macro_crate = crate_types.contains(&CrateType::ProcMacro);
671675
let playground_url = matches.opt_str("playground-url");
672676
let maybe_sysroot = matches.opt_str("sysroot").map(PathBuf::from);
@@ -718,6 +722,7 @@ impl Options {
718722
rustc_feature::UnstableFeatures::from_environment(crate_name.as_deref());
719723
let options = Options {
720724
input,
725+
bin_crate,
721726
proc_macro_crate,
722727
error_format,
723728
diagnostic_width,

src/librustdoc/html/render/mod.rs

+14-5
Original file line numberDiff line numberDiff line change
@@ -2957,14 +2957,23 @@ fn render_call_locations(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Ite
29572957

29582958
// The call locations are output in sequence, so that sequence needs to be determined.
29592959
// Ideally the most "relevant" examples would be shown first, but there's no general algorithm
2960-
// for determining relevance. Instead, we prefer the smallest examples being likely the easiest to
2961-
// understand at a glance.
2960+
// for determining relevance. We instead proxy relevance with the following heuristics:
2961+
// 1. Code written to be an example is better than code not written to be an example, e.g.
2962+
// a snippet from examples/foo.rs is better than src/lib.rs. We don't know the Cargo
2963+
// directory structure in Rustdoc, so we proxy this by prioritizing code that comes from
2964+
// a --crate-type bin.
2965+
// 2. Smaller examples are better than large examples. So we prioritize snippets that have
2966+
// the smallest number of lines in their enclosing item.
2967+
// 3. Finally we sort by the displayed file name, which is arbitrary but prevents the
2968+
// ordering of examples from randomly changing between Rustdoc invocations.
29622969
let ordered_locations = {
2963-
let sort_criterion = |(_, call_data): &(_, &CallData)| {
2970+
fn sort_criterion<'a>(
2971+
(_, call_data): &(&PathBuf, &'a CallData),
2972+
) -> (bool, u32, &'a String) {
29642973
// Use the first location because that's what the user will see initially
29652974
let (lo, hi) = call_data.locations[0].enclosing_item.byte_span;
2966-
hi - lo
2967-
};
2975+
(!call_data.is_bin, hi - lo, &call_data.display_name)
2976+
}
29682977

29692978
let mut locs = call_locations.iter().collect::<Vec<_>>();
29702979
locs.sort_by_key(sort_criterion);

src/librustdoc/html/static/css/rustdoc.css

+35-2
Original file line numberDiff line numberDiff line change
@@ -1813,6 +1813,22 @@ in storage.js
18131813
}
18141814
}
18151815

1816+
/* Should have min-width: (N + 1)px where N is the mobile breakpoint above. */
1817+
@media (min-width: 701px) {
1818+
/* Places file-link for a scraped example on top of the example to save space.
1819+
We only do this on large screens so the file-link doesn't overlap too much
1820+
with the example's content. */
1821+
.scraped-example-title {
1822+
position: absolute;
1823+
z-index: 10;
1824+
background: var(--main-background-color);
1825+
bottom: 8px;
1826+
right: 5px;
1827+
padding: 2px 4px;
1828+
box-shadow: 0 0 4px var(--main-background-color);
1829+
}
1830+
}
1831+
18161832
@media print {
18171833
nav.sidebar, nav.sub, .out-of-band, a.srclink, #copy-path,
18181834
details.rustdoc-toggle[open] > summary::before, details.rustdoc-toggle > summary::before,
@@ -1899,6 +1915,11 @@ in storage.js
18991915
border-radius: 50px;
19001916
}
19011917

1918+
.scraped-example {
1919+
/* So .scraped-example-title can be positioned absolutely */
1920+
position: relative;
1921+
}
1922+
19021923
.scraped-example .code-wrapper {
19031924
position: relative;
19041925
display: flex;
@@ -1908,18 +1929,30 @@ in storage.js
19081929
}
19091930

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

19141939
.scraped-example:not(.expanded) .code-wrapper pre {
19151940
overflow-y: hidden;
1916-
max-height: 240px;
19171941
padding-bottom: 0;
1942+
/* See above comment, should be the same max-height. */
1943+
max-height: calc(1.5em * 5 + 10px);
1944+
}
1945+
1946+
.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper,
1947+
.more-scraped-examples .scraped-example:not(.expanded) .code-wrapper pre {
1948+
/* See above comment, except this height is based on HIDDEN_MAX_LINES. */
1949+
max-height: calc(1.5em * 10 + 10px);
19181950
}
19191951

19201952
.scraped-example .code-wrapper .next,
19211953
.scraped-example .code-wrapper .prev,
19221954
.scraped-example .code-wrapper .expand {
1955+
color: var(--main-color);
19231956
position: absolute;
19241957
top: 0.25em;
19251958
z-index: 1;

src/librustdoc/html/static/js/main.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -622,7 +622,7 @@ function loadCss(cssUrl) {
622622
const innerToggle = document.getElementById(toggleAllDocsId);
623623
removeClass(innerToggle, "will-expand");
624624
onEachLazy(document.getElementsByClassName("rustdoc-toggle"), e => {
625-
if (!hasClass(e, "type-contents-toggle")) {
625+
if (!hasClass(e, "type-contents-toggle") && !hasClass(e, "more-examples-toggle")) {
626626
e.open = true;
627627
}
628628
});

src/librustdoc/html/static/js/scrape-examples.js

+20-12
Original file line numberDiff line numberDiff line change
@@ -3,33 +3,41 @@
33
"use strict";
44

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

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

1419
// If the block is greater than the size of the viewer,
1520
// then scroll to the top of the block. Otherwise scroll
1621
// to the middle of the block.
17-
if (loc[1] - loc[0] > MAX_LINES) {
22+
const maxLines = isHidden ? HIDDEN_MAX_LINES : DEFAULT_MAX_LINES;
23+
if (loc[1] - loc[0] > maxLines) {
1824
const line = Math.max(0, loc[0] - 1);
1925
scrollOffset = lines.children[line].offsetTop;
2026
} else {
2127
const wrapper = elt.querySelector(".code-wrapper");
2228
const halfHeight = wrapper.offsetHeight / 2;
23-
const offsetMid = (lines.children[loc[0]].offsetTop
24-
+ lines.children[loc[1]].offsetTop) / 2;
29+
const offsetTop = lines.children[loc[0]].offsetTop;
30+
const lastLine = lines.children[loc[1]];
31+
const offsetBot = lastLine.offsetTop + lastLine.offsetHeight;
32+
const offsetMid = (offsetTop + offsetBot) / 2;
2533
scrollOffset = offsetMid - halfHeight;
2634
}
2735

2836
lines.scrollTo(0, scrollOffset);
2937
elt.querySelector(".rust").scrollTo(0, scrollOffset);
3038
}
3139

32-
function updateScrapedExample(example) {
40+
function updateScrapedExample(example, isHidden) {
3341
const locs = JSON.parse(example.attributes.getNamedItem("data-locs").textContent);
3442
let locIndex = 0;
3543
const highlights = Array.prototype.slice.call(example.querySelectorAll(".highlight"));
@@ -40,7 +48,7 @@
4048
const onChangeLoc = changeIndex => {
4149
removeClass(highlights[locIndex], "focus");
4250
changeIndex();
43-
scrollToLoc(example, locs[locIndex][0]);
51+
scrollToLoc(example, locs[locIndex][0], isHidden);
4452
addClass(highlights[locIndex], "focus");
4553

4654
const url = locs[locIndex][1];
@@ -70,19 +78,19 @@
7078
expandButton.addEventListener("click", () => {
7179
if (hasClass(example, "expanded")) {
7280
removeClass(example, "expanded");
73-
scrollToLoc(example, locs[0][0]);
81+
scrollToLoc(example, locs[0][0], isHidden);
7482
} else {
7583
addClass(example, "expanded");
7684
}
7785
});
7886
}
7987

8088
// Start with the first example in view
81-
scrollToLoc(example, locs[0][0]);
89+
scrollToLoc(example, locs[0][0], isHidden);
8290
}
8391

8492
const firstExamples = document.querySelectorAll(".scraped-example-list > .scraped-example");
85-
onEachLazy(firstExamples, updateScrapedExample);
93+
onEachLazy(firstExamples, el => updateScrapedExample(el, false));
8694
onEachLazy(document.querySelectorAll(".more-examples-toggle"), toggle => {
8795
// Allow users to click the left border of the <details> section to close it,
8896
// since the section can be large and finding the [+] button is annoying.
@@ -99,7 +107,7 @@
99107
// depends on offsetHeight, a property that requires an element to be visible to
100108
// compute correctly.
101109
setTimeout(() => {
102-
onEachLazy(moreExamples, updateScrapedExample);
110+
onEachLazy(moreExamples, el => updateScrapedExample(el, true));
103111
});
104112
}, {once: true});
105113
});

src/librustdoc/html/static/scrape-examples-help.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
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:
1+
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:
22

33
```rust
44
// src/lib.rs
@@ -16,6 +16,7 @@ fn main() {
1616

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

19+
1920
## How to read scraped examples
2021

2122
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.
@@ -25,7 +26,7 @@ If there is more than one file that contains examples, then you should click "Mo
2526

2627
## How Rustdoc scrapes examples
2728

28-
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.
29+
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.
2930

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

src/librustdoc/lib.rs

+9-1
Original file line numberDiff line numberDiff line change
@@ -774,6 +774,7 @@ fn main_args(at_args: &[String]) -> MainResult {
774774
let output_format = options.output_format;
775775
let externs = options.externs.clone();
776776
let scrape_examples_options = options.scrape_examples_options.clone();
777+
let bin_crate = options.bin_crate;
777778

778779
let config = core::create_config(options);
779780

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

834835
if let Some(options) = scrape_examples_options {
835-
return scrape_examples::run(krate, render_opts, cache, tcx, options);
836+
return scrape_examples::run(
837+
krate,
838+
render_opts,
839+
cache,
840+
tcx,
841+
options,
842+
bin_crate,
843+
);
836844
}
837845

838846
cache.crate_version = crate_version;

src/librustdoc/scrape_examples.rs

+8-2
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ pub(crate) struct CallData {
110110
pub(crate) url: String,
111111
pub(crate) display_name: String,
112112
pub(crate) edition: Edition,
113+
pub(crate) is_bin: bool,
113114
}
114115

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

127129
impl<'a, 'tcx> Visitor<'tcx> for FindCalls<'a, 'tcx>
@@ -245,7 +247,9 @@ where
245247
let mk_call_data = || {
246248
let display_name = file_path.display().to_string();
247249
let edition = call_span.edition();
248-
CallData { locations: Vec::new(), url, display_name, edition }
250+
let is_bin = self.bin_crate;
251+
252+
CallData { locations: Vec::new(), url, display_name, edition, is_bin }
249253
};
250254

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

301306
// Run call-finder on all items
302307
let mut calls = FxHashMap::default();
303-
let mut finder = FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates };
308+
let mut finder =
309+
FindCalls { calls: &mut calls, tcx, map: tcx.hir(), cx, target_crates, bin_crate };
304310
tcx.hir().visit_all_item_likes_in_crate(&mut finder);
305311

306312
// The visitor might have found a type error, which we need to

src/test/rustdoc-gui/scrape-examples-button-focus.goml

+2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
goto: "file://" + |DOC_PATH| + "/scrape_examples/fn.test.html"
22

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

16+
// The expand button increases the scrollHeight of the minimized code viewport
1517
store-property: (smallOffsetHeight, ".scraped-example-list > .scraped-example pre", "offsetHeight")
1618
assert-property-false: (".scraped-example-list > .scraped-example pre", {
1719
"scrollHeight": |smallOffsetHeight|
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
goto: "file://" + |DOC_PATH| + "/scrape_examples/fn.test_many.html"
2+
3+
// Clicking "More examples..." will open additional examples
4+
assert-attribute-false: (".more-examples-toggle", {"open": ""})
5+
click: ".more-examples-toggle"
6+
assert-attribute: (".more-examples-toggle", {"open": ""})
7+
8+
// Toggling all docs will close additional examples
9+
click: "#toggle-all-docs"
10+
assert-attribute-false: (".more-examples-toggle", {"open": ""})
11+
12+
// After re-opening the docs, the additional examples should stay closed
13+
click: "#toggle-all-docs"
14+
assert-attribute-false: (".more-examples-toggle", {"open": ""})

0 commit comments

Comments
 (0)