summaryrefslogtreecommitdiffstats
path: root/src/librustdoc/html/static/js
diff options
context:
space:
mode:
Diffstat (limited to 'src/librustdoc/html/static/js')
-rw-r--r--src/librustdoc/html/static/js/externs.js4
-rw-r--r--src/librustdoc/html/static/js/main.js170
-rw-r--r--src/librustdoc/html/static/js/search.js894
-rw-r--r--src/librustdoc/html/static/js/source-script.js4
-rw-r--r--src/librustdoc/html/static/js/storage.js2
5 files changed, 730 insertions, 344 deletions
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>-&gt; vec</code> or <code>String, enum:Cow -&gt; 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>-&gt; [u8]</code> or <code>[] -&gt; 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
- ", ",
- "&nbsp;", // whitespace
+ ",",
" or ",
endChar,
+ ...extra,
", found ",
c,
];
}
throw [
"Expected ",
- ",", // comma
- " or ",
- "&nbsp;", // 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
- ", ",
- "&nbsp;", // whitespace
+ ",",
" or ",
- "->", // arrow
+ "->",
+ ...extra,
", found ",
c,
];
}
throw [
"Expected ",
- ",", // comma
+ ",",
", ",
- "&nbsp;", // 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>&nbsp;- see&nbsp;</i></span>");
+ "<i class=\"grey\">&nbsp;- see&nbsp;</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("&lt;").split(">").join("&gt;");
if (index % 2 !== 0) {
- error[index] = `<code>${value}</code>`;
+ error[index] = `<code>${value.replaceAll(" ", "&nbsp;")}</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;
});