Skip to content

Commit a424813

Browse files
Rollup merge of #132155 - GuillaumeGomez:impl-block-doc, r=rustdoc
Always display first line of impl blocks even when collapsed Fixes #130612. It the line is too long, only the beginning will be visible: ![Screenshot from 2024-10-25 17-21-41](https://github.com/user-attachments/assets/dd2d912c-ad55-4410-8195-1d66a0a99ad4) Otherwise, it looks like this: ![image](https://github.com/user-attachments/assets/1f40b9e0-2143-4b9d-a4b0-338a0cd740df) Can be tested [here](https://rustdoc.crud.net/imperio/impl-block-doc/foo/struct.ImplDoc.html). r? `@notriddle`
2 parents c94848c + 854ebe7 commit a424813

File tree

11 files changed

+242
-58
lines changed

11 files changed

+242
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.18.1
1+
0.18.2

src/librustdoc/html/markdown.rs

+76-20
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ use std::iter::Peekable;
3232
use std::ops::{ControlFlow, Range};
3333
use std::path::PathBuf;
3434
use std::str::{self, CharIndices};
35+
use std::sync::atomic::AtomicUsize;
36+
use std::sync::{Arc, Weak};
3537

3638
use pulldown_cmark::{
3739
BrokenLink, CodeBlockKind, CowStr, Event, LinkType, Options, Parser, Tag, TagEnd, html,
@@ -1301,8 +1303,20 @@ impl LangString {
13011303
}
13021304
}
13031305

1304-
impl Markdown<'_> {
1306+
impl<'a> Markdown<'a> {
13051307
pub fn into_string(self) -> String {
1308+
// This is actually common enough to special-case
1309+
if self.content.is_empty() {
1310+
return String::new();
1311+
}
1312+
1313+
let mut s = String::with_capacity(self.content.len() * 3 / 2);
1314+
html::push_html(&mut s, self.into_iter());
1315+
1316+
s
1317+
}
1318+
1319+
fn into_iter(self) -> CodeBlocks<'a, 'a, impl Iterator<Item = Event<'a>>> {
13061320
let Markdown {
13071321
content: md,
13081322
links,
@@ -1313,32 +1327,72 @@ impl Markdown<'_> {
13131327
heading_offset,
13141328
} = self;
13151329

1316-
// This is actually common enough to special-case
1317-
if md.is_empty() {
1318-
return String::new();
1319-
}
1320-
let mut replacer = |broken_link: BrokenLink<'_>| {
1330+
let replacer = move |broken_link: BrokenLink<'_>| {
13211331
links
13221332
.iter()
13231333
.find(|link| *link.original_text == *broken_link.reference)
13241334
.map(|link| (link.href.as_str().into(), link.tooltip.as_str().into()))
13251335
};
13261336

1327-
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(&mut replacer));
1337+
let p = Parser::new_with_broken_link_callback(md, main_body_opts(), Some(replacer));
13281338
let p = p.into_offset_iter();
13291339

1330-
let mut s = String::with_capacity(md.len() * 3 / 2);
1331-
13321340
ids.handle_footnotes(|ids, existing_footnotes| {
13331341
let p = HeadingLinks::new(p, None, ids, heading_offset);
13341342
let p = footnotes::Footnotes::new(p, existing_footnotes);
13351343
let p = LinkReplacer::new(p.map(|(ev, _)| ev), links);
13361344
let p = TableWrapper::new(p);
1337-
let p = CodeBlocks::new(p, codes, edition, playground);
1338-
html::push_html(&mut s, p);
1339-
});
1345+
CodeBlocks::new(p, codes, edition, playground)
1346+
})
1347+
}
13401348

1341-
s
1349+
/// Convert markdown to (summary, remaining) HTML.
1350+
///
1351+
/// - The summary is the first top-level Markdown element (usually a paragraph, but potentially
1352+
/// any block).
1353+
/// - The remaining docs contain everything after the summary.
1354+
pub(crate) fn split_summary_and_content(self) -> (Option<String>, Option<String>) {
1355+
if self.content.is_empty() {
1356+
return (None, None);
1357+
}
1358+
let mut p = self.into_iter();
1359+
1360+
let mut event_level = 0;
1361+
let mut summary_events = Vec::new();
1362+
let mut get_next_tag = false;
1363+
1364+
let mut end_of_summary = false;
1365+
while let Some(event) = p.next() {
1366+
match event {
1367+
Event::Start(_) => event_level += 1,
1368+
Event::End(kind) => {
1369+
event_level -= 1;
1370+
if event_level == 0 {
1371+
// We're back at the "top" so it means we're done with the summary.
1372+
end_of_summary = true;
1373+
// We surround tables with `<div>` HTML tags so this is a special case.
1374+
get_next_tag = kind == TagEnd::Table;
1375+
}
1376+
}
1377+
_ => {}
1378+
}
1379+
summary_events.push(event);
1380+
if end_of_summary {
1381+
if get_next_tag && let Some(event) = p.next() {
1382+
summary_events.push(event);
1383+
}
1384+
break;
1385+
}
1386+
}
1387+
let mut summary = String::new();
1388+
html::push_html(&mut summary, summary_events.into_iter());
1389+
if summary.is_empty() {
1390+
return (None, None);
1391+
}
1392+
let mut content = String::new();
1393+
html::push_html(&mut content, p);
1394+
1395+
if content.is_empty() { (Some(summary), None) } else { (Some(summary), Some(content)) }
13421396
}
13431397
}
13441398

@@ -1882,7 +1936,7 @@ pub(crate) fn rust_code_blocks(md: &str, extra_info: &ExtraInfo<'_>) -> Vec<Rust
18821936
#[derive(Clone, Default, Debug)]
18831937
pub struct IdMap {
18841938
map: FxHashMap<String, usize>,
1885-
existing_footnotes: usize,
1939+
existing_footnotes: Arc<AtomicUsize>,
18861940
}
18871941

18881942
fn is_default_id(id: &str) -> bool {
@@ -1942,7 +1996,7 @@ fn is_default_id(id: &str) -> bool {
19421996

19431997
impl IdMap {
19441998
pub fn new() -> Self {
1945-
IdMap { map: FxHashMap::default(), existing_footnotes: 0 }
1999+
IdMap { map: FxHashMap::default(), existing_footnotes: Arc::new(AtomicUsize::new(0)) }
19462000
}
19472001

19482002
pub(crate) fn derive<S: AsRef<str> + ToString>(&mut self, candidate: S) -> String {
@@ -1970,15 +2024,17 @@ impl IdMap {
19702024

19712025
/// Method to handle `existing_footnotes` increment automatically (to prevent forgetting
19722026
/// about it).
1973-
pub(crate) fn handle_footnotes<F: FnOnce(&mut Self, &mut usize)>(&mut self, closure: F) {
1974-
let mut existing_footnotes = self.existing_footnotes;
2027+
pub(crate) fn handle_footnotes<'a, T, F: FnOnce(&'a mut Self, Weak<AtomicUsize>) -> T>(
2028+
&'a mut self,
2029+
closure: F,
2030+
) -> T {
2031+
let existing_footnotes = Arc::downgrade(&self.existing_footnotes);
19752032

1976-
closure(self, &mut existing_footnotes);
1977-
self.existing_footnotes = existing_footnotes;
2033+
closure(self, existing_footnotes)
19782034
}
19792035

19802036
pub(crate) fn clear(&mut self) {
19812037
self.map.clear();
1982-
self.existing_footnotes = 0;
2038+
self.existing_footnotes = Arc::new(AtomicUsize::new(0));
19832039
}
19842040
}

src/librustdoc/html/markdown/footnotes.rs

+16-9
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
//! Markdown footnote handling.
2+
23
use std::fmt::Write as _;
4+
use std::sync::atomic::{AtomicUsize, Ordering};
5+
use std::sync::{Arc, Weak};
36

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

912
/// Moves all footnote definitions to the end and add back links to the
1013
/// references.
11-
pub(super) struct Footnotes<'a, 'b, I> {
14+
pub(super) struct Footnotes<'a, I> {
1215
inner: I,
1316
footnotes: FxIndexMap<String, FootnoteDef<'a>>,
14-
existing_footnotes: &'b mut usize,
17+
existing_footnotes: Arc<AtomicUsize>,
18+
start_id: usize,
1519
}
1620

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

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

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

67-
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, '_, I> {
74+
impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, I> {
6875
type Item = SpannedEvent<'a>;
6976

7077
fn next(&mut self) -> Option<Self::Item> {
@@ -87,7 +94,7 @@ impl<'a, I: Iterator<Item = SpannedEvent<'a>>> Iterator for Footnotes<'a, '_, I>
8794
// After all the markdown is emmited, emit an <hr> then all the footnotes
8895
// in a list.
8996
let defs: Vec<_> = self.footnotes.drain(..).map(|(_, x)| x).collect();
90-
*self.existing_footnotes += defs.len();
97+
self.existing_footnotes.fetch_add(defs.len(), Ordering::Relaxed);
9198
let defs_html = render_footnotes_defs(defs);
9299
return Some((Event::Html(defs_html.into()), 0..0));
93100
} else {

src/librustdoc/html/render/mod.rs

+28-17
Original file line numberDiff line numberDiff line change
@@ -1904,7 +1904,6 @@ fn render_impl(
19041904
}
19051905
}
19061906

1907-
let trait_is_none = trait_.is_none();
19081907
// If we've implemented a trait, then also emit documentation for all
19091908
// default items which weren't overridden in the implementation block.
19101909
// We don't emit documentation for default items if they appear in the
@@ -1936,6 +1935,23 @@ fn render_impl(
19361935
if rendering_params.toggle_open_by_default { " open" } else { "" }
19371936
);
19381937
}
1938+
1939+
let (before_dox, after_dox) = i
1940+
.impl_item
1941+
.opt_doc_value()
1942+
.map(|dox| {
1943+
Markdown {
1944+
content: &*dox,
1945+
links: &i.impl_item.links(cx),
1946+
ids: &mut cx.id_map.borrow_mut(),
1947+
error_codes: cx.shared.codes,
1948+
edition: cx.shared.edition(),
1949+
playground: &cx.shared.playground,
1950+
heading_offset: HeadingOffset::H4,
1951+
}
1952+
.split_summary_and_content()
1953+
})
1954+
.unwrap_or((None, None));
19391955
render_impl_summary(
19401956
w,
19411957
cx,
@@ -1944,33 +1960,23 @@ fn render_impl(
19441960
rendering_params.show_def_docs,
19451961
use_absolute,
19461962
aliases,
1963+
&before_dox,
19471964
);
19481965
if toggled {
19491966
w.write_str("</summary>");
19501967
}
19511968

1952-
if let Some(ref dox) = i.impl_item.opt_doc_value() {
1953-
if trait_is_none && impl_.items.is_empty() {
1969+
if before_dox.is_some() {
1970+
if trait_.is_none() && impl_.items.is_empty() {
19541971
w.write_str(
19551972
"<div class=\"item-info\">\
19561973
<div class=\"stab empty-impl\">This impl block contains no items.</div>\
19571974
</div>",
19581975
);
19591976
}
1960-
write!(
1961-
w,
1962-
"<div class=\"docblock\">{}</div>",
1963-
Markdown {
1964-
content: dox,
1965-
links: &i.impl_item.links(cx),
1966-
ids: &mut cx.id_map.borrow_mut(),
1967-
error_codes: cx.shared.codes,
1968-
edition: cx.shared.edition(),
1969-
playground: &cx.shared.playground,
1970-
heading_offset: HeadingOffset::H4,
1971-
}
1972-
.into_string()
1973-
);
1977+
if let Some(after_dox) = after_dox {
1978+
write!(w, "<div class=\"docblock\">{after_dox}</div>");
1979+
}
19741980
}
19751981
if !default_impl_items.is_empty() || !impl_items.is_empty() {
19761982
w.write_str("<div class=\"impl-items\">");
@@ -2031,6 +2037,7 @@ pub(crate) fn render_impl_summary(
20312037
// This argument is used to reference same type with different paths to avoid duplication
20322038
// in documentation pages for trait with automatic implementations like "Send" and "Sync".
20332039
aliases: &[String],
2040+
doc: &Option<String>,
20342041
) {
20352042
let inner_impl = i.inner_impl();
20362043
let id = cx.derive_id(get_id_for_impl(cx.tcx(), i.impl_item.item_id));
@@ -2082,6 +2089,10 @@ pub(crate) fn render_impl_summary(
20822089
);
20832090
}
20842091

2092+
if let Some(doc) = doc {
2093+
write!(w, "<div class=\"docblock\">{doc}</div>");
2094+
}
2095+
20852096
w.write_str("</section>");
20862097
}
20872098

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

+33
Original file line numberDiff line numberDiff line change
@@ -2210,6 +2210,39 @@ details.toggle[open] > summary::after {
22102210
content: "Collapse";
22112211
}
22122212

2213+
details.toggle:not([open]) > summary .docblock {
2214+
max-height: calc(1.5em + 0.75em);
2215+
overflow-y: hidden;
2216+
}
2217+
details.toggle:not([open]) > summary .docblock > :first-child {
2218+
max-width: 100%;
2219+
overflow: hidden;
2220+
width: fit-content;
2221+
white-space: nowrap;
2222+
position: relative;
2223+
padding-right: 1em;
2224+
}
2225+
details.toggle:not([open]) > summary .docblock > :first-child::after {
2226+
content: "…";
2227+
position: absolute;
2228+
right: 0;
2229+
top: 0;
2230+
bottom: 0;
2231+
z-index: 1;
2232+
background-color: var(--main-background-color);
2233+
font: 1rem/1.5 "Source Serif 4", NanumBarunGothic, serif;
2234+
/* To make it look a bit better and not have it stuck to the preceding element. */
2235+
padding-left: 0.2em;
2236+
}
2237+
details.toggle:not([open]) > summary .docblock > div:first-child::after {
2238+
/* This is to make the "..." always appear at the bottom. */
2239+
padding-top: calc(1.5em + 0.75em - 1.2rem);
2240+
}
2241+
2242+
details.toggle > summary .docblock {
2243+
margin-top: 0.75em;
2244+
}
2245+
22132246
/* This is needed in docblocks to have the "▶" element to be on the same line. */
22142247
.docblock summary > * {
22152248
display: inline-block;

tests/rustdoc-gui/docblock-table-overflow.goml

+3-7
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,8 @@ assert-property: (".top-doc .docblock table", {"scrollWidth": "1572"})
1010

1111
// Checking it works on other doc blocks as well...
1212

13-
// Logically, the ".docblock" and the "<p>" should have the same scroll width.
14-
compare-elements-property: (
15-
"#implementations-list > details .docblock",
16-
"#implementations-list > details .docblock > p",
17-
["scrollWidth"],
18-
)
19-
assert-property: ("#implementations-list > details .docblock", {"scrollWidth": "835"})
13+
// Logically, the ".docblock" and the "<p>" should have the same scroll width (if we exclude the margin).
14+
assert-property: ("#implementations-list > details .docblock", {"scrollWidth": 816})
15+
assert-property: ("#implementations-list > details .docblock > p", {"scrollWidth": 835})
2016
// However, since there is overflow in the <table>, its scroll width is bigger.
2117
assert-property: ("#implementations-list > details .docblock table", {"scrollWidth": "1572"})

0 commit comments

Comments
 (0)