1/* global addClass, getNakedUrl, getSettingValue */ 2/* global onEachLazy, removeClass, searchState, browserSupportsHistoryApi, exports */ 3 4"use strict"; 5 6(function() { 7// This mapping table should match the discriminants of 8// `rustdoc::formats::item_type::ItemType` type in Rust. 9const itemTypes = [ 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]; 37 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 67// used for special search precedence 68const TY_PRIMITIVE = itemTypes.indexOf("primitive"); 69const TY_KEYWORD = itemTypes.indexOf("keyword"); 70const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; 71 72function hasOwnPropertyRustdoc(obj, property) { 73 return Object.prototype.hasOwnProperty.call(obj, property); 74} 75 76// In the search display, allows to switch between tabs. 77function printTab(nb) { 78 let iter = 0; 79 let foundCurrentTab = false; 80 let foundCurrentResultSet = false; 81 onEachLazy(document.getElementById("search-tabs").childNodes, elem => { 82 if (nb === iter) { 83 addClass(elem, "selected"); 84 foundCurrentTab = true; 85 } else { 86 removeClass(elem, "selected"); 87 } 88 iter += 1; 89 }); 90 const isTypeSearch = (nb > 0 || iter === 1); 91 iter = 0; 92 onEachLazy(document.getElementById("results").childNodes, elem => { 93 if (nb === iter) { 94 addClass(elem, "active"); 95 foundCurrentResultSet = true; 96 } else { 97 removeClass(elem, "active"); 98 } 99 iter += 1; 100 }); 101 if (foundCurrentTab && foundCurrentResultSet) { 102 searchState.currentTab = nb; 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 } 110 } else if (nb !== 0) { 111 printTab(0); 112 } 113} 114 115/** 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 119 */ 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); 221} 222 223function initSearch(rawSearchIndex) { 224 const MAX_RESULTS = 200; 225 const NO_TYPE_FILTER = -1; 226 /** 227 * @type {Array<Row>} 228 */ 229 let searchIndex; 230 let currentResults; 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; 238 const ALIASES = new Map(); 239 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 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) { 289 return ",>-]".indexOf(c) !== -1; 290 } 291 292 function isStopCharacter(c) { 293 return isEndCharacter(c); 294 } 295 296 function isErrorCharacter(c) { 297 return "()".indexOf(c) !== -1; 298 } 299 300 function itemTypeFromName(typename) { 301 const index = itemTypes.findIndex(i => i === typename); 302 if (index < 0) { 303 throw ["Unknown type filter ", typename]; 304 } 305 return index; 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) { 323 throw ["Unexpected ", "\"", " in generics"]; 324 } else if (query.literalSearch) { 325 throw ["Cannot have more than one literal search element"]; 326 } else if (parserState.totalElems - parserState.genericsElems > 0) { 327 throw ["Cannot use literal search when there is more than one element"]; 328 } 329 parserState.pos += 1; 330 const start = parserState.pos; 331 const end = getIdentEndPosition(parserState); 332 if (parserState.pos >= parserState.length) { 333 throw ["Unclosed ", "\""]; 334 } else if (parserState.userQuery[end] !== "\"") { 335 throw ["Unexpected ", parserState.userQuery[end], " in a string element"]; 336 } else if (start === end) { 337 throw ["Cannot have empty string element"]; 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) { 352 return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "::"; 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) { 363 return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) === "->"; 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) { 389 return c === ","; 390 } 391 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`. 395 * 396 * @param {string} c 397 * 398 * @return {boolean} 399 */ 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 } 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) { 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 ", "*"]; 468 } 469 if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { 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 ]; 483 } 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 }; 500 } 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+/); 513 // In case we only have something like `<p>`, there is no name. 514 if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { 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 } 528 } 529 parserState.totalElems += 1; 530 if (isInGenerics) { 531 parserState.genericsElems += 1; 532 } 533 return { 534 name: name.trim(), 535 id: -1, 536 fullPath: pathSegments, 537 pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), 538 pathLast: pathSegments[pathSegments.length - 1], 539 generics: generics, 540 typeFilter, 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) { 553 const start = parserState.pos; 554 let end = parserState.pos; 555 let foundExclamation = -1; 556 while (parserState.pos < parserState.length) { 557 const c = parserState.userQuery[parserState.pos]; 558 if (!isIdentCharacter(c)) { 559 if (c === "!") { 560 if (foundExclamation !== -1) { 561 throw ["Cannot have more than one ", "!", " in an ident"]; 562 } else if (parserState.pos + 1 < parserState.length && 563 isIdentCharacter(parserState.userQuery[parserState.pos + 1]) 564 ) { 565 throw ["Unexpected ", "!", ": it can only be at the end of an ident"]; 566 } 567 foundExclamation = parserState.pos; 568 } else if (isErrorCharacter(c)) { 569 throw ["Unexpected ", c]; 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 } 585 } 586 if (foundExclamation !== -1) { 587 if (foundExclamation !== start && 588 isIdentCharacter(parserState.userQuery[foundExclamation - 1]) 589 ) { 590 throw ["Cannot have associated items in macros"]; 591 } else { 592 // while the never type has no associated macros, we still 593 // can parse a path like that 594 foundExclamation = -1; 595 } 596 } 597 } else if ( 598 c === "[" || 599 isStopCharacter(c) || 600 isSpecialStartCharacter(c) || 601 isSeparatorCharacter(c) 602 ) { 603 break; 604 } else { 605 throw ["Unexpected ", c]; 606 } 607 } 608 parserState.pos += 1; 609 end = parserState.pos; 610 } 611 // if start == end - 1, we got the never type 612 if (foundExclamation !== -1 && 613 foundExclamation !== start && 614 isIdentCharacter(parserState.userQuery[foundExclamation - 1]) 615 ) { 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 } 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 641 skipWhitespace(parserState); 642 let start = parserState.pos; 643 let end; 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 }); 671 } else { 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); 680 } 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 ); 705 } 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; 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; 728 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 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)) { 749 throw ["Unexpected ", "::", ": paths cannot start with ", "::"]; 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) { 757 throw ["Cannot use quotes on type filter"]; 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)) { 769 throw ["Unexpected ", c, " after ", extra]; 770 } 771 if (!foundStopChar) { 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 } 778 if (endChar !== "") { 779 throw [ 780 "Expected ", 781 ",", 782 " or ", 783 endChar, 784 ...extra, 785 ", found ", 786 c, 787 ]; 788 } 789 throw [ 790 "Expected ", 791 ",", 792 ...extra, 793 ", found ", 794 c, 795 ]; 796 } 797 const posBefore = parserState.pos; 798 start = parserState.pos; 799 getNextElem(query, parserState, elems, endChar !== ""); 800 if (endChar !== "" && parserState.pos >= parserState.length) { 801 throw ["Unclosed ", extra]; 802 } 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. 806 if (posBefore === parserState.pos) { 807 parserState.pos += 1; 808 } 809 foundStopChar = false; 810 } 811 if (parserState.pos >= parserState.length && endChar !== "") { 812 throw ["Unclosed ", extra]; 813 } 814 // We are either at the end of the string or on the `endChar` character, let's move forward 815 // in any case. 816 parserState.pos += 1; 817 818 parserState.typeFilter = oldTypeFilter; 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 */ 827 function checkExtraTypeFilterCharacters(start, parserState) { 828 const query = parserState.userQuery.slice(start, parserState.pos).trim(); 829 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 ]; 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) { 851 let foundStopChar = true; 852 let start = parserState.pos; 853 854 while (parserState.pos < parserState.length) { 855 const c = parserState.userQuery[parserState.pos]; 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 } 865 throw ["Unexpected ", c, " (did you mean ", "->", "?)"]; 866 } 867 throw ["Unexpected ", c]; 868 } else if (c === ":" && !isPathStart(parserState)) { 869 if (parserState.typeFilter !== null) { 870 throw [ 871 "Unexpected ", 872 ":", 873 " (expected path after type filter ", 874 parserState.typeFilter + ":", 875 ")", 876 ]; 877 } else if (query.elems.length === 0) { 878 throw ["Expected type filter before ", ":"]; 879 } else if (query.literalSearch) { 880 throw ["Cannot use quotes on type filter"]; 881 } 882 // The type filter doesn't count as an element since it's a modifier. 883 const typeFilterElem = query.elems.pop(); 884 checkExtraTypeFilterCharacters(start, parserState); 885 parserState.typeFilter = typeFilterElem.name; 886 parserState.pos += 1; 887 parserState.totalElems -= 1; 888 query.literalSearch = false; 889 foundStopChar = true; 890 continue; 891 } else if (isWhitespace(c)) { 892 skipWhitespace(parserState); 893 continue; 894 } 895 if (!foundStopChar) { 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 } 902 if (parserState.typeFilter !== null) { 903 throw [ 904 "Expected ", 905 ",", 906 " or ", 907 "->", 908 ...extra, 909 ", found ", 910 c, 911 ]; 912 } 913 throw [ 914 "Expected ", 915 ",", 916 ", ", 917 ":", 918 " or ", 919 "->", 920 ...extra, 921 ", found ", 922 c, 923 ]; 924 } 925 const before = query.elems.length; 926 start = parserState.pos; 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 } 934 if (parserState.typeFilter !== null) { 935 throw [ 936 "Unexpected ", 937 ":", 938 " (expected path after type filter ", 939 parserState.typeFilter + ":", 940 ")", 941 ]; 942 } 943 while (parserState.pos < parserState.length) { 944 if (isReturnArrow(parserState)) { 945 parserState.pos += 2; 946 skipWhitespace(parserState); 947 // Get returned elements. 948 getItemsBefore(query, parserState, query.returned, ""); 949 // Nothing can come afterward! 950 if (query.returned.length === 0) { 951 throw ["Expected at least one item after ", "->"]; 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(), 971 elems: [], 972 returned: [], 973 // Total number of "top" elements (does not include generics). 974 foundElems: 0, 975 literalSearch: false, 976 error: null, 977 correction: null, 978 }; 979 } 980 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). 986 * 987 * @return {string} 988 */ 989 function buildUrl(search, filterCrates) { 990 let extra = "?search=" + encodeURIComponent(search); 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() { 1004 const elem = document.getElementById("crate-search"); 1005 1006 if (elem && 1007 elem.value !== "all crates" && 1008 hasOwnPropertyRustdoc(rawSearchIndex, elem.value) 1009 ) { 1010 return elem.value; 1011 } 1012 return null; 1013 } 1014 1015 /** 1016 * Parses the query. 1017 * 1018 * The supported syntax by this parser is as follow: 1019 * 1020 * ident = *(ALPHA / DIGIT / "_") 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) 1025 * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) 1026 * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list ] *(type-sep) 1027 * CLOSE-ANGLE-BRACKET 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 ] 1031 * type-search = [ nonempty-arg-list ] [ return-args ] 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 = ">" 1065 * OPEN-SQUARE-BRACKET = "[" 1066 * CLOSE-SQUARE-BRACKET = "]" 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 1080 */ 1081 function parseQuery(userQuery) { 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 } 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); 1110 for (const elem of query.elems) { 1111 convertTypeFilterOnElem(elem); 1112 } 1113 for (const elem of query.returned) { 1114 convertTypeFilterOnElem(elem); 1115 } 1116 } catch (err) { 1117 query = newParsedQuery(userQuery); 1118 query.error = err; 1119 return query; 1120 } 1121 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 1136 * @param {Array<Result>} results_others 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 } 1149 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 1156 * @param {Object} [currentCrate] - Current crate, to rank results from this crate higher 1157 * 1158 * @return {ResultsTable} 1159 */ 1160 function execQuery(parsedQuery, searchWords, filterCrates, currentCrate) { 1161 const results_others = new Map(), results_in_args = new Map(), 1162 results_returned = new Map(); 1163 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 */ 1171 function transformResults(results) { 1172 const duplicates = new Set(); 1173 const out = []; 1174 1175 for (const result of results) { 1176 if (result.id > -1) { 1177 const obj = searchIndex[result.id]; 1178 obj.dist = result.dist; 1179 const res = buildHrefAndPath(obj); 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; 1184 1185 if (duplicates.has(obj.fullPath)) { 1186 continue; 1187 } 1188 duplicates.add(obj.fullPath); 1189 1190 obj.href = res[1]; 1191 out.push(obj); 1192 if (out.length >= MAX_RESULTS) { 1193 break; 1194 } 1195 } 1196 } 1197 return out; 1198 } 1199 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 */ 1209 function sortResults(results, isType, preferredCrate) { 1210 // if there are no results then return to default and fail 1211 if (results.size === 0) { 1212 return []; 1213 } 1214 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) => { 1224 let a, b; 1225 1226 // sort by exact match with regard to the last word (mismatch goes later) 1227 a = (aaa.word !== userQuery); 1228 b = (bbb.word !== userQuery); 1229 if (a !== b) { 1230 return a - b; 1231 } 1232 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) 1242 a = aaa.path_dist; 1243 b = bbb.path_dist; 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 1256 // (less changes required to match means higher rankings) 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; 1266 if (a !== b) { 1267 return a - b; 1268 } 1269 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 } 1276 1277 // sort by item name length (longer goes later) 1278 a = aaa.word.length; 1279 b = bbb.word.length; 1280 if (a !== b) { 1281 return a - b; 1282 } 1283 1284 // sort by item name (lexicographically larger goes later) 1285 a = aaa.word; 1286 b = bbb.word; 1287 if (a !== b) { 1288 return (a > b ? +1 : -1); 1289 } 1290 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 === ""); 1304 if (a !== b) { 1305 return a - b; 1306 } 1307 1308 // sort by type (later occurrence in `itemTypes` goes later) 1309 a = aaa.item.ty; 1310 b = bbb.item.ty; 1311 if (a !== b) { 1312 return a - b; 1313 } 1314 1315 // sort by path (lexicographically larger goes later) 1316 a = aaa.item.path; 1317 b = bbb.item.path; 1318 if (a !== b) { 1319 return (a > b ? +1 : -1); 1320 } 1321 1322 // que sera, sera 1323 return 0; 1324 }); 1325 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 } 1331 1332 for (const result of result_list) { 1333 // this validation does not make sense when searching by types 1334 if (result.dontValidate) { 1335 continue; 1336 } 1337 const name = result.item.name.toLowerCase(), 1338 path = result.item.path.toLowerCase(), 1339 parent = result.item.parent; 1340 1341 if (!isType && !validateResult(name, path, nameSplit, parent)) { 1342 result.id = -1; 1343 } 1344 } 1345 return transformResults(result_list); 1346 } 1347 1348 /** 1349 * This function checks generics in search query `queryElem` can all be found in the 1350 * search index (`fnType`), 1351 * 1352 * @param {FunctionType} fnType - The object to check. 1353 * @param {QueryElement} queryElem - The element from the parsed query. 1354 * 1355 * @return {boolean} - Returns true if a match, false otherwise. 1356 */ 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) { 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. 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); 1414 } 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); 1437 } 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; 1465 } 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; 1471 } 1472 currentFnTypeList.splice(i, 0, fnType); 1473 } 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); 1482 } 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); 1497 } 1498 fnTypeSet.delete(id); 1499 } 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); 1508 } 1509 } 1510 } 1511 return queryElemSetSize === 0; 1512 } 1513 1514 /** 1515 * This function checks if the object (`row`) matches the given type (`elem`) and its 1516 * generics (if any). 1517 * 1518 * @param {Array<FunctionType>} list 1519 * @param {QueryElement} elem - The element from the parsed query. 1520 * 1521 * @return {boolean} - Returns true if found, false otherwise. 1522 */ 1523 function checkIfInList(list, elem) { 1524 for (const entry of list) { 1525 if (checkType(entry, elem)) { 1526 return true; 1527 } 1528 } 1529 return false; 1530 } 1531 1532 /** 1533 * This function checks if the object (`row`) matches the given type (`elem`) and its 1534 * generics (if any). 1535 * 1536 * @param {Row} row 1537 * @param {QueryElement} elem - The element from the parsed query. 1538 * 1539 * @return {boolean} - Returns true if the type matches, false otherwise. 1540 */ 1541 function checkType(row, elem) { 1542 if (row.id === -1) { 1543 // This is a pure "generic" search, no need to run other checks. 1544 return row.generics.length > 0 ? checkIfInList(row.generics, elem) : false; 1545 } 1546 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)) { 1553 if (elem.generics.length > 0) { 1554 return checkGenerics(row, elem); 1555 } 1556 return true; 1557 } 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 1562 return checkIfInList(row.generics, elem); 1563 } 1564 1565 function checkPath(contains, ty, maxEditDistance) { 1566 if (contains.length === 0) { 1567 return 0; 1568 } 1569 let ret_dist = maxEditDistance + 1; 1570 const path = ty.path.split("::"); 1571 1572 if (ty.parent && ty.parent.name) { 1573 path.push(ty.parent.name.toLowerCase()); 1574 } 1575 1576 const length = path.length; 1577 const clength = contains.length; 1578 if (clength > length) { 1579 return maxEditDistance + 1; 1580 } 1581 for (let i = 0; i < length; ++i) { 1582 if (i + clength > length) { 1583 break; 1584 } 1585 let dist_total = 0; 1586 let aborted = false; 1587 for (let x = 0; x < clength; ++x) { 1588 const dist = editDistance(path[i + x], contains[x], maxEditDistance); 1589 if (dist > maxEditDistance) { 1590 aborted = true; 1591 break; 1592 } 1593 dist_total += dist; 1594 } 1595 if (!aborted) { 1596 ret_dist = Math.min(ret_dist, Math.round(dist_total / clength)); 1597 } 1598 } 1599 return ret_dist; 1600 } 1601 1602 function typePassesFilter(filter, type) { 1603 // No filter or Exact mach 1604 if (filter <= NO_TYPE_FILTER || filter === type) return true; 1605 1606 // Match related items 1607 const name = itemTypes[type]; 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, 1633 deprecated: item.deprecated, 1634 }; 1635 } 1636 1637 function handleAliases(ret, query, filterCrates, currentCrate) { 1638 const lowerQuery = query.toLowerCase(); 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. 1641 const aliases = []; 1642 const crateAliases = []; 1643 if (filterCrates !== null) { 1644 if (ALIASES.has(filterCrates) && ALIASES.get(filterCrates).has(lowerQuery)) { 1645 const query_aliases = ALIASES.get(filterCrates).get(lowerQuery); 1646 for (const alias of query_aliases) { 1647 aliases.push(createAliasFromItem(searchIndex[alias])); 1648 } 1649 } 1650 } else { 1651 for (const [crate, crateAliasesIndex] of ALIASES) { 1652 if (crateAliasesIndex.has(lowerQuery)) { 1653 const pushTo = crate === currentCrate ? crateAliases : aliases; 1654 const query_aliases = crateAliasesIndex.get(lowerQuery); 1655 for (const alias of query_aliases) { 1656 pushTo.push(createAliasFromItem(searchIndex[alias])); 1657 } 1658 } 1659 } 1660 } 1661 1662 const sortFunc = (aaa, bbb) => { 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 1673 const pushFunc = alias => { 1674 alias.alias = query; 1675 const res = buildHrefAndPath(alias); 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 }; 1685 1686 aliases.forEach(pushFunc); 1687 crateAliases.forEach(pushFunc); 1688 } 1689 1690 /** 1691 * This function adds the given result into the provided `results` map if it matches the 1692 * following condition: 1693 * 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`. 1696 * 1697 * The `results` map contains information which will be used to sort the search results: 1698 * 1699 * * `fullId` is a `string`` used as the key of the object we use for the `results` map. 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. 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 1704 * distance computed for everything other than the last path component. 1705 * 1706 * @param {Results} results 1707 * @param {string} fullId 1708 * @param {integer} id 1709 * @param {integer} index 1710 * @param {integer} dist 1711 * @param {integer} path_dist 1712 */ 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) { 1719 return; 1720 } 1721 } 1722 results.set(fullId, { 1723 id: id, 1724 index: index, 1725 dontValidate: parsedQuery.literalSearch, 1726 dist: dist, 1727 path_dist: path_dist, 1728 }); 1729 } 1730 } 1731 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 * 1736 * Other important thing to note: since there is only one element, we use edit 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, 1753 results_returned, 1754 maxEditDistance 1755 ) { 1756 if (!row || (filterCrates !== null && row.crate !== filterCrates)) { 1757 return; 1758 } 1759 let index = -1, path_dist = 0; 1760 const fullId = row.id; 1761 const searchWord = searchWords[pos]; 1762 1763 const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem); 1764 if (in_args) { 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 } 1769 const returned = row.type && row.type.output && checkIfInList(row.type.output, elem); 1770 if (returned) { 1771 addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance); 1772 } 1773 1774 if (!typePassesFilter(elem.typeFilter, row.ty)) { 1775 return; 1776 } 1777 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; 1791 } 1792 1793 if (elem.fullPath.length > 1) { 1794 path_dist = checkPath(elem.pathWithoutLast, row, maxEditDistance); 1795 if (path_dist > maxEditDistance) { 1796 return; 1797 } 1798 } 1799 1800 if (parsedQuery.literalSearch) { 1801 if (searchWord === elem.name) { 1802 addIntoResults(results_others, fullId, pos, index, 0, path_dist); 1803 } 1804 return; 1805 } 1806 1807 const dist = editDistance(searchWord, elem.pathLast, maxEditDistance); 1808 1809 if (index === -1 && dist + path_dist > maxEditDistance) { 1810 return; 1811 } 1812 1813 addIntoResults(results_others, fullId, pos, index, dist, path_dist, maxEditDistance); 1814 } 1815 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 */ 1825 function handleArgs(row, pos, results) { 1826 if (!row || (filterCrates !== null && row.crate !== filterCrates) || !row.type) { 1827 return; 1828 } 1829 1830 // If the result is too "bad", we return false and it ends this search. 1831 if (!unifyFunctionTypes(row.type.inputs, parsedQuery.elems)) { 1832 return; 1833 } 1834 if (!unifyFunctionTypes(row.type.output, parsedQuery.returned)) { 1835 return; 1836 } 1837 1838 addIntoResults(results, row.id, pos, 0, 0, 0, Number.MAX_VALUE); 1839 } 1840 1841 function innerRunQuery() { 1842 let elem, i, nSearchWords, in_returned, row; 1843 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 } 1851 const maxEditDistance = Math.floor(queryLen / 3); 1852 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 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, 1912 results_returned, 1913 maxEditDistance 1914 ); 1915 } 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]; 1921 in_returned = row.type && 1922 unifyFunctionTypes(row.type.output, parsedQuery.returned); 1923 if (in_returned) { 1924 addIntoResults( 1925 results_others, 1926 row.id, 1927 i, 1928 -1, 1929 0, 1930 Number.MAX_VALUE 1931 ); 1932 } 1933 } 1934 } 1935 } else if (parsedQuery.foundElems > 0) { 1936 for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { 1937 handleArgs(searchIndex[i], i, results_others); 1938 } 1939 } 1940 } 1941 1942 if (parsedQuery.error === null) { 1943 innerRunQuery(); 1944 } 1945 1946 const ret = createQueryResults( 1947 sortResults(results_in_args, true, currentCrate), 1948 sortResults(results_returned, true, currentCrate), 1949 sortResults(results_others, false, currentCrate), 1950 parsedQuery); 1951 handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates, currentCrate); 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 } 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 * 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 1972 * 1973 * @return {boolean} - Whether the result is valid or not 1974 */ 1975 function validateResult(name, path, keys, parent, maxEditDistance) { 1976 if (!keys || !keys.length) { 1977 return true; 1978 } 1979 for (const key of keys) { 1980 // each check is for validation so we negate the conditions and invalidate 1981 if (!( 1982 // check for an exact name match 1983 name.indexOf(key) > -1 || 1984 // then an exact path match 1985 path.indexOf(key) > -1 || 1986 // next if there is a parent, check for exact parent match 1987 (parent !== undefined && parent.name !== undefined && 1988 parent.name.toLowerCase().indexOf(key) > -1) || 1989 // lastly check to see if the name was an editDistance match 1990 editDistance(name, key, maxEditDistance) <= maxEditDistance)) { 1991 return false; 1992 } 1993 } 1994 return true; 1995 } 1996 1997 function nextTab(direction) { 1998 const next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; 1999 searchState.focusedByTab[searchState.currentTab] = document.activeElement; 2000 printTab(next); 2001 focusSearchResult(); 2002 } 2003 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() { 2007 const target = searchState.focusedByTab[searchState.currentTab] || 2008 document.querySelectorAll(".search-results.active a").item(0) || 2009 document.querySelectorAll("#search-tabs button").item(searchState.currentTab); 2010 searchState.focusedByTab[searchState.currentTab] = null; 2011 if (target) { 2012 target.focus(); 2013 } 2014 } 2015 2016 function buildHrefAndPath(item) { 2017 let displayPath; 2018 let href; 2019 const type = itemTypes[item.ty]; 2020 const name = item.name; 2021 let path = item.path; 2022 2023 if (type === "mod") { 2024 displayPath = path + "::"; 2025 href = ROOT_PATH + path.replace(/::/g, "/") + "/" + 2026 name + "/index.html"; 2027 } else if (type === "import") { 2028 displayPath = item.path + "::"; 2029 href = ROOT_PATH + item.path.replace(/::/g, "/") + "/index.html#reexport." + name; 2030 } else if (type === "primitive" || type === "keyword") { 2031 displayPath = ""; 2032 href = ROOT_PATH + path.replace(/::/g, "/") + 2033 "/" + type + "." + name + ".html"; 2034 } else if (type === "externcrate") { 2035 displayPath = ""; 2036 href = ROOT_PATH + name + "/index.html"; 2037 } else if (item.parent !== undefined) { 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; 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. 2049 const enumNameIdx = item.path.lastIndexOf("::"); 2050 const enumName = item.path.substr(enumNameIdx + 2); 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 } 2059 href = ROOT_PATH + path.replace(/::/g, "/") + 2060 "/" + pageType + 2061 "." + pageName + 2062 ".html" + anchor; 2063 } else { 2064 displayPath = item.path + "::"; 2065 href = ROOT_PATH + item.path.replace(/::/g, "/") + 2066 "/" + type + "." + name + ".html"; 2067 } 2068 return [displayPath, href]; 2069 } 2070 2071 function pathSplitter(path) { 2072 const tmp = "<span>" + path.replace(/::/g, "::</span><span>"); 2073 if (tmp.endsWith("<span>")) { 2074 return tmp.slice(0, tmp.length - 6); 2075 } 2076 return tmp; 2077 } 2078 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 */ 2085 function addTab(array, query, display) { 2086 let extraClass = ""; 2087 if (display === true) { 2088 extraClass = " active"; 2089 } 2090 2091 const output = document.createElement("div"); 2092 let length = 0; 2093 if (array.length > 0) { 2094 output.className = "search-results " + extraClass; 2095 2096 array.forEach(item => { 2097 const name = item.name; 2098 const type = itemTypes[item.ty]; 2099 const longType = longItemTypes[item.ty]; 2100 const typeName = longType.length !== 0 ? `${longType}` : "?"; 2101 2102 length += 1; 2103 2104 const link = document.createElement("a"); 2105 link.className = "result-" + type; 2106 link.href = item.href; 2107 2108 const resultName = document.createElement("div"); 2109 resultName.className = "result-name"; 2110 2111 if (item.is_alias) { 2112 const alias = document.createElement("span"); 2113 alias.className = "alias"; 2114 2115 const bold = document.createElement("b"); 2116 bold.innerText = item.alias; 2117 alias.appendChild(bold); 2118 2119 alias.insertAdjacentHTML( 2120 "beforeend", 2121 "<i class=\"grey\"> - see </i>"); 2122 2123 resultName.appendChild(alias); 2124 } 2125 2126 resultName.insertAdjacentHTML( 2127 "beforeend", 2128 `\ 2129<span class="typename">${typeName}</span>\ 2130<div class="path">\ 2131 ${item.displayPath}<span class="${type}">${name}</span>\ 2132</div>`); 2133 link.appendChild(resultName); 2134 2135 const description = document.createElement("div"); 2136 description.className = "desc"; 2137 description.insertAdjacentHTML("beforeend", item.desc); 2138 2139 link.appendChild(description); 2140 output.appendChild(link); 2141 }); 2142 } else if (query.error === null) { 2143 output.className = "search-failed" + extraClass; 2144 output.innerHTML = "No results :(<br/>" + 2145 "Try on <a href=\"https://duckduckgo.com/?q=" + 2146 encodeURIComponent("rust " + query.userQuery) + 2147 "\">DuckDuckGo</a>?<br/><br/>" + 2148 "Or try looking in one of these:<ul><li>The <a " + 2149 "href=\"https://doc.rust-lang.org/reference/index.html\">Rust Reference</a> " + 2150 " for technical details about the language.</li><li><a " + 2151 "href=\"https://doc.rust-lang.org/rust-by-example/index.html\">Rust By " + 2152 "Example</a> for expository code examples.</a></li><li>The <a " + 2153 "href=\"https://doc.rust-lang.org/book/index.html\">Rust Book</a> for " + 2154 "introductions to language features and the language itself.</li><li><a " + 2155 "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" + 2156 " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>"; 2157 } 2158 return [output, length]; 2159 } 2160 2161 function makeTabHeader(tabNb, text, nbElems) { 2162 if (searchState.currentTab === tabNb) { 2163 return "<button class=\"selected\">" + text + 2164 " <span class=\"count\">(" + nbElems + ")</span></button>"; 2165 } 2166 return "<button>" + text + " <span class=\"count\">(" + nbElems + ")</span></button>"; 2167 } 2168 2169 /** 2170 * @param {ResultsTable} results 2171 * @param {boolean} go_to_first 2172 * @param {string} filterCrates 2173 */ 2174 function showResults(results, go_to_first, filterCrates) { 2175 const search = searchState.outputElement(); 2176 if (go_to_first || (results.others.length === 1 2177 && getSettingValue("go-to-only-result") === "true") 2178 ) { 2179 // Needed to force re-execution of JS when coming back to a page. Let's take this 2180 // scenario as example: 2181 // 2182 // 1. You have the "Directly go to item in search if there is only one result" option 2183 // enabled. 2184 // 2. You make a search which results only one result, leading you automatically to 2185 // this result. 2186 // 3. You go back to previous page. 2187 // 2188 // Now, without the call below, the JS will not be re-executed and the previous state 2189 // will be used, starting search again since the search input is not empty, leading you 2190 // back to the previous page again. 2191 window.onunload = () => {}; 2192 searchState.removeQueryParameters(); 2193 const elem = document.createElement("a"); 2194 elem.href = results.others[0].href; 2195 removeClass(elem, "active"); 2196 // For firefox, we need the element to be in the DOM so it can be clicked. 2197 document.body.appendChild(elem); 2198 elem.click(); 2199 return; 2200 } 2201 if (results.query === undefined) { 2202 results.query = parseQuery(searchState.input.value); 2203 } 2204 2205 currentResults = results.query.userQuery; 2206 2207 const ret_others = addTab(results.others, results.query, true); 2208 const ret_in_args = addTab(results.in_args, results.query, false); 2209 const ret_returned = addTab(results.returned, results.query, false); 2210 2211 // Navigate to the relevant tab if the current tab is empty, like in case users search 2212 // for "-> String". If they had selected another tab previously, they have to click on 2213 // it again. 2214 let currentTab = searchState.currentTab; 2215 if ((currentTab === 0 && ret_others[1] === 0) || 2216 (currentTab === 1 && ret_in_args[1] === 0) || 2217 (currentTab === 2 && ret_returned[1] === 0)) { 2218 if (ret_others[1] !== 0) { 2219 currentTab = 0; 2220 } else if (ret_in_args[1] !== 0) { 2221 currentTab = 1; 2222 } else if (ret_returned[1] !== 0) { 2223 currentTab = 2; 2224 } 2225 } 2226 2227 let crates = ""; 2228 const crates_list = Object.keys(rawSearchIndex); 2229 if (crates_list.length > 1) { 2230 crates = " in <div id=\"crate-search-div\"><select id=\"crate-search\">" + 2231 "<option value=\"all crates\">all crates</option>"; 2232 for (const c of crates_list) { 2233 crates += `<option value="${c}" ${c === filterCrates && "selected"}>${c}</option>`; 2234 } 2235 crates += "</select></div>"; 2236 } 2237 2238 let output = `<h1 class="search-results-title">Results${crates}</h1>`; 2239 if (results.query.error !== null) { 2240 const error = results.query.error; 2241 error.forEach((value, index) => { 2242 value = value.split("<").join("<").split(">").join(">"); 2243 if (index % 2 !== 0) { 2244 error[index] = `<code>${value.replaceAll(" ", " ")}</code>`; 2245 } else { 2246 error[index] = value; 2247 } 2248 }); 2249 output += `<h3 class="error">Query parser error: "${error.join("")}".</h3>`; 2250 output += "<div id=\"search-tabs\">" + 2251 makeTabHeader(0, "In Names", ret_others[1]) + 2252 "</div>"; 2253 currentTab = 0; 2254 } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { 2255 output += "<div id=\"search-tabs\">" + 2256 makeTabHeader(0, "In Names", ret_others[1]) + 2257 makeTabHeader(1, "In Parameters", ret_in_args[1]) + 2258 makeTabHeader(2, "In Return Types", ret_returned[1]) + 2259 "</div>"; 2260 } else { 2261 const signatureTabTitle = 2262 results.query.elems.length === 0 ? "In Function Return Types" : 2263 results.query.returned.length === 0 ? "In Function Parameters" : 2264 "In Function Signatures"; 2265 output += "<div id=\"search-tabs\">" + 2266 makeTabHeader(0, signatureTabTitle, ret_others[1]) + 2267 "</div>"; 2268 currentTab = 0; 2269 } 2270 2271 if (results.query.correction !== null) { 2272 const orig = results.query.returned.length > 0 2273 ? results.query.returned[0].name 2274 : results.query.elems[0].name; 2275 output += "<h3 class=\"search-corrections\">" + 2276 `Type "${orig}" not found. ` + 2277 "Showing results for closest type name " + 2278 `"${results.query.correction}" instead.</h3>`; 2279 } 2280 2281 const resultsElem = document.createElement("div"); 2282 resultsElem.id = "results"; 2283 resultsElem.appendChild(ret_others[0]); 2284 resultsElem.appendChild(ret_in_args[0]); 2285 resultsElem.appendChild(ret_returned[0]); 2286 2287 search.innerHTML = output; 2288 const crateSearch = document.getElementById("crate-search"); 2289 if (crateSearch) { 2290 crateSearch.addEventListener("input", updateCrate); 2291 } 2292 search.appendChild(resultsElem); 2293 // Reset focused elements. 2294 searchState.showResults(search); 2295 const elems = document.getElementById("search-tabs").childNodes; 2296 searchState.focusedByTab = []; 2297 let i = 0; 2298 for (const elem of elems) { 2299 const j = i; 2300 elem.onclick = () => printTab(j); 2301 searchState.focusedByTab.push(null); 2302 i += 1; 2303 } 2304 printTab(currentTab); 2305 } 2306 2307 function updateSearchHistory(url) { 2308 if (!browserSupportsHistoryApi()) { 2309 return; 2310 } 2311 const params = searchState.getQueryStringParams(); 2312 if (!history.state && !params.search) { 2313 history.pushState(null, "", url); 2314 } else { 2315 history.replaceState(null, "", url); 2316 } 2317 } 2318 2319 /** 2320 * Perform a search based on the current state of the search input element 2321 * and display the results. 2322 * @param {Event} [e] - The event that triggered this search, if any 2323 * @param {boolean} [forced] 2324 */ 2325 function search(e, forced) { 2326 if (e) { 2327 e.preventDefault(); 2328 } 2329 const query = parseQuery(searchState.input.value.trim()); 2330 let filterCrates = getFilterCrates(); 2331 2332 if (!forced && query.userQuery === currentResults) { 2333 if (query.userQuery.length > 0) { 2334 putBackSearch(); 2335 } 2336 return; 2337 } 2338 2339 searchState.setLoadingSearch(); 2340 2341 const params = searchState.getQueryStringParams(); 2342 2343 // In case we have no information about the saved crate and there is a URL query parameter, 2344 // we override it with the URL query parameter. 2345 if (filterCrates === null && params["filter-crate"] !== undefined) { 2346 filterCrates = params["filter-crate"]; 2347 } 2348 2349 // Update document title to maintain a meaningful browser history 2350 searchState.title = "Results for " + query.original + " - Rust"; 2351 2352 // Because searching is incremental by character, only the most 2353 // recent search query is added to the browser history. 2354 updateSearchHistory(buildUrl(query.original, filterCrates)); 2355 2356 showResults( 2357 execQuery(query, searchWords, filterCrates, window.currentCrate), 2358 params.go_to_first, 2359 filterCrates); 2360 } 2361 2362 /** 2363 * Convert a list of RawFunctionType / ID to object-based FunctionType. 2364 * 2365 * Crates often have lots of functions in them, and it's common to have a large number of 2366 * functions that operate on a small set of data types, so the search index compresses them 2367 * by encoding function parameter and return types as indexes into an array of names. 2368 * 2369 * Even when a general-purpose compression algorithm is used, this is still a win. I checked. 2370 * https://github.com/rust-lang/rust/pull/98475#issue-1284395985 2371 * 2372 * The format for individual function types is encoded in 2373 * librustdoc/html/render/mod.rs: impl Serialize for RenderType 2374 * 2375 * @param {null|Array<RawFunctionType>} types 2376 * @param {Array<{name: string, ty: number}>} lowercasePaths 2377 * 2378 * @return {Array<FunctionSearchType>} 2379 */ 2380 function buildItemSearchTypeAll(types, lowercasePaths) { 2381 const PATH_INDEX_DATA = 0; 2382 const GENERICS_DATA = 1; 2383 return types.map(type => { 2384 let pathIndex, generics; 2385 if (typeof type === "number") { 2386 pathIndex = type; 2387 generics = []; 2388 } else { 2389 pathIndex = type[PATH_INDEX_DATA]; 2390 generics = buildItemSearchTypeAll( 2391 type[GENERICS_DATA], 2392 lowercasePaths 2393 ); 2394 } 2395 return { 2396 // `0` is used as a sentinel because it's fewer bytes than `null` 2397 id: pathIndex === 0 2398 ? -1 2399 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), 2400 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, 2401 generics: generics, 2402 }; 2403 }); 2404 } 2405 2406 /** 2407 * Convert from RawFunctionSearchType to FunctionSearchType. 2408 * 2409 * Crates often have lots of functions in them, and function signatures are sometimes complex, 2410 * so rustdoc uses a pretty tight encoding for them. This function converts it to a simpler, 2411 * object-based encoding so that the actual search code is more readable and easier to debug. 2412 * 2413 * The raw function search type format is generated using serde in 2414 * librustdoc/html/render/mod.rs: impl Serialize for IndexItemFunctionType 2415 * 2416 * @param {RawFunctionSearchType} functionSearchType 2417 * @param {Array<{name: string, ty: number}>} lowercasePaths 2418 * @param {Map<string, integer>} 2419 * 2420 * @return {null|FunctionSearchType} 2421 */ 2422 function buildFunctionSearchType(functionSearchType, lowercasePaths) { 2423 const INPUTS_DATA = 0; 2424 const OUTPUT_DATA = 1; 2425 // `0` is used as a sentinel because it's fewer bytes than `null` 2426 if (functionSearchType === 0) { 2427 return null; 2428 } 2429 let inputs, output; 2430 if (typeof functionSearchType[INPUTS_DATA] === "number") { 2431 const pathIndex = functionSearchType[INPUTS_DATA]; 2432 inputs = [{ 2433 id: pathIndex === 0 2434 ? -1 2435 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), 2436 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, 2437 generics: [], 2438 }]; 2439 } else { 2440 inputs = buildItemSearchTypeAll( 2441 functionSearchType[INPUTS_DATA], 2442 lowercasePaths 2443 ); 2444 } 2445 if (functionSearchType.length > 1) { 2446 if (typeof functionSearchType[OUTPUT_DATA] === "number") { 2447 const pathIndex = functionSearchType[OUTPUT_DATA]; 2448 output = [{ 2449 id: pathIndex === 0 2450 ? -1 2451 : buildTypeMapIndex(lowercasePaths[pathIndex - 1].name), 2452 ty: pathIndex === 0 ? null : lowercasePaths[pathIndex - 1].ty, 2453 generics: [], 2454 }]; 2455 } else { 2456 output = buildItemSearchTypeAll( 2457 functionSearchType[OUTPUT_DATA], 2458 lowercasePaths 2459 ); 2460 } 2461 } else { 2462 output = []; 2463 } 2464 return { 2465 inputs, output, 2466 }; 2467 } 2468 2469 function buildIndex(rawSearchIndex) { 2470 searchIndex = []; 2471 /** 2472 * List of normalized search words (ASCII lowercased, and undescores removed). 2473 * 2474 * @type {Array<string>} 2475 */ 2476 const searchWords = []; 2477 typeNameIdMap = new Map(); 2478 const charA = "A".charCodeAt(0); 2479 let currentIndex = 0; 2480 let id = 0; 2481 2482 // Initialize type map indexes for primitive list types 2483 // that can be searched using `[]` syntax. 2484 typeNameIdOfArray = buildTypeMapIndex("array"); 2485 typeNameIdOfSlice = buildTypeMapIndex("slice"); 2486 typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]"); 2487 2488 for (const crate in rawSearchIndex) { 2489 if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) { 2490 continue; 2491 } 2492 2493 let crateSize = 0; 2494 2495 /** 2496 * The raw search data for a given crate. `n`, `t`, `d`, and `q`, `i`, and `f` 2497 * are arrays with the same length. n[i] contains the name of an item. 2498 * t[i] contains the type of that item (as a string of characters that represent an 2499 * offset in `itemTypes`). d[i] contains the description of that item. 2500 * 2501 * q[i] contains the full path of the item, or an empty string indicating 2502 * "same as q[i-1]". 2503 * 2504 * i[i] contains an item's parent, usually a module. For compactness, 2505 * it is a set of indexes into the `p` array. 2506 * 2507 * f[i] contains function signatures, or `0` if the item isn't a function. 2508 * Functions are themselves encoded as arrays. The first item is a list of 2509 * types representing the function's inputs, and the second list item is a list 2510 * of types representing the function's output. Tuples are flattened. 2511 * Types are also represented as arrays; the first item is an index into the `p` 2512 * array, while the second is a list of types representing any generic parameters. 2513 * 2514 * `a` defines aliases with an Array of pairs: [name, offset], where `offset` 2515 * points into the n/t/d/q/i/f arrays. 2516 * 2517 * `doc` contains the description of the crate. 2518 * 2519 * `p` is a list of path/type pairs. It is used for parents and function parameters. 2520 * 2521 * @type {{ 2522 * doc: string, 2523 * a: Object, 2524 * n: Array<string>, 2525 * t: String, 2526 * d: Array<string>, 2527 * q: Array<[Number, string]>, 2528 * i: Array<Number>, 2529 * f: Array<RawFunctionSearchType>, 2530 * p: Array<Object>, 2531 * c: Array<Number> 2532 * }} 2533 */ 2534 const crateCorpus = rawSearchIndex[crate]; 2535 2536 searchWords.push(crate); 2537 // This object should have exactly the same set of fields as the "row" 2538 // object defined below. Your JavaScript runtime will thank you. 2539 // https://mathiasbynens.be/notes/shapes-ics 2540 const crateRow = { 2541 crate: crate, 2542 ty: 1, // == ExternCrate 2543 name: crate, 2544 path: "", 2545 desc: crateCorpus.doc, 2546 parent: undefined, 2547 type: null, 2548 id: id, 2549 normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), 2550 deprecated: null, 2551 }; 2552 id += 1; 2553 searchIndex.push(crateRow); 2554 currentIndex += 1; 2555 2556 // a String of one character item type codes 2557 const itemTypes = crateCorpus.t; 2558 // an array of (String) item names 2559 const itemNames = crateCorpus.n; 2560 // an array of [(Number) item index, 2561 // (String) full path] 2562 // an item whose index is not present will fall back to the previous present path 2563 // i.e. if indices 4 and 11 are present, but 5-10 and 12-13 are not present, 2564 // 5-10 will fall back to the path for 4 and 12-13 will fall back to the path for 11 2565 const itemPaths = new Map(crateCorpus.q); 2566 // an array of (String) descriptions 2567 const itemDescs = crateCorpus.d; 2568 // an array of (Number) the parent path index + 1 to `paths`, or 0 if none 2569 const itemParentIdxs = crateCorpus.i; 2570 // an array of (Object | null) the type of the function, if any 2571 const itemFunctionSearchTypes = crateCorpus.f; 2572 // an array of (Number) indices for the deprecated items 2573 const deprecatedItems = new Set(crateCorpus.c); 2574 // an array of [(Number) item type, 2575 // (String) name] 2576 const paths = crateCorpus.p; 2577 // an array of [(String) alias name 2578 // [Number] index to items] 2579 const aliases = crateCorpus.a; 2580 2581 // an array of [{name: String, ty: Number}] 2582 const lowercasePaths = []; 2583 2584 // convert `rawPaths` entries into object form 2585 // generate normalizedPaths for function search mode 2586 let len = paths.length; 2587 for (let i = 0; i < len; ++i) { 2588 lowercasePaths.push({ty: paths[i][0], name: paths[i][1].toLowerCase()}); 2589 paths[i] = {ty: paths[i][0], name: paths[i][1]}; 2590 } 2591 2592 // convert `item*` into an object form, and construct word indices. 2593 // 2594 // before any analysis is performed lets gather the search terms to 2595 // search against apart from the rest of the data. This is a quick 2596 // operation that is cached for the life of the page state so that 2597 // all other search operations have access to this cached data for 2598 // faster analysis operations 2599 len = itemTypes.length; 2600 let lastPath = ""; 2601 for (let i = 0; i < len; ++i) { 2602 let word = ""; 2603 // This object should have exactly the same set of fields as the "crateRow" 2604 // object defined above. 2605 if (typeof itemNames[i] === "string") { 2606 word = itemNames[i].toLowerCase(); 2607 } 2608 searchWords.push(word); 2609 const row = { 2610 crate: crate, 2611 ty: itemTypes.charCodeAt(i) - charA, 2612 name: itemNames[i], 2613 path: itemPaths.has(i) ? itemPaths.get(i) : lastPath, 2614 desc: itemDescs[i], 2615 parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined, 2616 type: buildFunctionSearchType( 2617 itemFunctionSearchTypes[i], 2618 lowercasePaths 2619 ), 2620 id: id, 2621 normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), 2622 deprecated: deprecatedItems.has(i), 2623 }; 2624 id += 1; 2625 searchIndex.push(row); 2626 lastPath = row.path; 2627 crateSize += 1; 2628 } 2629 2630 if (aliases) { 2631 const currentCrateAliases = new Map(); 2632 ALIASES.set(crate, currentCrateAliases); 2633 for (const alias_name in aliases) { 2634 if (!hasOwnPropertyRustdoc(aliases, alias_name)) { 2635 continue; 2636 } 2637 2638 let currentNameAliases; 2639 if (currentCrateAliases.has(alias_name)) { 2640 currentNameAliases = currentCrateAliases.get(alias_name); 2641 } else { 2642 currentNameAliases = []; 2643 currentCrateAliases.set(alias_name, currentNameAliases); 2644 } 2645 for (const local_alias of aliases[alias_name]) { 2646 currentNameAliases.push(local_alias + currentIndex); 2647 } 2648 } 2649 } 2650 currentIndex += crateSize; 2651 } 2652 return searchWords; 2653 } 2654 2655 /** 2656 * Callback for when the search form is submitted. 2657 * @param {Event} [e] - The event that triggered this call, if any 2658 */ 2659 function onSearchSubmit(e) { 2660 e.preventDefault(); 2661 searchState.clearInputTimeout(); 2662 search(); 2663 } 2664 2665 function putBackSearch() { 2666 const search_input = searchState.input; 2667 if (!searchState.input) { 2668 return; 2669 } 2670 if (search_input.value !== "" && !searchState.isDisplayed()) { 2671 searchState.showResults(); 2672 if (browserSupportsHistoryApi()) { 2673 history.replaceState(null, "", 2674 buildUrl(search_input.value, getFilterCrates())); 2675 } 2676 document.title = searchState.title; 2677 } 2678 } 2679 2680 function registerSearchEvents() { 2681 const params = searchState.getQueryStringParams(); 2682 2683 // Populate search bar with query string search term when provided, 2684 // but only if the input bar is empty. This avoid the obnoxious issue 2685 // where you start trying to do a search, and the index loads, and 2686 // suddenly your search is gone! 2687 if (searchState.input.value === "") { 2688 searchState.input.value = params.search || ""; 2689 } 2690 2691 const searchAfter500ms = () => { 2692 searchState.clearInputTimeout(); 2693 if (searchState.input.value.length === 0) { 2694 searchState.hideResults(); 2695 } else { 2696 searchState.timeout = setTimeout(search, 500); 2697 } 2698 }; 2699 searchState.input.onkeyup = searchAfter500ms; 2700 searchState.input.oninput = searchAfter500ms; 2701 document.getElementsByClassName("search-form")[0].onsubmit = onSearchSubmit; 2702 searchState.input.onchange = e => { 2703 if (e.target !== document.activeElement) { 2704 // To prevent doing anything when it's from a blur event. 2705 return; 2706 } 2707 // Do NOT e.preventDefault() here. It will prevent pasting. 2708 searchState.clearInputTimeout(); 2709 // zero-timeout necessary here because at the time of event handler execution the 2710 // pasted content is not in the input field yet. Shouldn’t make any difference for 2711 // change, though. 2712 setTimeout(search, 0); 2713 }; 2714 searchState.input.onpaste = searchState.input.onchange; 2715 2716 searchState.outputElement().addEventListener("keydown", e => { 2717 // We only handle unmodified keystrokes here. We don't want to interfere with, 2718 // for instance, alt-left and alt-right for history navigation. 2719 if (e.altKey || e.ctrlKey || e.shiftKey || e.metaKey) { 2720 return; 2721 } 2722 // up and down arrow select next/previous search result, or the 2723 // search box if we're already at the top. 2724 if (e.which === 38) { // up 2725 const previous = document.activeElement.previousElementSibling; 2726 if (previous) { 2727 previous.focus(); 2728 } else { 2729 searchState.focus(); 2730 } 2731 e.preventDefault(); 2732 } else if (e.which === 40) { // down 2733 const next = document.activeElement.nextElementSibling; 2734 if (next) { 2735 next.focus(); 2736 } 2737 const rect = document.activeElement.getBoundingClientRect(); 2738 if (window.innerHeight - rect.bottom < rect.height) { 2739 window.scrollBy(0, rect.height); 2740 } 2741 e.preventDefault(); 2742 } else if (e.which === 37) { // left 2743 nextTab(-1); 2744 e.preventDefault(); 2745 } else if (e.which === 39) { // right 2746 nextTab(1); 2747 e.preventDefault(); 2748 } 2749 }); 2750 2751 searchState.input.addEventListener("keydown", e => { 2752 if (e.which === 40) { // down 2753 focusSearchResult(); 2754 e.preventDefault(); 2755 } 2756 }); 2757 2758 searchState.input.addEventListener("focus", () => { 2759 putBackSearch(); 2760 }); 2761 2762 searchState.input.addEventListener("blur", () => { 2763 searchState.input.placeholder = searchState.input.origPlaceholder; 2764 }); 2765 2766 // Push and pop states are used to add search results to the browser 2767 // history. 2768 if (browserSupportsHistoryApi()) { 2769 // Store the previous <title> so we can revert back to it later. 2770 const previousTitle = document.title; 2771 2772 window.addEventListener("popstate", e => { 2773 const params = searchState.getQueryStringParams(); 2774 // Revert to the previous title manually since the History 2775 // API ignores the title parameter. 2776 document.title = previousTitle; 2777 // When browsing forward to search results the previous 2778 // search will be repeated, so the currentResults are 2779 // cleared to ensure the search is successful. 2780 currentResults = null; 2781 // Synchronize search bar with query string state and 2782 // perform the search. This will empty the bar if there's 2783 // nothing there, which lets you really go back to a 2784 // previous state with nothing in the bar. 2785 if (params.search && params.search.length > 0) { 2786 searchState.input.value = params.search; 2787 // Some browsers fire "onpopstate" for every page load 2788 // (Chrome), while others fire the event only when actually 2789 // popping a state (Firefox), which is why search() is 2790 // called both here and at the end of the startSearch() 2791 // function. 2792 search(e); 2793 } else { 2794 searchState.input.value = ""; 2795 // When browsing back from search results the main page 2796 // visibility must be reset. 2797 searchState.hideResults(); 2798 } 2799 }); 2800 } 2801 2802 // This is required in firefox to avoid this problem: Navigating to a search result 2803 // with the keyboard, hitting enter, and then hitting back would take you back to 2804 // the doc page, rather than the search that should overlay it. 2805 // This was an interaction between the back-forward cache and our handlers 2806 // that try to sync state between the URL and the search input. To work around it, 2807 // do a small amount of re-init on page show. 2808 window.onpageshow = () => { 2809 const qSearch = searchState.getQueryStringParams().search; 2810 if (searchState.input.value === "" && qSearch) { 2811 searchState.input.value = qSearch; 2812 } 2813 search(); 2814 }; 2815 } 2816 2817 function updateCrate(ev) { 2818 if (ev.target.value === "all crates") { 2819 // If we don't remove it from the URL, it'll be picked up again by the search. 2820 const query = searchState.input.value.trim(); 2821 updateSearchHistory(buildUrl(query, null)); 2822 } 2823 // In case you "cut" the entry from the search input, then change the crate filter 2824 // before paste back the previous search, you get the old search results without 2825 // the filter. To prevent this, we need to remove the previous results. 2826 currentResults = null; 2827 search(undefined, true); 2828 } 2829 2830 /** 2831 * @type {Array<string>} 2832 */ 2833 const searchWords = buildIndex(rawSearchIndex); 2834 if (typeof window !== "undefined") { 2835 registerSearchEvents(); 2836 // If there's a search term in the URL, execute the search now. 2837 if (window.searchState.getQueryStringParams().search) { 2838 search(); 2839 } 2840 } 2841 2842 if (typeof exports !== "undefined") { 2843 exports.initSearch = initSearch; 2844 exports.execQuery = execQuery; 2845 exports.parseQuery = parseQuery; 2846 } 2847 return searchWords; 2848} 2849 2850if (typeof window !== "undefined") { 2851 window.initSearch = initSearch; 2852 if (window.searchIndex !== undefined) { 2853 initSearch(window.searchIndex); 2854 } 2855} else { 2856 // Running in Node, not a browser. Run initSearch just to produce the 2857 // exports. 2858 initSearch({}); 2859} 2860 2861 2862})(); 2863