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

Always display first line of impl blocks even when collapsed #132155

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.18.1
0.18.2
98 changes: 78 additions & 20 deletions src/librustdoc/html/markdown.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ use std::iter::Peekable;
use std::ops::{ControlFlow, Range};
use std::path::PathBuf;
use std::str::{self, CharIndices};
use std::sync::OnceLock;
use std::sync::atomic::AtomicUsize;
use std::sync::{Arc, OnceLock, Weak};

use pulldown_cmark::{
BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
Expand Down Expand Up @@ -1305,8 +1306,20 @@ impl LangString {
}
}

impl Markdown<'_> {
impl<'a> Markdown<'a> {
pub fn into_string(self) -> String {
// This is actually common enough to special-case
if self.content.is_empty() {
return String::new();
}

let mut s = String::with_capacity(self.content.len() * 3 / 2);
html::push_html(&mut s, self.into_iter());

s
}

fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
let Markdown {
content: md,
links,
Expand All @@ -1317,32 +1330,72 @@ impl Markdown<'_> {
heading_offset,
} = self;

// This is actually common enough to special-case
if md.is_empty() {
return String::new();
}
let mut replacer = |broken_link: BrokenLink<'_>| {
let replacer = move |broken_link: BrokenLink<'_>| {
links
.iter()
.find(|link| &*link.original_text == &*broken_link.reference)
.map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
};

let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
let p = p.into_offset_iter();

let mut s = String::with_capacity(md.len() * 3 / 2);

ids.handle_footnotes(|ids, existing_footnotes| {
let p = HeadingLinks::new(p, None, ids, heading_offset);
let p = footnotes::Footnotes::new(p, existing_footnotes);
let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
let p = TableWrapper::new(p);
let p = CodeBlocks::new(p, codes, edition, playground);
html::push_html(&mut s, p);
});
CodeBlocks::new(p, codes, edition, playground)
})
}

s
/// Convert markdown to (summary, remaining) HTML.
///
/// - The summary is the first top-level Markdown element (usually a paragraph, but potentially
/// any block).
/// - The remaining docs contain everything after the summary.
pub(crate) fn split_summary_and_content(self) -> (Option<String>, Option<String>) {
if self.content.is_empty() {
return (None, None);
}
let mut p = self.into_iter();

let mut event_level = 0;
let mut summary_events = Vec::new();
let mut get_next_tag = false;

let mut end_of_summary = false;
while let Some(event) = p.next() {
match event {
Event::Start(_) => event_level += 1,
Event::End(kind) => {
event_level -= 1;
if event_level == 0 {
// We're back at the "top" so it means we're done with the summary.
end_of_summary = true;
// We surround tables with `<div>` HTML tags so this is a special case.
get_next_tag = kind == TagEnd::Table;
}
}
_ => {}
}
summary_events.push(event);
if end_of_summary {
if get_next_tag && let Some(event) = p.next() {
summary_events.push(event);
}
break;
}
}
let mut summary = String::new();
html::push_html(&mut summary, summary_events.into_iter());
if summary.is_empty() {
return (None, None);
}
let mut content = String::new();
html::push_html(&mut content, p);

if content.is_empty() { (Some(summary), None) } else { (Some(summary), Some(content)) }
}
}

Expand Down Expand Up @@ -1886,7 +1939,7 @@ pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec<Rust
#[derive(Clone, Default, Debug)]
pub struct IdMap {
map: FxHashMap<Cow<'static, str>, usize>,
existing_footnotes: usize,
existing_footnotes: Arc<AtomicUsize>,
}

// The map is pre-initialized and cloned each time to avoid reinitializing it repeatedly.
Expand Down Expand Up @@ -1948,7 +2001,10 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {

impl IdMap {
pub fn new() -> Self {
IdMap { map: DEFAULT_ID_MAP.get_or_init(init_id_map).clone(), existing_footnotes: 0 }
IdMap {
map: DEFAULT_ID_MAP.get_or_init(init_id_map).clone(),
existing_footnotes: Arc::new(AtomicUsize::new(0)),
}
}

pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
Expand All @@ -1967,10 +2023,12 @@ impl IdMap {

/// Method to handle `existing_footnotes` increment automatically (to prevent forgetting
/// about it).
pub(crate) fn handle_footnotes<F: FnOnce(&mut Self, &mut usize)>(&mut self, closure: F) {
let mut existing_footnotes = self.existing_footnotes;
pub(crate) fn handle_footnotes<'a, T, F: FnOnce(&'a mut Self, Weak<AtomicUsize>) -> T>(
&'a mut self,
closure: F,
) -> T {
let existing_footnotes = Arc::downgrade(&self.existing_footnotes);

closure(self, &mut existing_footnotes);
self.existing_footnotes = existing_footnotes;
closure(self, existing_footnotes)
}
}
25 changes: 16 additions & 9 deletions src/librustdoc/html/markdown/footnotes.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
//! Markdown footnote handling.

use std::fmt::Write as _;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Weak};

use pulldown_cmark::{CowStr, Event, Tag, TagEnd, html};
use rustc_data_structures::fx::FxIndexMap;
Expand All @@ -8,10 +11,11 @@ use super::SpannedEvent;

/// Moves all footnote definitions to the end and add back links to the
/// references.
pub(super) struct Footnotes<'a, 'b, I> {
pub(super) struct Footnotes<'a, I> {
inner: I,
footnotes: FxIndexMap<String, FootnoteDef<'a>>,
existing_footnotes: &'b mut usize,
existing_footnotes: Arc<AtomicUsize>,
start_id: usize,
}

/// The definition of a single footnote.
Expand All @@ -21,13 +25,16 @@ struct FootnoteDef<'a> {
id: usize,
}

impl<'a, 'b, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, 'b, I> {
pub(super) fn new(iter: I, existing_footnotes: &'b mut usize) -> Self {
Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes }
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, I> {
pub(super) fn new(iter: I, existing_footnotes: Weak<AtomicUsize>) -> Self {
let existing_footnotes =
existing_footnotes.upgrade().expect("`existing_footnotes` was dropped");
let start_id = existing_footnotes.load(Ordering::Relaxed);
Footnotes { inner: iter, footnotes: FxIndexMap::default(), existing_footnotes, start_id }
}

fn get_entry(&mut self, key: &str) -> (&mut Vec<Event<'a>>, usize) {
let new_id = self.footnotes.len() + 1 + *self.existing_footnotes;
let new_id = self.footnotes.len() + 1 + self.start_id;
let key = key.to_owned();
let FootnoteDef { content, id } =
self.footnotes.entry(key).or_insert(FootnoteDef { content: Vec::new(), id: new_id });
Expand All @@ -44,7 +51,7 @@ impl<'a, 'b, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, 'b, I> {
id,
// Although the ID count is for the whole page, the footnote reference
// are local to the item so we make this ID "local" when displayed.
id - *self.existing_footnotes
id - self.start_id
);
Event::Html(reference.into())
}
Expand All @@ -64,7 +71,7 @@ impl<'a, 'b, I: Iterator<Item = SpannedEvent<'a>>> Footnotes<'a, 'b, I> {
}
}

impl<'a, 'b, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, 'b, I> {
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
type Item = SpannedEvent<'a>;

fn next(&mut self) -> Option<Self::Item> {
Expand All @@ -87,7 +94,7 @@ impl<'a, 'b, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, 'b
// After all the markdown is emmited, emit an <hr> then all the footnotes
// in a list.
let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
*self.existing_footnotes += defs.len();
self.existing_footnotes.fetch_add(defs.len(), Ordering::Relaxed);
let defs_html = render_footnotes_defs(defs);
return Some((Event::Html(defs_html.into()), 0..0));
} else {
Expand Down
42 changes: 27 additions & 15 deletions src/librustdoc/html/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1938,6 +1938,23 @@ fn render_impl(
if rendering_params.toggle_open_by_default { " open" } else { "" }
);
}

let (before_dox, after_dox) = i
.impl_item
.opt_doc_value()
.map(|dox| {
Markdown {
content: &*dox,
links: &i.impl_item.links(cx),
ids: &mut cx.id_map,
error_codes: cx.shared.codes,
edition: cx.shared.edition(),
playground: &cx.shared.playground,
heading_offset: HeadingOffset::H4,
}
.split_summary_and_content()
})
.unwrap_or((None, None));
render_impl_summary(
w,
cx,
Expand All @@ -1946,33 +1963,23 @@ fn render_impl(
rendering_params.show_def_docs,
use_absolute,
aliases,
&before_dox,
);
if toggled {
w.write_str("</summary>");
}

if let Some(ref dox) = i.impl_item.opt_doc_value() {
if before_dox.is_some() {
if trait_.is_none() && impl_.items.is_empty() {
w.write_str(
"<div class=\"item-info\">\
<div class=\"stab empty-impl\">This impl block contains no items.</div>\
</div>",
);
}
write!(
w,
"<div class=\"docblock\">{}</div>",
Markdown {
content: &*dox,
links: &i.impl_item.links(cx),
ids: &mut cx.id_map,
error_codes: cx.shared.codes,
edition: cx.shared.edition(),
playground: &cx.shared.playground,
heading_offset: HeadingOffset::H4,
}
.into_string()
);
if let Some(after_dox) = after_dox {
write!(w, "<div class=\"docblock\">{after_dox}</div>");
}
}
if !default_impl_items.is_empty() || !impl_items.is_empty() {
w.write_str("<div class=\"impl-items\">");
Expand Down Expand Up @@ -2033,6 +2040,7 @@ pub(crate) fn render_impl_summary(
// This argument is used to reference same type with different paths to avoid duplication
// in documentation pages for trait with automatic implementations like "Send" and "Sync".
aliases: &[String],
doc: &Option<String>,
) {
let inner_impl = i.inner_impl();
let id = cx.derive_id(get_id_for_impl(cx.tcx(), i.impl_item.item_id));
Expand Down Expand Up @@ -2084,6 +2092,10 @@ pub(crate) fn render_impl_summary(
);
}

if let Some(doc) = doc {
write!(w, "<div class=\"docblock\">{doc}</div>");
}

w.write_str("</section>");
}

Expand Down
28 changes: 28 additions & 0 deletions src/librustdoc/html/static/css/rustdoc.css
Original file line number Diff line number Diff line change
Expand Up @@ -2195,6 +2195,34 @@ details.toggle[open] > summary::after {
content: "Collapse";
}

details.toggle:not([open]) > summary .docblock {
max-height: calc(1.5em + 0.75em);
overflow-y: hidden;
}
details.toggle:not([open]) > summary .docblock > :first-child {
max-width: calc(100% - 1em);
overflow: hidden;
width: fit-content;
white-space: nowrap;
position: relative;
padding-right: 1em;
}
details.toggle:not([open]) > summary .docblock > :first-child::after {
content: "…";
position: absolute;
right: 0;
top: 0;
bottom: 0;
z-index: 1;
background-color: var(--main-background-color);
/* In case this ends up in a heading or a `<code>` item. */
font-weight: normal;
font: 1rem/1.5 "Source Serif 4", NanumBarunGothic, serif;
}
details.toggle > summary .docblock {
margin-top: 0.75em;
}

/* This is needed in docblocks to have the "▶" element to be on the same line. */
.docblock summary > * {
display: inline-block;
Expand Down
33 changes: 33 additions & 0 deletions tests/rustdoc-gui/impl-block-doc.goml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Checks that the first sentence of an impl block doc is always visible even when the impl
// block is collapsed.
go-to: "file://" + |DOC_PATH| + "/test_docs/struct.ImplDoc.html"

set-window-size: (900, 600)

define-function: (
"compare-size-and-pos",
[nth_impl],
block {
// First we collapse the impl block.
store-value: (impl_path, "#implementations-list details:nth-of-type(" + |nth_impl| + ")")
set-property: (|impl_path|, {"open": false})
wait-for: |impl_path| + ":not([open])"

store-value: (impl_path, |impl_path| + " summary")
store-size: (|impl_path|, {"height": impl_height})
store-position: (|impl_path|, {"y": impl_y})

store-size: (|impl_path| + " .docblock", {"height": doc_height})
store-position: (|impl_path| + " .docblock", {"y": doc_y})

assert: |impl_y| + |impl_height| >= |doc_y|
}
)

call-function: ("compare-size-and-pos", {"nth_impl": 1})
// Since the first impl block has a long line, we ensure that it doesn't display all of it.
assert: (|impl_y| + |impl_height|) <= (|doc_y| + |doc_height|)

call-function: ("compare-size-and-pos", {"nth_impl": 2})
// The second impl block has a short line.
assert: (|impl_y| + |impl_height|) >= (|doc_y| + |doc_height|)
2 changes: 1 addition & 1 deletion tests/rustdoc-gui/impl-doc.goml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ go-to: "file://" + |DOC_PATH| + "/test_docs/struct.TypeWithImplDoc.html"

// The text is about 24px tall, so if there's a margin, then their position will be >24px apart
compare-elements-position-near-false: (
"#implementations-list > .implementors-toggle > .docblock > p",
"#implementations-list > .implementors-toggle .docblock > p",
"#implementations-list > .implementors-toggle > .impl-items",
{"y": 24}
)
2 changes: 1 addition & 1 deletion tests/rustdoc-gui/item-info-overflow.goml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ assert-text: (
go-to: "file://" + |DOC_PATH| + "/lib2/struct.LongItemInfo2.html"
compare-elements-property: (
"#impl-SimpleTrait-for-LongItemInfo2 .item-info",
"#impl-SimpleTrait-for-LongItemInfo2 + .docblock",
"#impl-SimpleTrait-for-LongItemInfo2 .docblock",
["scrollWidth"],
)
assert-property: (
Expand Down
Loading
Loading