]> git.proxmox.com Git - rustc.git/blame - src/librustdoc/html/static/js/search.js
New upstream version 1.73.0+dfsg1
[rustc.git] / src / librustdoc / html / static / js / search.js
CommitLineData
923072b8
FG
1/* global addClass, getNakedUrl, getSettingValue */
2/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */
04454e1e
FG
3
4"use strict";
17df50a5 5
cdc7bbd5
XL
6(function() {
7// This mapping table should match the discriminants of
04454e1e
FG
8// `rustdoc::formats::item_type::ItemType` type in Rust.
9const itemTypes = [
a2a8927a
XL
10 "mod",
11 "externcrate",
12 "import",
13 "struct",
14 "enum",
15 "fn",
16 "type",
17 "static",
18 "trait",
19 "impl",
20 "tymethod",
21 "method",
22 "structfield",
23 "variant",
24 "macro",
25 "primitive",
26 "associatedtype",
27 "constant",
28 "associatedconstant",
29 "union",
30 "foreigntype",
31 "keyword",
32 "existential",
33 "attr",
34 "derive",
35 "traitalias",
36];
cdc7bbd5 37
fe692bf9
FG
38const longItemTypes = [
39 "module",
40 "extern crate",
41 "re-export",
42 "struct",
43 "enum",
44 "function",
45 "type alias",
46 "static",
47 "trait",
48 "",
49 "trait method",
50 "method",
51 "struct field",
52 "enum variant",
53 "macro",
54 "primitive type",
55 "assoc type",
56 "constant",
57 "assoc const",
58 "union",
59 "foreign type",
60 "keyword",
61 "existential type",
62 "attribute macro",
63 "derive macro",
64 "trait alias",
65];
66
cdc7bbd5 67// used for special search precedence
04454e1e
FG
68const TY_PRIMITIVE = itemTypes.indexOf("primitive");
69const TY_KEYWORD = itemTypes.indexOf("keyword");
923072b8
FG
70const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../";
71
72function hasOwnPropertyRustdoc(obj, property) {
73 return Object.prototype.hasOwnProperty.call(obj, property);
74}
cdc7bbd5
XL
75
76// In the search display, allows to switch between tabs.
77function printTab(nb) {
04454e1e
FG
78 let iter = 0;
79 let foundCurrentTab = false;
80 let foundCurrentResultSet = false;
9c376795 81 onEachLazy(document.getElementById("search-tabs").childNodes, elem => {
04454e1e 82 if (nb === iter) {
cdc7bbd5 83 addClass(elem, "selected");
04454e1e 84 foundCurrentTab = true;
cdc7bbd5
XL
85 } else {
86 removeClass(elem, "selected");
87 }
04454e1e 88 iter += 1;
cdc7bbd5 89 });
49aad941 90 const isTypeSearch = (nb > 0 || iter === 1);
04454e1e
FG
91 iter = 0;
92 onEachLazy(document.getElementById("results").childNodes, elem => {
93 if (nb === iter) {
17df50a5 94 addClass(elem, "active");
04454e1e 95 foundCurrentResultSet = true;
cdc7bbd5 96 } else {
17df50a5 97 removeClass(elem, "active");
cdc7bbd5 98 }
04454e1e 99 iter += 1;
cdc7bbd5 100 });
04454e1e
FG
101 if (foundCurrentTab && foundCurrentResultSet) {
102 searchState.currentTab = nb;
49aad941
FG
103 // Corrections only kick in on type-based searches.
104 const correctionsElem = document.getElementsByClassName("search-corrections");
105 if (isTypeSearch) {
106 removeClass(correctionsElem[0], "hidden");
107 } else {
108 addClass(correctionsElem[0], "hidden");
109 }
923072b8 110 } else if (nb !== 0) {
04454e1e 111 printTab(0);
cdc7bbd5
XL
112 }
113}
114
115/**
353b0b11
FG
116 * The [edit distance] is a metric for measuring the difference between two strings.
117 *
118 * [edit distance]: https://en.wikipedia.org/wiki/Edit_distance
cdc7bbd5 119 */
353b0b11
FG
120
121/*
122 * This function was translated, mostly line-for-line, from
123 * https://github.com/rust-lang/rust/blob/ff4b772f805ec1e/compiler/rustc_span/src/edit_distance.rs
124 *
125 * The current implementation is the restricted Damerau-Levenshtein algorithm. It is restricted
126 * because it does not permit modifying characters that have already been transposed. The specific
127 * algorithm should not matter to the caller of the methods, which is why it is not noted in the
128 * documentation.
129 */
130const editDistanceState = {
131 current: [],
132 prev: [],
133 prevPrev: [],
134 calculate: function calculate(a, b, limit) {
135 // Ensure that `b` is the shorter string, minimizing memory use.
136 if (a.length < b.length) {
137 const aTmp = a;
138 a = b;
139 b = aTmp;
140 }
141
142 const minDist = a.length - b.length;
143 // If we know the limit will be exceeded, we can return early.
144 if (minDist > limit) {
145 return limit + 1;
146 }
147
148 // Strip common prefix.
149 // We know that `b` is the shorter string, so we don't need to check
150 // `a.length`.
151 while (b.length > 0 && b[0] === a[0]) {
152 a = a.substring(1);
153 b = b.substring(1);
154 }
155 // Strip common suffix.
156 while (b.length > 0 && b[b.length - 1] === a[a.length - 1]) {
157 a = a.substring(0, a.length - 1);
158 b = b.substring(0, b.length - 1);
159 }
160
161 // If either string is empty, the distance is the length of the other.
162 // We know that `b` is the shorter string, so we don't need to check `a`.
163 if (b.length === 0) {
164 return minDist;
165 }
166
167 const aLength = a.length;
168 const bLength = b.length;
169
170 for (let i = 0; i <= bLength; ++i) {
171 this.current[i] = 0;
172 this.prev[i] = i;
173 this.prevPrev[i] = Number.MAX_VALUE;
174 }
175
176 // row by row
177 for (let i = 1; i <= aLength; ++i) {
178 this.current[0] = i;
179 const aIdx = i - 1;
180
181 // column by column
182 for (let j = 1; j <= bLength; ++j) {
183 const bIdx = j - 1;
184
185 // There is no cost to substitute a character with itself.
186 const substitutionCost = a[aIdx] === b[bIdx] ? 0 : 1;
187
188 this.current[j] = Math.min(
189 // deletion
190 this.prev[j] + 1,
191 // insertion
192 this.current[j - 1] + 1,
193 // substitution
194 this.prev[j - 1] + substitutionCost
195 );
196
197 if ((i > 1) && (j > 1) && (a[aIdx] === b[bIdx - 1]) && (a[aIdx - 1] === b[bIdx])) {
198 // transposition
199 this.current[j] = Math.min(
200 this.current[j],
201 this.prevPrev[j - 2] + 1
202 );
203 }
204 }
205
206 // Rotate the buffers, reusing the memory
207 const prevPrevTmp = this.prevPrev;
208 this.prevPrev = this.prev;
209 this.prev = this.current;
210 this.current = prevPrevTmp;
211 }
212
213 // `prev` because we already rotated the buffers.
214 const distance = this.prev[bLength];
215 return distance <= limit ? distance : (limit + 1);
216 },
217};
218
219function editDistance(a, b, limit) {
220 return editDistanceState.calculate(a, b, limit);
cdc7bbd5
XL
221}
222
923072b8 223function initSearch(rawSearchIndex) {
04454e1e 224 const MAX_RESULTS = 200;
04454e1e 225 const NO_TYPE_FILTER = -1;
a2a8927a
XL
226 /**
227 * @type {Array<Row>}
228 */
04454e1e
FG
229 let searchIndex;
230 let currentResults;
49aad941
FG
231 /**
232 * Map from normalized type names to integers. Used to make type search
233 * more efficient.
234 *
235 * @type {Map<string, integer>}
236 */
237 let typeNameIdMap;
353b0b11 238 const ALIASES = new Map();
cdc7bbd5 239
fe692bf9
FG
240 /**
241 * Special type name IDs for searching by array.
242 */
243 let typeNameIdOfArray;
244 /**
245 * Special type name IDs for searching by slice.
246 */
247 let typeNameIdOfSlice;
248 /**
249 * Special type name IDs for searching by both array and slice (`[]` syntax).
250 */
251 let typeNameIdOfArrayOrSlice;
252
253 /**
254 * Add an item to the type Name->ID map, or, if one already exists, use it.
255 * Returns the number. If name is "" or null, return -1 (pure generic).
256 *
257 * This is effectively string interning, so that function matching can be
258 * done more quickly. Two types with the same name but different item kinds
259 * get the same ID.
260 *
261 * @param {string} name
262 *
263 * @returns {integer}
264 */
265 function buildTypeMapIndex(name) {
266
267 if (name === "" || name === null) {
268 return -1;
269 }
270
271 if (typeNameIdMap.has(name)) {
272 return typeNameIdMap.get(name);
273 } else {
274 const id = typeNameIdMap.size;
275 typeNameIdMap.set(name, id);
276 return id;
277 }
278 }
279
04454e1e
FG
280 function isWhitespace(c) {
281 return " \t\n\r".indexOf(c) !== -1;
282 }
283
284 function isSpecialStartCharacter(c) {
285 return "<\"".indexOf(c) !== -1;
286 }
287
288 function isEndCharacter(c) {
fe692bf9 289 return ",>-]".indexOf(c) !== -1;
04454e1e
FG
290 }
291
292 function isStopCharacter(c) {
fe692bf9 293 return isEndCharacter(c);
04454e1e
FG
294 }
295
296 function isErrorCharacter(c) {
297 return "()".indexOf(c) !== -1;
298 }
299
300 function itemTypeFromName(typename) {
9ffffee4
FG
301 const index = itemTypes.findIndex(i => i === typename);
302 if (index < 0) {
303 throw ["Unknown type filter ", typename];
04454e1e 304 }
9ffffee4 305 return index;
04454e1e
FG
306 }
307
308 /**
309 * If we encounter a `"`, then we try to extract the string from it until we find another `"`.
310 *
311 * This function will throw an error in the following cases:
312 * * There is already another string element.
313 * * We are parsing a generic argument.
314 * * There is more than one element.
315 * * There is no closing `"`.
316 *
317 * @param {ParsedQuery} query
318 * @param {ParserState} parserState
319 * @param {boolean} isInGenerics
320 */
321 function getStringElem(query, parserState, isInGenerics) {
322 if (isInGenerics) {
9ffffee4 323 throw ["Unexpected ", "\"", " in generics"];
04454e1e 324 } else if (query.literalSearch) {
9ffffee4 325 throw ["Cannot have more than one literal search element"];
04454e1e 326 } else if (parserState.totalElems - parserState.genericsElems > 0) {
9ffffee4 327 throw ["Cannot use literal search when there is more than one element"];
04454e1e
FG
328 }
329 parserState.pos += 1;
330 const start = parserState.pos;
331 const end = getIdentEndPosition(parserState);
332 if (parserState.pos >= parserState.length) {
9ffffee4 333 throw ["Unclosed ", "\""];
04454e1e 334 } else if (parserState.userQuery[end] !== "\"") {
9ffffee4 335 throw ["Unexpected ", parserState.userQuery[end], " in a string element"];
04454e1e 336 } else if (start === end) {
9ffffee4 337 throw ["Cannot have empty string element"];
04454e1e
FG
338 }
339 // To skip the quote at the end.
340 parserState.pos += 1;
341 query.literalSearch = true;
342 }
343
344 /**
345 * Returns `true` if the current parser position is starting with "::".
346 *
347 * @param {ParserState} parserState
348 *
349 * @return {boolean}
350 */
351 function isPathStart(parserState) {
923072b8 352 return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "::";
04454e1e
FG
353 }
354
355 /**
356 * Returns `true` if the current parser position is starting with "->".
357 *
358 * @param {ParserState} parserState
359 *
360 * @return {boolean}
361 */
362 function isReturnArrow(parserState) {
923072b8 363 return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "->";
04454e1e
FG
364 }
365
366 /**
367 * Returns `true` if the given `c` character is valid for an ident.
368 *
369 * @param {string} c
370 *
371 * @return {boolean}
372 */
373 function isIdentCharacter(c) {
374 return (
375 c === "_" ||
376 (c >= "0" && c <= "9") ||
377 (c >= "a" && c <= "z") ||
378 (c >= "A" && c <= "Z"));
379 }
380
381 /**
382 * Returns `true` if the given `c` character is a separator.
383 *
384 * @param {string} c
385 *
386 * @return {boolean}
387 */
388 function isSeparatorCharacter(c) {
fe692bf9 389 return c === ",";
04454e1e
FG
390 }
391
fe692bf9
FG
392/**
393 * Returns `true` if the given `c` character is a path separator. For example
394 * `:` in `a::b` or a whitespace in `a b`.
04454e1e
FG
395 *
396 * @param {string} c
397 *
398 * @return {boolean}
399 */
fe692bf9
FG
400 function isPathSeparator(c) {
401 return c === ":" || isWhitespace(c);
402 }
403
404 /**
405 * Returns `true` if the previous character is `lookingFor`.
406 *
407 * @param {ParserState} parserState
408 * @param {String} lookingFor
409 *
410 * @return {boolean}
411 */
412 function prevIs(parserState, lookingFor) {
413 let pos = parserState.pos;
414 while (pos > 0) {
415 const c = parserState.userQuery[pos - 1];
416 if (c === lookingFor) {
417 return true;
418 } else if (!isWhitespace(c)) {
419 break;
420 }
421 pos -= 1;
422 }
423 return false;
424 }
425
426 /**
427 * Returns `true` if the last element in the `elems` argument has generics.
428 *
429 * @param {Array<QueryElement>} elems
430 * @param {ParserState} parserState
431 *
432 * @return {boolean}
433 */
434 function isLastElemGeneric(elems, parserState) {
435 return (elems.length > 0 && elems[elems.length - 1].generics.length > 0) ||
436 prevIs(parserState, ">");
437 }
438
439 /**
440 * Increase current parser position until it doesn't find a whitespace anymore.
441 *
442 * @param {ParserState} parserState
443 */
444 function skipWhitespace(parserState) {
445 while (parserState.pos < parserState.userQuery.length) {
446 const c = parserState.userQuery[parserState.pos];
447 if (!isWhitespace(c)) {
448 break;
449 }
450 parserState.pos += 1;
451 }
04454e1e
FG
452 }
453
454 /**
455 * @param {ParsedQuery} query
456 * @param {ParserState} parserState
457 * @param {string} name - Name of the query element.
458 * @param {Array<QueryElement>} generics - List of generics of this query element.
459 *
460 * @return {QueryElement} - The newly created `QueryElement`.
461 */
462 function createQueryElement(query, parserState, name, generics, isInGenerics) {
fe692bf9
FG
463 const path = name.trim();
464 if (path.length === 0 && generics.length === 0) {
465 throw ["Unexpected ", parserState.userQuery[parserState.pos]];
466 } else if (path === "*") {
467 throw ["Unexpected ", "*"];
04454e1e
FG
468 }
469 if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) {
fe692bf9
FG
470 throw ["Cannot have more than one element if you use quotes"];
471 }
472 const typeFilter = parserState.typeFilter;
473 parserState.typeFilter = null;
474 if (name === "!") {
475 if (typeFilter !== null && typeFilter !== "primitive") {
476 throw [
477 "Invalid search type: primitive never type ",
478 "!",
479 " and ",
480 typeFilter,
481 " both specified",
482 ];
04454e1e 483 }
fe692bf9
FG
484 if (generics.length !== 0) {
485 throw [
486 "Never type ",
487 "!",
488 " does not accept generic parameters",
489 ];
490 }
491 return {
492 name: "never",
493 id: -1,
494 fullPath: ["never"],
495 pathWithoutLast: [],
496 pathLast: "never",
497 generics: [],
498 typeFilter: "primitive",
499 };
04454e1e 500 }
fe692bf9
FG
501 if (path.startsWith("::")) {
502 throw ["Paths cannot start with ", "::"];
503 } else if (path.endsWith("::")) {
504 throw ["Paths cannot end with ", "::"];
505 } else if (path.includes("::::")) {
506 throw ["Unexpected ", "::::"];
507 } else if (path.includes(" ::")) {
508 throw ["Unexpected ", " ::"];
509 } else if (path.includes(":: ")) {
510 throw ["Unexpected ", ":: "];
511 }
512 const pathSegments = path.split(/::|\s+/);
04454e1e
FG
513 // In case we only have something like `<p>`, there is no name.
514 if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) {
fe692bf9
FG
515 if (generics.length > 0 || prevIs(parserState, ">")) {
516 throw ["Found generics without a path"];
517 } else {
518 throw ["Unexpected ", parserState.userQuery[parserState.pos]];
519 }
520 }
521 for (const [i, pathSegment] of pathSegments.entries()) {
522 if (pathSegment === "!") {
523 if (i !== 0) {
524 throw ["Never type ", "!", " is not associated item"];
525 }
526 pathSegments[i] = "never";
527 }
04454e1e
FG
528 }
529 parserState.totalElems += 1;
530 if (isInGenerics) {
531 parserState.genericsElems += 1;
532 }
533 return {
fe692bf9 534 name: name.trim(),
49aad941 535 id: -1,
04454e1e
FG
536 fullPath: pathSegments,
537 pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1),
538 pathLast: pathSegments[pathSegments.length - 1],
539 generics: generics,
353b0b11 540 typeFilter,
04454e1e
FG
541 };
542 }
543
544 /**
545 * This function goes through all characters until it reaches an invalid ident character or the
546 * end of the query. It returns the position of the last character of the ident.
547 *
548 * @param {ParserState} parserState
549 *
550 * @return {integer}
551 */
552 function getIdentEndPosition(parserState) {
9ffffee4 553 const start = parserState.pos;
04454e1e 554 let end = parserState.pos;
9ffffee4 555 let foundExclamation = -1;
04454e1e
FG
556 while (parserState.pos < parserState.length) {
557 const c = parserState.userQuery[parserState.pos];
558 if (!isIdentCharacter(c)) {
559 if (c === "!") {
9ffffee4
FG
560 if (foundExclamation !== -1) {
561 throw ["Cannot have more than one ", "!", " in an ident"];
04454e1e 562 } else if (parserState.pos + 1 < parserState.length &&
923072b8
FG
563 isIdentCharacter(parserState.userQuery[parserState.pos + 1])
564 ) {
9ffffee4 565 throw ["Unexpected ", "!", ": it can only be at the end of an ident"];
04454e1e 566 }
9ffffee4 567 foundExclamation = parserState.pos;
04454e1e 568 } else if (isErrorCharacter(c)) {
9ffffee4 569 throw ["Unexpected ", c];
fe692bf9
FG
570 } else if (isPathSeparator(c)) {
571 if (c === ":") {
572 if (!isPathStart(parserState)) {
573 break;
574 }
575 // Skip current ":".
576 parserState.pos += 1;
577 } else {
578 while (parserState.pos + 1 < parserState.length) {
579 const next_c = parserState.userQuery[parserState.pos + 1];
580 if (!isWhitespace(next_c)) {
581 break;
582 }
583 parserState.pos += 1;
584 }
04454e1e 585 }
9ffffee4 586 if (foundExclamation !== -1) {
fe692bf9
FG
587 if (foundExclamation !== start &&
588 isIdentCharacter(parserState.userQuery[foundExclamation - 1])
589 ) {
9ffffee4
FG
590 throw ["Cannot have associated items in macros"];
591 } else {
9ffffee4
FG
592 // while the never type has no associated macros, we still
593 // can parse a path like that
594 foundExclamation = -1;
595 }
596 }
fe692bf9
FG
597 } else if (
598 c === "[" ||
599 isStopCharacter(c) ||
600 isSpecialStartCharacter(c) ||
601 isSeparatorCharacter(c)
602 ) {
603 break;
04454e1e 604 } else {
9ffffee4 605 throw ["Unexpected ", c];
04454e1e
FG
606 }
607 }
608 parserState.pos += 1;
609 end = parserState.pos;
610 }
9ffffee4 611 // if start == end - 1, we got the never type
fe692bf9
FG
612 if (foundExclamation !== -1 &&
613 foundExclamation !== start &&
614 isIdentCharacter(parserState.userQuery[foundExclamation - 1])
615 ) {
9ffffee4
FG
616 if (parserState.typeFilter === null) {
617 parserState.typeFilter = "macro";
618 } else if (parserState.typeFilter !== "macro") {
619 throw [
620 "Invalid search type: macro ",
621 "!",
622 " and ",
623 parserState.typeFilter,
624 " both specified",
625 ];
626 }
627 end = foundExclamation;
628 }
04454e1e
FG
629 return end;
630 }
631
632 /**
633 * @param {ParsedQuery} query
634 * @param {ParserState} parserState
635 * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added.
636 * @param {boolean} isInGenerics
637 */
638 function getNextElem(query, parserState, elems, isInGenerics) {
639 const generics = [];
640
fe692bf9 641 skipWhitespace(parserState);
04454e1e
FG
642 let start = parserState.pos;
643 let end;
fe692bf9
FG
644 if (parserState.userQuery[parserState.pos] === "[") {
645 parserState.pos += 1;
646 getItemsBefore(query, parserState, generics, "]");
647 const typeFilter = parserState.typeFilter;
648 if (typeFilter !== null && typeFilter !== "primitive") {
649 throw [
650 "Invalid search type: primitive ",
651 "[]",
652 " and ",
653 typeFilter,
654 " both specified",
655 ];
656 }
657 parserState.typeFilter = null;
658 parserState.totalElems += 1;
659 if (isInGenerics) {
660 parserState.genericsElems += 1;
661 }
662 elems.push({
663 name: "[]",
664 id: -1,
665 fullPath: ["[]"],
666 pathWithoutLast: [],
667 pathLast: "[]",
668 generics,
669 typeFilter: "primitive",
670 });
04454e1e 671 } else {
fe692bf9
FG
672 const isStringElem = parserState.userQuery[start] === "\"";
673 // We handle the strings on their own mostly to make code easier to follow.
674 if (isStringElem) {
675 start += 1;
676 getStringElem(query, parserState, isInGenerics);
677 end = parserState.pos - 1;
678 } else {
679 end = getIdentEndPosition(parserState);
04454e1e 680 }
fe692bf9
FG
681 if (parserState.pos < parserState.length &&
682 parserState.userQuery[parserState.pos] === "<"
683 ) {
684 if (start >= end) {
685 throw ["Found generics without a path"];
686 }
687 parserState.pos += 1;
688 getItemsBefore(query, parserState, generics, ">");
689 }
690 if (isStringElem) {
691 skipWhitespace(parserState);
692 }
693 if (start >= end && generics.length === 0) {
694 return;
695 }
696 elems.push(
697 createQueryElement(
698 query,
699 parserState,
700 parserState.userQuery.slice(start, end),
701 generics,
702 isInGenerics
703 )
704 );
04454e1e 705 }
04454e1e
FG
706 }
707
708 /**
709 * This function parses the next query element until it finds `endChar`, calling `getNextElem`
710 * to collect each element.
711 *
712 * If there is no `endChar`, this function will implicitly stop at the end without raising an
713 * error.
714 *
715 * @param {ParsedQuery} query
716 * @param {ParserState} parserState
717 * @param {Array<QueryElement>} elems - This is where the new {QueryElement} will be added.
718 * @param {string} endChar - This function will stop when it'll encounter this
719 * character.
720 */
721 function getItemsBefore(query, parserState, elems, endChar) {
722 let foundStopChar = true;
353b0b11
FG
723 let start = parserState.pos;
724
725 // If this is a generic, keep the outer item's type filter around.
726 const oldTypeFilter = parserState.typeFilter;
727 parserState.typeFilter = null;
04454e1e 728
fe692bf9
FG
729 let extra = "";
730 if (endChar === ">") {
731 extra = "<";
732 } else if (endChar === "]") {
733 extra = "[";
734 } else if (endChar === "") {
735 extra = "->";
736 } else {
737 extra = endChar;
738 }
739
04454e1e
FG
740 while (parserState.pos < parserState.length) {
741 const c = parserState.userQuery[parserState.pos];
742 if (c === endChar) {
743 break;
744 } else if (isSeparatorCharacter(c)) {
745 parserState.pos += 1;
746 foundStopChar = true;
747 continue;
748 } else if (c === ":" && isPathStart(parserState)) {
9ffffee4 749 throw ["Unexpected ", "::", ": paths cannot start with ", "::"];
353b0b11
FG
750 } else if (c === ":") {
751 if (parserState.typeFilter !== null) {
752 throw ["Unexpected ", ":"];
753 }
754 if (elems.length === 0) {
755 throw ["Expected type filter before ", ":"];
756 } else if (query.literalSearch) {
fe692bf9 757 throw ["Cannot use quotes on type filter"];
353b0b11
FG
758 }
759 // The type filter doesn't count as an element since it's a modifier.
760 const typeFilterElem = elems.pop();
761 checkExtraTypeFilterCharacters(start, parserState);
762 parserState.typeFilter = typeFilterElem.name;
763 parserState.pos += 1;
764 parserState.totalElems -= 1;
765 query.literalSearch = false;
766 foundStopChar = true;
767 continue;
768 } else if (isEndCharacter(c)) {
9ffffee4 769 throw ["Unexpected ", c, " after ", extra];
04454e1e
FG
770 }
771 if (!foundStopChar) {
fe692bf9
FG
772 let extra = [];
773 if (isLastElemGeneric(query.elems, parserState)) {
774 extra = [" after ", ">"];
775 } else if (prevIs(parserState, "\"")) {
776 throw ["Cannot have more than one element if you use quotes"];
777 }
04454e1e 778 if (endChar !== "") {
9ffffee4
FG
779 throw [
780 "Expected ",
fe692bf9 781 ",",
9ffffee4
FG
782 " or ",
783 endChar,
fe692bf9 784 ...extra,
9ffffee4
FG
785 ", found ",
786 c,
787 ];
04454e1e 788 }
9ffffee4
FG
789 throw [
790 "Expected ",
fe692bf9
FG
791 ",",
792 ...extra,
9ffffee4
FG
793 ", found ",
794 c,
795 ];
04454e1e
FG
796 }
797 const posBefore = parserState.pos;
353b0b11 798 start = parserState.pos;
fe692bf9 799 getNextElem(query, parserState, elems, endChar !== "");
353b0b11 800 if (endChar !== "" && parserState.pos >= parserState.length) {
fe692bf9 801 throw ["Unclosed ", extra];
9ffffee4 802 }
f2b60f7d
FG
803 // This case can be encountered if `getNextElem` encountered a "stop character" right
804 // from the start. For example if you have `,,` or `<>`. In this case, we simply move up
805 // the current position to continue the parsing.
04454e1e
FG
806 if (posBefore === parserState.pos) {
807 parserState.pos += 1;
808 }
809 foundStopChar = false;
810 }
9ffffee4 811 if (parserState.pos >= parserState.length && endChar !== "") {
fe692bf9 812 throw ["Unclosed ", extra];
9ffffee4
FG
813 }
814 // We are either at the end of the string or on the `endChar` character, let's move forward
04454e1e
FG
815 // in any case.
816 parserState.pos += 1;
353b0b11
FG
817
818 parserState.typeFilter = oldTypeFilter;
04454e1e
FG
819 }
820
821 /**
822 * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored
823 * if empty).
824 *
825 * @param {ParserState} parserState
826 */
353b0b11 827 function checkExtraTypeFilterCharacters(start, parserState) {
fe692bf9 828 const query = parserState.userQuery.slice(start, parserState.pos).trim();
04454e1e 829
fe692bf9
FG
830 for (const c in query) {
831 if (!isIdentCharacter(query[c])) {
832 throw [
833 "Unexpected ",
834 query[c],
835 " in type filter (before ",
836 ":",
837 ")",
838 ];
04454e1e
FG
839 }
840 }
841 }
842
843 /**
844 * Parses the provided `query` input to fill `parserState`. If it encounters an error while
845 * parsing `query`, it'll throw an error.
846 *
847 * @param {ParsedQuery} query
848 * @param {ParserState} parserState
849 */
850 function parseInput(query, parserState) {
04454e1e 851 let foundStopChar = true;
353b0b11 852 let start = parserState.pos;
04454e1e
FG
853
854 while (parserState.pos < parserState.length) {
9ffffee4 855 const c = parserState.userQuery[parserState.pos];
04454e1e
FG
856 if (isStopCharacter(c)) {
857 foundStopChar = true;
858 if (isSeparatorCharacter(c)) {
859 parserState.pos += 1;
860 continue;
861 } else if (c === "-" || c === ">") {
862 if (isReturnArrow(parserState)) {
863 break;
864 }
9ffffee4 865 throw ["Unexpected ", c, " (did you mean ", "->", "?)"];
04454e1e 866 }
9ffffee4 867 throw ["Unexpected ", c];
04454e1e
FG
868 } else if (c === ":" && !isPathStart(parserState)) {
869 if (parserState.typeFilter !== null) {
fe692bf9
FG
870 throw [
871 "Unexpected ",
872 ":",
873 " (expected path after type filter ",
874 parserState.typeFilter + ":",
875 ")",
876 ];
877 } else if (query.elems.length === 0) {
9ffffee4 878 throw ["Expected type filter before ", ":"];
04454e1e 879 } else if (query.literalSearch) {
fe692bf9 880 throw ["Cannot use quotes on type filter"];
04454e1e 881 }
04454e1e 882 // The type filter doesn't count as an element since it's a modifier.
353b0b11
FG
883 const typeFilterElem = query.elems.pop();
884 checkExtraTypeFilterCharacters(start, parserState);
885 parserState.typeFilter = typeFilterElem.name;
04454e1e 886 parserState.pos += 1;
353b0b11 887 parserState.totalElems -= 1;
04454e1e
FG
888 query.literalSearch = false;
889 foundStopChar = true;
890 continue;
fe692bf9
FG
891 } else if (isWhitespace(c)) {
892 skipWhitespace(parserState);
893 continue;
04454e1e
FG
894 }
895 if (!foundStopChar) {
fe692bf9
FG
896 let extra = "";
897 if (isLastElemGeneric(query.elems, parserState)) {
898 extra = [" after ", ">"];
899 } else if (prevIs(parserState, "\"")) {
900 throw ["Cannot have more than one element if you use quotes"];
901 }
04454e1e 902 if (parserState.typeFilter !== null) {
9ffffee4
FG
903 throw [
904 "Expected ",
fe692bf9 905 ",",
9ffffee4 906 " or ",
fe692bf9
FG
907 "->",
908 ...extra,
9ffffee4
FG
909 ", found ",
910 c,
911 ];
04454e1e 912 }
9ffffee4
FG
913 throw [
914 "Expected ",
fe692bf9 915 ",",
9ffffee4 916 ", ",
fe692bf9 917 ":",
9ffffee4 918 " or ",
fe692bf9
FG
919 "->",
920 ...extra,
9ffffee4
FG
921 ", found ",
922 c,
923 ];
04454e1e 924 }
9ffffee4 925 const before = query.elems.length;
353b0b11 926 start = parserState.pos;
04454e1e
FG
927 getNextElem(query, parserState, query.elems, false);
928 if (query.elems.length === before) {
929 // Nothing was added, weird... Let's increase the position to not remain stuck.
930 parserState.pos += 1;
931 }
932 foundStopChar = false;
933 }
353b0b11 934 if (parserState.typeFilter !== null) {
fe692bf9
FG
935 throw [
936 "Unexpected ",
937 ":",
938 " (expected path after type filter ",
939 parserState.typeFilter + ":",
940 ")",
941 ];
353b0b11 942 }
04454e1e 943 while (parserState.pos < parserState.length) {
04454e1e
FG
944 if (isReturnArrow(parserState)) {
945 parserState.pos += 2;
fe692bf9 946 skipWhitespace(parserState);
04454e1e
FG
947 // Get returned elements.
948 getItemsBefore(query, parserState, query.returned, "");
949 // Nothing can come afterward!
950 if (query.returned.length === 0) {
9ffffee4 951 throw ["Expected at least one item after ", "->"];
04454e1e
FG
952 }
953 break;
954 } else {
955 parserState.pos += 1;
956 }
957 }
958 }
959
960 /**
961 * Takes the user search input and returns an empty `ParsedQuery`.
962 *
963 * @param {string} userQuery
964 *
965 * @return {ParsedQuery}
966 */
967 function newParsedQuery(userQuery) {
968 return {
969 original: userQuery,
970 userQuery: userQuery.toLowerCase(),
04454e1e
FG
971 elems: [],
972 returned: [],
973 // Total number of "top" elements (does not include generics).
974 foundElems: 0,
975 literalSearch: false,
976 error: null,
49aad941 977 correction: null,
04454e1e
FG
978 };
979 }
980
5099ac24
FG
981 /**
982 * Build an URL with search parameters.
983 *
984 * @param {string} search - The current search being performed.
985 * @param {string|null} filterCrates - The current filtering crate (if any).
04454e1e 986 *
5099ac24
FG
987 * @return {string}
988 */
989 function buildUrl(search, filterCrates) {
04454e1e 990 let extra = "?search=" + encodeURIComponent(search);
5099ac24
FG
991
992 if (filterCrates !== null) {
993 extra += "&filter-crate=" + encodeURIComponent(filterCrates);
994 }
995 return getNakedUrl() + extra + window.location.hash;
996 }
997
998 /**
999 * Return the filtering crate or `null` if there is none.
1000 *
1001 * @return {string|null}
1002 */
1003 function getFilterCrates() {
04454e1e 1004 const elem = document.getElementById("crate-search");
5099ac24
FG
1005
1006 if (elem &&
f2b60f7d 1007 elem.value !== "all crates" &&
923072b8
FG
1008 hasOwnPropertyRustdoc(rawSearchIndex, elem.value)
1009 ) {
5099ac24
FG
1010 return elem.value;
1011 }
1012 return null;
1013 }
1014
cdc7bbd5 1015 /**
04454e1e
FG
1016 * Parses the query.
1017 *
1018 * The supported syntax by this parser is as follow:
1019 *
9ffffee4 1020 * ident = *(ALPHA / DIGIT / "_")
fe692bf9
FG
1021 * path = ident *(DOUBLE-COLON/{WS} ident) [!]
1022 * slice = OPEN-SQUARE-BRACKET [ nonempty-arg-list ] CLOSE-SQUARE-BRACKET
1023 * arg = [type-filter *WS COLON *WS] (path [generics] / slice)
1024 * type-sep = *WS COMMA *(COMMA)
04454e1e 1025 * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep)
353b0b11
FG
1026 * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list ] *(type-sep)
1027 * CLOSE-ANGLE-BRACKET
04454e1e
FG
1028 * return-args = RETURN-ARROW *(type-sep) nonempty-arg-list
1029 *
1030 * exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ]
353b0b11 1031 * type-search = [ nonempty-arg-list ] [ return-args ]
04454e1e
FG
1032 *
1033 * query = *WS (exact-search / type-search) *WS
1034 *
1035 * type-filter = (
1036 * "mod" /
1037 * "externcrate" /
1038 * "import" /
1039 * "struct" /
1040 * "enum" /
1041 * "fn" /
1042 * "type" /
1043 * "static" /
1044 * "trait" /
1045 * "impl" /
1046 * "tymethod" /
1047 * "method" /
1048 * "structfield" /
1049 * "variant" /
1050 * "macro" /
1051 * "primitive" /
1052 * "associatedtype" /
1053 * "constant" /
1054 * "associatedconstant" /
1055 * "union" /
1056 * "foreigntype" /
1057 * "keyword" /
1058 * "existential" /
1059 * "attr" /
1060 * "derive" /
1061 * "traitalias")
1062 *
1063 * OPEN-ANGLE-BRACKET = "<"
1064 * CLOSE-ANGLE-BRACKET = ">"
fe692bf9
FG
1065 * OPEN-SQUARE-BRACKET = "["
1066 * CLOSE-SQUARE-BRACKET = "]"
04454e1e
FG
1067 * COLON = ":"
1068 * DOUBLE-COLON = "::"
1069 * QUOTE = %x22
1070 * COMMA = ","
1071 * RETURN-ARROW = "->"
1072 *
1073 * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z
1074 * DIGIT = %x30-39
1075 * WS = %x09 / " "
1076 *
1077 * @param {string} val - The user query
1078 *
1079 * @return {ParsedQuery} - The parsed query
cdc7bbd5 1080 */
04454e1e 1081 function parseQuery(userQuery) {
353b0b11
FG
1082 function convertTypeFilterOnElem(elem) {
1083 if (elem.typeFilter !== null) {
1084 let typeFilter = elem.typeFilter;
1085 if (typeFilter === "const") {
1086 typeFilter = "constant";
1087 }
1088 elem.typeFilter = itemTypeFromName(typeFilter);
1089 } else {
1090 elem.typeFilter = NO_TYPE_FILTER;
1091 }
1092 for (const elem2 of elem.generics) {
1093 convertTypeFilterOnElem(elem2);
1094 }
1095 }
04454e1e
FG
1096 userQuery = userQuery.trim();
1097 const parserState = {
1098 length: userQuery.length,
1099 pos: 0,
1100 // Total number of elements (includes generics).
1101 totalElems: 0,
1102 genericsElems: 0,
1103 typeFilter: null,
1104 userQuery: userQuery.toLowerCase(),
1105 };
1106 let query = newParsedQuery(userQuery);
1107
1108 try {
1109 parseInput(query, parserState);
353b0b11
FG
1110 for (const elem of query.elems) {
1111 convertTypeFilterOnElem(elem);
1112 }
1113 for (const elem of query.returned) {
1114 convertTypeFilterOnElem(elem);
cdc7bbd5 1115 }
04454e1e
FG
1116 } catch (err) {
1117 query = newParsedQuery(userQuery);
9ffffee4 1118 query.error = err;
04454e1e 1119 return query;
cdc7bbd5
XL
1120 }
1121
04454e1e
FG
1122 if (!query.literalSearch) {
1123 // If there is more than one element in the query, we switch to literalSearch in any
1124 // case.
1125 query.literalSearch = parserState.totalElems > 1;
1126 }
1127 query.foundElems = query.elems.length + query.returned.length;
1128 return query;
1129 }
1130
1131 /**
1132 * Creates the query results.
1133 *
1134 * @param {Array<Result>} results_in_args
1135 * @param {Array<Result>} results_returned
49aad941 1136 * @param {Array<Result>} results_others
04454e1e
FG
1137 * @param {ParsedQuery} parsedQuery
1138 *
1139 * @return {ResultsTable}
1140 */
1141 function createQueryResults(results_in_args, results_returned, results_others, parsedQuery) {
1142 return {
1143 "in_args": results_in_args,
1144 "returned": results_returned,
1145 "others": results_others,
1146 "query": parsedQuery,
1147 };
1148 }
cdc7bbd5 1149
04454e1e
FG
1150 /**
1151 * Executes the parsed query and builds a {ResultsTable}.
1152 *
1153 * @param {ParsedQuery} parsedQuery - The parsed user query
1154 * @param {Object} searchWords - The list of search words to query against
1155 * @param {Object} [filterCrates] - Crate to search in if defined
923072b8 1156 * @param {Object} [currentCrate] - Current crate, to rank results from this crate higher
04454e1e
FG
1157 *
1158 * @return {ResultsTable}
1159 */
923072b8 1160 function execQuery(parsedQuery, searchWords, filterCrates, currentCrate) {
353b0b11
FG
1161 const results_others = new Map(), results_in_args = new Map(),
1162 results_returned = new Map();
cdc7bbd5 1163
353b0b11
FG
1164 /**
1165 * Add extra data to result objects, and filter items that have been
1166 * marked for removal.
1167 *
1168 * @param {[ResultObject]} results
1169 * @returns {[ResultObject]}
1170 */
17df50a5 1171 function transformResults(results) {
353b0b11 1172 const duplicates = new Set();
04454e1e 1173 const out = [];
a2a8927a 1174
04454e1e 1175 for (const result of results) {
a2a8927a 1176 if (result.id > -1) {
04454e1e 1177 const obj = searchIndex[result.id];
353b0b11 1178 obj.dist = result.dist;
04454e1e 1179 const res = buildHrefAndPath(obj);
17df50a5
XL
1180 obj.displayPath = pathSplitter(res[0]);
1181 obj.fullPath = obj.displayPath + obj.name;
1182 // To be sure than it some items aren't considered as duplicate.
1183 obj.fullPath += "|" + obj.ty;
a2a8927a 1184
353b0b11 1185 if (duplicates.has(obj.fullPath)) {
a2a8927a
XL
1186 continue;
1187 }
353b0b11 1188 duplicates.add(obj.fullPath);
a2a8927a 1189
17df50a5
XL
1190 obj.href = res[1];
1191 out.push(obj);
1192 if (out.length >= MAX_RESULTS) {
1193 break;
cdc7bbd5
XL
1194 }
1195 }
1196 }
1197 return out;
1198 }
1199
353b0b11
FG
1200 /**
1201 * This function takes a result map, and sorts it by various criteria, including edit
1202 * distance, substring match, and the crate it comes from.
1203 *
1204 * @param {Results} results
1205 * @param {boolean} isType
1206 * @param {string} preferredCrate
1207 * @returns {[ResultObject]}
1208 */
923072b8 1209 function sortResults(results, isType, preferredCrate) {
cdc7bbd5 1210 // if there are no results then return to default and fail
353b0b11 1211 if (results.size === 0) {
cdc7bbd5
XL
1212 return [];
1213 }
1214
353b0b11
FG
1215 const userQuery = parsedQuery.userQuery;
1216 const result_list = [];
1217 for (const result of results.values()) {
1218 result.word = searchWords[result.id];
1219 result.item = searchIndex[result.id] || {};
1220 result_list.push(result);
1221 }
1222
1223 result_list.sort((aaa, bbb) => {
04454e1e 1224 let a, b;
cdc7bbd5
XL
1225
1226 // sort by exact match with regard to the last word (mismatch goes later)
04454e1e
FG
1227 a = (aaa.word !== userQuery);
1228 b = (bbb.word !== userQuery);
923072b8
FG
1229 if (a !== b) {
1230 return a - b;
1231 }
cdc7bbd5 1232
9c376795
FG
1233 // sort by index of keyword in item name (no literal occurrence goes later)
1234 a = (aaa.index < 0);
1235 b = (bbb.index < 0);
1236 if (a !== b) {
1237 return a - b;
1238 }
1239
1240 // Sort by distance in the path part, if specified
1241 // (less changes required to match means higher rankings)
353b0b11
FG
1242 a = aaa.path_dist;
1243 b = bbb.path_dist;
9c376795
FG
1244 if (a !== b) {
1245 return a - b;
1246 }
1247
1248 // (later literal occurrence, if any, goes later)
1249 a = aaa.index;
1250 b = bbb.index;
1251 if (a !== b) {
1252 return a - b;
1253 }
1254
1255 // Sort by distance in the name part, the last part of the path
cdc7bbd5 1256 // (less changes required to match means higher rankings)
353b0b11
FG
1257 a = (aaa.dist);
1258 b = (bbb.dist);
1259 if (a !== b) {
1260 return a - b;
1261 }
1262
1263 // sort deprecated items later
1264 a = aaa.item.deprecated;
1265 b = bbb.item.deprecated;
923072b8
FG
1266 if (a !== b) {
1267 return a - b;
1268 }
cdc7bbd5 1269
923072b8
FG
1270 // sort by crate (current crate comes first)
1271 a = (aaa.item.crate !== preferredCrate);
1272 b = (bbb.item.crate !== preferredCrate);
1273 if (a !== b) {
1274 return a - b;
1275 }
cdc7bbd5
XL
1276
1277 // sort by item name length (longer goes later)
1278 a = aaa.word.length;
1279 b = bbb.word.length;
923072b8
FG
1280 if (a !== b) {
1281 return a - b;
1282 }
cdc7bbd5
XL
1283
1284 // sort by item name (lexicographically larger goes later)
1285 a = aaa.word;
1286 b = bbb.word;
923072b8
FG
1287 if (a !== b) {
1288 return (a > b ? +1 : -1);
1289 }
cdc7bbd5 1290
cdc7bbd5
XL
1291 // special precedence for primitive and keyword pages
1292 if ((aaa.item.ty === TY_PRIMITIVE && bbb.item.ty !== TY_KEYWORD) ||
1293 (aaa.item.ty === TY_KEYWORD && bbb.item.ty !== TY_PRIMITIVE)) {
1294 return -1;
1295 }
1296 if ((bbb.item.ty === TY_PRIMITIVE && aaa.item.ty !== TY_PRIMITIVE) ||
1297 (bbb.item.ty === TY_KEYWORD && aaa.item.ty !== TY_KEYWORD)) {
1298 return 1;
1299 }
1300
1301 // sort by description (no description goes later)
1302 a = (aaa.item.desc === "");
1303 b = (bbb.item.desc === "");
923072b8
FG
1304 if (a !== b) {
1305 return a - b;
1306 }
cdc7bbd5
XL
1307
1308 // sort by type (later occurrence in `itemTypes` goes later)
1309 a = aaa.item.ty;
1310 b = bbb.item.ty;
923072b8
FG
1311 if (a !== b) {
1312 return a - b;
1313 }
cdc7bbd5
XL
1314
1315 // sort by path (lexicographically larger goes later)
1316 a = aaa.item.path;
1317 b = bbb.item.path;
923072b8
FG
1318 if (a !== b) {
1319 return (a > b ? +1 : -1);
1320 }
cdc7bbd5
XL
1321
1322 // que sera, sera
1323 return 0;
1324 });
1325
04454e1e
FG
1326 let nameSplit = null;
1327 if (parsedQuery.elems.length === 1) {
1328 const hasPath = typeof parsedQuery.elems[0].path === "undefined";
1329 nameSplit = hasPath ? null : parsedQuery.elems[0].path;
1330 }
cdc7bbd5 1331
353b0b11 1332 for (const result of result_list) {
cdc7bbd5
XL
1333 // this validation does not make sense when searching by types
1334 if (result.dontValidate) {
1335 continue;
1336 }
04454e1e 1337 const name = result.item.name.toLowerCase(),
cdc7bbd5
XL
1338 path = result.item.path.toLowerCase(),
1339 parent = result.item.parent;
1340
04454e1e 1341 if (!isType && !validateResult(name, path, nameSplit, parent)) {
cdc7bbd5
XL
1342 result.id = -1;
1343 }
1344 }
353b0b11 1345 return transformResults(result_list);
cdc7bbd5
XL
1346 }
1347
04454e1e 1348 /**
fe692bf9
FG
1349 * This function checks generics in search query `queryElem` can all be found in the
1350 * search index (`fnType`),
04454e1e 1351 *
fe692bf9
FG
1352 * @param {FunctionType} fnType - The object to check.
1353 * @param {QueryElement} queryElem - The element from the parsed query.
04454e1e 1354 *
fe692bf9 1355 * @return {boolean} - Returns true if a match, false otherwise.
04454e1e 1356 */
fe692bf9
FG
1357 function checkGenerics(fnType, queryElem) {
1358 return unifyFunctionTypes(fnType.generics, queryElem.generics);
1359 }
1360 /**
1361 * This function checks if a list of search query `queryElems` can all be found in the
1362 * search index (`fnTypes`).
1363 *
1364 * @param {Array<FunctionType>} fnTypes - The objects to check.
1365 * @param {Array<QueryElement>} queryElems - The elements from the parsed query.
1366 *
1367 * @return {boolean} - Returns true if a match, false otherwise.
1368 */
1369 function unifyFunctionTypes(fnTypes, queryElems) {
49aad941
FG
1370 // This search engine implements order-agnostic unification. There
1371 // should be no missing duplicates (generics have "bag semantics"),
1372 // and the row is allowed to have extras.
fe692bf9
FG
1373 if (queryElems.length === 0) {
1374 return true;
1375 }
1376 if (!fnTypes || fnTypes.length === 0) {
1377 return false;
1378 }
1379 /**
1380 * @type Map<integer, QueryElement[]>
1381 */
1382 const queryElemSet = new Map();
1383 const addQueryElemToQueryElemSet = function addQueryElemToQueryElemSet(queryElem) {
1384 let currentQueryElemList;
1385 if (queryElemSet.has(queryElem.id)) {
1386 currentQueryElemList = queryElemSet.get(queryElem.id);
1387 } else {
1388 currentQueryElemList = [];
1389 queryElemSet.set(queryElem.id, currentQueryElemList);
1390 }
1391 currentQueryElemList.push(queryElem);
1392 };
1393 for (const queryElem of queryElems) {
1394 addQueryElemToQueryElemSet(queryElem);
1395 }
1396 /**
1397 * @type Map<integer, FunctionType[]>
1398 */
1399 const fnTypeSet = new Map();
1400 const addFnTypeToFnTypeSet = function addFnTypeToFnTypeSet(fnType) {
1401 // Pure generic, or an item that's not matched by any query elems.
1402 // Try [unboxing] it.
1403 //
1404 // [unboxing]:
1405 // http://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf
1406 const queryContainsArrayOrSliceElem = queryElemSet.has(typeNameIdOfArrayOrSlice);
1407 if (fnType.id === -1 || !(
1408 queryElemSet.has(fnType.id) ||
1409 (fnType.id === typeNameIdOfSlice && queryContainsArrayOrSliceElem) ||
1410 (fnType.id === typeNameIdOfArray && queryContainsArrayOrSliceElem)
1411 )) {
1412 for (const innerFnType of fnType.generics) {
1413 addFnTypeToFnTypeSet(innerFnType);
cdc7bbd5 1414 }
fe692bf9
FG
1415 return;
1416 }
1417 let currentQueryElemList = queryElemSet.get(fnType.id) || [];
1418 let matchIdx = currentQueryElemList.findIndex(queryElem => {
1419 return typePassesFilter(queryElem.typeFilter, fnType.ty) &&
1420 checkGenerics(fnType, queryElem);
1421 });
1422 if (matchIdx === -1 &&
1423 (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) &&
1424 queryContainsArrayOrSliceElem
1425 ) {
1426 currentQueryElemList = queryElemSet.get(typeNameIdOfArrayOrSlice) || [];
1427 matchIdx = currentQueryElemList.findIndex(queryElem => {
1428 return typePassesFilter(queryElem.typeFilter, fnType.ty) &&
1429 checkGenerics(fnType, queryElem);
1430 });
1431 }
1432 // None of the query elems match the function type.
1433 // Try [unboxing] it.
1434 if (matchIdx === -1) {
1435 for (const innerFnType of fnType.generics) {
1436 addFnTypeToFnTypeSet(innerFnType);
04454e1e 1437 }
fe692bf9
FG
1438 return;
1439 }
1440 let currentFnTypeList;
1441 if (fnTypeSet.has(fnType.id)) {
1442 currentFnTypeList = fnTypeSet.get(fnType.id);
1443 } else {
1444 currentFnTypeList = [];
1445 fnTypeSet.set(fnType.id, currentFnTypeList);
1446 }
1447 currentFnTypeList.push(fnType);
1448 };
1449 for (const fnType of fnTypes) {
1450 addFnTypeToFnTypeSet(fnType);
1451 }
1452 const doHandleQueryElemList = (currentFnTypeList, queryElemList) => {
1453 if (queryElemList.length === 0) {
1454 return true;
1455 }
1456 // Multiple items in one list might match multiple items in another.
1457 // Since an item with fewer generics can match an item with more, we
1458 // need to check all combinations for a potential match.
1459 const queryElem = queryElemList.pop();
1460 const l = currentFnTypeList.length;
1461 for (let i = 0; i < l; i += 1) {
1462 const fnType = currentFnTypeList[i];
1463 if (!typePassesFilter(queryElem.typeFilter, fnType.ty)) {
1464 continue;
353b0b11 1465 }
fe692bf9
FG
1466 if (queryElem.generics.length === 0 || checkGenerics(fnType, queryElem)) {
1467 currentFnTypeList.splice(i, 1);
1468 const result = doHandleQueryElemList(currentFnTypeList, queryElemList);
1469 if (result) {
1470 return true;
cdc7bbd5 1471 }
fe692bf9 1472 currentFnTypeList.splice(i, 0, fnType);
353b0b11 1473 }
fe692bf9
FG
1474 }
1475 return false;
1476 };
1477 const handleQueryElemList = (id, queryElemList) => {
1478 if (!fnTypeSet.has(id)) {
1479 if (id === typeNameIdOfArrayOrSlice) {
1480 return handleQueryElemList(typeNameIdOfSlice, queryElemList) ||
1481 handleQueryElemList(typeNameIdOfArray, queryElemList);
cdc7bbd5 1482 }
fe692bf9
FG
1483 return false;
1484 }
1485 const currentFnTypeList = fnTypeSet.get(id);
1486 if (currentFnTypeList.length < queryElemList.length) {
1487 // It's not possible for all the query elems to find a match.
1488 return false;
1489 }
1490 const result = doHandleQueryElemList(currentFnTypeList, queryElemList);
1491 if (result) {
1492 // Found a solution.
1493 // Any items that weren't used for it can be unboxed, and might form
1494 // part of the solution for another item.
1495 for (const innerFnType of currentFnTypeList) {
1496 addFnTypeToFnTypeSet(innerFnType);
04454e1e 1497 }
fe692bf9 1498 fnTypeSet.delete(id);
353b0b11 1499 }
fe692bf9
FG
1500 return result;
1501 };
1502 let queryElemSetSize = -1;
1503 while (queryElemSetSize !== queryElemSet.size) {
1504 queryElemSetSize = queryElemSet.size;
1505 for (const [id, queryElemList] of queryElemSet) {
1506 if (handleQueryElemList(id, queryElemList)) {
1507 queryElemSet.delete(id);
04454e1e 1508 }
cdc7bbd5
XL
1509 }
1510 }
fe692bf9 1511 return queryElemSetSize === 0;
cdc7bbd5
XL
1512 }
1513
a2a8927a 1514 /**
04454e1e
FG
1515 * This function checks if the object (`row`) matches the given type (`elem`) and its
1516 * generics (if any).
1517 *
fe692bf9 1518 * @param {Array<FunctionType>} list
04454e1e
FG
1519 * @param {QueryElement} elem - The element from the parsed query.
1520 *
49aad941 1521 * @return {boolean} - Returns true if found, false otherwise.
04454e1e 1522 */
fe692bf9
FG
1523 function checkIfInList(list, elem) {
1524 for (const entry of list) {
49aad941
FG
1525 if (checkType(entry, elem)) {
1526 return true;
04454e1e
FG
1527 }
1528 }
49aad941 1529 return false;
04454e1e
FG
1530 }
1531
1532 /**
1533 * This function checks if the object (`row`) matches the given type (`elem`) and its
a2a8927a
XL
1534 * generics (if any).
1535 *
04454e1e
FG
1536 * @param {Row} row
1537 * @param {QueryElement} elem - The element from the parsed query.
a2a8927a 1538 *
49aad941 1539 * @return {boolean} - Returns true if the type matches, false otherwise.
a2a8927a 1540 */
49aad941
FG
1541 function checkType(row, elem) {
1542 if (row.id === -1) {
04454e1e 1543 // This is a pure "generic" search, no need to run other checks.
fe692bf9 1544 return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false;
04454e1e 1545 }
cdc7bbd5 1546
fe692bf9
FG
1547 const matchesExact = row.id === elem.id;
1548 const matchesArrayOrSlice = elem.id === typeNameIdOfArrayOrSlice &&
1549 (row.id === typeNameIdOfSlice || row.id === typeNameIdOfArray);
1550
1551 if ((matchesExact || matchesArrayOrSlice) &&
1552 typePassesFilter(elem.typeFilter, row.ty)) {
49aad941
FG
1553 if (elem.generics.length > 0) {
1554 return checkGenerics(row, elem);
cdc7bbd5 1555 }
49aad941 1556 return true;
cdc7bbd5 1557 }
49aad941
FG
1558
1559 // If the current item does not match, try [unboxing] the generic.
1560 // [unboxing]:
1561 // https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf
fe692bf9 1562 return checkIfInList(row.generics, elem);
cdc7bbd5
XL
1563 }
1564
353b0b11 1565 function checkPath(contains, ty, maxEditDistance) {
cdc7bbd5
XL
1566 if (contains.length === 0) {
1567 return 0;
1568 }
353b0b11 1569 let ret_dist = maxEditDistance + 1;
04454e1e 1570 const path = ty.path.split("::");
cdc7bbd5
XL
1571
1572 if (ty.parent && ty.parent.name) {
1573 path.push(ty.parent.name.toLowerCase());
1574 }
1575
04454e1e
FG
1576 const length = path.length;
1577 const clength = contains.length;
cdc7bbd5 1578 if (clength > length) {
353b0b11 1579 return maxEditDistance + 1;
cdc7bbd5 1580 }
04454e1e 1581 for (let i = 0; i < length; ++i) {
cdc7bbd5
XL
1582 if (i + clength > length) {
1583 break;
1584 }
353b0b11 1585 let dist_total = 0;
04454e1e
FG
1586 let aborted = false;
1587 for (let x = 0; x < clength; ++x) {
353b0b11
FG
1588 const dist = editDistance(path[i + x], contains[x], maxEditDistance);
1589 if (dist > maxEditDistance) {
cdc7bbd5
XL
1590 aborted = true;
1591 break;
1592 }
353b0b11 1593 dist_total += dist;
cdc7bbd5 1594 }
17df50a5 1595 if (!aborted) {
353b0b11 1596 ret_dist = Math.min(ret_dist, Math.round(dist_total / clength));
cdc7bbd5
XL
1597 }
1598 }
353b0b11 1599 return ret_dist;
cdc7bbd5
XL
1600 }
1601
1602 function typePassesFilter(filter, type) {
5099ac24
FG
1603 // No filter or Exact mach
1604 if (filter <= NO_TYPE_FILTER || filter === type) return true;
cdc7bbd5
XL
1605
1606 // Match related items
04454e1e 1607 const name = itemTypes[type];
cdc7bbd5
XL
1608 switch (itemTypes[filter]) {
1609 case "constant":
1610 return name === "associatedconstant";
1611 case "fn":
1612 return name === "method" || name === "tymethod";
1613 case "type":
1614 return name === "primitive" || name === "associatedtype";
1615 case "trait":
1616 return name === "traitalias";
1617 }
1618
1619 // No match
1620 return false;
1621 }
1622
1623 function createAliasFromItem(item) {
1624 return {
1625 crate: item.crate,
1626 name: item.name,
1627 path: item.path,
1628 desc: item.desc,
1629 ty: item.ty,
1630 parent: item.parent,
1631 type: item.type,
1632 is_alias: true,
353b0b11 1633 deprecated: item.deprecated,
cdc7bbd5
XL
1634 };
1635 }
1636
923072b8 1637 function handleAliases(ret, query, filterCrates, currentCrate) {
04454e1e 1638 const lowerQuery = query.toLowerCase();
cdc7bbd5
XL
1639 // We separate aliases and crate aliases because we want to have current crate
1640 // aliases to be before the others in the displayed results.
04454e1e
FG
1641 const aliases = [];
1642 const crateAliases = [];
5099ac24 1643 if (filterCrates !== null) {
353b0b11
FG
1644 if (ALIASES.has(filterCrates) && ALIASES.get(filterCrates).has(lowerQuery)) {
1645 const query_aliases = ALIASES.get(filterCrates).get(lowerQuery);
04454e1e
FG
1646 for (const alias of query_aliases) {
1647 aliases.push(createAliasFromItem(searchIndex[alias]));
cdc7bbd5
XL
1648 }
1649 }
1650 } else {
353b0b11
FG
1651 for (const [crate, crateAliasesIndex] of ALIASES) {
1652 if (crateAliasesIndex.has(lowerQuery)) {
923072b8 1653 const pushTo = crate === currentCrate ? crateAliases : aliases;
353b0b11 1654 const query_aliases = crateAliasesIndex.get(lowerQuery);
04454e1e
FG
1655 for (const alias of query_aliases) {
1656 pushTo.push(createAliasFromItem(searchIndex[alias]));
cdc7bbd5
XL
1657 }
1658 }
353b0b11 1659 }
cdc7bbd5
XL
1660 }
1661
04454e1e 1662 const sortFunc = (aaa, bbb) => {
cdc7bbd5
XL
1663 if (aaa.path < bbb.path) {
1664 return 1;
1665 } else if (aaa.path === bbb.path) {
1666 return 0;
1667 }
1668 return -1;
1669 };
1670 crateAliases.sort(sortFunc);
1671 aliases.sort(sortFunc);
1672
04454e1e
FG
1673 const pushFunc = alias => {
1674 alias.alias = query;
1675 const res = buildHrefAndPath(alias);
cdc7bbd5
XL
1676 alias.displayPath = pathSplitter(res[0]);
1677 alias.fullPath = alias.displayPath + alias.name;
1678 alias.href = res[1];
1679
1680 ret.others.unshift(alias);
1681 if (ret.others.length > MAX_RESULTS) {
1682 ret.others.pop();
1683 }
1684 };
923072b8
FG
1685
1686 aliases.forEach(pushFunc);
1687 crateAliases.forEach(pushFunc);
cdc7bbd5
XL
1688 }
1689
a2a8927a 1690 /**
04454e1e 1691 * This function adds the given result into the provided `results` map if it matches the
a2a8927a
XL
1692 * following condition:
1693 *
353b0b11
FG
1694 * * If it is a "literal search" (`parsedQuery.literalSearch`), then `dist` must be 0.
1695 * * If it is not a "literal search", `dist` must be <= `maxEditDistance`.
a2a8927a 1696 *
04454e1e 1697 * The `results` map contains information which will be used to sort the search results:
a2a8927a 1698 *
04454e1e 1699 * * `fullId` is a `string`` used as the key of the object we use for the `results` map.
a2a8927a
XL
1700 * * `id` is the index in both `searchWords` and `searchIndex` arrays for this element.
1701 * * `index` is an `integer`` used to sort by the position of the word in the item's name.
353b0b11
FG
1702 * * `dist` is the main metric used to sort the search results.
1703 * * `path_dist` is zero if a single-component search query is used, otherwise it's the
9c376795 1704 * distance computed for everything other than the last path component.
a2a8927a 1705 *
04454e1e 1706 * @param {Results} results
a2a8927a
XL
1707 * @param {string} fullId
1708 * @param {integer} id
1709 * @param {integer} index
353b0b11
FG
1710 * @param {integer} dist
1711 * @param {integer} path_dist
a2a8927a 1712 */
353b0b11
FG
1713 function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) {
1714 const inBounds = dist <= maxEditDistance || index !== -1;
1715 if (dist === 0 || (!parsedQuery.literalSearch && inBounds)) {
1716 if (results.has(fullId)) {
1717 const result = results.get(fullId);
1718 if (result.dontValidate || result.dist <= dist) {
a2a8927a
XL
1719 return;
1720 }
1721 }
353b0b11 1722 results.set(fullId, {
a2a8927a
XL
1723 id: id,
1724 index: index,
04454e1e 1725 dontValidate: parsedQuery.literalSearch,
353b0b11
FG
1726 dist: dist,
1727 path_dist: path_dist,
1728 });
a2a8927a
XL
1729 }
1730 }
1731
04454e1e
FG
1732 /**
1733 * This function is called in case the query is only one element (with or without generics).
1734 * This element will be compared to arguments' and returned values' items and also to items.
1735 *
353b0b11 1736 * Other important thing to note: since there is only one element, we use edit
04454e1e
FG
1737 * distance for name comparisons.
1738 *
1739 * @param {Row} row
1740 * @param {integer} pos - Position in the `searchIndex`.
1741 * @param {QueryElement} elem - The element from the parsed query.
1742 * @param {Results} results_others - Unqualified results (not in arguments nor in
1743 * returned values).
1744 * @param {Results} results_in_args - Matching arguments results.
1745 * @param {Results} results_returned - Matching returned arguments results.
1746 */
1747 function handleSingleArg(
1748 row,
1749 pos,
1750 elem,
1751 results_others,
1752 results_in_args,
9ffffee4 1753 results_returned,
353b0b11 1754 maxEditDistance
04454e1e
FG
1755 ) {
1756 if (!row || (filterCrates !== null && row.crate !== filterCrates)) {
1757 return;
1758 }
49aad941 1759 let index = -1, path_dist = 0;
04454e1e 1760 const fullId = row.id;
9c376795 1761 const searchWord = searchWords[pos];
04454e1e 1762
fe692bf9
FG
1763 const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem);
1764 if (in_args) {
49aad941
FG
1765 // path_dist is 0 because no parent path information is currently stored
1766 // in the search index
1767 addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance);
1768 }
fe692bf9
FG
1769 const returned = row.type && row.type.output && checkIfInList(row.type.output, elem);
1770 if (returned) {
49aad941
FG
1771 addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance);
1772 }
04454e1e 1773
353b0b11 1774 if (!typePassesFilter(elem.typeFilter, row.ty)) {
04454e1e
FG
1775 return;
1776 }
04454e1e 1777
9c376795
FG
1778 const row_index = row.normalizedName.indexOf(elem.pathLast);
1779 const word_index = searchWord.indexOf(elem.pathLast);
1780
1781 // lower indexes are "better" matches
1782 // rank based on the "best" match
1783 if (row_index === -1) {
1784 index = word_index;
1785 } else if (word_index === -1) {
1786 index = row_index;
1787 } else if (word_index < row_index) {
1788 index = word_index;
1789 } else {
1790 index = row_index;
04454e1e 1791 }
cdc7bbd5 1792
04454e1e 1793 if (elem.fullPath.length > 1) {
353b0b11
FG
1794 path_dist = checkPath(elem.pathWithoutLast, row, maxEditDistance);
1795 if (path_dist > maxEditDistance) {
04454e1e 1796 return;
cdc7bbd5
XL
1797 }
1798 }
04454e1e 1799
9c376795
FG
1800 if (parsedQuery.literalSearch) {
1801 if (searchWord === elem.name) {
353b0b11 1802 addIntoResults(results_others, fullId, pos, index, 0, path_dist);
cdc7bbd5 1803 }
04454e1e 1804 return;
04454e1e 1805 }
9c376795 1806
49aad941 1807 const dist = editDistance(searchWord, elem.pathLast, maxEditDistance);
9c376795 1808
353b0b11 1809 if (index === -1 && dist + path_dist > maxEditDistance) {
9c376795 1810 return;
04454e1e 1811 }
9c376795 1812
353b0b11 1813 addIntoResults(results_others, fullId, pos, index, dist, path_dist, maxEditDistance);
04454e1e 1814 }
cdc7bbd5 1815
04454e1e
FG
1816 /**
1817 * This function is called in case the query has more than one element. In this case, it'll
1818 * try to match the items which validates all the elements. For `aa -> bb` will look for
1819 * functions which have a parameter `aa` and has `bb` in its returned values.
1820 *
1821 * @param {Row} row
1822 * @param {integer} pos - Position in the `searchIndex`.
1823 * @param {Object} results
1824 */
49aad941 1825 function handleArgs(row, pos, results) {
fe692bf9 1826 if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) {
04454e1e
FG
1827 return;
1828 }
cdc7bbd5 1829
04454e1e 1830 // If the result is too "bad", we return false and it ends this search.
fe692bf9 1831 if (!unifyFunctionTypes(row.type.inputs, parsedQuery.elems)) {
04454e1e
FG
1832 return;
1833 }
fe692bf9 1834 if (!unifyFunctionTypes(row.type.output, parsedQuery.returned)) {
04454e1e
FG
1835 return;
1836 }
cdc7bbd5 1837
49aad941 1838 addIntoResults(results, row.id, pos, 0, 0, 0, Number.MAX_VALUE);
04454e1e
FG
1839 }
1840
1841 function innerRunQuery() {
1842 let elem, i, nSearchWords, in_returned, row;
1843
9ffffee4
FG
1844 let queryLen = 0;
1845 for (const elem of parsedQuery.elems) {
1846 queryLen += elem.name.length;
1847 }
1848 for (const elem of parsedQuery.returned) {
1849 queryLen += elem.name.length;
1850 }
353b0b11 1851 const maxEditDistance = Math.floor(queryLen / 3);
9ffffee4 1852
49aad941
FG
1853 /**
1854 * Convert names to ids in parsed query elements.
1855 * This is not used for the "In Names" tab, but is used for the
1856 * "In Params", "In Returns", and "In Function Signature" tabs.
1857 *
1858 * If there is no matching item, but a close-enough match, this
1859 * function also that correction.
1860 *
1861 * See `buildTypeMapIndex` for more information.
1862 *
1863 * @param {QueryElement} elem
1864 */
1865 function convertNameToId(elem) {
1866 if (typeNameIdMap.has(elem.name)) {
1867 elem.id = typeNameIdMap.get(elem.name);
1868 } else if (!parsedQuery.literalSearch) {
1869 let match = -1;
1870 let matchDist = maxEditDistance + 1;
1871 let matchName = "";
1872 for (const [name, id] of typeNameIdMap) {
1873 const dist = editDistance(name, elem.name, maxEditDistance);
1874 if (dist <= matchDist && dist <= maxEditDistance) {
1875 if (dist === matchDist && matchName > name) {
1876 continue;
1877 }
1878 match = id;
1879 matchDist = dist;
1880 matchName = name;
1881 }
1882 }
1883 if (match !== -1) {
1884 parsedQuery.correction = matchName;
1885 }
1886 elem.id = match;
1887 }
1888 for (const elem2 of elem.generics) {
1889 convertNameToId(elem2);
1890 }
1891 }
1892
1893 for (const elem of parsedQuery.elems) {
1894 convertNameToId(elem);
1895 }
1896 for (const elem of parsedQuery.returned) {
1897 convertNameToId(elem);
1898 }
1899
04454e1e
FG
1900 if (parsedQuery.foundElems === 1) {
1901 if (parsedQuery.elems.length === 1) {
1902 elem = parsedQuery.elems[0];
1903 for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) {
1904 // It means we want to check for this element everywhere (in names, args and
1905 // returned).
1906 handleSingleArg(
1907 searchIndex[i],
1908 i,
1909 elem,
1910 results_others,
1911 results_in_args,
9ffffee4 1912 results_returned,
353b0b11 1913 maxEditDistance
04454e1e 1914 );
cdc7bbd5 1915 }
04454e1e
FG
1916 } else if (parsedQuery.returned.length === 1) {
1917 // We received one returned argument to check, so looking into returned values.
1918 elem = parsedQuery.returned[0];
1919 for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) {
1920 row = searchIndex[i];
fe692bf9
FG
1921 in_returned = row.type &&
1922 unifyFunctionTypes(row.type.output, parsedQuery.returned);
1923 if (in_returned) {
49aad941
FG
1924 addIntoResults(
1925 results_others,
1926 row.id,
1927 i,
1928 -1,
1929 0,
1930 Number.MAX_VALUE
1931 );
1932 }
cdc7bbd5 1933 }
04454e1e
FG
1934 }
1935 } else if (parsedQuery.foundElems > 0) {
1936 for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) {
49aad941 1937 handleArgs(searchIndex[i], i, results_others);
cdc7bbd5
XL
1938 }
1939 }
1940 }
1941
04454e1e
FG
1942 if (parsedQuery.error === null) {
1943 innerRunQuery();
1944 }
1945
1946 const ret = createQueryResults(
923072b8
FG
1947 sortResults(results_in_args, true, currentCrate),
1948 sortResults(results_returned, true, currentCrate),
1949 sortResults(results_others, false, currentCrate),
04454e1e 1950 parsedQuery);
923072b8 1951 handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates, currentCrate);
04454e1e
FG
1952 if (parsedQuery.error !== null && ret.others.length !== 0) {
1953 // It means some doc aliases were found so let's "remove" the error!
1954 ret.query.error = null;
1955 }
cdc7bbd5
XL
1956 return ret;
1957 }
1958
1959 /**
1960 * Validate performs the following boolean logic. For example:
1961 * "File::open" will give IF A PARENT EXISTS => ("file" && "open")
1962 * exists in (name || path || parent) OR => ("file" && "open") exists in
1963 * (name || path )
1964 *
1965 * This could be written functionally, but I wanted to minimise
1966 * functions on stack.
1967 *
a2a8927a
XL
1968 * @param {string} name - The name of the result
1969 * @param {string} path - The path of the result
1970 * @param {string} keys - The keys to be used (["file", "open"])
1971 * @param {Object} parent - The parent of the result
04454e1e 1972 *
a2a8927a 1973 * @return {boolean} - Whether the result is valid or not
cdc7bbd5 1974 */
353b0b11 1975 function validateResult(name, path, keys, parent, maxEditDistance) {
04454e1e
FG
1976 if (!keys || !keys.length) {
1977 return true;
1978 }
1979 for (const key of keys) {
cdc7bbd5
XL
1980 // each check is for validation so we negate the conditions and invalidate
1981 if (!(
1982 // check for an exact name match
04454e1e 1983 name.indexOf(key) > -1 ||
cdc7bbd5 1984 // then an exact path match
04454e1e 1985 path.indexOf(key) > -1 ||
cdc7bbd5
XL
1986 // next if there is a parent, check for exact parent match
1987 (parent !== undefined && parent.name !== undefined &&
04454e1e 1988 parent.name.toLowerCase().indexOf(key) > -1) ||
353b0b11
FG
1989 // lastly check to see if the name was an editDistance match
1990 editDistance(name, key, maxEditDistance) <= maxEditDistance)) {
cdc7bbd5
XL
1991 return false;
1992 }
1993 }
1994 return true;
1995 }
1996
17df50a5 1997 function nextTab(direction) {
04454e1e 1998 const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length;
17df50a5
XL
1999 searchState.focusedByTab[searchState.currentTab] = document.activeElement;
2000 printTab(next);
2001 focusSearchResult();
2002 }
cdc7bbd5 2003
17df50a5
XL
2004 // Focus the first search result on the active tab, or the result that
2005 // was focused last time this tab was active.
2006 function focusSearchResult() {
04454e1e 2007 const target = searchState.focusedByTab[searchState.currentTab] ||
17df50a5 2008 document.querySelectorAll(".search-results.active a").item(0) ||
9c376795 2009 document.querySelectorAll("#search-tabs button").item(searchState.currentTab);
487cf647 2010 searchState.focusedByTab[searchState.currentTab] = null;
17df50a5
XL
2011 if (target) {
2012 target.focus();
2013 }
cdc7bbd5
XL
2014 }
2015
2016 function buildHrefAndPath(item) {
04454e1e
FG
2017 let displayPath;
2018 let href;
2019 const type = itemTypes[item.ty];
2020 const name = item.name;
2021 let path = item.path;
cdc7bbd5
XL
2022
2023 if (type === "mod") {
2024 displayPath = path + "::";
923072b8
FG
2025 href = ROOT_PATH + path.replace(/::/g, "/") + "/" +
2026 name + "/index.html";
04454e1e
FG
2027 } else if (type === "import") {
2028 displayPath = item.path + "::";
923072b8 2029 href = ROOT_PATH + item.path.replace(/::/g, "/") + "/index.html#reexport." + name;
cdc7bbd5
XL
2030 } else if (type === "primitive" || type === "keyword") {
2031 displayPath = "";
923072b8
FG
2032 href = ROOT_PATH + path.replace(/::/g, "/") +
2033 "/" + type + "." + name + ".html";
cdc7bbd5
XL
2034 } else if (type === "externcrate") {
2035 displayPath = "";
923072b8 2036 href = ROOT_PATH + name + "/index.html";
cdc7bbd5 2037 } else if (item.parent !== undefined) {
04454e1e
FG
2038 const myparent = item.parent;
2039 let anchor = "#" + type + "." + name;
2040 const parentType = itemTypes[myparent.ty];
2041 let pageType = parentType;
2042 let pageName = myparent.name;
cdc7bbd5
XL
2043
2044 if (parentType === "primitive") {
2045 displayPath = myparent.name + "::";
2046 } else if (type === "structfield" && parentType === "variant") {
2047 // Structfields belonging to variants are special: the
2048 // final path element is the enum name.
04454e1e
FG
2049 const enumNameIdx = item.path.lastIndexOf("::");
2050 const enumName = item.path.substr(enumNameIdx + 2);
cdc7bbd5
XL
2051 path = item.path.substr(0, enumNameIdx);
2052 displayPath = path + "::" + enumName + "::" + myparent.name + "::";
2053 anchor = "#variant." + myparent.name + ".field." + name;
2054 pageType = "enum";
2055 pageName = enumName;
2056 } else {
2057 displayPath = path + "::" + myparent.name + "::";
2058 }
923072b8
FG
2059 href = ROOT_PATH + path.replace(/::/g, "/") +
2060 "/" + pageType +
2061 "." + pageName +
2062 ".html" + anchor;
cdc7bbd5
XL
2063 } else {
2064 displayPath = item.path + "::";
923072b8
FG
2065 href = ROOT_PATH + item.path.replace(/::/g, "/") +
2066 "/" + type + "." + name + ".html";
cdc7bbd5
XL
2067 }
2068 return [displayPath, href];
2069 }
2070
cdc7bbd5 2071 function pathSplitter(path) {
04454e1e 2072 const tmp = "<span>" + path.replace(/::/g, "::</span><span>");
cdc7bbd5
XL
2073 if (tmp.endsWith("<span>")) {
2074 return tmp.slice(0, tmp.length - 6);
2075 }
2076 return tmp;
2077 }
2078
a2a8927a
XL
2079 /**
2080 * Render a set of search results for a single tab.
2081 * @param {Array<?>} array - The search results for this tab
2082 * @param {ParsedQuery} query
2083 * @param {boolean} display - True if this is the active tab
2084 */
cdc7bbd5 2085 function addTab(array, query, display) {
04454e1e 2086 let extraClass = "";
17df50a5
XL
2087 if (display === true) {
2088 extraClass = " active";
cdc7bbd5
XL
2089 }
2090
04454e1e
FG
2091 const output = document.createElement("div");
2092 let length = 0;
cdc7bbd5 2093 if (array.length > 0) {
17df50a5 2094 output.className = "search-results " + extraClass;
cdc7bbd5 2095
04454e1e
FG
2096 array.forEach(item => {
2097 const name = item.name;
2098 const type = itemTypes[item.ty];
fe692bf9
FG
2099 const longType = longItemTypes[item.ty];
2100 const typeName = longType.length !== 0 ? `${longType}` : "?";
17df50a5 2101
cdc7bbd5
XL
2102 length += 1;
2103
04454e1e 2104 const link = document.createElement("a");
17df50a5
XL
2105 link.className = "result-" + type;
2106 link.href = item.href;
2107
04454e1e 2108 const resultName = document.createElement("div");
17df50a5
XL
2109 resultName.className = "result-name";
2110
add651ee
FG
2111 resultName.insertAdjacentHTML(
2112 "beforeend",
2113 `<span class="typename">${typeName}</span>`);
2114 link.appendChild(resultName);
17df50a5 2115
add651ee
FG
2116 let alias = " ";
2117 if (item.is_alias) {
2118 alias = ` <div class="alias">\
2119<b>${item.alias}</b><i class="grey">&nbsp;- see&nbsp;</i>\
2120</div>`;
17df50a5
XL
2121 }
2122 resultName.insertAdjacentHTML(
2123 "beforeend",
add651ee
FG
2124 `<div class="path">${alias}\
2125${item.displayPath}<span class="${type}">${name}</span>\
fe692bf9 2126</div>`);
17df50a5 2127
04454e1e 2128 const description = document.createElement("div");
17df50a5 2129 description.className = "desc";
487cf647 2130 description.insertAdjacentHTML("beforeend", item.desc);
17df50a5 2131
487cf647 2132 link.appendChild(description);
17df50a5 2133 output.appendChild(link);
cdc7bbd5 2134 });
04454e1e 2135 } else if (query.error === null) {
17df50a5
XL
2136 output.className = "search-failed" + extraClass;
2137 output.innerHTML = "No results :(<br/>" +
cdc7bbd5 2138 "Try on <a href=\"https://duckduckgo.com/?q=" +
04454e1e 2139 encodeURIComponent("rust " + query.userQuery) +
cdc7bbd5
XL
2140 "\">DuckDuckGo</a>?<br/><br/>" +
2141 "Or try looking in one of these:<ul><li>The <a " +
2142 "href=\"https://doc.rust-lang.org/reference/index.html\">Rust Reference</a> " +
2143 " for technical details about the language.</li><li><a " +
2144 "href=\"https://doc.rust-lang.org/rust-by-example/index.html\">Rust By " +
2145 "Example</a> for expository code examples.</a></li><li>The <a " +
2146 "href=\"https://doc.rust-lang.org/book/index.html\">Rust Book</a> for " +
2147 "introductions to language features and the language itself.</li><li><a " +
2148 "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" +
17df50a5 2149 " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>";
cdc7bbd5
XL
2150 }
2151 return [output, length];
2152 }
2153
2154 function makeTabHeader(tabNb, text, nbElems) {
2155 if (searchState.currentTab === tabNb) {
2156 return "<button class=\"selected\">" + text +
9c376795 2157 " <span class=\"count\">(" + nbElems + ")</span></button>";
cdc7bbd5 2158 }
9c376795 2159 return "<button>" + text + " <span class=\"count\">(" + nbElems + ")</span></button>";
cdc7bbd5
XL
2160 }
2161
04454e1e
FG
2162 /**
2163 * @param {ResultsTable} results
2164 * @param {boolean} go_to_first
2165 * @param {string} filterCrates
2166 */
5099ac24 2167 function showResults(results, go_to_first, filterCrates) {
04454e1e 2168 const search = searchState.outputElement();
136023e0 2169 if (go_to_first || (results.others.length === 1
353b0b11 2170 && getSettingValue("go-to-only-result") === "true")
923072b8 2171 ) {
fe692bf9
FG
2172 // Needed to force re-execution of JS when coming back to a page. Let's take this
2173 // scenario as example:
2174 //
2175 // 1. You have the "Directly go to item in search if there is only one result" option
2176 // enabled.
2177 // 2. You make a search which results only one result, leading you automatically to
2178 // this result.
2179 // 3. You go back to previous page.
2180 //
2181 // Now, without the call below, the JS will not be re-executed and the previous state
2182 // will be used, starting search again since the search input is not empty, leading you
2183 // back to the previous page again.
2184 window.onunload = () => {};
2185 searchState.removeQueryParameters();
04454e1e 2186 const elem = document.createElement("a");
cdc7bbd5 2187 elem.href = results.others[0].href;
17df50a5 2188 removeClass(elem, "active");
cdc7bbd5
XL
2189 // For firefox, we need the element to be in the DOM so it can be clicked.
2190 document.body.appendChild(elem);
2191 elem.click();
2192 return;
2193 }
04454e1e
FG
2194 if (results.query === undefined) {
2195 results.query = parseQuery(searchState.input.value);
2196 }
cdc7bbd5 2197
04454e1e 2198 currentResults = results.query.userQuery;
cdc7bbd5 2199
04454e1e
FG
2200 const ret_others = addTab(results.others, results.query, true);
2201 const ret_in_args = addTab(results.in_args, results.query, false);
2202 const ret_returned = addTab(results.returned, results.query, false);
cdc7bbd5
XL
2203
2204 // Navigate to the relevant tab if the current tab is empty, like in case users search
2205 // for "-> String". If they had selected another tab previously, they have to click on
2206 // it again.
04454e1e 2207 let currentTab = searchState.currentTab;
cdc7bbd5
XL
2208 if ((currentTab === 0 && ret_others[1] === 0) ||
2209 (currentTab === 1 && ret_in_args[1] === 0) ||
2210 (currentTab === 2 && ret_returned[1] === 0)) {
2211 if (ret_others[1] !== 0) {
2212 currentTab = 0;
2213 } else if (ret_in_args[1] !== 0) {
2214 currentTab = 1;
2215 } else if (ret_returned[1] !== 0) {
2216 currentTab = 2;
2217 }
2218 }
2219
5099ac24 2220 let crates = "";
923072b8
FG
2221 const crates_list = Object.keys(rawSearchIndex);
2222 if (crates_list.length > 1) {
f2b60f7d
FG
2223 crates = " in&nbsp;<div id=\"crate-search-div\"><select id=\"crate-search\">" +
2224 "<option value=\"all crates\">all crates</option>";
923072b8
FG
2225 for (const c of crates_list) {
2226 crates += `<option value="${c}" ${c === filterCrates && "selected"}>${c}</option>`;
5099ac24 2227 }
f2b60f7d 2228 crates += "</select></div>";
04454e1e
FG
2229 }
2230
f2b60f7d 2231 let output = `<h1 class="search-results-title">Results${crates}</h1>`;
04454e1e 2232 if (results.query.error !== null) {
9ffffee4
FG
2233 const error = results.query.error;
2234 error.forEach((value, index) => {
2235 value = value.split("<").join("&lt;").split(">").join("&gt;");
2236 if (index % 2 !== 0) {
fe692bf9 2237 error[index] = `<code>${value.replaceAll(" ", "&nbsp;")}</code>`;
9ffffee4
FG
2238 } else {
2239 error[index] = value;
2240 }
2241 });
2242 output += `<h3 class="error">Query parser error: "${error.join("")}".</h3>`;
9c376795 2243 output += "<div id=\"search-tabs\">" +
04454e1e
FG
2244 makeTabHeader(0, "In Names", ret_others[1]) +
2245 "</div>";
2246 currentTab = 0;
2247 } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) {
9c376795 2248 output += "<div id=\"search-tabs\">" +
04454e1e
FG
2249 makeTabHeader(0, "In Names", ret_others[1]) +
2250 makeTabHeader(1, "In Parameters", ret_in_args[1]) +
2251 makeTabHeader(2, "In Return Types", ret_returned[1]) +
2252 "</div>";
2253 } else {
2254 const signatureTabTitle =
2255 results.query.elems.length === 0 ? "In Function Return Types" :
2256 results.query.returned.length === 0 ? "In Function Parameters" :
2257 "In Function Signatures";
9c376795 2258 output += "<div id=\"search-tabs\">" +
04454e1e
FG
2259 makeTabHeader(0, signatureTabTitle, ret_others[1]) +
2260 "</div>";
2261 currentTab = 0;
2262 }
2263
49aad941
FG
2264 if (results.query.correction !== null) {
2265 const orig = results.query.returned.length > 0
2266 ? results.query.returned[0].name
2267 : results.query.elems[0].name;
2268 output += "<h3 class=\"search-corrections\">" +
2269 `Type "${orig}" not found. ` +
2270 "Showing results for closest type name " +
2271 `"${results.query.correction}" instead.</h3>`;
2272 }
2273
04454e1e 2274 const resultsElem = document.createElement("div");
17df50a5
XL
2275 resultsElem.id = "results";
2276 resultsElem.appendChild(ret_others[0]);
2277 resultsElem.appendChild(ret_in_args[0]);
2278 resultsElem.appendChild(ret_returned[0]);
cdc7bbd5
XL
2279
2280 search.innerHTML = output;
04454e1e 2281 const crateSearch = document.getElementById("crate-search");
5099ac24
FG
2282 if (crateSearch) {
2283 crateSearch.addEventListener("input", updateCrate);
2284 }
17df50a5
XL
2285 search.appendChild(resultsElem);
2286 // Reset focused elements.
cdc7bbd5 2287 searchState.showResults(search);
9c376795 2288 const elems = document.getElementById("search-tabs").childNodes;
04454e1e
FG
2289 searchState.focusedByTab = [];
2290 let i = 0;
2291 for (const elem of elems) {
2292 const j = i;
923072b8 2293 elem.onclick = () => printTab(j);
04454e1e
FG
2294 searchState.focusedByTab.push(null);
2295 i += 1;
cdc7bbd5 2296 }
04454e1e 2297 printTab(currentTab);
cdc7bbd5
XL
2298 }
2299
fe692bf9
FG
2300 function updateSearchHistory(url) {
2301 if (!browserSupportsHistoryApi()) {
2302 return;
2303 }
2304 const params = searchState.getQueryStringParams();
2305 if (!history.state && !params.search) {
2306 history.pushState(null, "", url);
2307 } else {
2308 history.replaceState(null, "", url);
2309 }
2310 }
2311
a2a8927a
XL
2312 /**
2313 * Perform a search based on the current state of the search input element
2314 * and display the results.
2315 * @param {Event} [e] - The event that triggered this search, if any
2316 * @param {boolean} [forced]
2317 */
cdc7bbd5 2318 function search(e, forced) {
cdc7bbd5
XL
2319 if (e) {
2320 e.preventDefault();
2321 }
487cf647
FG
2322 const query = parseQuery(searchState.input.value.trim());
2323 let filterCrates = getFilterCrates();
2324
04454e1e
FG
2325 if (!forced && query.userQuery === currentResults) {
2326 if (query.userQuery.length > 0) {
5099ac24 2327 putBackSearch();
cdc7bbd5
XL
2328 }
2329 return;
2330 }
2331
487cf647
FG
2332 searchState.setLoadingSearch();
2333
2334 const params = searchState.getQueryStringParams();
5099ac24
FG
2335
2336 // In case we have no information about the saved crate and there is a URL query parameter,
2337 // we override it with the URL query parameter.
2338 if (filterCrates === null && params["filter-crate"] !== undefined) {
2339 filterCrates = params["filter-crate"];
2340 }
2341
cdc7bbd5 2342 // Update document title to maintain a meaningful browser history
04454e1e 2343 searchState.title = "Results for " + query.original + " - Rust";
cdc7bbd5
XL
2344
2345 // Because searching is incremental by character, only the most
2346 // recent search query is added to the browser history.
fe692bf9 2347 updateSearchHistory(buildUrl(query.original, filterCrates));
cdc7bbd5 2348
04454e1e 2349 showResults(
923072b8 2350 execQuery(query, searchWords, filterCrates, window.currentCrate),
04454e1e
FG
2351 params.go_to_first,
2352 filterCrates);
cdc7bbd5
XL
2353 }
2354
064997fb
FG
2355 /**
2356 * Convert a list of RawFunctionType / ID to object-based FunctionType.
2357 *
2358 * Crates often have lots of functions in them, and it's common to have a large number of
2359 * functions that operate on a small set of data types, so the search index compresses them
2360 * by encoding function parameter and return types as indexes into an array of names.
2361 *
2362 * Even when a general-purpose compression algorithm is used, this is still a win. I checked.
2363 * https://github.com/rust-lang/rust/pull/98475#issue-1284395985
2364 *
2365 * The format for individual function types is encoded in
2366 * librustdoc/html/render/mod.rs: impl Serialize for RenderType
2367 *
2368 * @param {null|Array<RawFunctionType>} types
2369 * @param {Array<{name: string, ty: number}>} lowercasePaths
2370 *
2371 * @return {Array<FunctionSearchType>}
2372 */
fe692bf9 2373 function buildItemSearchTypeAll(types, lowercasePaths) {
064997fb
FG
2374 const PATH_INDEX_DATA = 0;
2375 const GENERICS_DATA = 1;
2376 return types.map(type => {
2377 let pathIndex, generics;
2378 if (typeof type === "number") {
2379 pathIndex = type;
2380 generics = [];
2381 } else {
2382 pathIndex = type[PATH_INDEX_DATA];
49aad941
FG
2383 generics = buildItemSearchTypeAll(
2384 type[GENERICS_DATA],
fe692bf9 2385 lowercasePaths
49aad941 2386 );
064997fb
FG
2387 }
2388 return {
2389 // `0` is used as a sentinel because it's fewer bytes than `null`
49aad941
FG
2390 id: pathIndex === 0
2391 ? -1
fe692bf9 2392 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
064997fb
FG
2393 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2394 generics: generics,
2395 };
2396 });
2397 }
2398
2399 /**
2400 * Convert from RawFunctionSearchType to FunctionSearchType.
2401 *
2402 * Crates often have lots of functions in them, and function signatures are sometimes complex,
2403 * so rustdoc uses a pretty tight encoding for them. This function converts it to a simpler,
2404 * object-based encoding so that the actual search code is more readable and easier to debug.
2405 *
2406 * The raw function search type format is generated using serde in
2407 * librustdoc/html/render/mod.rs: impl Serialize for IndexItemFunctionType
2408 *
2409 * @param {RawFunctionSearchType} functionSearchType
2410 * @param {Array<{name: string, ty: number}>} lowercasePaths
49aad941 2411 * @param {Map<string, integer>}
064997fb
FG
2412 *
2413 * @return {null|FunctionSearchType}
2414 */
fe692bf9 2415 function buildFunctionSearchType(functionSearchType, lowercasePaths) {
064997fb
FG
2416 const INPUTS_DATA = 0;
2417 const OUTPUT_DATA = 1;
2418 // `0` is used as a sentinel because it's fewer bytes than `null`
2419 if (functionSearchType === 0) {
2420 return null;
2421 }
2422 let inputs, output;
2423 if (typeof functionSearchType[INPUTS_DATA] === "number") {
2424 const pathIndex = functionSearchType[INPUTS_DATA];
2425 inputs = [{
49aad941
FG
2426 id: pathIndex === 0
2427 ? -1
fe692bf9 2428 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
064997fb
FG
2429 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2430 generics: [],
2431 }];
2432 } else {
49aad941
FG
2433 inputs = buildItemSearchTypeAll(
2434 functionSearchType[INPUTS_DATA],
fe692bf9 2435 lowercasePaths
49aad941 2436 );
064997fb
FG
2437 }
2438 if (functionSearchType.length > 1) {
2439 if (typeof functionSearchType[OUTPUT_DATA] === "number") {
2440 const pathIndex = functionSearchType[OUTPUT_DATA];
2441 output = [{
49aad941
FG
2442 id: pathIndex === 0
2443 ? -1
fe692bf9 2444 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name),
064997fb
FG
2445 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty,
2446 generics: [],
2447 }];
2448 } else {
49aad941
FG
2449 output = buildItemSearchTypeAll(
2450 functionSearchType[OUTPUT_DATA],
fe692bf9 2451 lowercasePaths
49aad941 2452 );
064997fb
FG
2453 }
2454 } else {
2455 output = [];
2456 }
2457 return {
2458 inputs, output,
2459 };
2460 }
2461
cdc7bbd5
XL
2462 function buildIndex(rawSearchIndex) {
2463 searchIndex = [];
a2a8927a 2464 /**
49aad941
FG
2465 * List of normalized search words (ASCII lowercased, and undescores removed).
2466 *
a2a8927a
XL
2467 * @type {Array<string>}
2468 */
04454e1e 2469 const searchWords = [];
49aad941 2470 typeNameIdMap = new Map();
9ffffee4 2471 const charA = "A".charCodeAt(0);
04454e1e
FG
2472 let currentIndex = 0;
2473 let id = 0;
cdc7bbd5 2474
fe692bf9
FG
2475 // Initialize type map indexes for primitive list types
2476 // that can be searched using `[]` syntax.
2477 typeNameIdOfArray = buildTypeMapIndex("array");
2478 typeNameIdOfSlice = buildTypeMapIndex("slice");
2479 typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]");
2480
04454e1e 2481 for (const crate in rawSearchIndex) {
17df50a5
XL
2482 if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) {
2483 continue;
2484 }
cdc7bbd5 2485
04454e1e 2486 let crateSize = 0;
cdc7bbd5 2487
a2a8927a
XL
2488 /**
2489 * The raw search data for a given crate. `n`, `t`, `d`, and `q`, `i`, and `f`
2490 * are arrays with the same length. n[i] contains the name of an item.
9ffffee4 2491 * t[i] contains the type of that item (as a string of characters that represent an
a2a8927a
XL
2492 * offset in `itemTypes`). d[i] contains the description of that item.
2493 *
2494 * q[i] contains the full path of the item, or an empty string indicating
2495 * "same as q[i-1]".
2496 *
064997fb
FG
2497 * i[i] contains an item's parent, usually a module. For compactness,
2498 * it is a set of indexes into the `p` array.
2499 *
2500 * f[i] contains function signatures, or `0` if the item isn't a function.
2501 * Functions are themselves encoded as arrays. The first item is a list of
2502 * types representing the function's inputs, and the second list item is a list
2503 * of types representing the function's output. Tuples are flattened.
2504 * Types are also represented as arrays; the first item is an index into the `p`
2505 * array, while the second is a list of types representing any generic parameters.
a2a8927a
XL
2506 *
2507 * `a` defines aliases with an Array of pairs: [name, offset], where `offset`
2508 * points into the n/t/d/q/i/f arrays.
2509 *
2510 * `doc` contains the description of the crate.
2511 *
064997fb 2512 * `p` is a list of path/type pairs. It is used for parents and function parameters.
a2a8927a
XL
2513 *
2514 * @type {{
2515 * doc: string,
2516 * a: Object,
2517 * n: Array<string>,
9ffffee4 2518 * t: String,
a2a8927a 2519 * d: Array<string>,
353b0b11 2520 * q: Array<[Number, string]>,
a2a8927a 2521 * i: Array<Number>,
064997fb 2522 * f: Array<RawFunctionSearchType>,
a2a8927a 2523 * p: Array<Object>,
353b0b11 2524 * c: Array<Number>
a2a8927a
XL
2525 * }}
2526 */
04454e1e 2527 const crateCorpus = rawSearchIndex[crate];
a2a8927a 2528
cdc7bbd5 2529 searchWords.push(crate);
cdc7bbd5
XL
2530 // This object should have exactly the same set of fields as the "row"
2531 // object defined below. Your JavaScript runtime will thank you.
2532 // https://mathiasbynens.be/notes/shapes-ics
04454e1e 2533 const crateRow = {
cdc7bbd5
XL
2534 crate: crate,
2535 ty: 1, // == ExternCrate
2536 name: crate,
2537 path: "",
a2a8927a 2538 desc: crateCorpus.doc,
cdc7bbd5
XL
2539 parent: undefined,
2540 type: null,
2541 id: id,
17df50a5 2542 normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""),
353b0b11 2543 deprecated: null,
cdc7bbd5
XL
2544 };
2545 id += 1;
2546 searchIndex.push(crateRow);
2547 currentIndex += 1;
2548
9ffffee4 2549 // a String of one character item type codes
04454e1e 2550 const itemTypes = crateCorpus.t;
cdc7bbd5 2551 // an array of (String) item names
04454e1e 2552 const itemNames = crateCorpus.n;
353b0b11
FG
2553 // an array of [(Number) item index,
2554 // (String) full path]
2555 // an item whose index is not present will fall back to the previous present path
2556 // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present,
2557 // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11
2558 const itemPaths = new Map(crateCorpus.q);
cdc7bbd5 2559 // an array of (String) descriptions
04454e1e 2560 const itemDescs = crateCorpus.d;
cdc7bbd5 2561 // an array of (Number) the parent path index + 1 to `paths`, or 0 if none
04454e1e 2562 const itemParentIdxs = crateCorpus.i;
cdc7bbd5 2563 // an array of (Object | null) the type of the function, if any
04454e1e 2564 const itemFunctionSearchTypes = crateCorpus.f;
353b0b11
FG
2565 // an array of (Number) indices for the deprecated items
2566 const deprecatedItems = new Set(crateCorpus.c);
cdc7bbd5
XL
2567 // an array of [(Number) item type,
2568 // (String) name]
04454e1e 2569 const paths = crateCorpus.p;
94222f64 2570 // an array of [(String) alias name
cdc7bbd5 2571 // [Number] index to items]
04454e1e 2572 const aliases = crateCorpus.a;
cdc7bbd5 2573
064997fb
FG
2574 // an array of [{name: String, ty: Number}]
2575 const lowercasePaths = [];
2576
cdc7bbd5 2577 // convert `rawPaths` entries into object form
064997fb 2578 // generate normalizedPaths for function search mode
04454e1e 2579 let len = paths.length;
9ffffee4 2580 for (let i = 0; i < len; ++i) {
064997fb 2581 lowercasePaths.push({ty: paths[i][0], name: paths[i][1].toLowerCase()});
cdc7bbd5
XL
2582 paths[i] = {ty: paths[i][0], name: paths[i][1]};
2583 }
2584
2585 // convert `item*` into an object form, and construct word indices.
2586 //
2587 // before any analysis is performed lets gather the search terms to
2588 // search against apart from the rest of the data. This is a quick
2589 // operation that is cached for the life of the page state so that
2590 // all other search operations have access to this cached data for
2591 // faster analysis operations
2592 len = itemTypes.length;
04454e1e 2593 let lastPath = "";
9ffffee4
FG
2594 for (let i = 0; i < len; ++i) {
2595 let word = "";
cdc7bbd5
XL
2596 // This object should have exactly the same set of fields as the "crateRow"
2597 // object defined above.
2598 if (typeof itemNames[i] === "string") {
2599 word = itemNames[i].toLowerCase();
cdc7bbd5 2600 }
9ffffee4 2601 searchWords.push(word);
04454e1e 2602 const row = {
cdc7bbd5 2603 crate: crate,
9ffffee4 2604 ty: itemTypes.charCodeAt(i) - charA,
cdc7bbd5 2605 name: itemNames[i],
353b0b11 2606 path: itemPaths.has(i) ? itemPaths.get(i) : lastPath,
cdc7bbd5
XL
2607 desc: itemDescs[i],
2608 parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined,
49aad941
FG
2609 type: buildFunctionSearchType(
2610 itemFunctionSearchTypes[i],
fe692bf9 2611 lowercasePaths
49aad941 2612 ),
cdc7bbd5 2613 id: id,
17df50a5 2614 normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""),
353b0b11 2615 deprecated: deprecatedItems.has(i),
cdc7bbd5
XL
2616 };
2617 id += 1;
2618 searchIndex.push(row);
2619 lastPath = row.path;
2620 crateSize += 1;
2621 }
2622
2623 if (aliases) {
353b0b11
FG
2624 const currentCrateAliases = new Map();
2625 ALIASES.set(crate, currentCrateAliases);
04454e1e 2626 for (const alias_name in aliases) {
17df50a5
XL
2627 if (!hasOwnPropertyRustdoc(aliases, alias_name)) {
2628 continue;
2629 }
cdc7bbd5 2630
353b0b11
FG
2631 let currentNameAliases;
2632 if (currentCrateAliases.has(alias_name)) {
2633 currentNameAliases = currentCrateAliases.get(alias_name);
2634 } else {
2635 currentNameAliases = [];
2636 currentCrateAliases.set(alias_name, currentNameAliases);
cdc7bbd5 2637 }
04454e1e 2638 for (const local_alias of aliases[alias_name]) {
353b0b11 2639 currentNameAliases.push(local_alias + currentIndex);
cdc7bbd5
XL
2640 }
2641 }
2642 }
2643 currentIndex += crateSize;
2644 }
2645 return searchWords;
2646 }
2647
a2a8927a
XL
2648 /**
2649 * Callback for when the search form is submitted.
2650 * @param {Event} [e] - The event that triggered this call, if any
2651 */
2652 function onSearchSubmit(e) {
2653 e.preventDefault();
2654 searchState.clearInputTimeout();
2655 search();
2656 }
2657
5099ac24 2658 function putBackSearch() {
04454e1e 2659 const search_input = searchState.input;
5099ac24
FG
2660 if (!searchState.input) {
2661 return;
2662 }
04454e1e
FG
2663 if (search_input.value !== "" && !searchState.isDisplayed()) {
2664 searchState.showResults();
2665 if (browserSupportsHistoryApi()) {
5099ac24
FG
2666 history.replaceState(null, "",
2667 buildUrl(search_input.value, getFilterCrates()));
2668 }
2669 document.title = searchState.title;
2670 }
2671 }
2672
cdc7bbd5 2673 function registerSearchEvents() {
923072b8
FG
2674 const params = searchState.getQueryStringParams();
2675
2676 // Populate search bar with query string search term when provided,
2677 // but only if the input bar is empty. This avoid the obnoxious issue
2678 // where you start trying to do a search, and the index loads, and
2679 // suddenly your search is gone!
2680 if (searchState.input.value === "") {
2681 searchState.input.value = params.search || "";
2682 }
2683
04454e1e 2684 const searchAfter500ms = () => {
cdc7bbd5
XL
2685 searchState.clearInputTimeout();
2686 if (searchState.input.value.length === 0) {
cdc7bbd5
XL
2687 searchState.hideResults();
2688 } else {
2689 searchState.timeout = setTimeout(search, 500);
2690 }
2691 };
2692 searchState.input.onkeyup = searchAfter500ms;
2693 searchState.input.oninput = searchAfter500ms;
a2a8927a 2694 document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit;
04454e1e 2695 searchState.input.onchange = e => {
cdc7bbd5
XL
2696 if (e.target !== document.activeElement) {
2697 // To prevent doing anything when it's from a blur event.
2698 return;
2699 }
2700 // Do NOT e.preventDefault() here. It will prevent pasting.
2701 searchState.clearInputTimeout();
2702 // zero-timeout necessary here because at the time of event handler execution the
2703 // pasted content is not in the input field yet. Shouldn’t make any difference for
2704 // change, though.
2705 setTimeout(search, 0);
2706 };
2707 searchState.input.onpaste = searchState.input.onchange;
2708
04454e1e 2709 searchState.outputElement().addEventListener("keydown", e => {
17df50a5
XL
2710 // We only handle unmodified keystrokes here. We don't want to interfere with,
2711 // for instance, alt-left and alt-right for history navigation.
2712 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) {
2713 return;
2714 }
2715 // up and down arrow select next/previous search result, or the
2716 // search box if we're already at the top.
2717 if (e.which === 38) { // up
04454e1e 2718 const previous = document.activeElement.previousElementSibling;
17df50a5
XL
2719 if (previous) {
2720 previous.focus();
2721 } else {
2722 searchState.focus();
2723 }
2724 e.preventDefault();
2725 } else if (e.which === 40) { // down
04454e1e 2726 const next = document.activeElement.nextElementSibling;
17df50a5
XL
2727 if (next) {
2728 next.focus();
2729 }
04454e1e 2730 const rect = document.activeElement.getBoundingClientRect();
17df50a5
XL
2731 if (window.innerHeight - rect.bottom < rect.height) {
2732 window.scrollBy(0, rect.height);
2733 }
2734 e.preventDefault();
2735 } else if (e.which === 37) { // left
2736 nextTab(-1);
2737 e.preventDefault();
2738 } else if (e.which === 39) { // right
2739 nextTab(1);
2740 e.preventDefault();
2741 }
2742 });
2743
04454e1e 2744 searchState.input.addEventListener("keydown", e => {
17df50a5
XL
2745 if (e.which === 40) { // down
2746 focusSearchResult();
2747 e.preventDefault();
2748 }
2749 });
2750
04454e1e 2751 searchState.input.addEventListener("focus", () => {
5099ac24
FG
2752 putBackSearch();
2753 });
17df50a5 2754
04454e1e 2755 searchState.input.addEventListener("blur", () => {
5099ac24
FG
2756 searchState.input.placeholder = searchState.input.origPlaceholder;
2757 });
cdc7bbd5
XL
2758
2759 // Push and pop states are used to add search results to the browser
2760 // history.
04454e1e 2761 if (browserSupportsHistoryApi()) {
cdc7bbd5 2762 // Store the previous <title> so we can revert back to it later.
04454e1e 2763 const previousTitle = document.title;
cdc7bbd5 2764
04454e1e
FG
2765 window.addEventListener("popstate", e => {
2766 const params = searchState.getQueryStringParams();
cdc7bbd5
XL
2767 // Revert to the previous title manually since the History
2768 // API ignores the title parameter.
2769 document.title = previousTitle;
2770 // When browsing forward to search results the previous
2771 // search will be repeated, so the currentResults are
2772 // cleared to ensure the search is successful.
2773 currentResults = null;
2774 // Synchronize search bar with query string state and
2775 // perform the search. This will empty the bar if there's
2776 // nothing there, which lets you really go back to a
2777 // previous state with nothing in the bar.
2778 if (params.search && params.search.length > 0) {
2779 searchState.input.value = params.search;
2780 // Some browsers fire "onpopstate" for every page load
2781 // (Chrome), while others fire the event only when actually
2782 // popping a state (Firefox), which is why search() is
2783 // called both here and at the end of the startSearch()
2784 // function.
2785 search(e);
2786 } else {
2787 searchState.input.value = "";
2788 // When browsing back from search results the main page
2789 // visibility must be reset.
2790 searchState.hideResults();
2791 }
2792 });
2793 }
2794
2795 // This is required in firefox to avoid this problem: Navigating to a search result
2796 // with the keyboard, hitting enter, and then hitting back would take you back to
2797 // the doc page, rather than the search that should overlay it.
2798 // This was an interaction between the back-forward cache and our handlers
2799 // that try to sync state between the URL and the search input. To work around it,
2800 // do a small amount of re-init on page show.
04454e1e
FG
2801 window.onpageshow = () => {
2802 const qSearch = searchState.getQueryStringParams().search;
cdc7bbd5
XL
2803 if (searchState.input.value === "" && qSearch) {
2804 searchState.input.value = qSearch;
2805 }
2806 search();
2807 };
2808 }
2809
5099ac24 2810 function updateCrate(ev) {
f2b60f7d 2811 if (ev.target.value === "all crates") {
5099ac24 2812 // If we don't remove it from the URL, it'll be picked up again by the search.
04454e1e 2813 const query = searchState.input.value.trim();
fe692bf9 2814 updateSearchHistory(buildUrl(query, null));
5099ac24
FG
2815 }
2816 // In case you "cut" the entry from the search input, then change the crate filter
2817 // before paste back the previous search, you get the old search results without
2818 // the filter. To prevent this, we need to remove the previous results.
2819 currentResults = null;
2820 search(undefined, true);
2821 }
2822
04454e1e
FG
2823 /**
2824 * @type {Array<string>}
2825 */
2826 const searchWords = buildIndex(rawSearchIndex);
923072b8
FG
2827 if (typeof window !== "undefined") {
2828 registerSearchEvents();
5099ac24 2829 // If there's a search term in the URL, execute the search now.
923072b8 2830 if (window.searchState.getQueryStringParams().search) {
5099ac24
FG
2831 search();
2832 }
cdc7bbd5 2833 }
5099ac24 2834
923072b8
FG
2835 if (typeof exports !== "undefined") {
2836 exports.initSearch = initSearch;
2837 exports.execQuery = execQuery;
2838 exports.parseQuery = parseQuery;
2839 }
2840 return searchWords;
2841}
cdc7bbd5 2842
923072b8
FG
2843if (typeof window !== "undefined") {
2844 window.initSearch = initSearch;
2845 if (window.searchIndex !== undefined) {
2846 initSearch(window.searchIndex);
2847 }
2848} else {
2849 // Running in Node, not a browser. Run initSearch just to produce the
2850 // exports.
2851 initSearch({});
cdc7bbd5
XL
2852}
2853
923072b8 2854
cdc7bbd5 2855})();