From eea8f0a39a2423cc7a4acd31e3a7309853f22509 Mon Sep 17 00:00:00 2001 From: Will Crichton Date: Wed, 25 Aug 2021 20:15:46 -0700 Subject: [PATCH] Sort examples by size Improve styling Start to clean up code, add comments --- src/librustdoc/clean/types.rs | 2 + src/librustdoc/config.rs | 25 +------- src/librustdoc/html/render/mod.rs | 75 ++++++++++++++++------ src/librustdoc/html/static/css/rustdoc.css | 58 ++++++++++++++--- src/librustdoc/html/static/js/main.js | 29 ++++++--- src/librustdoc/scrape_examples.rs | 30 ++++++++- 6 files changed, 158 insertions(+), 61 deletions(-) diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs index ab5b6000a185..eb507e4eecab 100644 --- a/src/librustdoc/clean/types.rs +++ b/src/librustdoc/clean/types.rs @@ -1259,6 +1259,8 @@ crate struct Function { } impl Function { + /// If --scrape-examples is used, then this function attempts to find call locations + /// for `self` within `RenderOptions::call_locations` and store them in `Function::call_locations`. crate fn load_call_locations(&mut self, def_id: hir::def_id::DefId, cx: &DocContext<'_>) { if let Some(call_locations) = cx.render_options.call_locations.as_ref() { let key = scrape_examples::def_id_call_key(cx.tcx, def_id); diff --git a/src/librustdoc/config.rs b/src/librustdoc/config.rs index 2f8bae5ded0a..4e019d4e15d3 100644 --- a/src/librustdoc/config.rs +++ b/src/librustdoc/config.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::convert::TryFrom; use std::ffi::OsStr; use std::fmt; -use std::fs; use std::path::PathBuf; use std::str::FromStr; @@ -680,29 +679,7 @@ impl Options { let scrape_examples = matches.opt_str("scrape-examples").map(PathBuf::from); let with_examples = matches.opt_strs("with-examples"); - let each_call_locations = with_examples - .into_iter() - .map(|path| { - let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; - let calls: AllCallLocations = - serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; - Ok(calls) - }) - .collect::, _>>() - .map_err(|e: String| { - diag.err(&format!("failed to load examples with error: {}", e)); - 1 - })?; - let call_locations = (each_call_locations.len() > 0).then(move || { - each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { - for (function, calls) in map.into_iter() { - acc.entry(function) - .or_insert_with(FxHashMap::default) - .extend(calls.into_iter()); - } - acc - }) - }); + let call_locations = crate::scrape_examples::load_call_locations(with_examples, &diag)?; let (lint_opts, describe_lints, lint_cap) = get_cmd_lint_options(matches, error_format); diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 693a9d7b8a3e..b50aab6351c0 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -2453,32 +2453,31 @@ fn collect_paths_for_type(first_ty: clean::Type, cache: &Cache) -> Vec { const MAX_FULL_EXAMPLES: usize = 5; +/// Generates the HTML for example call locations generated via the --scrape-examples flag. fn render_call_locations( w: &mut Buffer, cx: &Context<'_>, call_locations: &Option, ) { let call_locations = match call_locations.as_ref() { - Some(call_locations) => call_locations, - None => { + Some(call_locations) if call_locations.len() > 0 => call_locations, + _ => { return; } }; - if call_locations.len() == 0 { - return; - } - + // Generate a unique ID so users can link to this section for a given method let id = cx.id_map.borrow_mut().derive("scraped-examples"); write!( w, r##"

- Uses found in examples/ + Examples found in repository

"##, id ); + // Link to the source file containing a given example let example_url = |call_data: &CallData| -> String { format!( r#"{name}"#, @@ -2488,18 +2487,27 @@ fn render_call_locations( ) }; + // Generate the HTML for a single example, being the title and code block let write_example = |w: &mut Buffer, (path, call_data): (&PathBuf, &CallData)| { - let mut contents = + // FIXME(wcrichto): is there a better way to handle an I/O error than a panic? + // When would such an error arise? + let contents = fs::read_to_string(&path).expect(&format!("Failed to read file: {}", path.display())); + // To reduce file sizes, we only want to embed the source code needed to understand the example, not + // the entire file. So we find the smallest byte range that covers all items enclosing examples. let min_loc = call_data.locations.iter().min_by_key(|loc| loc.enclosing_item_span.0).unwrap(); let min_byte = min_loc.enclosing_item_span.0; let min_line = min_loc.enclosing_item_lines.0; let max_byte = call_data.locations.iter().map(|loc| loc.enclosing_item_span.1).max().unwrap(); - contents = contents[min_byte..max_byte].to_string(); + // The output code is limited to that byte range. + let contents_subset = &contents[min_byte..max_byte]; + + // The call locations need to be updated to reflect that the size of the program has changed. + // Specifically, the ranges are all subtracted by `min_byte` since that's the new zero point. let locations = call_data .locations .iter() @@ -2510,23 +2518,44 @@ fn render_call_locations( write!( w, r#"
- {title} +
{title}
"#, - code = contents.replace("\"", """), - locations = serde_json::to_string(&locations).unwrap(), title = example_url(call_data), + // The code and locations are encoded as data attributes, so they can be read + // later by the JS for interactions. + code = contents_subset.replace("\"", """), + locations = serde_json::to_string(&locations).unwrap(), ); write!(w, r#" "#); write!(w, r#""#); + + // FIXME(wcrichto): where should file_span and root_path come from? let file_span = rustc_span::DUMMY_SP; let root_path = "".to_string(); - sources::print_src(w, &contents, edition, file_span, cx, &root_path, Some(min_line)); + sources::print_src(w, contents_subset, edition, file_span, cx, &root_path, Some(min_line)); write!(w, "
"); }; - let mut it = call_locations.into_iter().peekable(); + // 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. + let ordered_locations = { + let sort_criterion = |(_, call_data): &(_, &CallData)| { + let (lo, hi) = call_data.locations[0].enclosing_item_span; + hi - lo + }; + + let mut locs = call_locations.into_iter().collect::>(); + locs.sort_by_key(|x| sort_criterion(x)); + locs + }; + + // Write just one example that's visible by default in the method's description. + let mut it = ordered_locations.into_iter().peekable(); write_example(w, it.next().unwrap()); + // Then add the remaining examples in a hidden section. if it.peek().is_some() { write!( w, @@ -2534,19 +2563,29 @@ fn render_call_locations( More examples -
"# +
+
+
+"# ); + + // Only generate inline code for MAX_FULL_EXAMPLES number of examples. Otherwise we could + // make the page arbitrarily huge! (&mut it).take(MAX_FULL_EXAMPLES).for_each(|ex| write_example(w, ex)); + // For the remaining examples, generate a
    containing links to the source files. if it.peek().is_some() { - write!(w, "Additional examples can be found in:
      "); + write!( + w, + r#"
"); } - write!(w, "
"); + write!(w, "
"); } write!(w, ""); diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index 89a205be023a..2767f6468fb5 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -137,7 +137,7 @@ h1.fqn { margin-top: 0; /* workaround to keep flex from breaking below 700 px width due to the float: right on the nav - above the h1 */ + above the h1 */ padding-left: 1px; } h1.fqn > .in-band > a:hover { @@ -974,7 +974,7 @@ body.blur > :not(#help) { text-shadow: 1px 0 0 black, -1px 0 0 black, - 0 1px 0 black, + 0 1px 0 black, 0 -1px 0 black; } @@ -1214,8 +1214,8 @@ a.test-arrow:hover{ .notable-traits-tooltip::after { /* The margin on the tooltip does not capture hover events, - this extends the area of hover enough so that mouse hover is not - lost when moving the mouse to the tooltip */ + this extends the area of hover enough so that mouse hover is not + lost when moving the mouse to the tooltip */ content: "\00a0\00a0\00a0"; } @@ -1715,7 +1715,7 @@ details.undocumented[open] > summary::before { } /* We do NOT hide this element so that alternative device readers still have this information - available. */ + available. */ .sidebar-elems { position: fixed; z-index: 1; @@ -1973,10 +1973,15 @@ details.undocumented[open] > summary::before { /* This part is for the new "examples" components */ +.scraped-example-title { + font-family: 'Fira Sans'; + font-weight: 500; +} + .scraped-example:not(.expanded) .code-wrapper pre.line-numbers, .scraped-example:not(.expanded) .code-wrapper .example-wrap pre.rust { overflow: hidden; - height: 240px; + max-height: 240px; } .scraped-example .code-wrapper .prev { @@ -2033,7 +2038,7 @@ details.undocumented[open] > summary::before { .scraped-example:not(.expanded) .code-wrapper { overflow: hidden; - height: 240px; + max-height: 240px; } .scraped-example .code-wrapper .line-numbers { @@ -2072,6 +2077,41 @@ details.undocumented[open] > summary::before { .more-scraped-examples { padding-left: 10px; - border-left: 1px solid #ccc; - margin-left: 24px; + margin-left: 15px; + display: flex; + flex-direction: row; +} + +.toggle-line { + align-self: stretch; + margin-right: 10px; + margin-top: 5px; + padding: 0 4px; + cursor: pointer; +} + +.toggle-line:hover .toggle-line-inner { + background: #aaa; +} + +.toggle-line-inner { + min-width: 2px; + background: #ddd; + height: 100%; +} + +h1 + .scraped-example { + margin-bottom: 10px; +} + +.more-scraped-examples .scraped-example { + margin-bottom: 20px; +} + +.example-links a { + font-family: 'Fira Sans'; +} + +.example-links ul { + margin-bottom: 0; } diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index fea1b4ecbf1e..a52e539fbd32 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -1121,15 +1121,22 @@ function hideThemeButtonState() { example.querySelector('.next').remove(); } - // Show full code on expansion - example.querySelector('.expand').addEventListener('click', function () { - if (hasClass(example, "expanded")) { - removeClass(example, "expanded"); - scrollToLoc(example, locs[0]); - } else { - addClass(example, "expanded"); - } - }); + let codeEl = example.querySelector('.rust'); + let expandButton = example.querySelector('.expand'); + if (codeEl.scrollHeight == codeEl.clientHeight) { + addClass(example, 'expanded'); + expandButton.remove(); + } else { + // Show full code on expansion + expandButton.addEventListener('click', function () { + if (hasClass(example, "expanded")) { + removeClass(example, "expanded"); + scrollToLoc(example, locs[0]); + } else { + addClass(example, "expanded"); + } + }); + } // Start with the first example in view scrollToLoc(example, locs[0]); @@ -1139,6 +1146,10 @@ function hideThemeButtonState() { var firstExamples = document.querySelectorAll('.scraped-example-list > .scraped-example'); onEach(firstExamples, updateScrapedExample); onEach(document.querySelectorAll('.more-examples-toggle'), function(toggle) { + toggle.querySelector('.toggle-line').addEventListener('click', function() { + toggle.open = false; + }); + var moreExamples = toggle.querySelectorAll('.scraped-example'); toggle.querySelector('summary').addEventListener('click', function() { // Wrapping in setTimeout ensures the update happens after the elements are actually diff --git a/src/librustdoc/scrape_examples.rs b/src/librustdoc/scrape_examples.rs index 950af8fbb632..16a40ed1cb31 100644 --- a/src/librustdoc/scrape_examples.rs +++ b/src/librustdoc/scrape_examples.rs @@ -1,4 +1,4 @@ -//! This module analyzes provided crates to find examples of uses for items in the +//! This module analyzes crates to find examples of uses for items in the //! current crate being documented. use crate::clean; @@ -158,3 +158,31 @@ crate fn run( rustc_errors::ErrorReported }) } + +crate fn load_call_locations( + with_examples: Vec, + diag: &rustc_errors::Handler, +) -> Result, i32> { + let each_call_locations = with_examples + .into_iter() + .map(|path| { + let bytes = fs::read(&path).map_err(|e| format!("{} (for path {})", e, path))?; + let calls: AllCallLocations = + serde_json::from_slice(&bytes).map_err(|e| format!("{}", e))?; + Ok(calls) + }) + .collect::, _>>() + .map_err(|e: String| { + diag.err(&format!("failed to load examples with error: {}", e)); + 1 + })?; + + Ok((each_call_locations.len() > 0).then(|| { + each_call_locations.into_iter().fold(FxHashMap::default(), |mut acc, map| { + for (function, calls) in map.into_iter() { + acc.entry(function).or_insert_with(FxHashMap::default).extend(calls.into_iter()); + } + acc + }) + })) +}