about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/doc/not_found.md2
-rw-r--r--src/librustdoc/clean/types.rs6
-rw-r--r--src/librustdoc/html/markdown.rs73
-rw-r--r--src/librustdoc/html/render/context.rs6
-rw-r--r--src/librustdoc/html/render/sidebar.rs282
-rw-r--r--src/librustdoc/html/static/css/rustdoc.css14
-rw-r--r--src/librustdoc/html/static/js/main.js4
-rw-r--r--src/librustdoc/html/static/js/settings.js29
-rw-r--r--src/librustdoc/html/static/js/storage.js13
-rw-r--r--src/librustdoc/html/templates/sidebar.html49
-rw-r--r--src/librustdoc/html/toc.rs26
-rw-r--r--src/librustdoc/html/toc/tests.rs6
-rw-r--r--src/librustdoc/markdown.rs1
13 files changed, 365 insertions, 146 deletions
diff --git a/src/doc/not_found.md b/src/doc/not_found.md
index f0794fc0be3..9552759d2b8 100644
--- a/src/doc/not_found.md
+++ b/src/doc/not_found.md
@@ -2,7 +2,7 @@
 
 <!-- Completely hide the TOC and the section numbers -->
 <style type="text/css">
-#TOC { display: none; }
+#rustdoc-toc { display: none; }
 .header-section-number { display: none; }
 li {list-style-type: none; }
 #search-input {
diff --git a/src/librustdoc/clean/types.rs b/src/librustdoc/clean/types.rs
index 58fcfc4ed8d..5666b229078 100644
--- a/src/librustdoc/clean/types.rs
+++ b/src/librustdoc/clean/types.rs
@@ -512,9 +512,6 @@ impl Item {
     pub(crate) fn is_mod(&self) -> bool {
         self.type_() == ItemType::Module
     }
-    pub(crate) fn is_trait(&self) -> bool {
-        self.type_() == ItemType::Trait
-    }
     pub(crate) fn is_struct(&self) -> bool {
         self.type_() == ItemType::Struct
     }
@@ -542,9 +539,6 @@ impl Item {
     pub(crate) fn is_ty_method(&self) -> bool {
         self.type_() == ItemType::TyMethod
     }
-    pub(crate) fn is_type_alias(&self) -> bool {
-        self.type_() == ItemType::TypeAlias
-    }
     pub(crate) fn is_primitive(&self) -> bool {
         self.type_() == ItemType::Primitive
     }
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index e37c24c1438..6f4665db6f1 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -51,12 +51,12 @@ use tracing::{debug, trace};
 use crate::clean::RenderedLink;
 use crate::doctest;
 use crate::doctest::GlobalTestOptions;
-use crate::html::escape::Escape;
+use crate::html::escape::{Escape, EscapeBodyText};
 use crate::html::format::Buffer;
 use crate::html::highlight;
 use crate::html::length_limit::HtmlWithLimit;
 use crate::html::render::small_url_encode;
-use crate::html::toc::TocBuilder;
+use crate::html::toc::{Toc, TocBuilder};
 
 #[cfg(test)]
 mod tests;
@@ -102,6 +102,7 @@ pub struct Markdown<'a> {
 /// A struct like `Markdown` that renders the markdown with a table of contents.
 pub(crate) struct MarkdownWithToc<'a> {
     pub(crate) content: &'a str,
+    pub(crate) links: &'a [RenderedLink],
     pub(crate) ids: &'a mut IdMap,
     pub(crate) error_codes: ErrorCodes,
     pub(crate) edition: Edition,
@@ -533,9 +534,11 @@ impl<'a, 'b, 'ids, I: Iterator<Item = SpannedEvent<'a>>> Iterator
             let id = self.id_map.derive(id);
 
             if let Some(ref mut builder) = self.toc {
+                let mut text_header = String::new();
+                plain_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut text_header);
                 let mut html_header = String::new();
-                html::push_html(&mut html_header, self.buf.iter().map(|(ev, _)| ev.clone()));
-                let sec = builder.push(level as u32, html_header, id.clone());
+                html_text_from_events(self.buf.iter().map(|(ev, _)| ev.clone()), &mut html_header);
+                let sec = builder.push(level as u32, text_header, html_header, id.clone());
                 self.buf.push_front((Event::Html(format!("{sec} ").into()), 0..0));
             }
 
@@ -1412,10 +1415,23 @@ impl Markdown<'_> {
 }
 
 impl MarkdownWithToc<'_> {
-    pub(crate) fn into_string(self) -> String {
-        let MarkdownWithToc { content: md, ids, error_codes: codes, edition, playground } = self;
+    pub(crate) fn into_parts(self) -> (Toc, String) {
+        let MarkdownWithToc { content: md, links, ids, error_codes: codes, edition, playground } =
+            self;
 
-        let p = Parser::new_ext(md, main_body_opts()).into_offset_iter();
+        // This is actually common enough to special-case
+        if md.is_empty() {
+            return (Toc { entries: Vec::new() }, String::new());
+        }
+        let mut replacer = |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 = p.into_offset_iter();
 
         let mut s = String::with_capacity(md.len() * 3 / 2);
 
@@ -1429,7 +1445,11 @@ impl MarkdownWithToc<'_> {
             html::push_html(&mut s, p);
         }
 
-        format!("<nav id=\"TOC\">{toc}</nav>{s}", toc = toc.into_toc().print())
+        (toc.into_toc(), s)
+    }
+    pub(crate) fn into_string(self) -> String {
+        let (toc, s) = self.into_parts();
+        format!("<nav id=\"rustdoc\">{toc}</nav>{s}", toc = toc.print())
     }
 }
 
@@ -1608,7 +1628,16 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
 
     let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
 
-    for event in p {
+    plain_text_from_events(p, &mut s);
+
+    s
+}
+
+pub(crate) fn plain_text_from_events<'a>(
+    events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
+    s: &mut String,
+) {
+    for event in events {
         match &event {
             Event::Text(text) => s.push_str(text),
             Event::Code(code) => {
@@ -1623,8 +1652,29 @@ pub(crate) fn plain_text_summary(md: &str, link_names: &[RenderedLink]) -> Strin
             _ => (),
         }
     }
+}
 
-    s
+pub(crate) fn html_text_from_events<'a>(
+    events: impl Iterator<Item = pulldown_cmark::Event<'a>>,
+    s: &mut String,
+) {
+    for event in events {
+        match &event {
+            Event::Text(text) => {
+                write!(s, "{}", EscapeBodyText(text)).expect("string alloc infallible")
+            }
+            Event::Code(code) => {
+                s.push_str("<code>");
+                write!(s, "{}", EscapeBodyText(code)).expect("string alloc infallible");
+                s.push_str("</code>");
+            }
+            Event::HardBreak | Event::SoftBreak => s.push(' '),
+            Event::Start(Tag::CodeBlock(..)) => break,
+            Event::End(TagEnd::Paragraph) => break,
+            Event::End(TagEnd::Heading(..)) => break,
+            _ => (),
+        }
+    }
 }
 
 #[derive(Debug)]
@@ -1975,7 +2025,8 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> {
     map.insert("default-settings".into(), 1);
     map.insert("sidebar-vars".into(), 1);
     map.insert("copy-path".into(), 1);
-    map.insert("TOC".into(), 1);
+    map.insert("rustdoc-toc".into(), 1);
+    map.insert("rustdoc-modnav".into(), 1);
     // This is the list of IDs used by rustdoc sections (but still generated by
     // rustdoc).
     map.insert("fields".into(), 1);
diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs
index 19386fe5e69..9fe43e428f2 100644
--- a/src/librustdoc/html/render/context.rs
+++ b/src/librustdoc/html/render/context.rs
@@ -15,7 +15,7 @@ use rustc_span::{sym, FileName, Symbol};
 use tracing::info;
 
 use super::print_item::{full_path, item_path, print_item};
-use super::sidebar::{print_sidebar, sidebar_module_like, Sidebar};
+use super::sidebar::{print_sidebar, sidebar_module_like, ModuleLike, Sidebar};
 use super::write_shared::write_shared;
 use super::{collect_spans_and_sources, scrape_examples_help, AllTypes, LinkFromSrc, StylePath};
 use crate::clean::types::ExternalLocation;
@@ -617,12 +617,14 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> {
         let all = shared.all.replace(AllTypes::new());
         let mut sidebar = Buffer::html();
 
-        let blocks = sidebar_module_like(all.item_sections());
+        // all.html is not customizable, so a blank id map is fine
+        let blocks = sidebar_module_like(all.item_sections(), &mut IdMap::new(), ModuleLike::Crate);
         let bar = Sidebar {
             title_prefix: "",
             title: "",
             is_crate: false,
             is_mod: false,
+            parent_is_crate: false,
             blocks: vec![blocks],
             path: String::new(),
         };
diff --git a/src/librustdoc/html/render/sidebar.rs b/src/librustdoc/html/render/sidebar.rs
index 5b9c93ef74a..c75c28ef0a3 100644
--- a/src/librustdoc/html/render/sidebar.rs
+++ b/src/librustdoc/html/render/sidebar.rs
@@ -13,7 +13,24 @@ use crate::clean;
 use crate::formats::item_type::ItemType;
 use crate::formats::Impl;
 use crate::html::format::Buffer;
-use crate::html::markdown::IdMap;
+use crate::html::markdown::{IdMap, MarkdownWithToc};
+
+#[derive(Clone, Copy)]
+pub(crate) enum ModuleLike {
+    Module,
+    Crate,
+}
+
+impl ModuleLike {
+    pub(crate) fn is_crate(self) -> bool {
+        matches!(self, ModuleLike::Crate)
+    }
+}
+impl<'a> From<&'a clean::Item> for ModuleLike {
+    fn from(it: &'a clean::Item) -> ModuleLike {
+        if it.is_crate() { ModuleLike::Crate } else { ModuleLike::Module }
+    }
+}
 
 #[derive(Template)]
 #[template(path = "sidebar.html")]
@@ -21,6 +38,7 @@ pub(super) struct Sidebar<'a> {
     pub(super) title_prefix: &'static str,
     pub(super) title: &'a str,
     pub(super) is_crate: bool,
+    pub(super) parent_is_crate: bool,
     pub(super) is_mod: bool,
     pub(super) blocks: Vec<LinkBlock<'a>>,
     pub(super) path: String,
@@ -63,15 +81,19 @@ impl<'a> LinkBlock<'a> {
 /// A link to an item. Content should not be escaped.
 #[derive(PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
 pub(crate) struct Link<'a> {
-    /// The content for the anchor tag
+    /// The content for the anchor tag and title attr
     name: Cow<'a, str>,
+    /// The content for the anchor tag (if different from name)
+    name_html: Option<Cow<'a, str>>,
     /// The id of an anchor within the page (without a `#` prefix)
     href: Cow<'a, str>,
+    /// Nested list of links (used only in top-toc)
+    children: Vec<Link<'a>>,
 }
 
 impl<'a> Link<'a> {
     pub fn new(href: impl Into<Cow<'a, str>>, name: impl Into<Cow<'a, str>>) -> Self {
-        Self { href: href.into(), name: name.into() }
+        Self { href: href.into(), name: name.into(), children: vec![], name_html: None }
     }
     pub fn empty() -> Link<'static> {
         Link::new("", "")
@@ -95,17 +117,21 @@ pub(crate) mod filters {
 }
 
 pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buffer) {
-    let blocks: Vec<LinkBlock<'_>> = match *it.kind {
-        clean::StructItem(ref s) => sidebar_struct(cx, it, s),
-        clean::TraitItem(ref t) => sidebar_trait(cx, it, t),
-        clean::PrimitiveItem(_) => sidebar_primitive(cx, it),
-        clean::UnionItem(ref u) => sidebar_union(cx, it, u),
-        clean::EnumItem(ref e) => sidebar_enum(cx, it, e),
-        clean::TypeAliasItem(ref t) => sidebar_type_alias(cx, it, t),
-        clean::ModuleItem(ref m) => vec![sidebar_module(&m.items)],
-        clean::ForeignTypeItem => sidebar_foreign_type(cx, it),
-        _ => vec![],
-    };
+    let mut ids = IdMap::new();
+    let mut blocks: Vec<LinkBlock<'_>> = docblock_toc(cx, it, &mut ids).into_iter().collect();
+    match *it.kind {
+        clean::StructItem(ref s) => sidebar_struct(cx, it, s, &mut blocks),
+        clean::TraitItem(ref t) => sidebar_trait(cx, it, t, &mut blocks),
+        clean::PrimitiveItem(_) => sidebar_primitive(cx, it, &mut blocks),
+        clean::UnionItem(ref u) => sidebar_union(cx, it, u, &mut blocks),
+        clean::EnumItem(ref e) => sidebar_enum(cx, it, e, &mut blocks),
+        clean::TypeAliasItem(ref t) => sidebar_type_alias(cx, it, t, &mut blocks),
+        clean::ModuleItem(ref m) => {
+            blocks.push(sidebar_module(&m.items, &mut ids, ModuleLike::from(it)))
+        }
+        clean::ForeignTypeItem => sidebar_foreign_type(cx, it, &mut blocks),
+        _ => {}
+    }
     // The sidebar is designed to display sibling functions, modules and
     // other miscellaneous information. since there are lots of sibling
     // items (and that causes quadratic growth in large modules),
@@ -113,15 +139,9 @@ pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buf
     // still, we don't move everything into JS because we want to preserve
     // as much HTML as possible in order to allow non-JS-enabled browsers
     // to navigate the documentation (though slightly inefficiently).
-    let (title_prefix, title) = if it.is_struct()
-        || it.is_trait()
-        || it.is_primitive()
-        || it.is_union()
-        || it.is_enum()
-        // crate title is displayed as part of logo lockup
-        || (it.is_mod() && !it.is_crate())
-        || it.is_type_alias()
-    {
+    //
+    // crate title is displayed as part of logo lockup
+    let (title_prefix, title) = if !blocks.is_empty() && !it.is_crate() {
         (
             match *it.kind {
                 clean::ModuleItem(..) => "Module ",
@@ -146,8 +166,15 @@ pub(super) fn print_sidebar(cx: &Context<'_>, it: &clean::Item, buffer: &mut Buf
     } else {
         "".into()
     };
-    let sidebar =
-        Sidebar { title_prefix, title, is_mod: it.is_mod(), is_crate: it.is_crate(), blocks, path };
+    let sidebar = Sidebar {
+        title_prefix,
+        title,
+        is_mod: it.is_mod(),
+        is_crate: it.is_crate(),
+        parent_is_crate: sidebar_path.len() == 1,
+        blocks,
+        path,
+    };
     sidebar.render_into(buffer).unwrap();
 }
 
@@ -163,30 +190,80 @@ fn get_struct_fields_name<'a>(fields: &'a [clean::Item]) -> Vec<Link<'a>> {
     fields
 }
 
+fn docblock_toc<'a>(
+    cx: &'a Context<'_>,
+    it: &'a clean::Item,
+    ids: &mut IdMap,
+) -> Option<LinkBlock<'a>> {
+    let (toc, _) = MarkdownWithToc {
+        content: &it.doc_value(),
+        links: &it.links(cx),
+        ids,
+        error_codes: cx.shared.codes,
+        edition: cx.shared.edition(),
+        playground: &cx.shared.playground,
+    }
+    .into_parts();
+    let links: Vec<Link<'_>> = toc
+        .entries
+        .into_iter()
+        .map(|entry| {
+            Link {
+                name_html: if entry.html == entry.name { None } else { Some(entry.html.into()) },
+                name: entry.name.into(),
+                href: entry.id.into(),
+                children: entry
+                    .children
+                    .entries
+                    .into_iter()
+                    .map(|entry| Link {
+                        name_html: if entry.html == entry.name {
+                            None
+                        } else {
+                            Some(entry.html.into())
+                        },
+                        name: entry.name.into(),
+                        href: entry.id.into(),
+                        // Only a single level of nesting is shown here.
+                        // Going the full six could break the layout,
+                        // so we have to cut it off somewhere.
+                        children: vec![],
+                    })
+                    .collect(),
+            }
+        })
+        .collect();
+    if links.is_empty() {
+        None
+    } else {
+        Some(LinkBlock::new(Link::new("", "Sections"), "top-toc", links))
+    }
+}
+
 fn sidebar_struct<'a>(
     cx: &'a Context<'_>,
     it: &'a clean::Item,
     s: &'a clean::Struct,
-) -> Vec<LinkBlock<'a>> {
+    items: &mut Vec<LinkBlock<'a>>,
+) {
     let fields = get_struct_fields_name(&s.fields);
     let field_name = match s.ctor_kind {
         Some(CtorKind::Fn) => Some("Tuple Fields"),
         None => Some("Fields"),
         _ => None,
     };
-    let mut items = vec![];
     if let Some(name) = field_name {
         items.push(LinkBlock::new(Link::new("fields", name), "structfield", fields));
     }
-    sidebar_assoc_items(cx, it, &mut items);
-    items
+    sidebar_assoc_items(cx, it, items);
 }
 
 fn sidebar_trait<'a>(
     cx: &'a Context<'_>,
     it: &'a clean::Item,
     t: &'a clean::Trait,
-) -> Vec<LinkBlock<'a>> {
+    blocks: &mut Vec<LinkBlock<'a>>,
+) {
     fn filter_items<'a>(
         items: &'a [clean::Item],
         filt: impl Fn(&clean::Item) -> bool,
@@ -223,19 +300,20 @@ fn sidebar_trait<'a>(
         foreign_impls.sort();
     }
 
-    let mut blocks: Vec<LinkBlock<'_>> = [
-        ("required-associated-types", "Required Associated Types", req_assoc),
-        ("provided-associated-types", "Provided Associated Types", prov_assoc),
-        ("required-associated-consts", "Required Associated Constants", req_assoc_const),
-        ("provided-associated-consts", "Provided Associated Constants", prov_assoc_const),
-        ("required-methods", "Required Methods", req_method),
-        ("provided-methods", "Provided Methods", prov_method),
-        ("foreign-impls", "Implementations on Foreign Types", foreign_impls),
-    ]
-    .into_iter()
-    .map(|(id, title, items)| LinkBlock::new(Link::new(id, title), "", items))
-    .collect();
-    sidebar_assoc_items(cx, it, &mut blocks);
+    blocks.extend(
+        [
+            ("required-associated-types", "Required Associated Types", req_assoc),
+            ("provided-associated-types", "Provided Associated Types", prov_assoc),
+            ("required-associated-consts", "Required Associated Constants", req_assoc_const),
+            ("provided-associated-consts", "Provided Associated Constants", prov_assoc_const),
+            ("required-methods", "Required Methods", req_method),
+            ("provided-methods", "Provided Methods", prov_method),
+            ("foreign-impls", "Implementations on Foreign Types", foreign_impls),
+        ]
+        .into_iter()
+        .map(|(id, title, items)| LinkBlock::new(Link::new(id, title), "", items)),
+    );
+    sidebar_assoc_items(cx, it, blocks);
 
     if !t.is_object_safe(cx.tcx()) {
         blocks.push(LinkBlock::forced(
@@ -251,20 +329,17 @@ fn sidebar_trait<'a>(
             "impl-auto",
         ));
     }
-    blocks
 }
 
-fn sidebar_primitive<'a>(cx: &'a Context<'_>, it: &'a clean::Item) -> Vec<LinkBlock<'a>> {
+fn sidebar_primitive<'a>(cx: &'a Context<'_>, it: &'a clean::Item, items: &mut Vec<LinkBlock<'a>>) {
     if it.name.map(|n| n.as_str() != "reference").unwrap_or(false) {
-        let mut items = vec![];
-        sidebar_assoc_items(cx, it, &mut items);
-        items
+        sidebar_assoc_items(cx, it, items);
     } else {
         let shared = Rc::clone(&cx.shared);
         let (concrete, synthetic, blanket_impl) =
             super::get_filtered_impls_for_reference(&shared, it);
 
-        sidebar_render_assoc_items(cx, &mut IdMap::new(), concrete, synthetic, blanket_impl).into()
+        sidebar_render_assoc_items(cx, &mut IdMap::new(), concrete, synthetic, blanket_impl, items);
     }
 }
 
@@ -272,8 +347,8 @@ fn sidebar_type_alias<'a>(
     cx: &'a Context<'_>,
     it: &'a clean::Item,
     t: &'a clean::TypeAlias,
-) -> Vec<LinkBlock<'a>> {
-    let mut items = vec![];
+    items: &mut Vec<LinkBlock<'a>>,
+) {
     if let Some(inner_type) = &t.inner_type {
         items.push(LinkBlock::forced(Link::new("aliased-type", "Aliased type"), "type"));
         match inner_type {
@@ -295,19 +370,18 @@ fn sidebar_type_alias<'a>(
             }
         }
     }
-    sidebar_assoc_items(cx, it, &mut items);
-    items
+    sidebar_assoc_items(cx, it, items);
 }
 
 fn sidebar_union<'a>(
     cx: &'a Context<'_>,
     it: &'a clean::Item,
     u: &'a clean::Union,
-) -> Vec<LinkBlock<'a>> {
+    items: &mut Vec<LinkBlock<'a>>,
+) {
     let fields = get_struct_fields_name(&u.fields);
-    let mut items = vec![LinkBlock::new(Link::new("fields", "Fields"), "structfield", fields)];
-    sidebar_assoc_items(cx, it, &mut items);
-    items
+    items.push(LinkBlock::new(Link::new("fields", "Fields"), "structfield", fields));
+    sidebar_assoc_items(cx, it, items);
 }
 
 /// Adds trait implementations into the blocks of links
@@ -346,21 +420,22 @@ fn sidebar_assoc_items<'a>(
             methods.sort();
         }
 
-        let mut deref_methods = Vec::new();
-        let [concrete, synthetic, blanket] = if v.iter().any(|i| i.inner_impl().trait_.is_some()) {
+        let mut blocks = vec![
+            LinkBlock::new(
+                Link::new("implementations", "Associated Constants"),
+                "associatedconstant",
+                assoc_consts,
+            ),
+            LinkBlock::new(Link::new("implementations", "Methods"), "method", methods),
+        ];
+
+        if v.iter().any(|i| i.inner_impl().trait_.is_some()) {
             if let Some(impl_) =
                 v.iter().find(|i| i.trait_did() == cx.tcx().lang_items().deref_trait())
             {
                 let mut derefs = DefIdSet::default();
                 derefs.insert(did);
-                sidebar_deref_methods(
-                    cx,
-                    &mut deref_methods,
-                    impl_,
-                    v,
-                    &mut derefs,
-                    &mut used_links,
-                );
+                sidebar_deref_methods(cx, &mut blocks, impl_, v, &mut derefs, &mut used_links);
             }
 
             let (synthetic, concrete): (Vec<&Impl>, Vec<&Impl>) =
@@ -368,21 +443,15 @@ fn sidebar_assoc_items<'a>(
             let (blanket_impl, concrete): (Vec<&Impl>, Vec<&Impl>) =
                 concrete.into_iter().partition::<Vec<_>, _>(|i| i.inner_impl().kind.is_blanket());
 
-            sidebar_render_assoc_items(cx, &mut id_map, concrete, synthetic, blanket_impl)
-        } else {
-            std::array::from_fn(|_| LinkBlock::new(Link::empty(), "", vec![]))
-        };
-
-        let mut blocks = vec![
-            LinkBlock::new(
-                Link::new("implementations", "Associated Constants"),
-                "associatedconstant",
-                assoc_consts,
-            ),
-            LinkBlock::new(Link::new("implementations", "Methods"), "method", methods),
-        ];
-        blocks.append(&mut deref_methods);
-        blocks.extend([concrete, synthetic, blanket]);
+            sidebar_render_assoc_items(
+                cx,
+                &mut id_map,
+                concrete,
+                synthetic,
+                blanket_impl,
+                &mut blocks,
+            );
+        }
         links.append(&mut blocks);
     }
 }
@@ -472,7 +541,8 @@ fn sidebar_enum<'a>(
     cx: &'a Context<'_>,
     it: &'a clean::Item,
     e: &'a clean::Enum,
-) -> Vec<LinkBlock<'a>> {
+    items: &mut Vec<LinkBlock<'a>>,
+) {
     let mut variants = e
         .variants()
         .filter_map(|v| v.name)
@@ -480,24 +550,37 @@ fn sidebar_enum<'a>(
         .collect::<Vec<_>>();
     variants.sort_unstable();
 
-    let mut items = vec![LinkBlock::new(Link::new("variants", "Variants"), "variant", variants)];
-    sidebar_assoc_items(cx, it, &mut items);
-    items
+    items.push(LinkBlock::new(Link::new("variants", "Variants"), "variant", variants));
+    sidebar_assoc_items(cx, it, items);
 }
 
 pub(crate) fn sidebar_module_like(
     item_sections_in_use: FxHashSet<ItemSection>,
+    ids: &mut IdMap,
+    module_like: ModuleLike,
 ) -> LinkBlock<'static> {
-    let item_sections = ItemSection::ALL
+    let item_sections: Vec<Link<'_>> = ItemSection::ALL
         .iter()
         .copied()
         .filter(|sec| item_sections_in_use.contains(sec))
-        .map(|sec| Link::new(sec.id(), sec.name()))
+        .map(|sec| Link::new(ids.derive(sec.id()), sec.name()))
         .collect();
-    LinkBlock::new(Link::empty(), "", item_sections)
+    let header = if let Some(first_section) = item_sections.get(0) {
+        Link::new(
+            first_section.href.to_owned(),
+            if module_like.is_crate() { "Crate Items" } else { "Module Items" },
+        )
+    } else {
+        Link::empty()
+    };
+    LinkBlock::new(header, "", item_sections)
 }
 
-fn sidebar_module(items: &[clean::Item]) -> LinkBlock<'static> {
+fn sidebar_module(
+    items: &[clean::Item],
+    ids: &mut IdMap,
+    module_like: ModuleLike,
+) -> LinkBlock<'static> {
     let item_sections_in_use: FxHashSet<_> = items
         .iter()
         .filter(|it| {
@@ -518,13 +601,15 @@ fn sidebar_module(items: &[clean::Item]) -> LinkBlock<'static> {
         .map(|it| item_ty_to_section(it.type_()))
         .collect();
 
-    sidebar_module_like(item_sections_in_use)
+    sidebar_module_like(item_sections_in_use, ids, module_like)
 }
 
-fn sidebar_foreign_type<'a>(cx: &'a Context<'_>, it: &'a clean::Item) -> Vec<LinkBlock<'a>> {
-    let mut items = vec![];
-    sidebar_assoc_items(cx, it, &mut items);
-    items
+fn sidebar_foreign_type<'a>(
+    cx: &'a Context<'_>,
+    it: &'a clean::Item,
+    items: &mut Vec<LinkBlock<'a>>,
+) {
+    sidebar_assoc_items(cx, it, items);
 }
 
 /// Renders the trait implementations for this type
@@ -534,7 +619,8 @@ fn sidebar_render_assoc_items(
     concrete: Vec<&Impl>,
     synthetic: Vec<&Impl>,
     blanket_impl: Vec<&Impl>,
-) -> [LinkBlock<'static>; 3] {
+    items: &mut Vec<LinkBlock<'_>>,
+) {
     let format_impls = |impls: Vec<&Impl>, id_map: &mut IdMap| {
         let mut links = FxHashSet::default();
 
@@ -559,7 +645,7 @@ fn sidebar_render_assoc_items(
     let concrete = format_impls(concrete, id_map);
     let synthetic = format_impls(synthetic, id_map);
     let blanket = format_impls(blanket_impl, id_map);
-    [
+    items.extend([
         LinkBlock::new(
             Link::new("trait-implementations", "Trait Implementations"),
             "trait-implementation",
@@ -575,7 +661,7 @@ fn sidebar_render_assoc_items(
             "blanket-implementation",
             blanket,
         ),
-    ]
+    ]);
 }
 
 fn get_next_url(used_links: &mut FxHashSet<String>, url: String) -> String {
diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css
index 1e483062289..271de1b1ccf 100644
--- a/src/librustdoc/html/static/css/rustdoc.css
+++ b/src/librustdoc/html/static/css/rustdoc.css
@@ -568,12 +568,16 @@ img {
 	width: 48px;
 }
 
-ul.block, .block li {
+ul.block, .block li, .block ul {
 	padding: 0;
 	margin: 0;
 	list-style: none;
 }
 
+.block ul a {
+	padding-left: 1rem;
+}
+
 .sidebar-elems a,
 .sidebar > h2 a {
 	display: block;
@@ -585,6 +589,14 @@ ul.block, .block li {
 	background-clip: border-box;
 }
 
+.hide-toc #rustdoc-toc, .hide-toc .in-crate {
+	display: none;
+}
+
+.hide-modnav #rustdoc-modnav {
+	display: none;
+}
+
 .sidebar h2 {
 	text-wrap: balance;
 	overflow-wrap: anywhere;
diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js
index c3e219f2c87..0291b6ed6f5 100644
--- a/src/librustdoc/html/static/js/main.js
+++ b/src/librustdoc/html/static/js/main.js
@@ -499,7 +499,7 @@ function preLoadCss(cssUrl) {
         if (!window.SIDEBAR_ITEMS) {
             return;
         }
-        const sidebar = document.getElementsByClassName("sidebar-elems")[0];
+        const sidebar = document.getElementById("rustdoc-modnav");
 
         /**
          * Append to the sidebar a "block" of links - a heading along with a list (`<ul>`) of items.
@@ -885,7 +885,7 @@ function preLoadCss(cssUrl) {
         if (!window.ALL_CRATES) {
             return;
         }
-        const sidebarElems = document.getElementsByClassName("sidebar-elems")[0];
+        const sidebarElems = document.getElementById("rustdoc-modnav");
         if (!sidebarElems) {
             return;
         }
diff --git a/src/librustdoc/html/static/js/settings.js b/src/librustdoc/html/static/js/settings.js
index 2b42fbebb80..c52a19ef987 100644
--- a/src/librustdoc/html/static/js/settings.js
+++ b/src/librustdoc/html/static/js/settings.js
@@ -36,6 +36,20 @@
                     removeClass(document.documentElement, "hide-sidebar");
                 }
                 break;
+            case "hide-toc":
+                if (value === true) {
+                    addClass(document.documentElement, "hide-toc");
+                } else {
+                    removeClass(document.documentElement, "hide-toc");
+                }
+                break;
+            case "hide-modnav":
+                if (value === true) {
+                    addClass(document.documentElement, "hide-modnav");
+                } else {
+                    removeClass(document.documentElement, "hide-modnav");
+                }
+                break;
         }
     }
 
@@ -102,6 +116,11 @@
         let output = "";
 
         for (const setting of settings) {
+            if (setting === "hr") {
+                output += "<hr>";
+                continue;
+            }
+
             const js_data_name = setting["js_name"];
             const setting_name = setting["name"];
 
@@ -199,6 +218,16 @@
                 "default": false,
             },
             {
+                "name": "Hide table of contents",
+                "js_name": "hide-toc",
+                "default": false,
+            },
+            {
+                "name": "Hide module navigation",
+                "js_name": "hide-modnav",
+                "default": false,
+            },
+            {
                 "name": "Disable keyboard shortcuts",
                 "js_name": "disable-shortcuts",
                 "default": false,
diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js
index 4a27ca92fff..d75fb7a7fb5 100644
--- a/src/librustdoc/html/static/js/storage.js
+++ b/src/librustdoc/html/static/js/storage.js
@@ -196,16 +196,21 @@ updateTheme();
 // This needs to be done here because this JS is render-blocking,
 // so that the sidebar doesn't "jump" after appearing on screen.
 // The user interaction to change this is set up in main.js.
+//
+// At this point in page load, `document.body` is not available yet.
+// Set a class on the `<html>` element instead.
 if (getSettingValue("source-sidebar-show") === "true") {
-    // At this point in page load, `document.body` is not available yet.
-    // Set a class on the `<html>` element instead.
     addClass(document.documentElement, "src-sidebar-expanded");
 }
 if (getSettingValue("hide-sidebar") === "true") {
-    // At this point in page load, `document.body` is not available yet.
-    // Set a class on the `<html>` element instead.
     addClass(document.documentElement, "hide-sidebar");
 }
+if (getSettingValue("hide-toc") === "true") {
+    addClass(document.documentElement, "hide-toc");
+}
+if (getSettingValue("hide-modnav") === "true") {
+    addClass(document.documentElement, "hide-modnav");
+}
 function updateSidebarWidth() {
     const desktopSidebarWidth = getSettingValue("desktop-sidebar-width");
     if (desktopSidebarWidth && desktopSidebarWidth !== "null") {
diff --git a/src/librustdoc/html/templates/sidebar.html b/src/librustdoc/html/templates/sidebar.html
index 0990c2716b8..fccf65cbefc 100644
--- a/src/librustdoc/html/templates/sidebar.html
+++ b/src/librustdoc/html/templates/sidebar.html
@@ -1,8 +1,3 @@
-{% if !title.is_empty() %}
-    <h2 class="location"> {# #}
-        <a href="#">{{title_prefix}}{{title|wrapped|safe}}</a> {# #}
-    </h2>
-{% endif %}
 <div class="sidebar-elems">
     {% if is_crate %}
         <ul class="block"> {# #}
@@ -11,18 +6,46 @@
     {% endif %}
 
     {% if self.should_render_blocks() %}
-        <section>
+        <section id="rustdoc-toc">
+            {% if !title.is_empty() %}
+            <h2 class="location"> {# #}
+                <a href="#">{{title_prefix}}{{title|wrapped|safe}}</a> {# #}
+            </h2>
+            {% endif %}
             {% for block in blocks %}
                 {% if block.should_render() %}
                     {% if !block.heading.name.is_empty() %}
-                        <h3><a href="#{{block.heading.href|safe}}"> {# #}
-                            {{block.heading.name|wrapped|safe}} {# #}
-                        </a></h3> {# #}
+                        <h3> {# #}
+                            <a href="#{{block.heading.href|safe}}">{{block.heading.name|wrapped|safe}}</a> {# #}
+                        </h3>
                     {% endif %}
                     {% if !block.links.is_empty() %}
                         <ul class="block{% if !block.class.is_empty() +%} {{+block.class}}{% endif %}">
                             {% for link in block.links %}
-                                <li><a href="#{{link.href|safe}}">{{link.name}}</a></li>
+                                <li> {# #}
+                                    <a href="#{{link.href|safe}}" title="{{link.name}}">
+                                        {% match link.name_html %}
+                                            {% when Some with (html) %}
+                                                {{html|safe}}
+                                            {% else %}
+                                                {{link.name}}
+                                        {% endmatch %}
+                                    </a> {# #}
+                                    {% if !link.children.is_empty() %}
+                                        <ul>
+                                            {% for child in link.children %}
+                                                <li><a href="#{{child.href|safe}}" title="{{child.name}}">
+                                                    {% match child.name_html %}
+                                                        {% when Some with (html) %}
+                                                            {{html|safe}}
+                                                        {% else %}
+                                                            {{child.name}}
+                                                    {% endmatch %}
+                                                </a></li>
+                                            {% endfor %}
+                                        </ul>
+                                    {% endif %}
+                                </li>
                             {% endfor %}
                         </ul>
                     {% endif %}
@@ -30,7 +53,11 @@
             {% endfor %}
         </section>
     {% endif %}
+    <div id="rustdoc-modnav">
     {% if !path.is_empty() %}
-        <h2><a href="{% if is_mod %}../{% endif %}index.html">In {{+ path|wrapped|safe}}</a></h2>
+        <h2{% if parent_is_crate +%} class="in-crate"{% endif %}> {# #}
+            <a href="{% if is_mod %}../{% endif %}index.html">In {{+ path|wrapped|safe}}</a> {# #}
+        </h2>
     {% endif %}
+    </div> {# #}
 </div>
diff --git a/src/librustdoc/html/toc.rs b/src/librustdoc/html/toc.rs
index a12c2a6a16c..7fdce41a634 100644
--- a/src/librustdoc/html/toc.rs
+++ b/src/librustdoc/html/toc.rs
@@ -1,4 +1,5 @@
 //! Table-of-contents creation.
+use crate::html::escape::Escape;
 
 /// A (recursive) table of contents
 #[derive(Debug, PartialEq)]
@@ -16,7 +17,7 @@ pub(crate) struct Toc {
     /// ### A
     /// ## B
     /// ```
-    entries: Vec<TocEntry>,
+    pub(crate) entries: Vec<TocEntry>,
 }
 
 impl Toc {
@@ -27,11 +28,16 @@ impl Toc {
 
 #[derive(Debug, PartialEq)]
 pub(crate) struct TocEntry {
-    level: u32,
-    sec_number: String,
-    name: String,
-    id: String,
-    children: Toc,
+    pub(crate) level: u32,
+    pub(crate) sec_number: String,
+    // name is a plain text header that works in a `title` tag
+    // html includes `<code>` tags
+    // the tooltip is used so that, when a toc is truncated,
+    // you can mouse over it to see the whole thing
+    pub(crate) name: String,
+    pub(crate) html: String,
+    pub(crate) id: String,
+    pub(crate) children: Toc,
 }
 
 /// Progressive construction of a table of contents.
@@ -115,7 +121,7 @@ impl TocBuilder {
     /// Push a level `level` heading into the appropriate place in the
     /// hierarchy, returning a string containing the section number in
     /// `<num>.<num>.<num>` format.
-    pub(crate) fn push(&mut self, level: u32, name: String, id: String) -> &str {
+    pub(crate) fn push(&mut self, level: u32, name: String, html: String, id: String) -> &str {
         assert!(level >= 1);
 
         // collapse all previous sections into their parents until we
@@ -149,6 +155,7 @@ impl TocBuilder {
         self.chain.push(TocEntry {
             level,
             name,
+            html,
             sec_number,
             id,
             children: Toc { entries: Vec::new() },
@@ -170,10 +177,11 @@ impl Toc {
             // recursively format this table of contents
             let _ = write!(
                 v,
-                "\n<li><a href=\"#{id}\">{num} {name}</a>",
+                "\n<li><a href=\"#{id}\" title=\"{name}\">{num} {html}</a>",
                 id = entry.id,
                 num = entry.sec_number,
-                name = entry.name
+                name = Escape(&entry.name),
+                html = &entry.html,
             );
             entry.children.print_inner(&mut *v);
             v.push_str("</li>");
diff --git a/src/librustdoc/html/toc/tests.rs b/src/librustdoc/html/toc/tests.rs
index 014f346862b..81aca737baf 100644
--- a/src/librustdoc/html/toc/tests.rs
+++ b/src/librustdoc/html/toc/tests.rs
@@ -9,7 +9,10 @@ fn builder_smoke() {
     // there's been no macro mistake.
     macro_rules! push {
         ($level: expr, $name: expr) => {
-            assert_eq!(builder.push($level, $name.to_string(), "".to_string()), $name);
+            assert_eq!(
+                builder.push($level, $name.to_string(), $name.to_string(), "".to_string()),
+                $name
+            );
         };
     }
     push!(2, "0.1");
@@ -48,6 +51,7 @@ fn builder_smoke() {
                         TocEntry {
                             level: $level,
                             name: $name.to_string(),
+                            html: $name.to_string(),
                             sec_number: $name.to_string(),
                             id: "".to_string(),
                             children: toc!($($sub),*)
diff --git a/src/librustdoc/markdown.rs b/src/librustdoc/markdown.rs
index a98f81d011e..581ebbbe58d 100644
--- a/src/librustdoc/markdown.rs
+++ b/src/librustdoc/markdown.rs
@@ -72,6 +72,7 @@ pub(crate) fn render<P: AsRef<Path>>(
     let text = if !options.markdown_no_toc {
         MarkdownWithToc {
             content: text,
+            links: &[],
             ids: &mut ids,
             error_codes,
             edition,