about summary refs log tree commit diff
path: root/src/librustdoc/html
diff options
context:
space:
mode:
Diffstat (limited to 'src/librustdoc/html')
-rw-r--r--src/librustdoc/html/format.rs24
-rw-r--r--src/librustdoc/html/layout.rs2
-rw-r--r--src/librustdoc/html/markdown.rs110
-rw-r--r--src/librustdoc/html/markdown/tests.rs176
-rw-r--r--src/librustdoc/html/render/cache.rs6
-rw-r--r--src/librustdoc/html/render/mod.rs298
-rw-r--r--src/librustdoc/html/sources.rs19
-rw-r--r--src/librustdoc/html/static/main.js126
-rw-r--r--src/librustdoc/html/static/rustdoc.css45
-rw-r--r--src/librustdoc/html/static/settings.js72
-rw-r--r--src/librustdoc/html/static/themes/ayu.css14
-rw-r--r--src/librustdoc/html/static/themes/dark.css11
-rw-r--r--src/librustdoc/html/static/themes/light.css11
13 files changed, 565 insertions, 349 deletions
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs
index d2722ed1cd2..536c2e08fde 100644
--- a/src/librustdoc/html/format.rs
+++ b/src/librustdoc/html/format.rs
@@ -11,7 +11,7 @@ use std::fmt;
 
 use rustc_data_structures::fx::FxHashSet;
 use rustc_hir as hir;
-use rustc_span::def_id::DefId;
+use rustc_span::def_id::{DefId, CRATE_DEF_INDEX};
 use rustc_target::spec::abi::Abi;
 
 use crate::clean::{self, PrimitiveType};
@@ -1089,19 +1089,31 @@ impl Function<'_> {
 
 impl clean::Visibility {
     crate fn print_with_space(&self) -> impl fmt::Display + '_ {
+        use rustc_span::symbol::kw;
+
         display_fn(move |f| match *self {
             clean::Public => f.write_str("pub "),
             clean::Inherited => Ok(()),
-            clean::Visibility::Crate => write!(f, "pub(crate) "),
+            // If this is `pub(crate)`, `path` will be empty.
+            clean::Visibility::Restricted(did, _) if did.index == CRATE_DEF_INDEX => {
+                write!(f, "pub(crate) ")
+            }
             clean::Visibility::Restricted(did, ref path) => {
                 f.write_str("pub(")?;
-                if path.segments.len() != 1
-                    || (path.segments[0].name != "self" && path.segments[0].name != "super")
+                debug!("path={:?}", path);
+                let first_name =
+                    path.data[0].data.get_opt_name().expect("modules are always named");
+                if path.data.len() != 1 || (first_name != kw::SelfLower && first_name != kw::Super)
                 {
                     f.write_str("in ")?;
                 }
-                resolved_path(f, did, path, true, false)?;
-                f.write_str(") ")
+                // modified from `resolved_path()` to work with `DefPathData`
+                let last_name = path.data.last().unwrap().data.get_opt_name().unwrap();
+                for seg in &path.data[..path.data.len() - 1] {
+                    write!(f, "{}::", seg.data.get_opt_name().unwrap())?;
+                }
+                let path = anchor(did, &last_name.as_str()).to_string();
+                write!(f, "{}) ", path)
             }
         })
     }
diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs
index e8039942f4f..b5169b05997 100644
--- a/src/librustdoc/html/layout.rs
+++ b/src/librustdoc/html/layout.rs
@@ -98,7 +98,7 @@ crate fn render<T: Print, S: Print>(
                            placeholder=\"Click or press ‘S’ to search, ‘?’ for more options…\" \
                            type=\"search\">\
                 </div>\
-                <span class=\"help-button\">?</span>
+                <button type=\"button\" class=\"help-button\">?</button>
                 <a id=\"settings-menu\" href=\"{root_path}settings.html\">\
                     <img src=\"{static_root_path}wheel{suffix}.svg\" \
                          width=\"18\" \
diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs
index 880c859dd1b..22096203d4c 100644
--- a/src/librustdoc/html/markdown.rs
+++ b/src/librustdoc/html/markdown.rs
@@ -17,8 +17,6 @@
 //! // ... something using html
 //! ```
 
-#![allow(non_camel_case_types)]
-
 use rustc_data_structures::fx::FxHashMap;
 use rustc_hir::def_id::DefId;
 use rustc_hir::HirId;
@@ -43,10 +41,16 @@ use pulldown_cmark::{html, BrokenLink, CodeBlockKind, CowStr, Event, Options, Pa
 #[cfg(test)]
 mod tests;
 
+/// Options for rendering Markdown in the main body of documentation.
 pub(crate) fn opts() -> Options {
     Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES | Options::ENABLE_STRIKETHROUGH
 }
 
+/// A subset of [`opts()`] used for rendering summaries.
+pub(crate) fn summary_opts() -> Options {
+    Options::ENABLE_STRIKETHROUGH
+}
+
 /// When `to_string` is called, this struct will emit the HTML corresponding to
 /// the rendered version of the contained markdown string.
 pub struct Markdown<'a>(
@@ -135,9 +139,9 @@ fn map_line(s: &str) -> Line<'_> {
     let trimmed = s.trim();
     if trimmed.starts_with("##") {
         Line::Shown(Cow::Owned(s.replacen("##", "#", 1)))
-    } else if trimmed.starts_with("# ") {
+    } else if let Some(stripped) = trimmed.strip_prefix("# ") {
         // # text
-        Line::Hidden(&trimmed[2..])
+        Line::Hidden(&stripped)
     } else if trimmed == "#" {
         // We cannot handle '#text' because it could be #[attr].
         Line::Hidden("")
@@ -387,7 +391,7 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> {
                 _,
             ))) => {
                 debug!("saw end of shortcut link to {}", dest);
-                if self.links.iter().find(|&link| *link.href == **dest).is_some() {
+                if self.links.iter().any(|link| *link.href == **dest) {
                     assert!(self.shortcut_link.is_some(), "saw closing link without opening tag");
                     self.shortcut_link = None;
                 }
@@ -1021,11 +1025,7 @@ impl MarkdownSummaryLine<'_> {
             }
         };
 
-        let p = Parser::new_with_broken_link_callback(
-            md,
-            Options::ENABLE_STRIKETHROUGH,
-            Some(&mut replacer),
-        );
+        let p = Parser::new_with_broken_link_callback(md, summary_opts(), Some(&mut replacer));
 
         let mut s = String::new();
 
@@ -1035,7 +1035,95 @@ impl MarkdownSummaryLine<'_> {
     }
 }
 
+/// Renders a subset of Markdown in the first paragraph of the provided Markdown.
+///
+/// - *Italics*, **bold**, and `inline code` styles **are** rendered.
+/// - Headings and links are stripped (though the text *is* rendered).
+/// - HTML, code blocks, and everything else are ignored.
+///
+/// Returns a tuple of the rendered HTML string and whether the output was shortened
+/// due to the provided `length_limit`.
+fn markdown_summary_with_limit(md: &str, length_limit: usize) -> (String, bool) {
+    if md.is_empty() {
+        return (String::new(), false);
+    }
+
+    let mut s = String::with_capacity(md.len() * 3 / 2);
+    let mut text_length = 0;
+    let mut stopped_early = false;
+
+    fn push(s: &mut String, text_length: &mut usize, text: &str) {
+        s.push_str(text);
+        *text_length += text.len();
+    };
+
+    'outer: for event in Parser::new_ext(md, summary_opts()) {
+        match &event {
+            Event::Text(text) => {
+                for word in text.split_inclusive(char::is_whitespace) {
+                    if text_length + word.len() >= length_limit {
+                        stopped_early = true;
+                        break 'outer;
+                    }
+
+                    push(&mut s, &mut text_length, word);
+                }
+            }
+            Event::Code(code) => {
+                if text_length + code.len() >= length_limit {
+                    stopped_early = true;
+                    break;
+                }
+
+                s.push_str("<code>");
+                push(&mut s, &mut text_length, code);
+                s.push_str("</code>");
+            }
+            Event::Start(tag) => match tag {
+                Tag::Emphasis => s.push_str("<em>"),
+                Tag::Strong => s.push_str("<strong>"),
+                Tag::CodeBlock(..) => break,
+                _ => {}
+            },
+            Event::End(tag) => match tag {
+                Tag::Emphasis => s.push_str("</em>"),
+                Tag::Strong => s.push_str("</strong>"),
+                Tag::Paragraph => break,
+                _ => {}
+            },
+            Event::HardBreak | Event::SoftBreak => {
+                if text_length + 1 >= length_limit {
+                    stopped_early = true;
+                    break;
+                }
+
+                push(&mut s, &mut text_length, " ");
+            }
+            _ => {}
+        }
+    }
+
+    (s, stopped_early)
+}
+
+/// Renders a shortened first paragraph of the given Markdown as a subset of Markdown,
+/// making it suitable for contexts like the search index.
+///
+/// Will shorten to 59 or 60 characters, including an ellipsis (…) if it was shortened.
+///
+/// See [`markdown_summary_with_limit`] for details about what is rendered and what is not.
+crate fn short_markdown_summary(markdown: &str) -> String {
+    let (mut s, was_shortened) = markdown_summary_with_limit(markdown, 59);
+
+    if was_shortened {
+        s.push('…');
+    }
+
+    s
+}
+
 /// Renders the first paragraph of the provided markdown as plain text.
+/// Useful for alt-text.
 ///
 /// - Headings, links, and formatting are stripped.
 /// - Inline code is rendered as-is, surrounded by backticks.
@@ -1047,7 +1135,7 @@ crate fn plain_text_summary(md: &str) -> String {
 
     let mut s = String::with_capacity(md.len() * 3 / 2);
 
-    for event in Parser::new_ext(md, Options::ENABLE_STRIKETHROUGH) {
+    for event in Parser::new_ext(md, summary_opts()) {
         match &event {
             Event::Text(text) => s.push_str(text),
             Event::Code(code) => {
diff --git a/src/librustdoc/html/markdown/tests.rs b/src/librustdoc/html/markdown/tests.rs
index 8e618733f07..75ff3c5af2f 100644
--- a/src/librustdoc/html/markdown/tests.rs
+++ b/src/librustdoc/html/markdown/tests.rs
@@ -1,4 +1,4 @@
-use super::plain_text_summary;
+use super::{plain_text_summary, short_markdown_summary};
 use super::{ErrorCodes, IdMap, Ignore, LangString, Markdown, MarkdownHtml};
 use rustc_span::edition::{Edition, DEFAULT_EDITION};
 use std::cell::RefCell;
@@ -51,82 +51,77 @@ fn test_unique_id() {
 
 #[test]
 fn test_lang_string_parse() {
-    fn t(
-        s: &str,
-        should_panic: bool,
-        no_run: bool,
-        ignore: Ignore,
-        rust: bool,
-        test_harness: bool,
-        compile_fail: bool,
-        allow_fail: bool,
-        error_codes: Vec<String>,
-        edition: Option<Edition>,
-    ) {
-        assert_eq!(
-            LangString::parse(s, ErrorCodes::Yes, true, None),
-            LangString {
-                should_panic,
-                no_run,
-                ignore,
-                rust,
-                test_harness,
-                compile_fail,
-                error_codes,
-                original: s.to_owned(),
-                allow_fail,
-                edition,
-            }
-        )
+    fn t(lg: LangString) {
+        let s = &lg.original;
+        assert_eq!(LangString::parse(s, ErrorCodes::Yes, true, None), lg)
     }
-    let ignore_foo = Ignore::Some(vec!["foo".to_string()]);
 
-    fn v() -> Vec<String> {
-        Vec::new()
-    }
-
-    // marker                | should_panic | no_run | ignore | rust | test_harness
-    //                       | compile_fail | allow_fail | error_codes | edition
-    t("", false, false, Ignore::None, true, false, false, false, v(), None);
-    t("rust", false, false, Ignore::None, true, false, false, false, v(), None);
-    t("sh", false, false, Ignore::None, false, false, false, false, v(), None);
-    t("ignore", false, false, Ignore::All, true, false, false, false, v(), None);
-    t("ignore-foo", false, false, ignore_foo, true, false, false, false, v(), None);
-    t("should_panic", true, false, Ignore::None, true, false, false, false, v(), None);
-    t("no_run", false, true, Ignore::None, true, false, false, false, v(), None);
-    t("test_harness", false, false, Ignore::None, true, true, false, false, v(), None);
-    t("compile_fail", false, true, Ignore::None, true, false, true, false, v(), None);
-    t("allow_fail", false, false, Ignore::None, true, false, false, true, v(), None);
-    t("{.no_run .example}", false, true, Ignore::None, true, false, false, false, v(), None);
-    t("{.sh .should_panic}", true, false, Ignore::None, false, false, false, false, v(), None);
-    t("{.example .rust}", false, false, Ignore::None, true, false, false, false, v(), None);
-    t("{.test_harness .rust}", false, false, Ignore::None, true, true, false, false, v(), None);
-    t("text, no_run", false, true, Ignore::None, false, false, false, false, v(), None);
-    t("text,no_run", false, true, Ignore::None, false, false, false, false, v(), None);
-    t(
-        "edition2015",
-        false,
-        false,
-        Ignore::None,
-        true,
-        false,
-        false,
-        false,
-        v(),
-        Some(Edition::Edition2015),
-    );
-    t(
-        "edition2018",
-        false,
-        false,
-        Ignore::None,
-        true,
-        false,
-        false,
-        false,
-        v(),
-        Some(Edition::Edition2018),
-    );
+    t(LangString::all_false());
+    t(LangString { original: "rust".into(), ..LangString::all_false() });
+    t(LangString { original: "sh".into(), rust: false, ..LangString::all_false() });
+    t(LangString { original: "ignore".into(), ignore: Ignore::All, ..LangString::all_false() });
+    t(LangString {
+        original: "ignore-foo".into(),
+        ignore: Ignore::Some(vec!["foo".to_string()]),
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "should_panic".into(),
+        should_panic: true,
+        ..LangString::all_false()
+    });
+    t(LangString { original: "no_run".into(), no_run: true, ..LangString::all_false() });
+    t(LangString {
+        original: "test_harness".into(),
+        test_harness: true,
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "compile_fail".into(),
+        no_run: true,
+        compile_fail: true,
+        ..LangString::all_false()
+    });
+    t(LangString { original: "allow_fail".into(), allow_fail: true, ..LangString::all_false() });
+    t(LangString {
+        original: "{.no_run .example}".into(),
+        no_run: true,
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "{.sh .should_panic}".into(),
+        should_panic: true,
+        rust: false,
+        ..LangString::all_false()
+    });
+    t(LangString { original: "{.example .rust}".into(), ..LangString::all_false() });
+    t(LangString {
+        original: "{.test_harness .rust}".into(),
+        test_harness: true,
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "text, no_run".into(),
+        no_run: true,
+        rust: false,
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "text,no_run".into(),
+        no_run: true,
+        rust: false,
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "edition2015".into(),
+        edition: Some(Edition::Edition2015),
+        ..LangString::all_false()
+    });
+    t(LangString {
+        original: "edition2018".into(),
+        edition: Some(Edition::Edition2018),
+        ..LangString::all_false()
+    });
 }
 
 #[test]
@@ -205,6 +200,33 @@ fn test_header_ids_multiple_blocks() {
 }
 
 #[test]
+fn test_short_markdown_summary() {
+    fn t(input: &str, expect: &str) {
+        let output = short_markdown_summary(input);
+        assert_eq!(output, expect, "original: {}", input);
+    }
+
+    t("hello [Rust](https://www.rust-lang.org) :)", "hello Rust :)");
+    t("*italic*", "<em>italic</em>");
+    t("**bold**", "<strong>bold</strong>");
+    t("Multi-line\nsummary", "Multi-line summary");
+    t("Hard-break  \nsummary", "Hard-break summary");
+    t("hello [Rust] :)\n\n[Rust]: https://www.rust-lang.org", "hello Rust :)");
+    t("hello [Rust](https://www.rust-lang.org \"Rust\") :)", "hello Rust :)");
+    t("code `let x = i32;` ...", "code <code>let x = i32;</code> ...");
+    t("type `Type<'static>` ...", "type <code>Type<'static></code> ...");
+    t("# top header", "top header");
+    t("## header", "header");
+    t("first paragraph\n\nsecond paragraph", "first paragraph");
+    t("```\nfn main() {}\n```", "");
+    t("<div>hello</div>", "");
+    t(
+        "a *very*, **very** long first paragraph. it has lots of `inline code: Vec<T>`. and it has a [link](https://www.rust-lang.org).\nthat was a soft line break!  \nthat was a hard one\n\nsecond paragraph.",
+        "a <em>very</em>, <strong>very</strong> long first paragraph. it has lots of …",
+    );
+}
+
+#[test]
 fn test_plain_text_summary() {
     fn t(input: &str, expect: &str) {
         let output = plain_text_summary(input);
@@ -224,6 +246,10 @@ fn test_plain_text_summary() {
     t("first paragraph\n\nsecond paragraph", "first paragraph");
     t("```\nfn main() {}\n```", "");
     t("<div>hello</div>", "");
+    t(
+        "a *very*, **very** long first paragraph. it has lots of `inline code: Vec<T>`. and it has a [link](https://www.rust-lang.org).\nthat was a soft line break!  \nthat was a hard one\n\nsecond paragraph.",
+        "a very, very long first paragraph. it has lots of `inline code: Vec<T>`. and it has a link. that was a soft line break! that was a hard one",
+    );
 }
 
 #[test]
diff --git a/src/librustdoc/html/render/cache.rs b/src/librustdoc/html/render/cache.rs
index 085ca01f58d..97f764517fa 100644
--- a/src/librustdoc/html/render/cache.rs
+++ b/src/librustdoc/html/render/cache.rs
@@ -9,7 +9,7 @@ use crate::clean::types::GetDefId;
 use crate::clean::{self, AttributesExt};
 use crate::formats::cache::Cache;
 use crate::formats::item_type::ItemType;
-use crate::html::render::{plain_text_summary, shorten};
+use crate::html::markdown::short_markdown_summary;
 use crate::html::render::{Generic, IndexItem, IndexItemFunctionType, RenderType, TypeWithKind};
 
 /// Indicates where an external crate can be found.
@@ -78,7 +78,7 @@ crate fn build_index(krate: &clean::Crate, cache: &mut Cache) -> String {
                 ty: item.type_(),
                 name: item.name.clone().unwrap(),
                 path: fqp[..fqp.len() - 1].join("::"),
-                desc: shorten(plain_text_summary(item.doc_value())),
+                desc: item.doc_value().map_or_else(|| String::new(), short_markdown_summary),
                 parent: Some(did),
                 parent_idx: None,
                 search_type: get_index_search_type(&item),
@@ -127,7 +127,7 @@ crate fn build_index(krate: &clean::Crate, cache: &mut Cache) -> String {
     let crate_doc = krate
         .module
         .as_ref()
-        .map(|module| shorten(plain_text_summary(module.doc_value())))
+        .map(|module| module.doc_value().map_or_else(|| String::new(), short_markdown_summary))
         .unwrap_or_default();
 
     #[derive(Serialize)]
diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs
index 28f7a4d3162..db04624dca8 100644
--- a/src/librustdoc/html/render/mod.rs
+++ b/src/librustdoc/html/render/mod.rs
@@ -52,10 +52,12 @@ use rustc_ast_pretty::pprust;
 use rustc_attr::StabilityLevel;
 use rustc_data_structures::flock;
 use rustc_data_structures::fx::{FxHashMap, FxHashSet};
+use rustc_data_structures::sync::Lrc;
 use rustc_hir as hir;
 use rustc_hir::def_id::{DefId, LOCAL_CRATE};
 use rustc_hir::Mutability;
 use rustc_middle::middle::stability;
+use rustc_session::Session;
 use rustc_span::edition::Edition;
 use rustc_span::hygiene::MacroKind;
 use rustc_span::source_map::FileName;
@@ -76,7 +78,9 @@ use crate::html::format::fmt_impl_for_trait_page;
 use crate::html::format::Function;
 use crate::html::format::{href, print_default_space, print_generic_bounds, WhereClause};
 use crate::html::format::{print_abi_with_space, Buffer, PrintWithSpace};
-use crate::html::markdown::{self, ErrorCodes, IdMap, Markdown, MarkdownHtml, MarkdownSummaryLine};
+use crate::html::markdown::{
+    self, plain_text_summary, ErrorCodes, IdMap, Markdown, MarkdownHtml, MarkdownSummaryLine,
+};
 use crate::html::sources;
 use crate::html::{highlight, layout, static_files};
 use cache::{build_index, ExternalLocation};
@@ -119,6 +123,7 @@ crate struct Context {
 }
 
 crate struct SharedContext {
+    crate sess: Lrc<Session>,
     /// The path to the crate root source minus the file name.
     /// Used for simplifying paths to the highlighted source code files.
     crate src_root: PathBuf,
@@ -165,12 +170,14 @@ impl Context {
         // `style-suffix.min.css`.  Path::extension would just return `css`
         // which would result in `style.min-suffix.css` which isn't what we
         // want.
-        let mut iter = filename.splitn(2, '.');
-        let base = iter.next().unwrap();
-        let ext = iter.next().unwrap();
-        let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext,);
+        let (base, ext) = filename.split_once('.').unwrap();
+        let filename = format!("{}{}.{}", base, self.shared.resource_suffix, ext);
         self.dst.join(&filename)
     }
+
+    fn sess(&self) -> &Session {
+        &self.shared.sess
+    }
 }
 
 impl SharedContext {
@@ -381,6 +388,7 @@ impl FormatRenderer for Context {
         _render_info: RenderInfo,
         edition: Edition,
         cache: &mut Cache,
+        sess: Lrc<Session>,
     ) -> Result<(Context, clean::Crate), Error> {
         // need to save a copy of the options for rendering the index page
         let md_opts = options.clone();
@@ -453,6 +461,7 @@ impl FormatRenderer for Context {
         }
         let (sender, receiver) = channel();
         let mut scx = SharedContext {
+            sess,
             collapsed: krate.collapsed,
             src_root,
             include_sources,
@@ -1194,6 +1203,16 @@ fn write_minify(
     }
 }
 
+fn write_srclink(cx: &Context, item: &clean::Item, buf: &mut Buffer, cache: &Cache) {
+    if let Some(l) = cx.src_href(item, cache) {
+        write!(
+            buf,
+            "<a class=\"srclink\" href=\"{}\" title=\"{}\">[src]</a>",
+            l, "goto source code"
+        )
+    }
+}
+
 #[derive(Debug, Eq, PartialEq, Hash)]
 struct ItemEntry {
     url: String,
@@ -1594,9 +1613,10 @@ impl Context {
                 Some(ref s) => s.to_string(),
             };
             let short = short.to_string();
-            map.entry(short)
-                .or_default()
-                .push((myname, Some(plain_text_summary(item.doc_value()))));
+            map.entry(short).or_default().push((
+                myname,
+                Some(item.doc_value().map_or_else(|| String::new(), plain_text_summary)),
+            ));
         }
 
         if self.shared.sort_modules_alphabetically {
@@ -1618,24 +1638,24 @@ impl Context {
     /// of their crate documentation isn't known.
     fn src_href(&self, item: &clean::Item, cache: &Cache) -> Option<String> {
         let mut root = self.root_path();
-
         let mut path = String::new();
+        let cnum = item.source.cnum(self.sess());
 
         // We can safely ignore synthetic `SourceFile`s.
-        let file = match item.source.filename {
+        let file = match item.source.filename(self.sess()) {
             FileName::Real(ref path) => path.local_path().to_path_buf(),
             _ => return None,
         };
         let file = &file;
 
-        let (krate, path) = if item.source.cnum == LOCAL_CRATE {
+        let (krate, path) = if cnum == LOCAL_CRATE {
             if let Some(path) = self.shared.local_sources.get(file) {
                 (&self.shared.layout.krate, path)
             } else {
                 return None;
             }
         } else {
-            let (krate, src_root) = match *cache.extern_locations.get(&item.source.cnum)? {
+            let (krate, src_root) = match *cache.extern_locations.get(&cnum)? {
                 (ref name, ref src, ExternalLocation::Local) => (name, src),
                 (ref name, ref src, ExternalLocation::Remote(ref s)) => {
                     root = s.to_string();
@@ -1654,11 +1674,10 @@ impl Context {
             (krate, &path)
         };
 
-        let lines = if item.source.loline == item.source.hiline {
-            item.source.loline.to_string()
-        } else {
-            format!("{}-{}", item.source.loline, item.source.hiline)
-        };
+        let loline = item.source.lo(self.sess()).line;
+        let hiline = item.source.hi(self.sess()).line;
+        let lines =
+            if loline == hiline { loline.to_string() } else { format!("{}-{}", loline, hiline) };
         Some(format!(
             "{root}src/{krate}/{path}#{lines}",
             root = Escape(&root),
@@ -1682,13 +1701,13 @@ fn print_item(cx: &Context, item: &clean::Item, buf: &mut Buffer, cache: &Cache)
     debug_assert!(!item.is_stripped());
     // Write the breadcrumb trail header for the top
     write!(buf, "<h1 class=\"fqn\"><span class=\"out-of-band\">");
-    if let Some(version) = item.stable_since() {
-        write!(
-            buf,
-            "<span class=\"since\" title=\"Stable since Rust version {0}\">{0}</span>",
-            version
-        );
-    }
+    render_stability_since_raw(
+        buf,
+        item.stable_since().as_deref(),
+        item.const_stable_since().as_deref(),
+        None,
+        None,
+    );
     write!(
         buf,
         "<span id=\"render-detail\">\
@@ -1706,13 +1725,7 @@ fn print_item(cx: &Context, item: &clean::Item, buf: &mut Buffer, cache: &Cache)
     // this page, and this link will be auto-clicked. The `id` attribute is
     // used to find the link to auto-click.
     if cx.shared.include_sources && !item.is_primitive() {
-        if let Some(l) = cx.src_href(item, cache) {
-            write!(
-                buf,
-                "<a class=\"srclink\" href=\"{}\" title=\"{}\">[src]</a>",
-                l, "goto source code"
-            );
-        }
+        write_srclink(cx, item, buf, cache);
     }
 
     write!(buf, "</span>"); // out-of-band
@@ -1806,41 +1819,11 @@ fn full_path(cx: &Context, item: &clean::Item) -> String {
     s
 }
 
-/// Renders the first paragraph of the given markdown as plain text, making it suitable for
-/// contexts like alt-text or the search index.
-///
-/// If no markdown is supplied, the empty string is returned.
-///
-/// See [`markdown::plain_text_summary`] for further details.
-#[inline]
-crate fn plain_text_summary(s: Option<&str>) -> String {
-    s.map(markdown::plain_text_summary).unwrap_or_default()
-}
-
-crate fn shorten(s: String) -> String {
-    if s.chars().count() > 60 {
-        let mut len = 0;
-        let mut ret = s
-            .split_whitespace()
-            .take_while(|p| {
-                // + 1 for the added character after the word.
-                len += p.chars().count() + 1;
-                len < 60
-            })
-            .collect::<Vec<_>>()
-            .join(" ");
-        ret.push('…');
-        ret
-    } else {
-        s
-    }
-}
-
 fn document(w: &mut Buffer, cx: &Context, item: &clean::Item, parent: Option<&clean::Item>) {
     if let Some(ref name) = item.name {
         info!("Documenting {}", name);
     }
-    document_stability(w, cx, item, false, parent);
+    document_item_info(w, cx, item, false, parent);
     document_full(w, item, cx, "", false);
 }
 
@@ -1876,10 +1859,17 @@ fn render_markdown(
 fn document_short(
     w: &mut Buffer,
     item: &clean::Item,
+    cx: &Context,
     link: AssocItemLink<'_>,
     prefix: &str,
     is_hidden: bool,
+    parent: Option<&clean::Item>,
+    show_def_docs: bool,
 ) {
+    document_item_info(w, cx, item, is_hidden, parent);
+    if !show_def_docs {
+        return;
+    }
     if let Some(s) = item.doc_value() {
         let mut summary_html = MarkdownSummaryLine(s, &item.links()).into_string();
 
@@ -1924,18 +1914,23 @@ fn document_full(w: &mut Buffer, item: &clean::Item, cx: &Context, prefix: &str,
     }
 }
 
-fn document_stability(
+/// Add extra information about an item such as:
+///
+/// * Stability
+/// * Deprecated
+/// * Required features (through the `doc_cfg` feature)
+fn document_item_info(
     w: &mut Buffer,
     cx: &Context,
     item: &clean::Item,
     is_hidden: bool,
     parent: Option<&clean::Item>,
 ) {
-    let stabilities = short_stability(item, cx, parent);
-    if !stabilities.is_empty() {
-        write!(w, "<div class=\"stability{}\">", if is_hidden { " hidden" } else { "" });
-        for stability in stabilities {
-            write!(w, "{}", stability);
+    let item_infos = short_item_info(item, cx, parent);
+    if !item_infos.is_empty() {
+        write!(w, "<div class=\"item-info{}\">", if is_hidden { " hidden" } else { "" });
+        for info in item_infos {
+            write!(w, "{}", info);
         }
         write!(w, "</div>");
     }
@@ -2190,7 +2185,7 @@ fn item_module(w: &mut Buffer, cx: &Context, item: &clean::Item, items: &[clean:
                          <td class=\"docblock-short\">{stab_tags}{docs}</td>\
                      </tr>",
                     name = *myitem.name.as_ref().unwrap(),
-                    stab_tags = stability_tags(myitem, item),
+                    stab_tags = extra_info_tags(myitem, item),
                     docs = MarkdownSummaryLine(doc_value, &myitem.links()).into_string(),
                     class = myitem.type_(),
                     add = add,
@@ -2212,9 +2207,9 @@ fn item_module(w: &mut Buffer, cx: &Context, item: &clean::Item, items: &[clean:
     }
 }
 
-/// Render the stability and deprecation tags that are displayed in the item's summary at the
-/// module level.
-fn stability_tags(item: &clean::Item, parent: &clean::Item) -> String {
+/// Render the stability, deprecation and portability tags that are displayed in the item's summary
+/// at the module level.
+fn extra_info_tags(item: &clean::Item, parent: &clean::Item) -> String {
     let mut tags = String::new();
 
     fn tag_html(class: &str, title: &str, contents: &str) -> String {
@@ -2267,10 +2262,10 @@ fn portability(item: &clean::Item, parent: Option<&clean::Item>) -> Option<Strin
     Some(format!("<div class=\"stab portability\">{}</div>", cfg?.render_long_html()))
 }
 
-/// Render the stability and/or deprecation warning that is displayed at the top of the item's
-/// documentation.
-fn short_stability(item: &clean::Item, cx: &Context, parent: Option<&clean::Item>) -> Vec<String> {
-    let mut stability = vec![];
+/// Render the stability, deprecation and portability information that is displayed at the top of
+/// the item's documentation.
+fn short_item_info(item: &clean::Item, cx: &Context, parent: Option<&clean::Item>) -> Vec<String> {
+    let mut extra_info = vec![];
     let error_codes = cx.shared.codes;
 
     if let Some(Deprecation { ref note, ref since, is_since_rustc_version }) = item.deprecation {
@@ -2297,7 +2292,7 @@ fn short_stability(item: &clean::Item, cx: &Context, parent: Option<&clean::Item
             );
             message.push_str(&format!(": {}", html.into_string()));
         }
-        stability.push(format!(
+        extra_info.push(format!(
             "<div class=\"stab deprecated\"><span class=\"emoji\">👎</span> {}</div>",
             message,
         ));
@@ -2341,14 +2336,14 @@ fn short_stability(item: &clean::Item, cx: &Context, parent: Option<&clean::Item
             );
         }
 
-        stability.push(format!("<div class=\"stab unstable\">{}</div>", message));
+        extra_info.push(format!("<div class=\"stab unstable\">{}</div>", message));
     }
 
     if let Some(portability) = portability(item, parent) {
-        stability.push(portability);
+        extra_info.push(portability);
     }
 
-    stability
+    extra_info
 }
 
 fn item_constant(w: &mut Buffer, cx: &Context, it: &clean::Item, c: &clean::Constant) {
@@ -2460,6 +2455,7 @@ fn render_implementor(
         AssocItemLink::Anchor(None),
         RenderMode::Normal,
         implementor.impl_item.stable_since().as_deref(),
+        implementor.impl_item.const_stable_since().as_deref(),
         false,
         Some(use_absolute),
         false,
@@ -2490,6 +2486,7 @@ fn render_impls(
                 assoc_link,
                 RenderMode::Normal,
                 containing_item.stable_since().as_deref(),
+                containing_item.const_stable_since().as_deref(),
                 true,
                 None,
                 false,
@@ -2624,7 +2621,7 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
         write!(w, "{}<span class=\"loading-content\">Loading content...</span>", extra_content)
     }
 
-    fn trait_item(w: &mut Buffer, cx: &Context, m: &clean::Item, t: &clean::Item) {
+    fn trait_item(w: &mut Buffer, cx: &Context, m: &clean::Item, t: &clean::Item, cache: &Cache) {
         let name = m.name.as_ref().unwrap();
         info!("Documenting {} on {}", name, t.name.as_deref().unwrap_or_default());
         let item_type = m.type_();
@@ -2633,6 +2630,7 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
         render_assoc_item(w, m, AssocItemLink::Anchor(Some(&id)), ItemType::Impl);
         write!(w, "</code>");
         render_stability_since(w, m, t);
+        write_srclink(cx, m, w, cache);
         write!(w, "</h3>");
         document(w, cx, m, Some(t));
     }
@@ -2644,8 +2642,8 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
             "Associated Types",
             "<div class=\"methods\">",
         );
-        for t in &types {
-            trait_item(w, cx, *t, it);
+        for t in types {
+            trait_item(w, cx, t, it, cache);
         }
         write_loading_content(w, "</div>");
     }
@@ -2657,8 +2655,8 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
             "Associated Constants",
             "<div class=\"methods\">",
         );
-        for t in &consts {
-            trait_item(w, cx, *t, it);
+        for t in consts {
+            trait_item(w, cx, t, it, cache);
         }
         write_loading_content(w, "</div>");
     }
@@ -2671,8 +2669,8 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
             "Required methods",
             "<div class=\"methods\">",
         );
-        for m in &required {
-            trait_item(w, cx, *m, it);
+        for m in required {
+            trait_item(w, cx, m, it, cache);
         }
         write_loading_content(w, "</div>");
     }
@@ -2683,8 +2681,8 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
             "Provided methods",
             "<div class=\"methods\">",
         );
-        for m in &provided {
-            trait_item(w, cx, *m, it);
+        for m in provided {
+            trait_item(w, cx, m, it, cache);
         }
         write_loading_content(w, "</div>");
     }
@@ -2739,6 +2737,7 @@ fn item_trait(w: &mut Buffer, cx: &Context, it: &clean::Item, t: &clean::Trait,
                     assoc_link,
                     RenderMode::Normal,
                     implementor.impl_item.stable_since().as_deref(),
+                    implementor.impl_item.const_stable_since().as_deref(),
                     false,
                     None,
                     true,
@@ -2881,10 +2880,40 @@ fn assoc_type(
     }
 }
 
-fn render_stability_since_raw(w: &mut Buffer, ver: Option<&str>, containing_ver: Option<&str>) {
+fn render_stability_since_raw(
+    w: &mut Buffer,
+    ver: Option<&str>,
+    const_ver: Option<&str>,
+    containing_ver: Option<&str>,
+    containing_const_ver: Option<&str>,
+) {
+    let ver = ver.and_then(|inner| if !inner.is_empty() { Some(inner) } else { None });
+
+    let const_ver = const_ver.and_then(|inner| if !inner.is_empty() { Some(inner) } else { None });
+
     if let Some(v) = ver {
-        if containing_ver != ver && !v.is_empty() {
-            write!(w, "<span class=\"since\" title=\"Stable since Rust version {0}\">{0}</span>", v)
+        if let Some(cv) = const_ver {
+            if const_ver != containing_const_ver {
+                write!(
+                    w,
+                    "<span class=\"since\" title=\"Stable since Rust version {0}, const since {1}\">{0} (const: {1})</span>",
+                    v, cv
+                );
+            } else if ver != containing_ver {
+                write!(
+                    w,
+                    "<span class=\"since\" title=\"Stable since Rust version {0}\">{0}</span>",
+                    v
+                );
+            }
+        } else {
+            if ver != containing_ver {
+                write!(
+                    w,
+                    "<span class=\"since\" title=\"Stable since Rust version {0}\">{0}</span>",
+                    v
+                );
+            }
         }
     }
 }
@@ -2893,7 +2922,9 @@ fn render_stability_since(w: &mut Buffer, item: &clean::Item, containing_item: &
     render_stability_since_raw(
         w,
         item.stable_since().as_deref(),
+        item.const_stable_since().as_deref(),
         containing_item.stable_since().as_deref(),
+        containing_item.const_stable_since().as_deref(),
     )
 }
 
@@ -3445,6 +3476,7 @@ fn render_assoc_items(
                 AssocItemLink::Anchor(None),
                 render_mode,
                 containing_item.stable_since().as_deref(),
+                containing_item.const_stable_since().as_deref(),
                 true,
                 None,
                 false,
@@ -3637,6 +3669,7 @@ fn render_impl(
     link: AssocItemLink<'_>,
     render_mode: RenderMode,
     outer_version: Option<&str>,
+    outer_const_version: Option<&str>,
     show_def_docs: bool,
     use_absolute: Option<bool>,
     is_on_foreign_type: bool,
@@ -3688,23 +3721,19 @@ fn render_impl(
             );
         }
         write!(w, "<a href=\"#{}\" class=\"anchor\"></a>", id);
-        let since = i.impl_item.stability.as_ref().and_then(|s| match s.level {
-            StabilityLevel::Stable { since } => Some(since.as_str()),
-            StabilityLevel::Unstable { .. } => None,
-        });
-        render_stability_since_raw(w, since.as_deref(), outer_version);
-        if let Some(l) = cx.src_href(&i.impl_item, cache) {
-            write!(
-                w,
-                "<a class=\"srclink\" href=\"{}\" title=\"{}\">[src]</a>",
-                l, "goto source code"
-            );
-        }
+        render_stability_since_raw(
+            w,
+            i.impl_item.stable_since().as_deref(),
+            i.impl_item.const_stable_since().as_deref(),
+            outer_version,
+            outer_const_version,
+        );
+        write_srclink(cx, &i.impl_item, w, cache);
         write!(w, "</h3>");
 
         if trait_.is_some() {
             if let Some(portability) = portability(&i.impl_item, Some(parent)) {
-                write!(w, "<div class=\"stability\">{}</div>", portability);
+                write!(w, "<div class=\"item-info\">{}</div>", portability);
             }
         }
 
@@ -3735,6 +3764,7 @@ fn render_impl(
         render_mode: RenderMode,
         is_default_item: bool,
         outer_version: Option<&str>,
+        outer_const_version: Option<&str>,
         trait_: Option<&clean::Trait>,
         show_def_docs: bool,
         cache: &Cache,
@@ -3764,14 +3794,14 @@ fn render_impl(
                     write!(w, "<code>");
                     render_assoc_item(w, item, link.anchor(&id), ItemType::Impl);
                     write!(w, "</code>");
-                    render_stability_since_raw(w, item.stable_since().as_deref(), outer_version);
-                    if let Some(l) = cx.src_href(item, cache) {
-                        write!(
-                            w,
-                            "<a class=\"srclink\" href=\"{}\" title=\"{}\">[src]</a>",
-                            l, "goto source code"
-                        );
-                    }
+                    render_stability_since_raw(
+                        w,
+                        item.stable_since().as_deref(),
+                        item.const_stable_since().as_deref(),
+                        outer_version,
+                        outer_const_version,
+                    );
+                    write_srclink(cx, item, w, cache);
                     write!(w, "</h4>");
                 }
             }
@@ -3786,14 +3816,14 @@ fn render_impl(
                 write!(w, "<h4 id=\"{}\" class=\"{}{}\"><code>", id, item_type, extra_class);
                 assoc_const(w, item, ty, default.as_ref(), link.anchor(&id), "");
                 write!(w, "</code>");
-                render_stability_since_raw(w, item.stable_since().as_deref(), outer_version);
-                if let Some(l) = cx.src_href(item, cache) {
-                    write!(
-                        w,
-                        "<a class=\"srclink\" href=\"{}\" title=\"{}\">[src]</a>",
-                        l, "goto source code"
-                    );
-                }
+                render_stability_since_raw(
+                    w,
+                    item.stable_since().as_deref(),
+                    item.const_stable_since().as_deref(),
+                    outer_version,
+                    outer_const_version,
+                );
+                write_srclink(cx, item, w, cache);
                 write!(w, "</h4>");
             }
             clean::AssocTypeItem(ref bounds, ref default) => {
@@ -3814,26 +3844,32 @@ fn render_impl(
                     if let Some(it) = t.items.iter().find(|i| i.name == item.name) {
                         // We need the stability of the item from the trait
                         // because impls can't have a stability.
-                        document_stability(w, cx, it, is_hidden, Some(parent));
                         if item.doc_value().is_some() {
+                            document_item_info(w, cx, it, is_hidden, Some(parent));
                             document_full(w, item, cx, "", is_hidden);
-                        } else if show_def_docs {
+                        } else {
                             // In case the item isn't documented,
                             // provide short documentation from the trait.
-                            document_short(w, it, link, "", is_hidden);
+                            document_short(
+                                w,
+                                it,
+                                cx,
+                                link,
+                                "",
+                                is_hidden,
+                                Some(parent),
+                                show_def_docs,
+                            );
                         }
                     }
                 } else {
-                    document_stability(w, cx, item, is_hidden, Some(parent));
+                    document_item_info(w, cx, item, is_hidden, Some(parent));
                     if show_def_docs {
                         document_full(w, item, cx, "", is_hidden);
                     }
                 }
             } else {
-                document_stability(w, cx, item, is_hidden, Some(parent));
-                if show_def_docs {
-                    document_short(w, item, link, "", is_hidden);
-                }
+                document_short(w, item, cx, link, "", is_hidden, Some(parent), show_def_docs);
             }
         }
     }
@@ -3849,6 +3885,7 @@ fn render_impl(
             render_mode,
             false,
             outer_version,
+            outer_const_version,
             trait_,
             show_def_docs,
             cache,
@@ -3863,6 +3900,7 @@ fn render_impl(
         parent: &clean::Item,
         render_mode: RenderMode,
         outer_version: Option<&str>,
+        outer_const_version: Option<&str>,
         show_def_docs: bool,
         cache: &Cache,
     ) {
@@ -3883,6 +3921,7 @@ fn render_impl(
                 render_mode,
                 true,
                 outer_version,
+                outer_const_version,
                 None,
                 show_def_docs,
                 cache,
@@ -3904,6 +3943,7 @@ fn render_impl(
                 &i.impl_item,
                 render_mode,
                 outer_version,
+                outer_const_version,
                 show_def_docs,
                 cache,
             );
diff --git a/src/librustdoc/html/sources.rs b/src/librustdoc/html/sources.rs
index 0f82649409f..ef9e9f350fb 100644
--- a/src/librustdoc/html/sources.rs
+++ b/src/librustdoc/html/sources.rs
@@ -7,6 +7,7 @@ use crate::html::highlight;
 use crate::html::layout;
 use crate::html::render::{SharedContext, BASIC_KEYWORDS};
 use rustc_hir::def_id::LOCAL_CRATE;
+use rustc_session::Session;
 use rustc_span::source_map::FileName;
 use std::ffi::OsStr;
 use std::fs;
@@ -34,37 +35,45 @@ struct SourceCollector<'a> {
 
 impl<'a> DocFolder for SourceCollector<'a> {
     fn fold_item(&mut self, item: clean::Item) -> Option<clean::Item> {
+        // If we're not rendering sources, there's nothing to do.
         // If we're including source files, and we haven't seen this file yet,
         // then we need to render it out to the filesystem.
         if self.scx.include_sources
             // skip all synthetic "files"
-            && item.source.filename.is_real()
+            && item.source.filename(self.sess()).is_real()
             // skip non-local files
-            && item.source.cnum == LOCAL_CRATE
+            && item.source.cnum(self.sess()) == LOCAL_CRATE
         {
+            let filename = item.source.filename(self.sess());
             // If it turns out that we couldn't read this file, then we probably
             // can't read any of the files (generating html output from json or
             // something like that), so just don't include sources for the
             // entire crate. The other option is maintaining this mapping on a
             // per-file basis, but that's probably not worth it...
-            self.scx.include_sources = match self.emit_source(&item.source.filename) {
+            self.scx.include_sources = match self.emit_source(&filename) {
                 Ok(()) => true,
                 Err(e) => {
                     println!(
                         "warning: source code was requested to be rendered, \
                          but processing `{}` had an error: {}",
-                        item.source.filename, e
+                        filename, e
                     );
                     println!("         skipping rendering of source code");
                     false
                 }
             };
         }
-        self.fold_item_recur(item)
+        // FIXME: if `include_sources` isn't set and DocFolder didn't require consuming the crate by value,
+        // we could return None here without having to walk the rest of the crate.
+        Some(self.fold_item_recur(item))
     }
 }
 
 impl<'a> SourceCollector<'a> {
+    fn sess(&self) -> &Session {
+        &self.scx.sess
+    }
+
     /// Renders the given filename into its corresponding HTML source file.
     fn emit_source(&mut self, filename: &FileName) -> Result<(), Error> {
         let p = match *filename {
diff --git a/src/librustdoc/html/static/main.js b/src/librustdoc/html/static/main.js
index b8377dc1569..47847ccb5f6 100644
--- a/src/librustdoc/html/static/main.js
+++ b/src/librustdoc/html/static/main.js
@@ -40,6 +40,29 @@ if (!DOMTokenList.prototype.remove) {
     };
 }
 
+
+// Gets the human-readable string for the virtual-key code of the
+// given KeyboardEvent, ev.
+//
+// This function is meant as a polyfill for KeyboardEvent#key,
+// since it is not supported in IE 11 or Chrome for Android. We also test for
+// KeyboardEvent#keyCode because the handleShortcut handler is
+// also registered for the keydown event, because Blink doesn't fire
+// keypress on hitting the Escape key.
+//
+// So I guess you could say things are getting pretty interoperable.
+function getVirtualKey(ev) {
+    if ("key" in ev && typeof ev.key != "undefined") {
+        return ev.key;
+    }
+
+    var c = ev.charCode || ev.keyCode;
+    if (c == 27) {
+        return "Escape";
+    }
+    return String.fromCharCode(c);
+}
+
 function getSearchInput() {
     return document.getElementsByClassName("search-input")[0];
 }
@@ -326,28 +349,6 @@ function defocusSearchBar() {
         }
     }
 
-    // Gets the human-readable string for the virtual-key code of the
-    // given KeyboardEvent, ev.
-    //
-    // This function is meant as a polyfill for KeyboardEvent#key,
-    // since it is not supported in Trident.  We also test for
-    // KeyboardEvent#keyCode because the handleShortcut handler is
-    // also registered for the keydown event, because Blink doesn't fire
-    // keypress on hitting the Escape key.
-    //
-    // So I guess you could say things are getting pretty interoperable.
-    function getVirtualKey(ev) {
-        if ("key" in ev && typeof ev.key != "undefined") {
-            return ev.key;
-        }
-
-        var c = ev.charCode || ev.keyCode;
-        if (c == 27) {
-            return "Escape";
-        }
-        return String.fromCharCode(c);
-    }
-
     function getHelpElement() {
         buildHelperPopup();
         return document.getElementById("help");
@@ -1468,16 +1469,21 @@ function defocusSearchBar() {
                 });
 
                 if (e.which === 38) { // up
-                    if (!actives[currentTab].length ||
-                        !actives[currentTab][0].previousElementSibling) {
-                        return;
+                    if (e.ctrlKey) { // Going through result tabs.
+                        printTab(currentTab > 0 ? currentTab - 1 : 2);
+                    } else {
+                        if (!actives[currentTab].length ||
+                            !actives[currentTab][0].previousElementSibling) {
+                            return;
+                        }
+                        addClass(actives[currentTab][0].previousElementSibling, "highlighted");
+                        removeClass(actives[currentTab][0], "highlighted");
                     }
-
-                    addClass(actives[currentTab][0].previousElementSibling, "highlighted");
-                    removeClass(actives[currentTab][0], "highlighted");
                     e.preventDefault();
                 } else if (e.which === 40) { // down
-                    if (!actives[currentTab].length) {
+                    if (e.ctrlKey) { // Going through result tabs.
+                        printTab(currentTab > 1 ? 0 : currentTab + 1);
+                    } else if (!actives[currentTab].length) {
                         var results = document.getElementById("results").childNodes;
                         if (results.length > 0) {
                             var res = results[currentTab].getElementsByClassName("result");
@@ -1495,13 +1501,6 @@ function defocusSearchBar() {
                         document.location.href =
                             actives[currentTab][0].getElementsByTagName("a")[0].href;
                     }
-                } else if (e.which === 9) { // tab
-                    if (e.shiftKey) {
-                        printTab(currentTab > 0 ? currentTab - 1 : 2);
-                    } else {
-                        printTab(currentTab > 1 ? 0 : currentTab + 1);
-                    }
-                    e.preventDefault();
                 } else if (e.which === 16) { // shift
                     // Does nothing, it's just to avoid losing "focus" on the highlighted element.
                 } else if (actives[currentTab].length > 0) {
@@ -1610,7 +1609,7 @@ function defocusSearchBar() {
                               item.displayPath + "<span class=\"" + type + "\">" +
                               name + "</span></a></td><td>" +
                               "<a href=\"" + item.href + "\">" +
-                              "<span class=\"desc\">" + escape(item.desc) +
+                              "<span class=\"desc\">" + item.desc +
                               "&nbsp;</span></a></td></tr>";
                 });
                 output += "</table>";
@@ -1634,10 +1633,10 @@ function defocusSearchBar() {
 
         function makeTabHeader(tabNb, text, nbElems) {
             if (currentTab === tabNb) {
-                return "<div class=\"selected\">" + text +
-                       " <div class=\"count\">(" + nbElems + ")</div></div>";
+                return "<button class=\"selected\">" + text +
+                       " <div class=\"count\">(" + nbElems + ")</div></button>";
             }
-            return "<div>" + text + " <div class=\"count\">(" + nbElems + ")</div></div>";
+            return "<button>" + text + " <div class=\"count\">(" + nbElems + ")</div></button>";
         }
 
         function showResults(results) {
@@ -2012,7 +2011,9 @@ function defocusSearchBar() {
                     }
                     var link = document.createElement("a");
                     link.href = rootPath + crates[i] + "/index.html";
-                    link.title = rawSearchIndex[crates[i]].doc;
+                    // The summary in the search index has HTML, so we need to
+                    // dynamically render it as plaintext.
+                    link.title = convertHTMLToPlaintext(rawSearchIndex[crates[i]].doc);
                     link.className = klass;
                     link.textContent = crates[i];
 
@@ -2025,6 +2026,23 @@ function defocusSearchBar() {
         }
     };
 
+    /**
+     * Convert HTML to plaintext:
+     *
+     *   * Replace "<code>foo</code>" with "`foo`"
+     *   * Strip all other HTML tags
+     *
+     * Used by the dynamic sidebar crate list renderer.
+     *
+     * @param  {[string]} html [The HTML to convert]
+     * @return {[string]}      [The resulting plaintext]
+     */
+    function convertHTMLToPlaintext(html) {
+        var x = document.createElement("div");
+        x.innerHTML = html.replace('<code>', '`').replace('</code>', '`');
+        return x.innerText;
+    }
+
 
     // delayed sidebar rendering.
     window.initSidebarItems = function(items) {
@@ -2266,7 +2284,7 @@ function defocusSearchBar() {
                         }
                     }
                     var ns = n.nextElementSibling;
-                    while (ns && (hasClass(ns, "docblock") || hasClass(ns, "stability"))) {
+                    while (ns && (hasClass(ns, "docblock") || hasClass(ns, "item-info"))) {
                         if (addOrRemove) {
                             addClass(ns, "hidden-by-impl-hider");
                         } else {
@@ -2282,7 +2300,7 @@ function defocusSearchBar() {
         var action = mode;
         if (hasClass(toggle.parentNode, "impl") === false) {
             relatedDoc = toggle.parentNode.nextElementSibling;
-            if (hasClass(relatedDoc, "stability")) {
+            if (hasClass(relatedDoc, "item-info")) {
                 relatedDoc = relatedDoc.nextElementSibling;
             }
             if (hasClass(relatedDoc, "docblock") || hasClass(relatedDoc, "sub-variant")) {
@@ -2332,12 +2350,19 @@ function defocusSearchBar() {
             var dontApplyBlockRule = toggle.parentNode.parentNode.id !== "main";
             if (action === "show") {
                 removeClass(relatedDoc, "fns-now-collapsed");
-                removeClass(docblock, "hidden-by-usual-hider");
+                // Stability/deprecation/portability information is never hidden.
+                if (hasClass(docblock, "item-info") === false) {
+                    removeClass(docblock, "hidden-by-usual-hider");
+                }
                 onEachLazy(toggle.childNodes, adjustToggle(false, dontApplyBlockRule));
                 onEachLazy(relatedDoc.childNodes, implHider(false, dontApplyBlockRule));
             } else if (action === "hide") {
                 addClass(relatedDoc, "fns-now-collapsed");
-                addClass(docblock, "hidden-by-usual-hider");
+                // Stability/deprecation/portability information should be shown even when detailed
+                // info is hidden.
+                if (hasClass(docblock, "item-info") === false) {
+                    addClass(docblock, "hidden-by-usual-hider");
+                }
                 onEachLazy(toggle.childNodes, adjustToggle(true, dontApplyBlockRule));
                 onEachLazy(relatedDoc.childNodes, implHider(true, dontApplyBlockRule));
             }
@@ -2439,7 +2464,7 @@ function defocusSearchBar() {
 
         var func = function(e) {
             var next = e.nextElementSibling;
-            if (next && hasClass(next, "stability")) {
+            if (next && hasClass(next, "item-info")) {
               next = next.nextElementSibling;
             }
             if (!next) {
@@ -2456,7 +2481,7 @@ function defocusSearchBar() {
 
         var funcImpl = function(e) {
             var next = e.nextElementSibling;
-            if (next && hasClass(next, "stability")) {
+            if (next && hasClass(next, "item-info")) {
                 next = next.nextElementSibling;
             }
             if (next && hasClass(next, "docblock")) {
@@ -2871,11 +2896,14 @@ function defocusSearchBar() {
             ["T", "Focus the theme picker menu"],
             ["↑", "Move up in search results"],
             ["↓", "Move down in search results"],
-            ["↹", "Switch tab"],
+            ["ctrl + ↑ / ↓", "Switch result tab"],
             ["&#9166;", "Go to active search result"],
             ["+", "Expand all sections"],
             ["-", "Collapse all sections"],
-        ].map(x => "<dt><kbd>" + x[0] + "</kbd></dt><dd>" + x[1] + "</dd>").join("");
+        ].map(x => "<dt>" +
+            x[0].split(" ")
+                .map((y, index) => (index & 1) === 0 ? "<kbd>" + y + "</kbd>" : y)
+                .join("") + "</dt><dd>" + x[1] + "</dd>").join("");
         var div_shortcuts = document.createElement("div");
         addClass(div_shortcuts, "shortcuts");
         div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>";
diff --git a/src/librustdoc/html/static/rustdoc.css b/src/librustdoc/html/static/rustdoc.css
index 7eccb09b073..42e4fa05152 100644
--- a/src/librustdoc/html/static/rustdoc.css
+++ b/src/librustdoc/html/static/rustdoc.css
@@ -553,21 +553,21 @@ h4 > code, h3 > code, .invisible > code {
 	border: none;
 }
 
-.content .stability code {
+.content .item-info code {
 	font-size: 90%;
 }
 
-.content .stability {
+.content .item-info {
 	position: relative;
 	margin-left: 33px;
 	margin-top: -13px;
 }
 
-.sub-variant > div > .stability {
+.sub-variant > div > .item-info {
 	margin-top: initial;
 }
 
-.content .stability::before {
+.content .item-info::before {
 	content: '⬑';
 	font-size: 25px;
 	position: absolute;
@@ -579,15 +579,15 @@ h4 > code, h3 > code, .invisible > code {
 	margin-left: 20px;
 }
 
-.content .impl-items .docblock, .content .impl-items .stability {
+.content .impl-items .docblock, .content .impl-items .item-info {
 	margin-bottom: .6em;
 }
 
-.content .impl-items > .stability {
+.content .impl-items > .item-info {
 	margin-left: 40px;
 }
 
-.methods > .stability, .content .impl-items > .stability {
+.methods > .item-info, .content .impl-items > .item-info {
 	margin-top: -8px;
 }
 
@@ -595,7 +595,7 @@ h4 > code, h3 > code, .invisible > code {
 	flex-basis: 100%;
 }
 
-#main > .stability {
+#main > .item-info {
 	margin-top: 0;
 }
 
@@ -655,11 +655,11 @@ a {
 }
 
 .docblock a:not(.srclink):not(.test-arrow):hover,
-.docblock-short a:not(.srclink):not(.test-arrow):hover, .stability a {
+.docblock-short a:not(.srclink):not(.test-arrow):hover, .item-info a {
 	text-decoration: underline;
 }
 
-.invisible > .srclink, h4 > code + .srclink {
+.invisible > .srclink, h4 > code + .srclink, h3 > code + .srclink {
 	position: absolute;
 	top: 0;
 	right: 0;
@@ -857,25 +857,25 @@ body.blur > :not(#help) {
 	top: 0;
 }
 
-.impl-items .since, .impl .since {
+.impl-items .since, .impl .since, .methods .since {
 	flex-grow: 0;
 	padding-left: 12px;
 	padding-right: 2px;
 	position: initial;
 }
 
-.impl-items .srclink, .impl .srclink {
+.impl-items .srclink, .impl .srclink, .methods .srclink {
 	flex-grow: 0;
 	/* Override header settings otherwise it's too bold */
 	font-size: 17px;
 	font-weight: normal;
 }
 
-.impl-items code, .impl code {
+.impl-items code, .impl code, .methods code {
 	flex-grow: 1;
 }
 
-.impl-items h4, h4.impl, h3.impl {
+.impl-items h4, h4.impl, h3.impl, .methods h3 {
 	display: flex;
 	flex-basis: 100%;
 	font-size: 16px;
@@ -1176,21 +1176,22 @@ pre.rust {
 	height: 35px;
 }
 
-#titles > div {
+#titles > button {
 	float: left;
 	width: 33.3%;
 	text-align: center;
 	font-size: 18px;
 	cursor: pointer;
+	border: 0;
 	border-top: 2px solid;
 }
 
-#titles > div:not(:last-child) {
+#titles > button:not(:last-child) {
 	margin-right: 1px;
 	width: calc(33.3% - 1px);
 }
 
-#titles > div > div.count {
+#titles > button > div.count {
 	display: inline-block;
 	font-size: 16px;
 }
@@ -1459,7 +1460,7 @@ h4 > .notable-traits {
 		top: 24px;
 	}
 
-	#titles > div > div.count {
+	#titles > button > div.count {
 		float: left;
 		width: 100%;
 	}
@@ -1565,13 +1566,17 @@ h4 > .notable-traits {
 }
 
 @media (max-width: 416px) {
-	#titles, #titles > div {
+	#titles, #titles > button {
 		height: 73px;
 	}
 
+	#main {
+		margin-top: 100px;
+	}
+
 	#main > table:not(.table-display) td {
 		word-break: break-word;
-		min-width: 10%;
+		width: 50%;
 	}
 
 	.search-container > div {
diff --git a/src/librustdoc/html/static/settings.js b/src/librustdoc/html/static/settings.js
index da3378ccf0d..bc14420232c 100644
--- a/src/librustdoc/html/static/settings.js
+++ b/src/librustdoc/html/static/settings.js
@@ -1,5 +1,5 @@
 // Local js definitions:
-/* global getCurrentValue, updateLocalStorage, updateSystemTheme */
+/* global getCurrentValue, getVirtualKey, updateLocalStorage, updateSystemTheme */
 
 (function () {
     function changeSetting(settingName, value) {
@@ -14,41 +14,47 @@
         }
     }
 
-    function setEvents() {
-        var elems = {
-            toggles: document.getElementsByClassName("slider"),
-            selects: document.getElementsByClassName("select-wrapper")
-        };
-        var i;
-
-        if (elems.toggles && elems.toggles.length > 0) {
-            for (i = 0; i < elems.toggles.length; ++i) {
-                var toggle = elems.toggles[i].previousElementSibling;
-                var settingId = toggle.id;
-                var settingValue = getSettingValue(settingId);
-                if (settingValue !== null) {
-                    toggle.checked = settingValue === "true";
-                }
-                toggle.onchange = function() {
-                    changeSetting(this.id, this.checked);
-                };
-            }
+    function handleKey(ev) {
+        // Don't interfere with browser shortcuts
+        if (ev.ctrlKey || ev.altKey || ev.metaKey) {
+            return;
+        }
+        switch (getVirtualKey(ev)) {
+            case "Enter":
+            case "Return":
+            case "Space":
+                ev.target.checked = !ev.target.checked;
+                ev.preventDefault();
+                break;
         }
+    }
 
-        if (elems.selects && elems.selects.length > 0) {
-            for (i = 0; i < elems.selects.length; ++i) {
-                var select = elems.selects[i].getElementsByTagName("select")[0];
-                var settingId = select.id;
-                var settingValue = getSettingValue(settingId);
-                if (settingValue !== null) {
-                    select.value = settingValue;
-                }
-                select.onchange = function() {
-                    changeSetting(this.id, this.value);
-                };
+    function setEvents() {
+        onEachLazy(document.getElementsByClassName("slider"), function(elem) {
+            var toggle = elem.previousElementSibling;
+            var settingId = toggle.id;
+            var settingValue = getSettingValue(settingId);
+            if (settingValue !== null) {
+                toggle.checked = settingValue === "true";
             }
-        }
+            toggle.onchange = function() {
+                changeSetting(this.id, this.checked);
+            };
+            toggle.onkeyup = handleKey;
+            toggle.onkeyrelease = handleKey;
+        });
+        onEachLazy(document.getElementsByClassName("select-wrapper"), function(elem) {
+            var select = elem.getElementsByTagName("select")[0];
+            var settingId = select.id;
+            var settingValue = getSettingValue(settingId);
+            if (settingValue !== null) {
+                select.value = settingValue;
+            }
+            select.onchange = function() {
+                changeSetting(this.id, this.value);
+            };
+        });
     }
 
-    setEvents();
+    window.addEventListener("DOMContentLoaded", setEvents);
 })();
diff --git a/src/librustdoc/html/static/themes/ayu.css b/src/librustdoc/html/static/themes/ayu.css
index d1cddf0d656..76bbe4f6201 100644
--- a/src/librustdoc/html/static/themes/ayu.css
+++ b/src/librustdoc/html/static/themes/ayu.css
@@ -166,7 +166,7 @@ pre {
 	color: #c5c5c5;
 }
 
-.content .stability::before { color: #ccc; }
+.content .item-info::before { color: #ccc; }
 
 .content span.foreigntype, .content a.foreigntype { color: #ef57ff; }
 .content span.union, .content a.union { color: #98a01c; }
@@ -219,7 +219,7 @@ a {
 }
 
 .docblock:not(.type-decl) a:not(.srclink):not(.test-arrow),
-.docblock-short a:not(.srclink):not(.test-arrow), .stability a,
+.docblock-short a:not(.srclink):not(.test-arrow), .item-info a,
 #help a {
 	color: #39AFD7;
 }
@@ -403,22 +403,22 @@ pre.ignore:hover, .information:hover + pre.ignore {
 	border-color: #5c6773;
 }
 
-#titles > div.selected {
+#titles > button.selected {
 	background-color: #141920 !important;
 	border-bottom: 1px solid #ffb44c !important;
 	border-top: none;
 }
 
-#titles > div:not(.selected) {
+#titles > button:not(.selected) {
 	background-color: transparent !important;
 	border: none;
 }
 
-#titles > div:hover {
+#titles > button:hover {
 	border-bottom: 1px solid rgba(242, 151, 24, 0.3);
 }
 
-#titles > div > div.count {
+#titles > button > div.count {
 	color: #888;
 }
 
@@ -434,7 +434,7 @@ above the `@media (max-width: 700px)` rules due to a bug in the css checker */
 .block a.current.derive,.content span.macro,.content a.macro,.block a.current.macro {}
 .content .highlighted.trait {}
 .content span.struct,.content a.struct,.block a.current.struct {}
-#titles>div:hover,#titles>div.selected {}
+#titles>button:hover,#titles>button.selected {}
 .content .highlighted.traitalias {}
 .content span.type,.content a.type,.block a.current.type {}
 .content span.union,.content a.union,.block a.current.union {}
diff --git a/src/librustdoc/html/static/themes/dark.css b/src/librustdoc/html/static/themes/dark.css
index 3545943b3fd..86ce99284eb 100644
--- a/src/librustdoc/html/static/themes/dark.css
+++ b/src/librustdoc/html/static/themes/dark.css
@@ -136,7 +136,7 @@ pre {
 .content .highlighted.primitive { background-color: #00708a; }
 .content .highlighted.keyword { background-color: #884719; }
 
-.content .stability::before { color: #ccc; }
+.content .item-info::before { color: #ccc; }
 
 .content span.enum, .content a.enum, .block a.current.enum { color: #82b089; }
 .content span.struct, .content a.struct, .block a.current.struct { color: #2dbfb8; }
@@ -177,7 +177,7 @@ a {
 }
 
 .docblock:not(.type-decl) a:not(.srclink):not(.test-arrow),
-.docblock-short a:not(.srclink):not(.test-arrow), .stability a,
+.docblock-short a:not(.srclink):not(.test-arrow), .item-info a,
 #help a {
 	color: #D2991D;
 }
@@ -352,16 +352,17 @@ pre.ignore:hover, .information:hover + pre.ignore {
 	border-color: #777;
 }
 
-#titles > div:not(.selected) {
+#titles > button:not(.selected) {
 	background-color: #252525;
 	border-top-color: #252525;
 }
 
-#titles > div:hover, #titles > div.selected {
+#titles > button:hover, #titles > button.selected {
 	border-top-color: #0089ff;
+	background-color: #353535;
 }
 
-#titles > div > div.count {
+#titles > button > div.count {
 	color: #888;
 }
 
diff --git a/src/librustdoc/html/static/themes/light.css b/src/librustdoc/html/static/themes/light.css
index 4ce4b63e2c6..997e1f00f85 100644
--- a/src/librustdoc/html/static/themes/light.css
+++ b/src/librustdoc/html/static/themes/light.css
@@ -134,7 +134,7 @@ pre {
 .content .highlighted.primitive { background-color: #9aecff; }
 .content .highlighted.keyword { background-color: #f99650; }
 
-.content .stability::before { color: #ccc; }
+.content .item-info::before { color: #ccc; }
 
 .content span.enum, .content a.enum, .block a.current.enum { color: #508157; }
 .content span.struct, .content a.struct, .block a.current.struct { color: #ad448e; }
@@ -175,7 +175,7 @@ a {
 }
 
 .docblock:not(.type-decl) a:not(.srclink):not(.test-arrow),
-.docblock-short a:not(.srclink):not(.test-arrow), .stability a,
+.docblock-short a:not(.srclink):not(.test-arrow), .item-info a,
 #help a {
 	color: #3873AD;
 }
@@ -343,16 +343,17 @@ pre.ignore:hover, .information:hover + pre.ignore {
 	border-color: #999;
 }
 
-#titles > div:not(.selected) {
+#titles > button:not(.selected) {
 	background-color: #e6e6e6;
 	border-top-color: #e6e6e6;
 }
 
-#titles > div:hover, #titles > div.selected {
+#titles > button:hover, #titles > button.selected {
+	background-color: #ffffff;
 	border-top-color: #0089ff;
 }
 
-#titles > div > div.count {
+#titles > button > div.count {
 	color: #888;
 }