diff options
Diffstat (limited to 'src/librustdoc/html')
-rw-r--r-- | src/librustdoc/html/format.rs | 58 | ||||
-rw-r--r-- | src/librustdoc/html/layout.rs | 2 | ||||
-rw-r--r-- | src/librustdoc/html/markdown.rs | 18 | ||||
-rw-r--r-- | src/librustdoc/html/render/context.rs | 41 | ||||
-rw-r--r-- | src/librustdoc/html/render/mod.rs | 29 | ||||
-rw-r--r-- | src/librustdoc/html/render/print_item.rs | 267 | ||||
-rw-r--r-- | src/librustdoc/html/render/search_index.rs | 23 | ||||
-rw-r--r-- | src/librustdoc/html/render/type_layout.rs | 2 | ||||
-rw-r--r-- | src/librustdoc/html/static/css/rustdoc.css | 54 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/externs.js | 4 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/main.js | 170 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/search.js | 894 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/source-script.js | 4 | ||||
-rw-r--r-- | src/librustdoc/html/static/js/storage.js | 2 | ||||
-rw-r--r-- | src/librustdoc/html/templates/item_info.html | 2 | ||||
-rw-r--r-- | src/librustdoc/html/templates/item_union.html | 19 | ||||
-rw-r--r-- | src/librustdoc/html/templates/page.html | 4 | ||||
-rw-r--r-- | src/librustdoc/html/templates/print_item.html | 4 |
18 files changed, 1063 insertions, 534 deletions
diff --git a/src/librustdoc/html/format.rs b/src/librustdoc/html/format.rs index d963d6092..54c0cd2ef 100644 --- a/src/librustdoc/html/format.rs +++ b/src/librustdoc/html/format.rs @@ -347,13 +347,19 @@ pub(crate) fn print_where_clause<'a, 'tcx: 'a>( } } else { let mut br_with_padding = String::with_capacity(6 * indent + 28); - br_with_padding.push_str("\n"); + br_with_padding.push('\n'); - let padding_amount = - if ending == Ending::Newline { indent + 4 } else { indent + "fn where ".len() }; + let where_indent = 3; + let padding_amount = if ending == Ending::Newline { + indent + 4 + } else if indent == 0 { + 4 + } else { + indent + where_indent + "where ".len() + }; for _ in 0..padding_amount { - br_with_padding.push_str(" "); + br_with_padding.push(' '); } let where_preds = where_preds.to_string().replace('\n', &br_with_padding); @@ -370,7 +376,8 @@ pub(crate) fn print_where_clause<'a, 'tcx: 'a>( let where_preds = where_preds.replacen(&br_with_padding, " ", 1); let mut clause = br_with_padding; - clause.truncate(clause.len() - "where ".len()); + // +1 is for `\n`. + clause.truncate(indent + 1 + where_indent); write!(clause, "<span class=\"where\">where{where_preds}</span>")?; clause @@ -1257,9 +1264,9 @@ impl clean::Impl { }; primitive_link_fragment(f, PrimitiveType::Tuple, &format!("fn ({name}₁, {name}₂, …, {name}ₙ{ellipsis})"), "#trait-implementations-1", cx)?; // Write output. - if let clean::FnRetTy::Return(ty) = &bare_fn.decl.output { + if !bare_fn.decl.output.is_unit() { write!(f, " -> ")?; - fmt_type(ty, f, use_absolute, cx)?; + fmt_type(&bare_fn.decl.output, f, use_absolute, cx)?; } } else if let Some(ty) = self.kind.as_blanket_ty() { fmt_type(ty, f, use_absolute, cx)?; @@ -1296,22 +1303,6 @@ impl clean::Arguments { } } -impl clean::FnRetTy { - pub(crate) fn print<'a, 'tcx: 'a>( - &'a self, - cx: &'a Context<'tcx>, - ) -> impl fmt::Display + 'a + Captures<'tcx> { - display_fn(move |f| match self { - clean::Return(clean::Tuple(tys)) if tys.is_empty() => Ok(()), - clean::Return(ty) if f.alternate() => { - write!(f, " -> {:#}", ty.print(cx)) - } - clean::Return(ty) => write!(f, " -> {}", ty.print(cx)), - clean::DefaultReturn => Ok(()), - }) - } -} - impl clean::BareFunctionDecl { fn print_hrtb_with_space<'a, 'tcx: 'a>( &'a self, @@ -1366,7 +1357,7 @@ impl clean::FnDecl { "({args:#}{ellipsis}){arrow:#}", args = self.inputs.print(cx), ellipsis = ellipsis, - arrow = self.output.print(cx) + arrow = self.print_output(cx) ) } else { write!( @@ -1374,7 +1365,7 @@ impl clean::FnDecl { "({args}{ellipsis}){arrow}", args = self.inputs.print(cx), ellipsis = ellipsis, - arrow = self.output.print(cx) + arrow = self.print_output(cx) ) } }) @@ -1417,7 +1408,7 @@ impl clean::FnDecl { let amp = if f.alternate() { "&" } else { "&" }; write!(f, "(")?; - if let Some(n) = line_wrapping_indent { + if let Some(n) = line_wrapping_indent && !self.inputs.values.is_empty() { write!(f, "\n{}", Indent(n + 4))?; } for (i, input) in self.inputs.values.iter().enumerate() { @@ -1464,9 +1455,22 @@ impl clean::FnDecl { Some(n) => write!(f, "\n{})", Indent(n))?, }; - fmt::Display::fmt(&self.output.print(cx), f)?; + fmt::Display::fmt(&self.print_output(cx), f)?; Ok(()) } + + fn print_output<'a, 'tcx: 'a>( + &'a self, + cx: &'a Context<'tcx>, + ) -> impl fmt::Display + 'a + Captures<'tcx> { + display_fn(move |f| match &self.output { + clean::Tuple(tys) if tys.is_empty() => Ok(()), + ty if f.alternate() => { + write!(f, " -> {:#}", ty.print(cx)) + } + ty => write!(f, " -> {}", ty.print(cx)), + }) + } } pub(crate) fn visibility_print_with_space<'a, 'tcx: 'a>( diff --git a/src/librustdoc/html/layout.rs b/src/librustdoc/html/layout.rs index 6ab849c92..8c5871d91 100644 --- a/src/librustdoc/html/layout.rs +++ b/src/librustdoc/html/layout.rs @@ -55,6 +55,7 @@ struct PageLayout<'a> { sidebar: String, content: String, krate_with_trailing_slash: String, + rust_channel: &'static str, pub(crate) rustdoc_version: &'a str, } @@ -82,6 +83,7 @@ pub(crate) fn render<T: Print, S: Print>( sidebar, content, krate_with_trailing_slash, + rust_channel: *crate::clean::utils::DOC_CHANNEL, rustdoc_version, } .render() diff --git a/src/librustdoc/html/markdown.rs b/src/librustdoc/html/markdown.rs index 9bb20022c..fd00277e2 100644 --- a/src/librustdoc/html/markdown.rs +++ b/src/librustdoc/html/markdown.rs @@ -381,7 +381,6 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> { Some(Event::Code(text)) => { trace!("saw code {}", text); if let Some(link) = self.shortcut_link { - trace!("original text was {}", link.original_text); // NOTE: this only replaces if the code block is the *entire* text. // If only part of the link has code highlighting, the disambiguator will not be removed. // e.g. [fn@`f`] @@ -390,8 +389,11 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> { // So we could never be sure we weren't replacing too much: // [fn@my_`f`unc] is treated the same as [my_func()] in that pass. // - // NOTE: &[1..len() - 1] is to strip the backticks - if **text == link.original_text[1..link.original_text.len() - 1] { + // NOTE: .get(1..len() - 1) is to strip the backticks + if let Some(link) = self.links.iter().find(|l| { + l.href == link.href + && Some(&**text) == l.original_text.get(1..l.original_text.len() - 1) + }) { debug!("replacing {} with {}", text, link.new_text); *text = CowStr::Borrowed(&link.new_text); } @@ -402,9 +404,12 @@ impl<'a, I: Iterator<Item = Event<'a>>> Iterator for LinkReplacer<'a, I> { Some(Event::Text(text)) => { trace!("saw text {}", text); if let Some(link) = self.shortcut_link { - trace!("original text was {}", link.original_text); // NOTE: same limitations as `Event::Code` - if **text == *link.original_text { + if let Some(link) = self + .links + .iter() + .find(|l| l.href == link.href && **text == *l.original_text) + { debug!("replacing {} with {}", text, link.new_text); *text = CowStr::Borrowed(&link.new_text); } @@ -781,7 +786,7 @@ impl<'tcx> ExtraInfo<'tcx> { ExtraInfo { def_id, sp, tcx } } - fn error_invalid_codeblock_attr(&self, msg: String, help: &str) { + fn error_invalid_codeblock_attr(&self, msg: String, help: &'static str) { if let Some(def_id) = self.def_id.as_local() { self.tcx.struct_span_lint_hir( crate::lint::INVALID_CODEBLOCK_ATTRIBUTES, @@ -1520,7 +1525,6 @@ fn init_id_map() -> FxHashMap<Cow<'static, str>, usize> { map.insert("toggle-all-docs".into(), 1); map.insert("all-types".into(), 1); map.insert("default-settings".into(), 1); - map.insert("rustdoc-vars".into(), 1); map.insert("sidebar-vars".into(), 1); map.insert("copy-path".into(), 1); map.insert("TOC".into(), 1); diff --git a/src/librustdoc/html/render/context.rs b/src/librustdoc/html/render/context.rs index 56af257fd..4c4762636 100644 --- a/src/librustdoc/html/render/context.rs +++ b/src/librustdoc/html/render/context.rs @@ -73,6 +73,8 @@ pub(crate) struct Context<'tcx> { pub(crate) include_sources: bool, /// Collection of all types with notable traits referenced in the current module. pub(crate) types_with_notable_traits: FxHashSet<clean::Type>, + /// Field used during rendering, to know if we're inside an inlined item. + pub(crate) is_inside_inlined_module: bool, } // `Context` is cloned a lot, so we don't want the size to grow unexpectedly. @@ -171,6 +173,19 @@ impl<'tcx> Context<'tcx> { } fn render_item(&mut self, it: &clean::Item, is_module: bool) -> String { + let mut render_redirect_pages = self.render_redirect_pages; + // If the item is stripped but inlined, links won't point to the item so no need to generate + // a file for it. + if it.is_stripped() && + let Some(def_id) = it.def_id() && + def_id.is_local() + { + if self.is_inside_inlined_module || self.shared.cache.inlined_items.contains(&def_id) { + // For now we're forced to generate a redirect page for stripped items until + // `record_extern_fqn` correctly points to external items. + render_redirect_pages = true; + } + } let mut title = String::new(); if !is_module { title.push_str(it.name.unwrap().as_str()); @@ -205,7 +220,7 @@ impl<'tcx> Context<'tcx> { tyname.as_str() }; - if !self.render_redirect_pages { + if !render_redirect_pages { let clone_shared = Rc::clone(&self.shared); let page = layout::Page { css_class: tyname_s, @@ -545,6 +560,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { shared: Rc::new(scx), include_sources, types_with_notable_traits: FxHashSet::default(), + is_inside_inlined_module: false, }; if emit_crate { @@ -574,6 +590,7 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { shared: Rc::clone(&self.shared), include_sources: self.include_sources, types_with_notable_traits: FxHashSet::default(), + is_inside_inlined_module: self.is_inside_inlined_module, } } @@ -768,12 +785,22 @@ impl<'tcx> FormatRenderer<'tcx> for Context<'tcx> { info!("Recursing into {}", self.dst.display()); - let buf = self.render_item(item, true); - // buf will be empty if the module is stripped and there is no redirect for it - if !buf.is_empty() { - self.shared.ensure_dir(&self.dst)?; - let joint_dst = self.dst.join("index.html"); - self.shared.fs.write(joint_dst, buf)?; + if !item.is_stripped() { + let buf = self.render_item(item, true); + // buf will be empty if the module is stripped and there is no redirect for it + if !buf.is_empty() { + self.shared.ensure_dir(&self.dst)?; + let joint_dst = self.dst.join("index.html"); + self.shared.fs.write(joint_dst, buf)?; + } + } + if !self.is_inside_inlined_module { + if let Some(def_id) = item.def_id() && self.cache().inlined_items.contains(&def_id) { + self.is_inside_inlined_module = true; + } + } else if item.is_doc_hidden() { + // We're not inside an inlined module anymore since this one cannot be re-exported. + self.is_inside_inlined_module = false; } // Render sidebar-items.js used throughout this module. diff --git a/src/librustdoc/html/render/mod.rs b/src/librustdoc/html/render/mod.rs index 9e3b5d10a..f923f9054 100644 --- a/src/librustdoc/html/render/mod.rs +++ b/src/librustdoc/html/render/mod.rs @@ -421,11 +421,10 @@ fn document<'a, 'cx: 'a>( display_fn(move |f| { document_item_info(cx, item, parent).render_into(f).unwrap(); if parent.is_none() { - write!(f, "{}", document_full_collapsible(item, cx, heading_offset))?; + write!(f, "{}", document_full_collapsible(item, cx, heading_offset)) } else { - write!(f, "{}", document_full(item, cx, heading_offset))?; + write!(f, "{}", document_full(item, cx, heading_offset)) } - Ok(()) }) } @@ -787,10 +786,12 @@ fn assoc_type( indent: usize, cx: &Context<'_>, ) { + let tcx = cx.tcx(); write!( w, - "{indent}type <a{href} class=\"associatedtype\">{name}</a>{generics}", + "{indent}{vis}type <a{href} class=\"associatedtype\">{name}</a>{generics}", indent = " ".repeat(indent), + vis = visibility_print_with_space(it.visibility(tcx), it.item_id, cx), href = assoc_href_attr(it, link, cx), name = it.name.as_ref().unwrap(), generics = generics.print(cx), @@ -798,10 +799,11 @@ fn assoc_type( if !bounds.is_empty() { write!(w, ": {}", print_generic_bounds(bounds, cx)) } - write!(w, "{}", print_where_clause(generics, cx, indent, Ending::NoNewline)); + // Render the default before the where-clause which aligns with the new recommended style. See #89122. if let Some(default) = default { write!(w, " = {}", default.print(cx)) } + write!(w, "{}", print_where_clause(generics, cx, indent, Ending::NoNewline)); } fn assoc_method( @@ -844,7 +846,7 @@ fn assoc_method( + name.as_str().len() + generics_len; - let notable_traits = d.output.as_return().and_then(|output| notable_traits_button(output, cx)); + let notable_traits = notable_traits_button(&d.output, cx); let (indent, indent_str, end_newline) = if parent == ItemType::Trait { header_len += 4; @@ -858,8 +860,8 @@ fn assoc_method( w.reserve(header_len + "<a href=\"\" class=\"fn\">{".len() + "</a>".len()); write!( w, - "{indent}{vis}{constness}{asyncness}{unsafety}{defaultness}{abi}fn <a{href} class=\"fn\">{name}</a>\ - {generics}{decl}{notable_traits}{where_clause}", + "{indent}{vis}{constness}{asyncness}{unsafety}{defaultness}{abi}fn \ + <a{href} class=\"fn\">{name}</a>{generics}{decl}{notable_traits}{where_clause}", indent = indent_str, vis = vis, constness = constness, @@ -1038,9 +1040,9 @@ fn render_attributes_in_pre<'a, 'b: 'a>( // When an attribute is rendered inside a <code> tag, it is formatted using // a div to produce a newline after it. -fn render_attributes_in_code(w: &mut Buffer, it: &clean::Item, tcx: TyCtxt<'_>) { - for a in it.attributes(tcx, false) { - write!(w, "<div class=\"code-attribute\">{}</div>", a); +fn render_attributes_in_code(w: &mut impl fmt::Write, it: &clean::Item, tcx: TyCtxt<'_>) { + for attr in it.attributes(tcx, false) { + write!(w, "<div class=\"code-attribute\">{attr}</div>").unwrap(); } } @@ -1282,6 +1284,11 @@ fn should_render_item(item: &clean::Item, deref_mut_: bool, tcx: TyCtxt<'_>) -> pub(crate) fn notable_traits_button(ty: &clean::Type, cx: &mut Context<'_>) -> Option<String> { let mut has_notable_trait = false; + if ty.is_unit() { + // Very common fast path. + return None; + } + let did = ty.def_id(cx.cache())?; // Box has pass-through impls for Read, Write, Iterator, and Future when the diff --git a/src/librustdoc/html/render/print_item.rs b/src/librustdoc/html/render/print_item.rs index 62027a3fa..383e3c170 100644 --- a/src/librustdoc/html/render/print_item.rs +++ b/src/librustdoc/html/render/print_item.rs @@ -9,7 +9,6 @@ use rustc_middle::middle::stability; use rustc_middle::ty::{self, TyCtxt}; use rustc_span::hygiene::MacroKind; use rustc_span::symbol::{kw, sym, Symbol}; -use std::borrow::Borrow; use std::cell::{RefCell, RefMut}; use std::cmp::Ordering; use std::fmt; @@ -40,6 +39,110 @@ use crate::html::{highlight, static_files}; use askama::Template; use itertools::Itertools; +/// Generates an Askama template struct for rendering items with common methods. +/// +/// Usage: +/// ```ignore (illustrative) +/// item_template!( +/// #[template(path = "<template.html>", /* additional values */)] +/// /* additional meta items */ +/// struct MyItem<'a, 'cx> { +/// cx: RefCell<&'a mut Context<'cx>>, +/// it: &'a clean::Item, +/// /* additional fields */ +/// }, +/// methods = [ /* method names (comma separated; refer to macro definition of `item_template_methods!()`) */ ] +/// ) +/// ``` +/// +/// NOTE: ensure that the generic lifetimes (`'a`, `'cx`) and +/// required fields (`cx`, `it`) are identical (in terms of order and definition). +macro_rules! item_template { + ( + $(#[$meta:meta])* + struct $name:ident<'a, 'cx> { + cx: RefCell<&'a mut Context<'cx>>, + it: &'a clean::Item, + $($field_name:ident: $field_ty:ty),*, + }, + methods = [$($methods:tt),* $(,)?] + ) => { + #[derive(Template)] + $(#[$meta])* + struct $name<'a, 'cx> { + cx: RefCell<&'a mut Context<'cx>>, + it: &'a clean::Item, + $($field_name: $field_ty),* + } + + impl<'a, 'cx: 'a> ItemTemplate<'a, 'cx> for $name<'a, 'cx> { + fn item_and_mut_cx(&self) -> (&'a clean::Item, RefMut<'_, &'a mut Context<'cx>>) { + (&self.it, self.cx.borrow_mut()) + } + } + + impl<'a, 'cx: 'a> $name<'a, 'cx> { + item_template_methods!($($methods)*); + } + }; +} + +/// Implement common methods for item template structs generated by `item_template!()`. +/// +/// NOTE: this macro is intended to be used only by `item_template!()`. +macro_rules! item_template_methods { + () => {}; + (document $($rest:tt)*) => { + fn document<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { + display_fn(move |f| { + let (item, mut cx) = self.item_and_mut_cx(); + let v = document(*cx, item, None, HeadingOffset::H2); + write!(f, "{v}") + }) + } + item_template_methods!($($rest)*); + }; + (document_type_layout $($rest:tt)*) => { + fn document_type_layout<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { + display_fn(move |f| { + let (item, cx) = self.item_and_mut_cx(); + let def_id = item.item_id.expect_def_id(); + let v = document_type_layout(*cx, def_id); + write!(f, "{v}") + }) + } + item_template_methods!($($rest)*); + }; + (render_attributes_in_pre $($rest:tt)*) => { + fn render_attributes_in_pre<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { + display_fn(move |f| { + let (item, cx) = self.item_and_mut_cx(); + let tcx = cx.tcx(); + let v = render_attributes_in_pre(item, "", tcx); + write!(f, "{v}") + }) + } + item_template_methods!($($rest)*); + }; + (render_assoc_items $($rest:tt)*) => { + fn render_assoc_items<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { + display_fn(move |f| { + let (item, mut cx) = self.item_and_mut_cx(); + let def_id = item.item_id.expect_def_id(); + let v = render_assoc_items(*cx, item, def_id, AssocItemRender::All); + write!(f, "{v}") + }) + } + item_template_methods!($($rest)*); + }; + ($method:ident $($rest:tt)*) => { + compile_error!(concat!("unknown method: ", stringify!($method))); + }; + ($token:tt $($rest:tt)*) => { + compile_error!(concat!("unexpected token: ", stringify!($token))); + }; +} + const ITEM_TABLE_OPEN: &str = "<ul class=\"item-table\">"; const ITEM_TABLE_CLOSE: &str = "</ul>"; const ITEM_TABLE_ROW_OPEN: &str = "<li>"; @@ -222,49 +325,6 @@ trait ItemTemplate<'a, 'cx: 'a>: askama::Template + fmt::Display { fn item_and_mut_cx(&self) -> (&'a clean::Item, RefMut<'_, &'a mut Context<'cx>>); } -fn item_template_document<'a: 'b, 'b, 'cx: 'a>( - templ: &'b impl ItemTemplate<'a, 'cx>, -) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { - display_fn(move |f| { - let (item, mut cx) = templ.item_and_mut_cx(); - let v = document(*cx, item, None, HeadingOffset::H2); - write!(f, "{v}") - }) -} - -fn item_template_document_type_layout<'a: 'b, 'b, 'cx: 'a>( - templ: &'b impl ItemTemplate<'a, 'cx>, -) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { - display_fn(move |f| { - let (item, cx) = templ.item_and_mut_cx(); - let def_id = item.item_id.expect_def_id(); - let v = document_type_layout(*cx, def_id); - write!(f, "{v}") - }) -} - -fn item_template_render_attributes_in_pre<'a: 'b, 'b, 'cx: 'a>( - templ: &'b impl ItemTemplate<'a, 'cx>, -) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { - display_fn(move |f| { - let (item, cx) = templ.item_and_mut_cx(); - let tcx = cx.tcx(); - let v = render_attributes_in_pre(item, "", tcx); - write!(f, "{v}") - }) -} - -fn item_template_render_assoc_items<'a: 'b, 'b, 'cx: 'a>( - templ: &'b impl ItemTemplate<'a, 'cx>, -) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { - display_fn(move |f| { - let (item, mut cx) = templ.item_and_mut_cx(); - let def_id = item.item_id.expect_def_id(); - let v = render_assoc_items(*cx, item, def_id, AssocItemRender::All); - write!(f, "{v}") - }) -} - fn item_module(w: &mut Buffer, cx: &mut Context<'_>, item: &clean::Item, items: &[clean::Item]) { write!(w, "{}", document(cx, item, None, HeadingOffset::H2)); @@ -587,8 +647,7 @@ fn item_function(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, f: &cle + name.as_str().len() + generics_len; - let notable_traits = - f.decl.output.as_return().and_then(|output| notable_traits_button(output, cx)); + let notable_traits = notable_traits_button(&f.decl.output, cx); wrap_item(w, |w| { w.reserve(header_len); @@ -1102,7 +1161,12 @@ fn item_trait(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: ); } -fn item_trait_alias(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean::TraitAlias) { +fn item_trait_alias( + w: &mut impl fmt::Write, + cx: &mut Context<'_>, + it: &clean::Item, + t: &clean::TraitAlias, +) { wrap_item(w, |w| { write!( w, @@ -1112,19 +1176,25 @@ fn item_trait_alias(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: & print_where_clause(&t.generics, cx, 0, Ending::Newline), bounds(&t.bounds, true, cx), attrs = render_attributes_in_pre(it, "", cx.tcx()), - ); + ) + .unwrap(); }); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); - + write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); // Render any items associated directly to this alias, as otherwise they // won't be visible anywhere in the docs. It would be nice to also show // associated items from the aliased type (see discussion in #32077), but // we need #14072 to make sense of the generics. write!(w, "{}", render_assoc_items(cx, it, it.item_id.expect_def_id(), AssocItemRender::All)) + .unwrap(); } -fn item_opaque_ty(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean::OpaqueTy) { +fn item_opaque_ty( + w: &mut impl fmt::Write, + cx: &mut Context<'_>, + it: &clean::Item, + t: &clean::OpaqueTy, +) { wrap_item(w, |w| { write!( w, @@ -1134,16 +1204,18 @@ fn item_opaque_ty(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &cl where_clause = print_where_clause(&t.generics, cx, 0, Ending::Newline), bounds = bounds(&t.bounds, false, cx), attrs = render_attributes_in_pre(it, "", cx.tcx()), - ); + ) + .unwrap(); }); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); + write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); // Render any items associated directly to this alias, as otherwise they // won't be visible anywhere in the docs. It would be nice to also show // associated items from the aliased type (see discussion in #32077), but // we need #14072 to make sense of the generics. write!(w, "{}", render_assoc_items(cx, it, it.item_id.expect_def_id(), AssocItemRender::All)) + .unwrap(); } fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean::Typedef) { @@ -1176,19 +1248,15 @@ fn item_typedef(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clea } fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean::Union) { - #[derive(Template)] - #[template(path = "item_union.html")] - struct ItemUnion<'a, 'cx> { - cx: RefCell<&'a mut Context<'cx>>, - it: &'a clean::Item, - s: &'a clean::Union, - } - - impl<'a, 'cx: 'a> ItemTemplate<'a, 'cx> for ItemUnion<'a, 'cx> { - fn item_and_mut_cx(&self) -> (&'a clean::Item, RefMut<'_, &'a mut Context<'cx>>) { - (self.it, self.cx.borrow_mut()) - } - } + item_template!( + #[template(path = "item_union.html")] + struct ItemUnion<'a, 'cx> { + cx: RefCell<&'a mut Context<'cx>>, + it: &'a clean::Item, + s: &'a clean::Union, + }, + methods = [document, document_type_layout, render_attributes_in_pre, render_assoc_items] + ); impl<'a, 'cx: 'a> ItemUnion<'a, 'cx> { fn render_union<'b>(&'b self) -> impl fmt::Display + Captures<'a> + 'b + Captures<'cx> { @@ -1198,6 +1266,7 @@ fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean: write!(f, "{v}") }) } + fn document_field<'b>( &'b self, field: &'a clean::Item, @@ -1208,10 +1277,12 @@ fn item_union(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean: write!(f, "{v}") }) } + fn stability_field(&self, field: &clean::Item) -> Option<String> { let cx = self.cx.borrow(); field.stability_class(cx.tcx()) } + fn print_ty<'b>( &'b self, ty: &'a clean::Type, @@ -1420,37 +1491,41 @@ fn item_macro(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, t: &clean: write!(w, "{}", document(cx, it, None, HeadingOffset::H2)) } -fn item_proc_macro(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, m: &clean::ProcMacro) { - wrap_item(w, |w| { +fn item_proc_macro( + w: &mut impl fmt::Write, + cx: &mut Context<'_>, + it: &clean::Item, + m: &clean::ProcMacro, +) { + wrap_item(w, |buffer| { let name = it.name.expect("proc-macros always have names"); match m.kind { MacroKind::Bang => { - write!(w, "{}!() {{ /* proc-macro */ }}", name); + write!(buffer, "{name}!() {{ /* proc-macro */ }}").unwrap(); } MacroKind::Attr => { - write!(w, "#[{}]", name); + write!(buffer, "#[{name}]").unwrap(); } MacroKind::Derive => { - write!(w, "#[derive({})]", name); + write!(buffer, "#[derive({name})]").unwrap(); if !m.helpers.is_empty() { - w.push_str("\n{\n"); - w.push_str(" // Attributes available to this derive:\n"); + buffer.write_str("\n{\n // Attributes available to this derive:\n").unwrap(); for attr in &m.helpers { - writeln!(w, " #[{}]", attr); + writeln!(buffer, " #[{attr}]").unwrap(); } - w.push_str("}\n"); + buffer.write_str("}\n").unwrap(); } } } }); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)) + write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); } -fn item_primitive(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item) { +fn item_primitive(w: &mut impl fmt::Write, cx: &mut Context<'_>, it: &clean::Item) { let def_id = it.item_id.expect_def_id(); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); + write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); if it.name.map(|n| n.as_str() != "reference").unwrap_or(false) { - write!(w, "{}", render_assoc_items(cx, it, def_id, AssocItemRender::All)); + write!(w, "{}", render_assoc_items(cx, it, def_id, AssocItemRender::All)).unwrap(); } else { // We handle the "reference" primitive type on its own because we only want to list // implementations on generic types. @@ -1560,8 +1635,7 @@ fn item_struct(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item, s: &clean } fn item_static(w: &mut impl fmt::Write, cx: &mut Context<'_>, it: &clean::Item, s: &clean::Static) { - let mut buffer = Buffer::new(); - wrap_item(&mut buffer, |buffer| { + wrap_item(w, |buffer| { render_attributes_in_code(buffer, it, cx.tcx()); write!( buffer, @@ -1570,29 +1644,29 @@ fn item_static(w: &mut impl fmt::Write, cx: &mut Context<'_>, it: &clean::Item, mutability = s.mutability.print_with_space(), name = it.name.unwrap(), typ = s.type_.print(cx) - ); + ) + .unwrap(); }); - write!(w, "{}", buffer.into_inner()).unwrap(); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); } -fn item_foreign_type(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item) { - wrap_item(w, |w| { - w.write_str("extern {\n"); - render_attributes_in_code(w, it, cx.tcx()); +fn item_foreign_type(w: &mut impl fmt::Write, cx: &mut Context<'_>, it: &clean::Item) { + wrap_item(w, |buffer| { + buffer.write_str("extern {\n").unwrap(); + render_attributes_in_code(buffer, it, cx.tcx()); write!( - w, + buffer, " {}type {};\n}}", visibility_print_with_space(it.visibility(cx.tcx()), it.item_id, cx), it.name.unwrap(), - ); + ) + .unwrap(); }); - write!(w, "{}", document(cx, it, None, HeadingOffset::H2)); - + write!(w, "{}", document(cx, it, None, HeadingOffset::H2)).unwrap(); write!(w, "{}", render_assoc_items(cx, it, it.item_id.expect_def_id(), AssocItemRender::All)) + .unwrap(); } fn item_keyword(w: &mut Buffer, cx: &mut Context<'_>, it: &clean::Item) { @@ -1666,13 +1740,14 @@ fn bounds(t_bounds: &[clean::GenericBound], trait_alias: bool, cx: &Context<'_>) bounds } -fn wrap_item<F>(w: &mut Buffer, f: F) +fn wrap_item<W, F>(w: &mut W, f: F) where - F: FnOnce(&mut Buffer), + W: fmt::Write, + F: FnOnce(&mut W), { - w.write_str(r#"<pre class="rust item-decl"><code>"#); + write!(w, r#"<pre class="rust item-decl"><code>"#).unwrap(); f(w); - w.write_str("</code></pre>"); + write!(w, "</code></pre>").unwrap(); } #[derive(PartialEq, Eq)] diff --git a/src/librustdoc/html/render/search_index.rs b/src/librustdoc/html/render/search_index.rs index 846299f02..f34be120d 100644 --- a/src/librustdoc/html/render/search_index.rs +++ b/src/librustdoc/html/render/search_index.rs @@ -7,7 +7,7 @@ use rustc_span::symbol::Symbol; use serde::ser::{Serialize, SerializeStruct, Serializer}; use crate::clean; -use crate::clean::types::{FnRetTy, Function, Generics, ItemId, Type, WherePredicate}; +use crate::clean::types::{Function, Generics, ItemId, Type, WherePredicate}; use crate::formats::cache::{Cache, OrphanImplItem}; use crate::formats::item_type::ItemType; use crate::html::format::join_with_double_colon; @@ -656,22 +656,9 @@ fn get_fn_inputs_and_outputs<'tcx>( } let mut ret_types = Vec::new(); - match decl.output { - FnRetTy::Return(ref return_type) => { - add_generics_and_bounds_as_types( - self_, - generics, - return_type, - tcx, - 0, - &mut ret_types, - cache, - ); - if ret_types.is_empty() { - ret_types.push(get_index_type(return_type, vec![])); - } - } - _ => {} - }; + add_generics_and_bounds_as_types(self_, generics, &decl.output, tcx, 0, &mut ret_types, cache); + if ret_types.is_empty() { + ret_types.push(get_index_type(&decl.output, vec![])); + } (all_types, ret_types) } diff --git a/src/librustdoc/html/render/type_layout.rs b/src/librustdoc/html/render/type_layout.rs index c9b95b1e6..0bc32ea5a 100644 --- a/src/librustdoc/html/render/type_layout.rs +++ b/src/librustdoc/html/render/type_layout.rs @@ -17,7 +17,7 @@ use crate::html::render::Context; #[template(path = "type_layout.html")] struct TypeLayout<'cx> { variants: Vec<(Symbol, TypeLayoutSize)>, - type_layout_size: Result<TypeLayoutSize, LayoutError<'cx>>, + type_layout_size: Result<TypeLayoutSize, &'cx LayoutError<'cx>>, } #[derive(Template)] diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index a7d5f4977..b7f455259 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -8,6 +8,7 @@ :root { --nav-sub-mobile-padding: 8px; + --search-typename-width: 6.75rem; } /* See FiraSans-LICENSE.txt for the Fira Sans license. */ @@ -213,7 +214,7 @@ a.anchor, h1 a, .search-results a, .stab, -.result-name .primitive > i, .result-name .keyword > i { +.result-name i { color: var(--main-color); } @@ -869,14 +870,11 @@ so that we can apply CSS-filters to change the arrow color in themes */ gap: 1em; } -.search-results > a > div { - flex: 1; -} - .search-results > a > div.desc { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; + flex: 2; } .search-results a:hover, @@ -884,12 +882,28 @@ so that we can apply CSS-filters to change the arrow color in themes */ background-color: var(--search-result-link-focus-background-color); } +.search-results .result-name { + display: flex; + align-items: center; + justify-content: start; + flex: 3; +} .search-results .result-name span.alias { color: var(--search-results-alias-color); } -.search-results .result-name span.grey { +.search-results .result-name .grey { color: var(--search-results-grey-color); } +.search-results .result-name .typename { + color: var(--search-results-grey-color); + font-size: 0.875rem; + width: var(--search-typename-width); +} +.search-results .result-name .path { + word-break: break-all; + max-width: calc(100% - var(--search-typename-width)); + display: inline-block; +} .popover { position: absolute; @@ -957,6 +971,8 @@ so that we can apply CSS-filters to change the arrow color in themes */ display: flex; padding: 3px; margin-bottom: 5px; + align-items: center; + vertical-align: text-bottom; } .item-name .stab { margin-left: 0.3125em; @@ -968,11 +984,9 @@ so that we can apply CSS-filters to change the arrow color in themes */ color: var(--main-color); background-color: var(--stab-background-color); width: fit-content; - align-items: center; white-space: pre-wrap; border-radius: 3px; - display: inline-flex; - vertical-align: text-bottom; + display: inline; } .stab.portability > code { @@ -1179,6 +1193,10 @@ a.test-arrow:hover { position: relative; } +.code-header a.tooltip:hover { + color: var(--link-color); +} + /* placeholder thunk so that the mouse can easily travel from "(i)" to popover the resulting "hover tunnel" is a stepped triangle, approximating https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown */ @@ -1191,6 +1209,14 @@ a.tooltip:hover::after { content: "\00a0"; } +/* This animation is layered onto the mistake-proofing delay for dismissing + a hovered tooltip, to ensure it feels responsive even with the delay. + */ +.fade-out { + opacity: 0; + transition: opacity 0.45s cubic-bezier(0, 0, 0.1, 1.0); +} + .popover.tooltip .content { margin: 0.25em 0.5em; } @@ -1712,6 +1738,16 @@ in source-script.js .search-results > a > div.desc, .item-table > li > div.desc { padding-left: 2em; } + .search-results .result-name { + display: block; + } + .search-results .result-name .typename { + width: initial; + margin-right: 0; + } + .search-results .result-name .typename, .search-results .result-name .path { + display: inline; + } .source-sidebar-expanded .source .sidebar { max-width: 100vw; diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index 8b931f74e..f697abd07 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -53,7 +53,7 @@ let ParsedQuery; * parent: (Object|null|undefined), * path: string, * ty: (Number|null|number), - * type: (Array<?>|null) + * type: FunctionSearchType? * }} */ let Row; @@ -135,7 +135,7 @@ let RawFunctionType; /** * @typedef {{ * inputs: Array<FunctionType>, - * outputs: Array<FunctionType>, + * output: Array<FunctionType>, * }} */ let FunctionSearchType; diff --git a/src/librustdoc/html/static/js/main.js b/src/librustdoc/html/static/js/main.js index bccf675c1..254b0d8bf 100644 --- a/src/librustdoc/html/static/js/main.js +++ b/src/librustdoc/html/static/js/main.js @@ -4,6 +4,13 @@ "use strict"; +// The amount of time that the cursor must remain still over a hover target before +// revealing a tooltip. +// +// https://www.nngroup.com/articles/timing-exposing-content/ +window.RUSTDOC_TOOLTIP_HOVER_MS = 300; +window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS = 450; + // Given a basename (e.g. "storage") and an extension (e.g. ".js"), return a URL // for a resource under the root-path, with the resource-suffix. function resourcePath(basename, extension) { @@ -270,14 +277,18 @@ function preLoadCss(cssUrl) { searchState.mouseMovedAfterSearch = false; document.title = searchState.title; }, - hideResults: () => { - switchDisplayedElement(null); + removeQueryParameters: () => { + // We change the document title. document.title = searchState.titleBeforeSearch; - // We also remove the query parameter from the URL. if (browserSupportsHistoryApi()) { history.replaceState(null, "", getNakedUrl() + window.location.hash); } }, + hideResults: () => { + switchDisplayedElement(null); + // We also remove the query parameter from the URL. + searchState.removeQueryParameters(); + }, getQueryStringParams: () => { const params = {}; window.location.search.substring(1).split("&"). @@ -772,6 +783,13 @@ function preLoadCss(cssUrl) { }); }); + /** + * Show a tooltip immediately. + * + * @param {DOMElement} e - The tooltip's anchor point. The DOM is consulted to figure + * out what the tooltip should contain, and where it should be + * positioned. + */ function showTooltip(e) { const notable_ty = e.getAttribute("data-notable-ty"); if (!window.NOTABLE_TRAITS && notable_ty) { @@ -782,8 +800,10 @@ function preLoadCss(cssUrl) { throw new Error("showTooltip() called with notable without any notable traits!"); } } + // Make this function idempotent. If the tooltip is already shown, avoid doing extra work + // and leave it alone. if (window.CURRENT_TOOLTIP_ELEMENT && window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE === e) { - // Make this function idempotent. + clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); return; } window.hideAllModals(false); @@ -791,11 +811,18 @@ function preLoadCss(cssUrl) { if (notable_ty) { wrapper.innerHTML = "<div class=\"content\">" + window.NOTABLE_TRAITS[notable_ty] + "</div>"; - } else if (e.getAttribute("title") !== undefined) { - const titleContent = document.createElement("div"); - titleContent.className = "content"; - titleContent.appendChild(document.createTextNode(e.getAttribute("title"))); - wrapper.appendChild(titleContent); + } else { + // Replace any `title` attribute with `data-title` to avoid double tooltips. + if (e.getAttribute("title") !== null) { + e.setAttribute("data-title", e.getAttribute("title")); + e.removeAttribute("title"); + } + if (e.getAttribute("data-title") !== null) { + const titleContent = document.createElement("div"); + titleContent.className = "content"; + titleContent.appendChild(document.createTextNode(e.getAttribute("data-title"))); + wrapper.appendChild(titleContent); + } } wrapper.className = "tooltip popover"; const focusCatcher = document.createElement("div"); @@ -824,17 +851,77 @@ function preLoadCss(cssUrl) { wrapper.style.visibility = ""; window.CURRENT_TOOLTIP_ELEMENT = wrapper; window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE = e; + clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); + wrapper.onpointerenter = function(ev) { + // If this is a synthetic touch event, ignore it. A click event will be along shortly. + if (ev.pointerType !== "mouse") { + return; + } + clearTooltipHoverTimeout(e); + }; wrapper.onpointerleave = function(ev) { // If this is a synthetic touch event, ignore it. A click event will be along shortly. if (ev.pointerType !== "mouse") { return; } - if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(event.relatedTarget, e)) { - hideTooltip(true); + if (!e.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(ev.relatedTarget, e)) { + // See "Tooltip pointer leave gesture" below. + setTooltipHoverTimeout(e, false); + addClass(wrapper, "fade-out"); } }; } + /** + * Show or hide the tooltip after a timeout. If a timeout was already set before this function + * was called, that timeout gets cleared. If the tooltip is already in the requested state, + * this function will still clear any pending timeout, but otherwise do nothing. + * + * @param {DOMElement} element - The tooltip's anchor point. The DOM is consulted to figure + * out what the tooltip should contain, and where it should be + * positioned. + * @param {boolean} show - If true, the tooltip will be made visible. If false, it will + * be hidden. + */ + function setTooltipHoverTimeout(element, show) { + clearTooltipHoverTimeout(element); + if (!show && !window.CURRENT_TOOLTIP_ELEMENT) { + // To "hide" an already hidden element, just cancel its timeout. + return; + } + if (show && window.CURRENT_TOOLTIP_ELEMENT) { + // To "show" an already visible element, just cancel its timeout. + return; + } + if (window.CURRENT_TOOLTIP_ELEMENT && + window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE !== element) { + // Don't do anything if another tooltip is already visible. + return; + } + element.TOOLTIP_HOVER_TIMEOUT = setTimeout(() => { + if (show) { + showTooltip(element); + } else if (!element.TOOLTIP_FORCE_VISIBLE) { + hideTooltip(false); + } + }, show ? window.RUSTDOC_TOOLTIP_HOVER_MS : window.RUSTDOC_TOOLTIP_HOVER_EXIT_MS); + } + + /** + * If a show/hide timeout was set by `setTooltipHoverTimeout`, cancel it. If none exists, + * do nothing. + * + * @param {DOMElement} element - The tooltip's anchor point, + * as passed to `setTooltipHoverTimeout`. + */ + function clearTooltipHoverTimeout(element) { + if (element.TOOLTIP_HOVER_TIMEOUT !== undefined) { + removeClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); + clearTimeout(element.TOOLTIP_HOVER_TIMEOUT); + delete element.TOOLTIP_HOVER_TIMEOUT; + } + } + function tooltipBlurHandler(event) { if (window.CURRENT_TOOLTIP_ELEMENT && !elemIsInParent(document.activeElement, window.CURRENT_TOOLTIP_ELEMENT) && @@ -854,6 +941,12 @@ function preLoadCss(cssUrl) { } } + /** + * Hide the current tooltip immediately. + * + * @param {boolean} focus - If set to `true`, move keyboard focus to the tooltip anchor point. + * If set to `false`, leave keyboard focus alone. + */ function hideTooltip(focus) { if (window.CURRENT_TOOLTIP_ELEMENT) { if (window.CURRENT_TOOLTIP_ELEMENT.TOOLTIP_BASE.TOOLTIP_FORCE_VISIBLE) { @@ -864,6 +957,7 @@ function preLoadCss(cssUrl) { } const body = document.getElementsByTagName("body")[0]; body.removeChild(window.CURRENT_TOOLTIP_ELEMENT); + clearTooltipHoverTimeout(window.CURRENT_TOOLTIP_ELEMENT); window.CURRENT_TOOLTIP_ELEMENT = null; } } @@ -886,7 +980,14 @@ function preLoadCss(cssUrl) { if (ev.pointerType !== "mouse") { return; } - showTooltip(this); + setTooltipHoverTimeout(this, true); + }; + e.onpointermove = function(ev) { + // If this is a synthetic touch event, ignore it. A click event will be along shortly. + if (ev.pointerType !== "mouse") { + return; + } + setTooltipHoverTimeout(this, true); }; e.onpointerleave = function(ev) { // If this is a synthetic touch event, ignore it. A click event will be along shortly. @@ -895,7 +996,38 @@ function preLoadCss(cssUrl) { } if (!this.TOOLTIP_FORCE_VISIBLE && !elemIsInParent(ev.relatedTarget, window.CURRENT_TOOLTIP_ELEMENT)) { - hideTooltip(true); + // Tooltip pointer leave gesture: + // + // Designing a good hover microinteraction is a matter of guessing user + // intent from what are, literally, vague gestures. In this case, guessing if + // hovering in or out of the tooltip base is intentional or not. + // + // To figure this out, a few different techniques are used: + // + // * When the mouse pointer enters a tooltip anchor point, its hitbox is grown + // on the bottom, where the popover is/will appear. Search "hover tunnel" in + // rustdoc.css for the implementation. + // * There's a delay when the mouse pointer enters the popover base anchor, in + // case the mouse pointer was just passing through and the user didn't want + // to open it. + // * Similarly, a delay is added when exiting the anchor, or the popover + // itself, before hiding it. + // * A fade-out animation is layered onto the pointer exit delay to immediately + // inform the user that they successfully dismissed the popover, while still + // providing a way for them to cancel it if it was a mistake and they still + // wanted to interact with it. + // * No animation is used for revealing it, because we don't want people to try + // to interact with an element while it's in the middle of fading in: either + // they're allowed to interact with it while it's fading in, meaning it can't + // serve as mistake-proofing for the popover, or they can't, but + // they might try and be frustrated. + // + // See also: + // * https://www.nngroup.com/articles/timing-exposing-content/ + // * https://www.nngroup.com/articles/tooltip-guidelines/ + // * https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown + setTooltipHoverTimeout(e, false); + addClass(window.CURRENT_TOOLTIP_ELEMENT, "fade-out"); } }; }); @@ -918,9 +1050,10 @@ function preLoadCss(cssUrl) { function buildHelpMenu() { const book_info = document.createElement("span"); + const channel = getVar("channel"); book_info.className = "top"; - book_info.innerHTML = "You can find more information in \ - <a href=\"https://doc.rust-lang.org/rustdoc/\">the rustdoc book</a>."; + book_info.innerHTML = `You can find more information in \ +<a href="https://doc.rust-lang.org/${channel}/rustdoc/">the rustdoc book</a>.`; const shortcuts = [ ["?", "Show this help dialog"], @@ -940,6 +1073,9 @@ function preLoadCss(cssUrl) { div_shortcuts.innerHTML = "<h2>Keyboard Shortcuts</h2><dl>" + shortcuts + "</dl></div>"; const infos = [ + `For a full list of all search features, take a look <a \ +href="https://doc.rust-lang.org/${channel}/rustdoc/how-to-read-rustdoc.html\ +#the-search-interface">here</a>.`, "Prefix searches with a type followed by a colon (e.g., <code>fn:</code>) to \ restrict the search to a given item kind.", "Accepted kinds are: <code>fn</code>, <code>mod</code>, <code>struct</code>, \ @@ -949,6 +1085,10 @@ function preLoadCss(cssUrl) { <code>-> vec</code> or <code>String, enum:Cow -> bool</code>)", "You can look for items with an exact name by putting double quotes around \ your request: <code>\"string\"</code>", + "Look for functions that accept or return \ + <a href=\"https://doc.rust-lang.org/std/primitive.slice.html\">slices</a> and \ + <a href=\"https://doc.rust-lang.org/std/primitive.array.html\">arrays</a> by writing \ + square brackets (e.g., <code>-> [u8]</code> or <code>[] -> Option</code>)", "Look for items inside another one by searching for a path: <code>vec::Vec</code>", ].map(x => "<p>" + x + "</p>").join(""); const div_infos = document.createElement("div"); diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 62afe40bb..51d8e81ca 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -35,6 +35,35 @@ const itemTypes = [ "traitalias", ]; +const longItemTypes = [ + "module", + "extern crate", + "re-export", + "struct", + "enum", + "function", + "type alias", + "static", + "trait", + "", + "trait method", + "method", + "struct field", + "enum variant", + "macro", + "primitive type", + "assoc type", + "constant", + "assoc const", + "union", + "foreign type", + "keyword", + "existential type", + "attribute macro", + "derive macro", + "trait alias", +]; + // used for special search precedence const TY_PRIMITIVE = itemTypes.indexOf("primitive"); const TY_KEYWORD = itemTypes.indexOf("keyword"); @@ -208,6 +237,46 @@ function initSearch(rawSearchIndex) { let typeNameIdMap; const ALIASES = new Map(); + /** + * Special type name IDs for searching by array. + */ + let typeNameIdOfArray; + /** + * Special type name IDs for searching by slice. + */ + let typeNameIdOfSlice; + /** + * Special type name IDs for searching by both array and slice (`[]` syntax). + */ + let typeNameIdOfArrayOrSlice; + + /** + * Add an item to the type Name->ID map, or, if one already exists, use it. + * Returns the number. If name is "" or null, return -1 (pure generic). + * + * This is effectively string interning, so that function matching can be + * done more quickly. Two types with the same name but different item kinds + * get the same ID. + * + * @param {string} name + * + * @returns {integer} + */ + function buildTypeMapIndex(name) { + + if (name === "" || name === null) { + return -1; + } + + if (typeNameIdMap.has(name)) { + return typeNameIdMap.get(name); + } else { + const id = typeNameIdMap.size; + typeNameIdMap.set(name, id); + return id; + } + } + function isWhitespace(c) { return " \t\n\r".indexOf(c) !== -1; } @@ -217,11 +286,11 @@ function initSearch(rawSearchIndex) { } function isEndCharacter(c) { - return ",>-".indexOf(c) !== -1; + return ",>-]".indexOf(c) !== -1; } function isStopCharacter(c) { - return isWhitespace(c) || isEndCharacter(c); + return isEndCharacter(c); } function isErrorCharacter(c) { @@ -317,18 +386,69 @@ function initSearch(rawSearchIndex) { * @return {boolean} */ function isSeparatorCharacter(c) { - return c === "," || isWhitespaceCharacter(c); + return c === ","; } - /** - * Returns `true` if the given `c` character is a whitespace. +/** + * Returns `true` if the given `c` character is a path separator. For example + * `:` in `a::b` or a whitespace in `a b`. * * @param {string} c * * @return {boolean} */ - function isWhitespaceCharacter(c) { - return c === " " || c === "\t"; + function isPathSeparator(c) { + return c === ":" || isWhitespace(c); + } + + /** + * Returns `true` if the previous character is `lookingFor`. + * + * @param {ParserState} parserState + * @param {String} lookingFor + * + * @return {boolean} + */ + function prevIs(parserState, lookingFor) { + let pos = parserState.pos; + while (pos > 0) { + const c = parserState.userQuery[pos - 1]; + if (c === lookingFor) { + return true; + } else if (!isWhitespace(c)) { + break; + } + pos -= 1; + } + return false; + } + + /** + * Returns `true` if the last element in the `elems` argument has generics. + * + * @param {Array<QueryElement>} elems + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isLastElemGeneric(elems, parserState) { + return (elems.length > 0 && elems[elems.length - 1].generics.length > 0) || + prevIs(parserState, ">"); + } + + /** + * Increase current parser position until it doesn't find a whitespace anymore. + * + * @param {ParserState} parserState + */ + function skipWhitespace(parserState) { + while (parserState.pos < parserState.userQuery.length) { + const c = parserState.userQuery[parserState.pos]; + if (!isWhitespace(c)) { + break; + } + parserState.pos += 1; + } } /** @@ -340,39 +460,78 @@ function initSearch(rawSearchIndex) { * @return {QueryElement} - The newly created `QueryElement`. */ function createQueryElement(query, parserState, name, generics, isInGenerics) { - if (name === "*" || (name.length === 0 && generics.length === 0)) { - return; + const path = name.trim(); + if (path.length === 0 && generics.length === 0) { + throw ["Unexpected ", parserState.userQuery[parserState.pos]]; + } else if (path === "*") { + throw ["Unexpected ", "*"]; } if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { - throw ["You cannot have more than one element if you use quotes"]; - } - const pathSegments = name.split("::"); - if (pathSegments.length > 1) { - for (let i = 0, len = pathSegments.length; i < len; ++i) { - const pathSegment = pathSegments[i]; - - if (pathSegment.length === 0) { - if (i === 0) { - throw ["Paths cannot start with ", "::"]; - } else if (i + 1 === len) { - throw ["Paths cannot end with ", "::"]; - } - throw ["Unexpected ", "::::"]; - } + throw ["Cannot have more than one element if you use quotes"]; + } + const typeFilter = parserState.typeFilter; + parserState.typeFilter = null; + if (name === "!") { + if (typeFilter !== null && typeFilter !== "primitive") { + throw [ + "Invalid search type: primitive never type ", + "!", + " and ", + typeFilter, + " both specified", + ]; } + if (generics.length !== 0) { + throw [ + "Never type ", + "!", + " does not accept generic parameters", + ]; + } + return { + name: "never", + id: -1, + fullPath: ["never"], + pathWithoutLast: [], + pathLast: "never", + generics: [], + typeFilter: "primitive", + }; } + if (path.startsWith("::")) { + throw ["Paths cannot start with ", "::"]; + } else if (path.endsWith("::")) { + throw ["Paths cannot end with ", "::"]; + } else if (path.includes("::::")) { + throw ["Unexpected ", "::::"]; + } else if (path.includes(" ::")) { + throw ["Unexpected ", " ::"]; + } else if (path.includes(":: ")) { + throw ["Unexpected ", ":: "]; + } + const pathSegments = path.split(/::|\s+/); // In case we only have something like `<p>`, there is no name. if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { - throw ["Found generics without a path"]; + if (generics.length > 0 || prevIs(parserState, ">")) { + throw ["Found generics without a path"]; + } else { + throw ["Unexpected ", parserState.userQuery[parserState.pos]]; + } + } + for (const [i, pathSegment] of pathSegments.entries()) { + if (pathSegment === "!") { + if (i !== 0) { + throw ["Never type ", "!", " is not associated item"]; + } + pathSegments[i] = "never"; + } } parserState.totalElems += 1; if (isInGenerics) { parserState.genericsElems += 1; } - const typeFilter = parserState.typeFilter; - parserState.typeFilter = null; return { - name: name, + name: name.trim(), id: -1, fullPath: pathSegments, pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), @@ -408,28 +567,40 @@ function initSearch(rawSearchIndex) { foundExclamation = parserState.pos; } else if (isErrorCharacter(c)) { throw ["Unexpected ", c]; - } else if ( - isStopCharacter(c) || - isSpecialStartCharacter(c) || - isSeparatorCharacter(c) - ) { - break; - } else if (c === ":") { // If we allow paths ("str::string" for example). - if (!isPathStart(parserState)) { - break; + } else if (isPathSeparator(c)) { + if (c === ":") { + if (!isPathStart(parserState)) { + break; + } + // Skip current ":". + parserState.pos += 1; + } else { + while (parserState.pos + 1 < parserState.length) { + const next_c = parserState.userQuery[parserState.pos + 1]; + if (!isWhitespace(next_c)) { + break; + } + parserState.pos += 1; + } } if (foundExclamation !== -1) { - if (start <= (end - 2)) { + if (foundExclamation !== start && + isIdentCharacter(parserState.userQuery[foundExclamation - 1]) + ) { throw ["Cannot have associated items in macros"]; } else { - // if start == end - 1, we got the never type // while the never type has no associated macros, we still // can parse a path like that foundExclamation = -1; } } - // Skip current ":". - parserState.pos += 1; + } else if ( + c === "[" || + isStopCharacter(c) || + isSpecialStartCharacter(c) || + isSeparatorCharacter(c) + ) { + break; } else { throw ["Unexpected ", c]; } @@ -438,7 +609,10 @@ function initSearch(rawSearchIndex) { end = parserState.pos; } // if start == end - 1, we got the never type - if (foundExclamation !== -1 && start <= (end - 2)) { + if (foundExclamation !== -1 && + foundExclamation !== start && + isIdentCharacter(parserState.userQuery[foundExclamation - 1]) + ) { if (parserState.typeFilter === null) { parserState.typeFilter = "macro"; } else if (parserState.typeFilter !== "macro") { @@ -464,37 +638,71 @@ function initSearch(rawSearchIndex) { function getNextElem(query, parserState, elems, isInGenerics) { const generics = []; + skipWhitespace(parserState); let start = parserState.pos; let end; - // We handle the strings on their own mostly to make code easier to follow. - if (parserState.userQuery[parserState.pos] === "\"") { - start += 1; - getStringElem(query, parserState, isInGenerics); - end = parserState.pos - 1; + if (parserState.userQuery[parserState.pos] === "[") { + parserState.pos += 1; + getItemsBefore(query, parserState, generics, "]"); + const typeFilter = parserState.typeFilter; + if (typeFilter !== null && typeFilter !== "primitive") { + throw [ + "Invalid search type: primitive ", + "[]", + " and ", + typeFilter, + " both specified", + ]; + } + parserState.typeFilter = null; + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + elems.push({ + name: "[]", + id: -1, + fullPath: ["[]"], + pathWithoutLast: [], + pathLast: "[]", + generics, + typeFilter: "primitive", + }); } else { - end = getIdentEndPosition(parserState); - } - if (parserState.pos < parserState.length && - parserState.userQuery[parserState.pos] === "<" - ) { - if (start >= end) { - throw ["Found generics without a path"]; + const isStringElem = parserState.userQuery[start] === "\""; + // We handle the strings on their own mostly to make code easier to follow. + if (isStringElem) { + start += 1; + getStringElem(query, parserState, isInGenerics); + end = parserState.pos - 1; + } else { + end = getIdentEndPosition(parserState); } - parserState.pos += 1; - getItemsBefore(query, parserState, generics, ">"); - } - if (start >= end && generics.length === 0) { - return; + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<" + ) { + if (start >= end) { + throw ["Found generics without a path"]; + } + parserState.pos += 1; + getItemsBefore(query, parserState, generics, ">"); + } + if (isStringElem) { + skipWhitespace(parserState); + } + if (start >= end && generics.length === 0) { + return; + } + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics + ) + ); } - elems.push( - createQueryElement( - query, - parserState, - parserState.userQuery.slice(start, end), - generics, - isInGenerics - ) - ); } /** @@ -518,6 +726,17 @@ function initSearch(rawSearchIndex) { const oldTypeFilter = parserState.typeFilter; parserState.typeFilter = null; + let extra = ""; + if (endChar === ">") { + extra = "<"; + } else if (endChar === "]") { + extra = "["; + } else if (endChar === "") { + extra = "->"; + } else { + extra = endChar; + } + while (parserState.pos < parserState.length) { const c = parserState.userQuery[parserState.pos]; if (c === endChar) { @@ -535,7 +754,7 @@ function initSearch(rawSearchIndex) { if (elems.length === 0) { throw ["Expected type filter before ", ":"]; } else if (query.literalSearch) { - throw ["You cannot use quotes on type filter"]; + throw ["Cannot use quotes on type filter"]; } // The type filter doesn't count as an element since it's a modifier. const typeFilterElem = elems.pop(); @@ -547,43 +766,39 @@ function initSearch(rawSearchIndex) { foundStopChar = true; continue; } else if (isEndCharacter(c)) { - let extra = ""; - if (endChar === ">") { - extra = "<"; - } else if (endChar === "") { - extra = "->"; - } else { - extra = endChar; - } throw ["Unexpected ", c, " after ", extra]; } if (!foundStopChar) { + let extra = []; + if (isLastElemGeneric(query.elems, parserState)) { + extra = [" after ", ">"]; + } else if (prevIs(parserState, "\"")) { + throw ["Cannot have more than one element if you use quotes"]; + } if (endChar !== "") { throw [ "Expected ", - ",", // comma - ", ", - " ", // whitespace + ",", " or ", endChar, + ...extra, ", found ", c, ]; } throw [ "Expected ", - ",", // comma - " or ", - " ", // whitespace + ",", + ...extra, ", found ", c, ]; } const posBefore = parserState.pos; start = parserState.pos; - getNextElem(query, parserState, elems, endChar === ">"); + getNextElem(query, parserState, elems, endChar !== ""); if (endChar !== "" && parserState.pos >= parserState.length) { - throw ["Unclosed ", "<"]; + throw ["Unclosed ", extra]; } // This case can be encountered if `getNextElem` encountered a "stop character" right // from the start. For example if you have `,,` or `<>`. In this case, we simply move up @@ -594,7 +809,7 @@ function initSearch(rawSearchIndex) { foundStopChar = false; } if (parserState.pos >= parserState.length && endChar !== "") { - throw ["Unclosed ", "<"]; + throw ["Unclosed ", extra]; } // We are either at the end of the string or on the `endChar` character, let's move forward // in any case. @@ -610,11 +825,17 @@ function initSearch(rawSearchIndex) { * @param {ParserState} parserState */ function checkExtraTypeFilterCharacters(start, parserState) { - const query = parserState.userQuery; + const query = parserState.userQuery.slice(start, parserState.pos).trim(); - for (let pos = start; pos < parserState.pos; ++pos) { - if (!isIdentCharacter(query[pos]) && !isWhitespaceCharacter(query[pos])) { - throw ["Unexpected ", query[pos], " in type filter"]; + for (const c in query) { + if (!isIdentCharacter(query[c])) { + throw [ + "Unexpected ", + query[c], + " in type filter (before ", + ":", + ")", + ]; } } } @@ -646,12 +867,17 @@ function initSearch(rawSearchIndex) { throw ["Unexpected ", c]; } else if (c === ":" && !isPathStart(parserState)) { if (parserState.typeFilter !== null) { - throw ["Unexpected ", ":"]; - } - if (query.elems.length === 0) { + throw [ + "Unexpected ", + ":", + " (expected path after type filter ", + parserState.typeFilter + ":", + ")", + ]; + } else if (query.elems.length === 0) { throw ["Expected type filter before ", ":"]; } else if (query.literalSearch) { - throw ["You cannot use quotes on type filter"]; + throw ["Cannot use quotes on type filter"]; } // The type filter doesn't count as an element since it's a modifier. const typeFilterElem = query.elems.pop(); @@ -662,29 +888,36 @@ function initSearch(rawSearchIndex) { query.literalSearch = false; foundStopChar = true; continue; + } else if (isWhitespace(c)) { + skipWhitespace(parserState); + continue; } if (!foundStopChar) { + let extra = ""; + if (isLastElemGeneric(query.elems, parserState)) { + extra = [" after ", ">"]; + } else if (prevIs(parserState, "\"")) { + throw ["Cannot have more than one element if you use quotes"]; + } if (parserState.typeFilter !== null) { throw [ "Expected ", - ",", // comma - ", ", - " ", // whitespace + ",", " or ", - "->", // arrow + "->", + ...extra, ", found ", c, ]; } throw [ "Expected ", - ",", // comma + ",", ", ", - " ", // whitespace - ", ", - ":", // colon + ":", " or ", - "->", // arrow + "->", + ...extra, ", found ", c, ]; @@ -699,11 +932,18 @@ function initSearch(rawSearchIndex) { foundStopChar = false; } if (parserState.typeFilter !== null) { - throw ["Unexpected ", ":", " (expected path after type filter)"]; + throw [ + "Unexpected ", + ":", + " (expected path after type filter ", + parserState.typeFilter + ":", + ")", + ]; } while (parserState.pos < parserState.length) { if (isReturnArrow(parserState)) { parserState.pos += 2; + skipWhitespace(parserState); // Get returned elements. getItemsBefore(query, parserState, query.returned, ""); // Nothing can come afterward! @@ -778,9 +1018,10 @@ function initSearch(rawSearchIndex) { * The supported syntax by this parser is as follow: * * ident = *(ALPHA / DIGIT / "_") - * path = ident *(DOUBLE-COLON ident) [!] - * arg = [type-filter *WS COLON *WS] path [generics] - * type-sep = COMMA/WS *(COMMA/WS) + * path = ident *(DOUBLE-COLON/{WS} ident) [!] + * slice = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET + * arg = [type-filter *WS COLON *WS] (path [generics] / slice) + * type-sep = *WS COMMA *(COMMA) * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list ] *(type-sep) * CLOSE-ANGLE-BRACKET @@ -821,6 +1062,8 @@ function initSearch(rawSearchIndex) { * * OPEN-ANGLE-BRACKET = "<" * CLOSE-ANGLE-BRACKET = ">" + * OPEN-SQUARE-BRACKET = "[" + * CLOSE-SQUARE-BRACKET = "]" * COLON = ":" * DOUBLE-COLON = "::" * QUOTE = %x22 @@ -1103,98 +1346,182 @@ function initSearch(rawSearchIndex) { } /** - * This function checks if the object (`row`) generics match the given type (`elem`) - * generics. If there are no generics on `row`, `defaultDistance` is returned. + * This function checks generics in search query `queryElem` can all be found in the + * search index (`fnType`), * - * @param {Row} row - The object to check. - * @param {QueryElement} elem - The element from the parsed query. + * @param {FunctionType} fnType - The object to check. + * @param {QueryElement} queryElem - The element from the parsed query. * - * @return {boolean} - Returns true if a match, false otherwise. + * @return {boolean} - Returns true if a match, false otherwise. */ - function checkGenerics(row, elem) { - if (row.generics.length === 0 || elem.generics.length === 0) { - return false; - } - // This function is called if the names match, but we need to make - // sure that all generics match as well. - // + function checkGenerics(fnType, queryElem) { + return unifyFunctionTypes(fnType.generics, queryElem.generics); + } + /** + * This function checks if a list of search query `queryElems` can all be found in the + * search index (`fnTypes`). + * + * @param {Array<FunctionType>} fnTypes - The objects to check. + * @param {Array<QueryElement>} queryElems - The elements from the parsed query. + * + * @return {boolean} - Returns true if a match, false otherwise. + */ + function unifyFunctionTypes(fnTypes, queryElems) { // This search engine implements order-agnostic unification. There // should be no missing duplicates (generics have "bag semantics"), // and the row is allowed to have extras. - if (elem.generics.length > 0 && row.generics.length >= elem.generics.length) { - const elems = new Map(); - const addEntryToElems = function addEntryToElems(entry) { - if (entry.id === -1) { - // Pure generic, needs to check into it. - for (const inner_entry of entry.generics) { - addEntryToElems(inner_entry); - } - return; + if (queryElems.length === 0) { + return true; + } + if (!fnTypes || fnTypes.length === 0) { + return false; + } + /** + * @type Map<integer, QueryElement[]> + */ + const queryElemSet = new Map(); + const addQueryElemToQueryElemSet = function addQueryElemToQueryElemSet(queryElem) { + let currentQueryElemList; + if (queryElemSet.has(queryElem.id)) { + currentQueryElemList = queryElemSet.get(queryElem.id); + } else { + currentQueryElemList = []; + queryElemSet.set(queryElem.id, currentQueryElemList); + } + currentQueryElemList.push(queryElem); + }; + for (const queryElem of queryElems) { + addQueryElemToQueryElemSet(queryElem); + } + /** + * @type Map<integer, FunctionType[]> + */ + const fnTypeSet = new Map(); + const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) { + // Pure generic, or an item that's not matched by any query elems. + // Try [unboxing] it. + // + // [unboxing]: + // http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf + const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice); + if (fnType.id === -1 || !( + queryElemSet.has(fnType.id) || + (fnType.id === typeNameIdOfSlice && queryContainsArrayOrSliceElem) || + (fnType.id === typeNameIdOfArray && queryContainsArrayOrSliceElem) + )) { + for (const innerFnType of fnType.generics) { + addFnTypeToFnTypeSet(innerFnType); } - let currentEntryElems; - if (elems.has(entry.id)) { - currentEntryElems = elems.get(entry.id); - } else { - currentEntryElems = []; - elems.set(entry.id, currentEntryElems); + return; + } + let currentQueryElemList = queryElemSet.get(fnType.id) || []; + let matchIdx = currentQueryElemList.findIndex(queryElem => { + return typePassesFilter(queryElem.typeFilter, fnType.ty) && + checkGenerics(fnType, queryElem); + }); + if (matchIdx === -1 && + (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) && + queryContainsArrayOrSliceElem + ) { + currentQueryElemList = queryElemSet.get(typeNameIdOfArrayOrSlice) || []; + matchIdx = currentQueryElemList.findIndex(queryElem => { + return typePassesFilter(queryElem.typeFilter, fnType.ty) && + checkGenerics(fnType, queryElem); + }); + } + // None of the query elems match the function type. + // Try [unboxing] it. + if (matchIdx === -1) { + for (const innerFnType of fnType.generics) { + addFnTypeToFnTypeSet(innerFnType); } - currentEntryElems.push(entry); - }; - for (const entry of row.generics) { - addEntryToElems(entry); - } - // We need to find the type that matches the most to remove it in order - // to move forward. - const handleGeneric = generic => { - if (!elems.has(generic.id)) { - return false; + return; + } + let currentFnTypeList; + if (fnTypeSet.has(fnType.id)) { + currentFnTypeList = fnTypeSet.get(fnType.id); + } else { + currentFnTypeList = []; + fnTypeSet.set(fnType.id, currentFnTypeList); + } + currentFnTypeList.push(fnType); + }; + for (const fnType of fnTypes) { + addFnTypeToFnTypeSet(fnType); + } + const doHandleQueryElemList = (currentFnTypeList, queryElemList) => { + if (queryElemList.length === 0) { + return true; + } + // Multiple items in one list might match multiple items in another. + // Since an item with fewer generics can match an item with more, we + // need to check all combinations for a potential match. + const queryElem = queryElemList.pop(); + const l = currentFnTypeList.length; + for (let i = 0; i < l; i += 1) { + const fnType = currentFnTypeList[i]; + if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) { + continue; } - const matchElems = elems.get(generic.id); - const matchIdx = matchElems.findIndex(tmp_elem => { - if (generic.generics.length > 0 && !checkGenerics(tmp_elem, generic)) { - return false; + if (queryElem.generics.length === 0 || checkGenerics(fnType, queryElem)) { + currentFnTypeList.splice(i, 1); + const result = doHandleQueryElemList(currentFnTypeList, queryElemList); + if (result) { + return true; } - return typePassesFilter(generic.typeFilter, tmp_elem.ty); - }); - if (matchIdx === -1) { - return false; + currentFnTypeList.splice(i, 0, fnType); } - matchElems.splice(matchIdx, 1); - if (matchElems.length === 0) { - elems.delete(generic.id); + } + return false; + }; + const handleQueryElemList = (id, queryElemList) => { + if (!fnTypeSet.has(id)) { + if (id === typeNameIdOfArrayOrSlice) { + return handleQueryElemList(typeNameIdOfSlice, queryElemList) || + handleQueryElemList(typeNameIdOfArray, queryElemList); } - return true; - }; - // To do the right thing with type filters, we first process generics - // that have them, removing matching ones from the "bag," then do the - // ones with no type filter, which can match any entry regardless of its - // own type. - for (const generic of elem.generics) { - if (generic.typeFilter !== -1 && !handleGeneric(generic)) { - return false; + return false; + } + const currentFnTypeList = fnTypeSet.get(id); + if (currentFnTypeList.length < queryElemList.length) { + // It's not possible for all the query elems to find a match. + return false; + } + const result = doHandleQueryElemList(currentFnTypeList, queryElemList); + if (result) { + // Found a solution. + // Any items that weren't used for it can be unboxed, and might form + // part of the solution for another item. + for (const innerFnType of currentFnTypeList) { + addFnTypeToFnTypeSet(innerFnType); } + fnTypeSet.delete(id); } - for (const generic of elem.generics) { - if (generic.typeFilter === -1 && !handleGeneric(generic)) { - return false; + return result; + }; + let queryElemSetSize = -1; + while (queryElemSetSize !== queryElemSet.size) { + queryElemSetSize = queryElemSet.size; + for (const [id, queryElemList] of queryElemSet) { + if (handleQueryElemList(id, queryElemList)) { + queryElemSet.delete(id); } } - return true; } - return false; + return queryElemSetSize === 0; } /** * This function checks if the object (`row`) matches the given type (`elem`) and its * generics (if any). * - * @param {Row} row + * @param {Array<FunctionType>} list * @param {QueryElement} elem - The element from the parsed query. * * @return {boolean} - Returns true if found, false otherwise. */ - function checkIfInGenerics(row, elem) { - for (const entry of row.generics) { + function checkIfInList(list, elem) { + for (const entry of list) { if (checkType(entry, elem)) { return true; } @@ -1214,10 +1541,15 @@ function initSearch(rawSearchIndex) { function checkType(row, elem) { if (row.id === -1) { // This is a pure "generic" search, no need to run other checks. - return row.generics.length > 0 ? checkIfInGenerics(row, elem) : false; + return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false; } - if (row.id === elem.id && typePassesFilter(elem.typeFilter, row.ty)) { + const matchesExact = row.id === elem.id; + const matchesArrayOrSlice = elem.id === typeNameIdOfArrayOrSlice && + (row.id === typeNameIdOfSlice || row.id === typeNameIdOfArray); + + if ((matchesExact || matchesArrayOrSlice) && + typePassesFilter(elem.typeFilter, row.ty)) { if (elem.generics.length > 0) { return checkGenerics(row, elem); } @@ -1227,59 +1559,7 @@ function initSearch(rawSearchIndex) { // If the current item does not match, try [unboxing] the generic. // [unboxing]: // https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - return checkIfInGenerics(row, elem); - } - - /** - * This function checks if the object (`row`) has an argument with the given type (`elem`). - * - * @param {Row} row - * @param {QueryElement} elem - The element from the parsed query. - * @param {Array<integer>} skipPositions - Do not return one of these positions. - * - * @return {integer} - Returns the position of the match, or -1 if none. - */ - function findArg(row, elem, skipPositions) { - if (row && row.type && row.type.inputs && row.type.inputs.length > 0) { - let i = 0; - for (const input of row.type.inputs) { - if (skipPositions.indexOf(i) !== -1) { - i += 1; - continue; - } - if (checkType(input, elem)) { - return i; - } - i += 1; - } - } - return -1; - } - - /** - * This function checks if the object (`row`) returns the given type (`elem`). - * - * @param {Row} row - * @param {QueryElement} elem - The element from the parsed query. - * @param {Array<integer>} skipPositions - Do not return one of these positions. - * - * @return {integer} - Returns the position of the matching item, or -1 if none. - */ - function checkReturned(row, elem, skipPositions) { - if (row && row.type && row.type.output.length > 0) { - let i = 0; - for (const ret_ty of row.type.output) { - if (skipPositions.indexOf(i) !== -1) { - i += 1; - continue; - } - if (checkType(ret_ty, elem)) { - return i; - } - i += 1; - } - } - return -1; + return checkIfInList(row.generics, elem); } function checkPath(contains, ty, maxEditDistance) { @@ -1480,14 +1760,14 @@ function initSearch(rawSearchIndex) { const fullId = row.id; const searchWord = searchWords[pos]; - const in_args = findArg(row, elem, []); - if (in_args !== -1) { + const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem); + if (in_args) { // path_dist is 0 because no parent path information is currently stored // in the search index addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance); } - const returned = checkReturned(row, elem, []); - if (returned !== -1) { + const returned = row.type && row.type.output && checkIfInList(row.type.output, elem); + if (returned) { addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance); } @@ -1543,32 +1823,15 @@ function initSearch(rawSearchIndex) { * @param {Object} results */ function handleArgs(row, pos, results) { - if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) { return; } // If the result is too "bad", we return false and it ends this search. - function checkArgs(elems, callback) { - const skipPositions = []; - for (const elem of elems) { - // There is more than one parameter to the query so all checks should be "exact" - const position = callback( - row, - elem, - skipPositions - ); - if (position !== -1) { - skipPositions.push(position); - } else { - return false; - } - } - return true; - } - if (!checkArgs(parsedQuery.elems, findArg)) { + if (!unifyFunctionTypes(row.type.inputs, parsedQuery.elems)) { return; } - if (!checkArgs(parsedQuery.returned, checkReturned)) { + if (!unifyFunctionTypes(row.type.output, parsedQuery.returned)) { return; } @@ -1655,12 +1918,9 @@ function initSearch(rawSearchIndex) { elem = parsedQuery.returned[0]; for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { row = searchIndex[i]; - in_returned = checkReturned( - row, - elem, - [] - ); - if (in_returned !== -1) { + in_returned = row.type && + unifyFunctionTypes(row.type.output, parsedQuery.returned); + if (in_returned) { addIntoResults( results_others, row.id, @@ -1836,16 +2096,11 @@ function initSearch(rawSearchIndex) { array.forEach(item => { const name = item.name; const type = itemTypes[item.ty]; + const longType = longItemTypes[item.ty]; + const typeName = longType.length !== 0 ? `${longType}` : "?"; length += 1; - let extra = ""; - if (type === "primitive") { - extra = " <i>(primitive type)</i>"; - } else if (type === "keyword") { - extra = " <i>(keyword)</i>"; - } - const link = document.createElement("a"); link.className = "result-" + type; link.href = item.href; @@ -1863,13 +2118,18 @@ function initSearch(rawSearchIndex) { alias.insertAdjacentHTML( "beforeend", - "<span class=\"grey\"><i> - see </i></span>"); + "<i class=\"grey\"> - see </i>"); resultName.appendChild(alias); } + resultName.insertAdjacentHTML( "beforeend", - item.displayPath + "<span class=\"" + type + "\">" + name + extra + "</span>"); + `\ +<span class="typename">${typeName}</span>\ +<div class="path">\ + ${item.displayPath}<span class="${type}">${name}</span>\ +</div>`); link.appendChild(resultName); const description = document.createElement("div"); @@ -1916,6 +2176,20 @@ function initSearch(rawSearchIndex) { if (go_to_first || (results.others.length === 1 && getSettingValue("go-to-only-result") === "true") ) { + // Needed to force re-execution of JS when coming back to a page. Let's take this + // scenario as example: + // + // 1. You have the "Directly go to item in search if there is only one result" option + // enabled. + // 2. You make a search which results only one result, leading you automatically to + // this result. + // 3. You go back to previous page. + // + // Now, without the call below, the JS will not be re-executed and the previous state + // will be used, starting search again since the search input is not empty, leading you + // back to the previous page again. + window.onunload = () => {}; + searchState.removeQueryParameters(); const elem = document.createElement("a"); elem.href = results.others[0].href; removeClass(elem, "active"); @@ -1967,7 +2241,7 @@ function initSearch(rawSearchIndex) { error.forEach((value, index) => { value = value.split("<").join("<").split(">").join(">"); if (index % 2 !== 0) { - error[index] = `<code>${value}</code>`; + error[index] = `<code>${value.replaceAll(" ", " ")}</code>`; } else { error[index] = value; } @@ -2030,6 +2304,18 @@ function initSearch(rawSearchIndex) { printTab(currentTab); } + function updateSearchHistory(url) { + if (!browserSupportsHistoryApi()) { + return; + } + const params = searchState.getQueryStringParams(); + if (!history.state && !params.search) { + history.pushState(null, "", url); + } else { + history.replaceState(null, "", url); + } + } + /** * Perform a search based on the current state of the search input element * and display the results. @@ -2040,7 +2326,6 @@ function initSearch(rawSearchIndex) { if (e) { e.preventDefault(); } - const query = parseQuery(searchState.input.value.trim()); let filterCrates = getFilterCrates(); @@ -2066,15 +2351,7 @@ function initSearch(rawSearchIndex) { // Because searching is incremental by character, only the most // recent search query is added to the browser history. - if (browserSupportsHistoryApi()) { - const newURL = buildUrl(query.original, filterCrates); - - if (!history.state && !params.search) { - history.pushState(null, "", newURL); - } else { - history.replaceState(null, "", newURL); - } - } + updateSearchHistory(buildUrl(query.original, filterCrates)); showResults( execQuery(query, searchWords, filterCrates, window.currentCrate), @@ -2083,34 +2360,6 @@ function initSearch(rawSearchIndex) { } /** - * Add an item to the type Name->ID map, or, if one already exists, use it. - * Returns the number. If name is "" or null, return -1 (pure generic). - * - * This is effectively string interning, so that function matching can be - * done more quickly. Two types with the same name but different item kinds - * get the same ID. - * - * @param {Map<string, integer>} typeNameIdMap - * @param {string} name - * - * @returns {integer} - */ - function buildTypeMapIndex(typeNameIdMap, name) { - - if (name === "" || name === null) { - return -1; - } - - if (typeNameIdMap.has(name)) { - return typeNameIdMap.get(name); - } else { - const id = typeNameIdMap.size; - typeNameIdMap.set(name, id); - return id; - } - } - - /** * Convert a list of RawFunctionType / ID to object-based FunctionType. * * Crates often have lots of functions in them, and it's common to have a large number of @@ -2128,7 +2377,7 @@ function initSearch(rawSearchIndex) { * * @return {Array<FunctionSearchType>} */ - function buildItemSearchTypeAll(types, lowercasePaths, typeNameIdMap) { + function buildItemSearchTypeAll(types, lowercasePaths) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; return types.map(type => { @@ -2140,15 +2389,14 @@ function initSearch(rawSearchIndex) { pathIndex = type[PATH_INDEX_DATA]; generics = buildItemSearchTypeAll( type[GENERICS_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } return { // `0` is used as a sentinel because it's fewer bytes than `null` id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: generics, }; @@ -2171,7 +2419,7 @@ function initSearch(rawSearchIndex) { * * @return {null|FunctionSearchType} */ - function buildFunctionSearchType(functionSearchType, lowercasePaths, typeNameIdMap) { + function buildFunctionSearchType(functionSearchType, lowercasePaths) { const INPUTS_DATA = 0; const OUTPUT_DATA = 1; // `0` is used as a sentinel because it's fewer bytes than `null` @@ -2184,15 +2432,14 @@ function initSearch(rawSearchIndex) { inputs = [{ id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: [], }]; } else { inputs = buildItemSearchTypeAll( functionSearchType[INPUTS_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } if (functionSearchType.length > 1) { @@ -2201,15 +2448,14 @@ function initSearch(rawSearchIndex) { output = [{ id: pathIndex === 0 ? -1 - : buildTypeMapIndex(typeNameIdMap, lowercasePaths[pathIndex - 1].name), + : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, generics: [], }]; } else { output = buildItemSearchTypeAll( functionSearchType[OUTPUT_DATA], - lowercasePaths, - typeNameIdMap + lowercasePaths ); } } else { @@ -2233,6 +2479,12 @@ function initSearch(rawSearchIndex) { let currentIndex = 0; let id = 0; + // Initialize type map indexes for primitive list types + // that can be searched using `[]` syntax. + typeNameIdOfArray = buildTypeMapIndex("array"); + typeNameIdOfSlice = buildTypeMapIndex("slice"); + typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]"); + for (const crate in rawSearchIndex) { if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) { continue; @@ -2363,8 +2615,7 @@ function initSearch(rawSearchIndex) { parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined, type: buildFunctionSearchType( itemFunctionSearchTypes[i], - lowercasePaths, - typeNameIdMap + lowercasePaths ), id: id, normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), @@ -2566,13 +2817,8 @@ function initSearch(rawSearchIndex) { function updateCrate(ev) { if (ev.target.value === "all crates") { // If we don't remove it from the URL, it'll be picked up again by the search. - const params = searchState.getQueryStringParams(); const query = searchState.input.value.trim(); - if (!history.state && !params.search) { - history.pushState(null, "", buildUrl(query, null)); - } else { - history.replaceState(null, "", buildUrl(query, null)); - } + updateSearchHistory(buildUrl(query, null)); } // In case you "cut" the entry from the search input, then change the crate filter // before paste back the previous search, you get the old search results without diff --git a/src/librustdoc/html/static/js/source-script.js b/src/librustdoc/html/static/js/source-script.js index d999f3b36..6eb991360 100644 --- a/src/librustdoc/html/static/js/source-script.js +++ b/src/librustdoc/html/static/js/source-script.js @@ -3,13 +3,13 @@ // Local js definitions: /* global addClass, getCurrentValue, onEachLazy, removeClass, browserSupportsHistoryApi */ -/* global updateLocalStorage */ +/* global updateLocalStorage, getVar */ "use strict"; (function() { -const rootPath = document.getElementById("rustdoc-vars").attributes["data-root-path"].value; +const rootPath = getVar("root-path"); const NAME_OFFSET = 0; const DIRS_OFFSET = 1; diff --git a/src/librustdoc/html/static/js/storage.js b/src/librustdoc/html/static/js/storage.js index 93979a944..71961f6f2 100644 --- a/src/librustdoc/html/static/js/storage.js +++ b/src/librustdoc/html/static/js/storage.js @@ -108,7 +108,7 @@ function getCurrentValue(name) { // Get a value from the rustdoc-vars div, which is used to convey data from // Rust to the JS. If there is no such element, return null. const getVar = (function getVar(name) { - const el = document.getElementById("rustdoc-vars"); + const el = document.querySelector("head > meta[name='rustdoc-vars']"); return el ? el.attributes["data-" + name].value : null; }); diff --git a/src/librustdoc/html/templates/item_info.html b/src/librustdoc/html/templates/item_info.html index d2ea9bdae..9e65ae95e 100644 --- a/src/librustdoc/html/templates/item_info.html +++ b/src/librustdoc/html/templates/item_info.html @@ -1,5 +1,5 @@ {% if !items.is_empty() %} - <span class="item-info"> {# #} + <span class="item-info"> {% for item in items %} {{item|safe}} {# #} {% endfor %} diff --git a/src/librustdoc/html/templates/item_union.html b/src/librustdoc/html/templates/item_union.html index c21967005..f6d2fa348 100644 --- a/src/librustdoc/html/templates/item_union.html +++ b/src/librustdoc/html/templates/item_union.html @@ -1,17 +1,18 @@ <pre class="rust item-decl"><code> - {{ self::item_template_render_attributes_in_pre(self.borrow()) | safe }} + {{ self.render_attributes_in_pre() | safe }} {{ self.render_union() | safe }} </code></pre> -{{ self::item_template_document(self.borrow()) | safe }} +{{ self.document() | safe }} {% if self.fields_iter().peek().is_some() %} - <h2 id="fields" class="fields small-section-header"> - Fields<a href="#fields" class="anchor">§</a> + <h2 id="fields" class="fields small-section-header"> {# #} + Fields<a href="#fields" class="anchor">§</a> {# #} </h2> {% for (field, ty) in self.fields_iter() %} {% let name = field.name.expect("union field name") %} - <span id="structfield.{{ name }}" class="{{ ItemType::StructField }} small-section-header"> - <a href="#structfield.{{ name }}" class="anchor field">§</a> - <code>{{ name }}: {{ self.print_ty(ty) | safe }}</code> + <span id="structfield.{{ name }}" {#+ #} + class="{{ ItemType::StructField +}} small-section-header"> {# #} + <a href="#structfield.{{ name }}" class="anchor field">§</a> {# #} + <code>{{ name }}: {{+ self.print_ty(ty) | safe }}</code> {# #} </span> {% if let Some(stability_class) = self.stability_field(field) %} <span class="stab {{ stability_class }}"></span> @@ -19,5 +20,5 @@ {{ self.document_field(field) | safe }} {% endfor %} {% endif %} -{{ self::item_template_render_assoc_items(self.borrow()) | safe }} -{{ self::item_template_document_type_layout(self.borrow()) | safe }} +{{ self.render_assoc_items() | safe }} +{{ self.document_type_layout() | safe }} diff --git a/src/librustdoc/html/templates/page.html b/src/librustdoc/html/templates/page.html index 9133f899a..d4ec9c34b 100644 --- a/src/librustdoc/html/templates/page.html +++ b/src/librustdoc/html/templates/page.html @@ -24,13 +24,14 @@ {% endfor %} ></script> {# #} {% endif %} - <div id="rustdoc-vars" {#+ #} + <meta name="rustdoc-vars" {#+ #} data-root-path="{{page.root_path|safe}}" {#+ #} data-static-root-path="{{static_root_path|safe}}" {#+ #} data-current-crate="{{layout.krate}}" {#+ #} data-themes="{{themes|join(",") }}" {#+ #} data-resource-suffix="{{page.resource_suffix}}" {#+ #} data-rustdoc-version="{{rustdoc_version}}" {#+ #} + data-channel="{{rust_channel}}" {#+ #} data-search-js="{{files.search_js}}" {#+ #} data-settings-js="{{files.settings_js}}" {#+ #} data-settings-css="{{files.settings_css}}" {#+ #} @@ -38,7 +39,6 @@ data-theme-dark-css="{{files.theme_dark_css}}" {#+ #} data-theme-ayu-css="{{files.theme_ayu_css}}" {#+ #} > {# #} - </div> {# #} <script src="{{static_root_path|safe}}{{files.storage_js}}"></script> {# #} {% if page.css_class.contains("crate") %} <script defer src="{{page.root_path|safe}}crates{{page.resource_suffix}}.js"></script> {# #} diff --git a/src/librustdoc/html/templates/print_item.html b/src/librustdoc/html/templates/print_item.html index edabac9a0..68a295ae0 100644 --- a/src/librustdoc/html/templates/print_item.html +++ b/src/librustdoc/html/templates/print_item.html @@ -1,5 +1,5 @@ <div class="main-heading"> {# #} - <h1> {# #} + <h1> {{typ}} {# The breadcrumbs of the item path, like std::string #} {% for component in path_components %} @@ -12,7 +12,7 @@ alt="Copy item path"> {# #} </button> {# #} </h1> {# #} - <span class="out-of-band"> {# #} + <span class="out-of-band"> {% if !stability_since_raw.is_empty() %} {{ stability_since_raw|safe +}} · {#+ #} {% endif %} |