]>
Commit | Line | Data |
---|---|---|
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. |
9 | const 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 |
38 | const 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 |
68 | const TY_PRIMITIVE = itemTypes.indexOf("primitive"); |
69 | const TY_KEYWORD = itemTypes.indexOf("keyword"); | |
923072b8 FG |
70 | const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; |
71 | ||
72 | function 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. | |
77 | function 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 | */ | |
130 | const 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 | ||
219 | function editDistance(a, b, limit) { | |
220 | return editDistanceState.calculate(a, b, limit); | |
cdc7bbd5 XL |
221 | } |
222 | ||
923072b8 | 223 | function 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"> - see </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 <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("<").split(">").join(">"); | |
2236 | if (index % 2 !== 0) { | |
fe692bf9 | 2237 | error[index] = `<code>${value.replaceAll(" ", " ")}</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 |
2843 | if (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 | })(); |