diff options
Diffstat (limited to 'src/librustdoc/html/static/js/search.js')
| -rw-r--r-- | src/librustdoc/html/static/js/search.js | 688 |
1 files changed, 608 insertions, 80 deletions
diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index a4471f1d885..0458b81d352 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -15,7 +15,16 @@ if (!Array.prototype.toSpliced) { }; } -(function() { +function onEachBtwn(arr, func, funcBtwn) { + let skipped = true; + for (const value of arr) { + if (!skipped) { + funcBtwn(value); + } + skipped = func(value); + } +} + // ==================== Core search logic begin ==================== // This mapping table should match the discriminants of // `rustdoc::formats::item_type::ItemType` type in Rust. @@ -50,8 +59,10 @@ const itemTypes = [ ]; // used for special search precedence +const TY_PRIMITIVE = itemTypes.indexOf("primitive"); const TY_GENERIC = itemTypes.indexOf("generic"); const TY_IMPORT = itemTypes.indexOf("import"); +const TY_TRAIT = itemTypes.indexOf("trait"); const ROOT_PATH = typeof window !== "undefined" ? window.rootPath : "../"; // Hard limit on how deep to recurse into generics when doing type-driven search. @@ -1117,6 +1128,13 @@ class DocSearch { * @type {Map<string, {id: integer, assocOnly: boolean}>} */ this.typeNameIdMap = new Map(); + /** + * Map from type ID to associated type name. Used for display, + * not for search. + * + * @type {Map<integer, string>} + */ + this.assocTypeIdNameMap = new Map(); this.ALIASES = new Map(); this.rootPath = rootPath; this.searchState = searchState; @@ -1161,6 +1179,14 @@ class DocSearch { * Special type name IDs for searching higher order functions (`->` syntax). */ this.typeNameIdOfHof = this.buildTypeMapIndex("->"); + /** + * Special type name IDs the output assoc type. + */ + this.typeNameIdOfOutput = this.buildTypeMapIndex("output", true); + /** + * Special type name IDs for searching by reference. + */ + this.typeNameIdOfReference = this.buildTypeMapIndex("reference"); /** * Empty, immutable map used in item search types with no bindings. @@ -1237,9 +1263,9 @@ class DocSearch { * * @return {Array<FunctionSearchType>} */ - buildItemSearchTypeAll(types, lowercasePaths) { + buildItemSearchTypeAll(types, paths, lowercasePaths) { return types.length > 0 ? - types.map(type => this.buildItemSearchType(type, lowercasePaths)) : + types.map(type => this.buildItemSearchType(type, paths, lowercasePaths)) : this.EMPTY_GENERICS_ARRAY; } @@ -1248,7 +1274,7 @@ class DocSearch { * * @param {RawFunctionType} type */ - buildItemSearchType(type, lowercasePaths, isAssocType) { + buildItemSearchType(type, paths, lowercasePaths, isAssocType) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; const BINDINGS_DATA = 2; @@ -1261,6 +1287,7 @@ class DocSearch { pathIndex = type[PATH_INDEX_DATA]; generics = this.buildItemSearchTypeAll( type[GENERICS_DATA], + paths, lowercasePaths, ); if (type.length > BINDINGS_DATA && type[BINDINGS_DATA].length > 0) { @@ -1277,8 +1304,8 @@ class DocSearch { // // As a result, the key should never have generics on it. return [ - this.buildItemSearchType(assocType, lowercasePaths, true).id, - this.buildItemSearchTypeAll(constraints, lowercasePaths), + this.buildItemSearchType(assocType, paths, lowercasePaths, true).id, + this.buildItemSearchTypeAll(constraints, paths, lowercasePaths), ]; })); } else { @@ -1294,6 +1321,7 @@ class DocSearch { // the actual names of generic parameters aren't stored, since they aren't API result = { id: pathIndex, + name: "", ty: TY_GENERIC, path: null, exactPath: null, @@ -1304,6 +1332,7 @@ class DocSearch { // `0` is used as a sentinel because it's fewer bytes than `null` result = { id: null, + name: "", ty: null, path: null, exactPath: null, @@ -1312,8 +1341,13 @@ class DocSearch { }; } else { const item = lowercasePaths[pathIndex - 1]; + const id = this.buildTypeMapIndex(item.name, isAssocType); + if (isAssocType) { + this.assocTypeIdNameMap.set(id, paths[pathIndex - 1].name); + } result = { - id: this.buildTypeMapIndex(item.name, isAssocType), + id, + name: paths[pathIndex - 1].name, ty: item.ty, path: item.path, exactPath: item.exactPath, @@ -1355,7 +1389,7 @@ class DocSearch { } if (cr.ty === result.ty && cr.path === result.path && cr.bindings === result.bindings && cr.generics === result.generics - && cr.ty === result.ty + && cr.ty === result.ty && cr.name === result.name ) { return cr; } @@ -1466,11 +1500,12 @@ class DocSearch { * The raw function search type format is generated using serde in * librustdoc/html/render/mod.rs: IndexItemFunctionType::write_to_string * + * @param {Array<{name: string, ty: number}>} paths * @param {Array<{name: string, ty: number}>} lowercasePaths * * @return {null|FunctionSearchType} */ - const buildFunctionSearchTypeCallback = lowercasePaths => { + const buildFunctionSearchTypeCallback = (paths, lowercasePaths) => { return functionSearchType => { if (functionSearchType === 0) { return null; @@ -1480,11 +1515,16 @@ class DocSearch { let inputs, output; if (typeof functionSearchType[INPUTS_DATA] === "number") { inputs = [ - this.buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths), + this.buildItemSearchType( + functionSearchType[INPUTS_DATA], + paths, + lowercasePaths, + ), ]; } else { inputs = this.buildItemSearchTypeAll( functionSearchType[INPUTS_DATA], + paths, lowercasePaths, ); } @@ -1493,12 +1533,14 @@ class DocSearch { output = [ this.buildItemSearchType( functionSearchType[OUTPUT_DATA], + paths, lowercasePaths, ), ]; } else { output = this.buildItemSearchTypeAll( functionSearchType[OUTPUT_DATA], + paths, lowercasePaths, ); } @@ -1509,8 +1551,12 @@ class DocSearch { const l = functionSearchType.length; for (let i = 2; i < l; ++i) { where_clause.push(typeof functionSearchType[i] === "number" - ? [this.buildItemSearchType(functionSearchType[i], lowercasePaths)] - : this.buildItemSearchTypeAll(functionSearchType[i], lowercasePaths)); + ? [this.buildItemSearchType(functionSearchType[i], paths, lowercasePaths)] + : this.buildItemSearchTypeAll( + functionSearchType[i], + paths, + lowercasePaths, + )); } return { inputs, output, where_clause, @@ -1554,6 +1600,13 @@ class DocSearch { this.searchIndexEmptyDesc.set(crate, new RoaringBitmap(crateCorpus.e)); let descIndex = 0; + /** + * List of generic function type parameter names. + * Used for display, not for searching. + * @type {[string]} + */ + let lastParamNames = []; + // This object should have exactly the same set of fields as the "row" // object defined below. Your JavaScript runtime will thank you. // https://mathiasbynens.be/notes/shapes-ics @@ -1568,6 +1621,7 @@ class DocSearch { desc: crateCorpus.doc, parent: undefined, type: null, + paramNames: lastParamNames, id, word: crate, normalizedName: crate.indexOf("_") === -1 ? crate : crate.replace(/_/g, ""), @@ -1604,6 +1658,10 @@ class DocSearch { // an array of [(String) alias name // [Number] index to items] const aliases = crateCorpus.a; + // an array of [(Number) item index, + // (String) comma-separated list of function generic param names] + // an item whose index is not present will fall back to the previous present path + const itemParamNames = new Map(crateCorpus.P); // an array of [{name: String, ty: Number}] const lowercasePaths = []; @@ -1611,7 +1669,7 @@ class DocSearch { // a string representing the list of function types const itemFunctionDecoder = new VlqHexDecoder( crateCorpus.f, - buildFunctionSearchTypeCallback(lowercasePaths), + buildFunctionSearchTypeCallback(paths, lowercasePaths), ); // convert `rawPaths` entries into object form @@ -1662,6 +1720,9 @@ class DocSearch { const name = itemNames[i] === "" ? lastName : itemNames[i]; const word = itemNames[i] === "" ? lastWord : itemNames[i].toLowerCase(); const path = itemPaths.has(i) ? itemPaths.get(i) : lastPath; + const paramNames = itemParamNames.has(i) ? + itemParamNames.get(i).split(",") : + lastParamNames; const type = itemFunctionDecoder.next(); if (type !== null) { if (type) { @@ -1694,6 +1755,7 @@ class DocSearch { itemPaths.get(itemReexports.get(i)) : path, parent: itemParentIdx > 0 ? paths[itemParentIdx - 1] : undefined, type, + paramNames, id, word, normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), @@ -1704,6 +1766,7 @@ class DocSearch { id += 1; searchIndex.push(row); lastPath = row.path; + lastParamNames = row.paramNames; if (!this.searchIndexEmptyDesc.get(crate).contains(bitIndex)) { descIndex += 1; } @@ -2048,18 +2111,21 @@ class DocSearch { * marked for removal. * * @param {[ResultObject]} results + * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {ParsedQuery} query * @returns {[ResultObject]} */ - const transformResults = results => { + const transformResults = (results, typeInfo) => { const duplicates = new Set(); const out = []; for (const result of results) { if (result.id !== -1) { - const obj = this.searchIndex[result.id]; - obj.dist = result.dist; - const res = buildHrefAndPath(obj); - obj.displayPath = pathSplitter(res[0]); + const res = buildHrefAndPath(this.searchIndex[result.id]); + const obj = Object.assign({ + dist: result.dist, + displayPath: pathSplitter(res[0]), + }, this.searchIndex[result.id]); // To be sure than it some items aren't considered as duplicate. obj.fullPath = res[2] + "|" + obj.ty; @@ -2078,6 +2144,11 @@ class DocSearch { duplicates.add(obj.fullPath); duplicates.add(res[2]); + if (typeInfo !== null) { + obj.displayTypeSignature = + this.formatDisplayTypeSignature(obj, typeInfo); + } + obj.href = res[1]; out.push(obj); if (out.length >= MAX_RESULTS) { @@ -2089,6 +2160,327 @@ class DocSearch { }; /** + * Add extra data to result objects, and filter items that have been + * marked for removal. + * + * The output is formatted as an array of hunks, where odd numbered + * hunks are highlighted and even numbered ones are not. + * + * @param {ResultObject} obj + * @param {"sig"|"elems"|"returned"|null} typeInfo + * @param {ParsedQuery} query + * @returns Promise< + * "type": Array<string>, + * "mappedNames": Map<string, string>, + * "whereClause": Map<string, Array<string>>, + * > + */ + this.formatDisplayTypeSignature = async(obj, typeInfo) => { + let fnInputs = null; + let fnOutput = null; + let mgens = null; + if (typeInfo !== "elems" && typeInfo !== "returned") { + fnInputs = unifyFunctionTypes( + obj.type.inputs, + parsedQuery.elems, + obj.type.where_clause, + null, + mgensScratch => { + fnOutput = unifyFunctionTypes( + obj.type.output, + parsedQuery.returned, + obj.type.where_clause, + mgensScratch, + mgensOut => { + mgens = mgensOut; + return true; + }, + 0, + ); + return !!fnOutput; + }, + 0, + ); + } else { + const arr = typeInfo === "elems" ? obj.type.inputs : obj.type.output; + const highlighted = unifyFunctionTypes( + arr, + parsedQuery.elems, + obj.type.where_clause, + null, + mgensOut => { + mgens = mgensOut; + return true; + }, + 0, + ); + if (typeInfo === "elems") { + fnInputs = highlighted; + } else { + fnOutput = highlighted; + } + } + if (!fnInputs) { + fnInputs = obj.type.inputs; + } + if (!fnOutput) { + fnOutput = obj.type.output; + } + const mappedNames = new Map(); + const whereClause = new Map(); + + const fnParamNames = obj.paramNames; + const queryParamNames = []; + /** + * Recursively writes a map of IDs to query generic names, + * which are later used to map query generic names to function generic names. + * For example, when the user writes `X -> Option<X>` and the function + * is actually written as `T -> Option<T>`, this function stores the + * mapping `(-1, "X")`, and the writeFn function looks up the entry + * for -1 to form the final, user-visible mapping of "X is T". + * + * @param {QueryElement} queryElem + */ + const remapQuery = queryElem => { + if (queryElem.id < 0) { + queryParamNames[-1 - queryElem.id] = queryElem.name; + } + if (queryElem.generics.length > 0) { + queryElem.generics.forEach(remapQuery); + } + if (queryElem.bindings.size > 0) { + [...queryElem.bindings.values()].flat().forEach(remapQuery); + } + }; + + parsedQuery.elems.forEach(remapQuery); + parsedQuery.returned.forEach(remapQuery); + + /** + * Write text to a highlighting array. + * Index 0 is not highlighted, index 1 is highlighted, + * index 2 is not highlighted, etc. + * + * @param {{name: string, highlighted: bool|undefined}} fnType - input + * @param {[string]} result + */ + const pushText = (fnType, result) => { + // If !!(result.length % 2) == false, then pushing a new slot starts an even + // numbered slot. Even numbered slots are not highlighted. + // + // `highlighted` will not be defined if an entire subtree is not highlighted, + // so `!!` is used to coerce it to boolean. `result.length % 2` is used to + // check if the number is even, but it evaluates to a number, so it also + // needs coerced to a boolean. + if (!!(result.length % 2) === !!fnType.highlighted) { + result.push(""); + } else if (result.length === 0 && !!fnType.highlighted) { + result.push(""); + result.push(""); + } + + result[result.length - 1] += fnType.name; + }; + + /** + * Write a higher order function type: either a function pointer + * or a trait bound on Fn, FnMut, or FnOnce. + * + * @param {FunctionType} fnType - input + * @param {[string]} result + */ + const writeHof = (fnType, result) => { + const hofOutput = fnType.bindings.get(this.typeNameIdOfOutput) || []; + const hofInputs = fnType.generics; + pushText(fnType, result); + pushText({name: " (", highlighted: false}, result); + let needsComma = false; + for (const fnType of hofInputs) { + if (needsComma) { + pushText({ name: ", ", highlighted: false }, result); + } + needsComma = true; + writeFn(fnType, result); + } + pushText({ + name: hofOutput.length === 0 ? ")" : ") -> ", + highlighted: false, + }, result); + if (hofOutput.length > 1) { + pushText({name: "(", highlighted: false}, result); + } + needsComma = false; + for (const fnType of hofOutput) { + if (needsComma) { + pushText({ name: ", ", highlighted: false }, result); + } + needsComma = true; + writeFn(fnType, result); + } + if (hofOutput.length > 1) { + pushText({name: ")", highlighted: false}, result); + } + }; + + /** + * Write a primitive type with special syntax, like `!` or `[T]`. + * Returns `false` if the supplied type isn't special. + * + * @param {FunctionType} fnType + * @param {[string]} result + */ + const writeSpecialPrimitive = (fnType, result) => { + if (fnType.id === this.typeNameIdOfArray || fnType.id === this.typeNameIdOfSlice || + fnType.id === this.typeNameIdOfTuple || fnType.id === this.typeNameIdOfUnit) { + const [ob, sb] = + fnType.id === this.typeNameIdOfArray || + fnType.id === this.typeNameIdOfSlice ? + ["[", "]"] : + ["(", ")"]; + pushText({ name: ob, highlighted: fnType.highlighted }, result); + onEachBtwn( + fnType.generics, + nested => writeFn(nested, result), + () => pushText({ name: ", ", highlighted: false }, result), + ); + pushText({ name: sb, highlighted: fnType.highlighted }, result); + return true; + } else if (fnType.id === this.typeNameIdOfReference) { + pushText({ name: "&", highlighted: fnType.highlighted }, result); + let prevHighlighted = false; + onEachBtwn( + fnType.generics, + value => { + prevHighlighted = value.highlighted; + writeFn(value, result); + }, + value => pushText({ + name: " ", + highlighted: prevHighlighted && value.highlighted, + }, result), + ); + return true; + } else if (fnType.id === this.typeNameIdOfFn) { + writeHof(fnType, result); + return true; + } + return false; + }; + /** + * Write a type. This function checks for special types, + * like slices, with their own formatting. It also handles + * updating the where clause and generic type param map. + * + * @param {FunctionType} fnType + * @param {[string]} result + */ + const writeFn = (fnType, result) => { + if (fnType.id < 0) { + const queryId = mgens && mgens.has(fnType.id) ? mgens.get(fnType.id) : null; + if (fnParamNames[-1 - fnType.id] === "") { + for (const nested of fnType.generics) { + writeFn(nested, result); + } + return; + } else if (queryId < 0) { + mappedNames.set( + fnParamNames[-1 - fnType.id], + queryParamNames[-1 - queryId], + ); + } + pushText({ + name: fnParamNames[-1 - fnType.id], + highlighted: !!fnType.highlighted, + }, result); + const where = []; + onEachBtwn( + fnType.generics, + nested => writeFn(nested, where), + () => pushText({ name: " + ", highlighted: false }, where), + ); + if (where.length > 0) { + whereClause.set(fnParamNames[-1 - fnType.id], where); + } + } else { + if (fnType.ty === TY_PRIMITIVE) { + if (writeSpecialPrimitive(fnType, result)) { + return; + } + } else if (fnType.ty === TY_TRAIT && ( + fnType.id === this.typeNameIdOfFn || + fnType.id === this.typeNameIdOfFnMut || + fnType.id === this.typeNameIdOfFnOnce)) { + writeHof(fnType, result); + return; + } + pushText(fnType, result); + let hasBindings = false; + if (fnType.bindings.size > 0) { + onEachBtwn( + fnType.bindings, + ([key, values]) => { + const name = this.assocTypeIdNameMap.get(key); + if (values.length === 1 && values[0].id < 0 && + `${fnType.name}::${name}` === fnParamNames[-1 - values[0].id]) { + // the internal `Item=Iterator::Item` type variable should be + // shown in the where clause and name mapping output, but is + // redundant in this spot + for (const value of values) { + writeFn(value, []); + } + return true; + } + if (!hasBindings) { + hasBindings = true; + pushText({ name: "<", highlighted: false }, result); + } + pushText({ name, highlighted: false }, result); + pushText({ + name: values.length > 1 ? "=(" : "=", + highlighted: false, + }, result); + onEachBtwn( + values, + value => writeFn(value, result), + () => pushText({ name: " + ", highlighted: false }, result), + ); + if (values.length > 1) { + pushText({ name: ")", highlighted: false }, result); + } + }, + () => pushText({ name: ", ", highlighted: false }, result), + ); + } + if (fnType.generics.length > 0) { + pushText({ name: hasBindings ? ", " : "<", highlighted: false }, result); + } + onEachBtwn( + fnType.generics, + value => writeFn(value, result), + () => pushText({ name: ", ", highlighted: false }, result), + ); + if (hasBindings || fnType.generics.length > 0) { + pushText({ name: ">", highlighted: false }, result); + } + } + }; + const type = []; + onEachBtwn( + fnInputs, + fnType => writeFn(fnType, type), + () => pushText({ name: ", ", highlighted: false }, type), + ); + pushText({ name: " -> ", highlighted: false }, type); + onEachBtwn( + fnOutput, + fnType => writeFn(fnType, type), + () => pushText({ name: ", ", highlighted: false }, type), + ); + + return {type, mappedNames, whereClause}; + }; + + /** * This function takes a result map, and sorts it by various criteria, including edit * distance, substring match, and the crate it comes from. * @@ -2097,7 +2489,7 @@ class DocSearch { * @param {string} preferredCrate * @returns {Promise<[ResultObject]>} */ - const sortResults = async(results, isType, preferredCrate) => { + const sortResults = async(results, typeInfo, preferredCrate) => { const userQuery = parsedQuery.userQuery; const normalizedUserQuery = parsedQuery.userQuery.toLowerCase(); const result_list = []; @@ -2207,17 +2599,17 @@ class DocSearch { return 0; }); - return transformResults(result_list); + return transformResults(result_list, typeInfo); }; /** * This function checks if a list of search query `queryElems` can all be found in the * search index (`fnTypes`). * - * This function returns `true` on a match, or `false` if none. If `solutionCb` is + * This function returns highlighted results on a match, or `null`. If `solutionCb` is * supplied, it will call that function with mgens, and that callback can accept or - * reject the result bu returning `true` or `false`. If the callback returns false, - * then this function will try with a different solution, or bail with false if it + * reject the result by returning `true` or `false`. If the callback returns false, + * then this function will try with a different solution, or bail with null if it * runs out of candidates. * * @param {Array<FunctionType>} fnTypesIn - The objects to check. @@ -2230,7 +2622,7 @@ class DocSearch { * - Limit checks that Ty matches Vec<Ty>, * but not Vec<ParamEnvAnd<WithInfcx<ConstTy<Interner<Ty=Ty>>>>> * - * @return {boolean} - Returns true if a match, false otherwise. + * @return {[FunctionType]|null} - Returns highlighed results if a match, null otherwise. */ function unifyFunctionTypes( fnTypesIn, @@ -2241,17 +2633,17 @@ class DocSearch { unboxingDepth, ) { if (unboxingDepth >= UNBOXING_LIMIT) { - return false; + return null; } /** * @type Map<integer, integer>|null */ const mgens = mgensIn === null ? null : new Map(mgensIn); if (queryElems.length === 0) { - return !solutionCb || solutionCb(mgens); + return (!solutionCb || solutionCb(mgens)) ? fnTypesIn : null; } if (!fnTypesIn || fnTypesIn.length === 0) { - return false; + return null; } const ql = queryElems.length; const fl = fnTypesIn.length; @@ -2260,7 +2652,7 @@ class DocSearch { if (ql === 1 && queryElems[0].generics.length === 0 && queryElems[0].bindings.size === 0) { const queryElem = queryElems[0]; - for (const fnType of fnTypesIn) { + for (const [i, fnType] of fnTypesIn.entries()) { if (!unifyFunctionTypeIsMatchCandidate(fnType, queryElem, mgens)) { continue; } @@ -2272,14 +2664,33 @@ class DocSearch { const mgensScratch = new Map(mgens); mgensScratch.set(fnType.id, queryElem.id); if (!solutionCb || solutionCb(mgensScratch)) { - return true; + const highlighted = [...fnTypesIn]; + highlighted[i] = Object.assign({ + highlighted: true, + }, fnType, { + generics: whereClause[-1 - fnType.id], + }); + return highlighted; } } else if (!solutionCb || solutionCb(mgens ? new Map(mgens) : null)) { // unifyFunctionTypeIsMatchCandidate already checks that ids match - return true; + const highlighted = [...fnTypesIn]; + highlighted[i] = Object.assign({ + highlighted: true, + }, fnType, { + generics: unifyFunctionTypes( + fnType.generics, + queryElem.generics, + whereClause, + mgens ? new Map(mgens) : null, + solutionCb, + unboxingDepth, + ) || fnType.generics, + }); + return highlighted; } } - for (const fnType of fnTypesIn) { + for (const [i, fnType] of fnTypesIn.entries()) { if (!unifyFunctionTypeIsUnboxCandidate( fnType, queryElem, @@ -2296,25 +2707,42 @@ class DocSearch { } const mgensScratch = new Map(mgens); mgensScratch.set(fnType.id, 0); - if (unifyFunctionTypes( + const highlightedGenerics = unifyFunctionTypes( whereClause[(-fnType.id) - 1], queryElems, whereClause, mgensScratch, solutionCb, unboxingDepth + 1, - )) { - return true; + ); + if (highlightedGenerics) { + const highlighted = [...fnTypesIn]; + highlighted[i] = Object.assign({ + highlighted: true, + }, fnType, { + generics: highlightedGenerics, + }); + return highlighted; + } + } else { + const highlightedGenerics = unifyFunctionTypes( + [...Array.from(fnType.bindings.values()).flat(), ...fnType.generics], + queryElems, + whereClause, + mgens ? new Map(mgens) : null, + solutionCb, + unboxingDepth + 1, + ); + if (highlightedGenerics) { + const highlighted = [...fnTypesIn]; + highlighted[i] = Object.assign({}, fnType, { + generics: highlightedGenerics, + bindings: new Map([...fnType.bindings.entries()].map(([k, v]) => { + return [k, highlightedGenerics.splice(0, v.length)]; + })), + }); + return highlighted; } - } else if (unifyFunctionTypes( - [...fnType.generics, ...Array.from(fnType.bindings.values()).flat()], - queryElems, - whereClause, - mgens ? new Map(mgens) : null, - solutionCb, - unboxingDepth + 1, - )) { - return true; } } return false; @@ -2371,6 +2799,8 @@ class DocSearch { if (!queryElemsTmp) { queryElemsTmp = queryElems.slice(0, qlast); } + let unifiedGenerics = []; + let unifiedGenericsMgens = null; const passesUnification = unifyFunctionTypes( fnTypes, queryElemsTmp, @@ -2393,7 +2823,7 @@ class DocSearch { } const simplifiedGenerics = solution.simplifiedGenerics; for (const simplifiedMgens of solution.mgens) { - const passesUnification = unifyFunctionTypes( + unifiedGenerics = unifyFunctionTypes( simplifiedGenerics, queryElem.generics, whereClause, @@ -2401,7 +2831,8 @@ class DocSearch { solutionCb, unboxingDepth, ); - if (passesUnification) { + if (unifiedGenerics) { + unifiedGenericsMgens = simplifiedMgens; return true; } } @@ -2410,7 +2841,23 @@ class DocSearch { unboxingDepth, ); if (passesUnification) { - return true; + passesUnification.length = fl; + passesUnification[flast] = passesUnification[i]; + passesUnification[i] = Object.assign({}, fnType, { + highlighted: true, + generics: unifiedGenerics, + bindings: new Map([...fnType.bindings.entries()].map(([k, v]) => { + return [k, queryElem.bindings.has(k) ? unifyFunctionTypes( + v, + queryElem.bindings.get(k), + whereClause, + unifiedGenericsMgens, + solutionCb, + unboxingDepth, + ) : unifiedGenerics.splice(0, v.length)]; + })), + }); + return passesUnification; } // backtrack fnTypes[flast] = fnTypes[i]; @@ -2445,7 +2892,7 @@ class DocSearch { Array.from(fnType.bindings.values()).flat() : []; const passesUnification = unifyFunctionTypes( - fnTypes.toSpliced(i, 1, ...generics, ...bindings), + fnTypes.toSpliced(i, 1, ...bindings, ...generics), queryElems, whereClause, mgensScratch, @@ -2453,10 +2900,24 @@ class DocSearch { unboxingDepth + 1, ); if (passesUnification) { - return true; + const highlightedGenerics = passesUnification.slice( + i, + i + generics.length + bindings.length, + ); + const highlightedFnType = Object.assign({}, fnType, { + generics: highlightedGenerics, + bindings: new Map([...fnType.bindings.entries()].map(([k, v]) => { + return [k, highlightedGenerics.splice(0, v.length)]; + })), + }); + return passesUnification.toSpliced( + i, + generics.length + bindings.length, + highlightedFnType, + ); } } - return false; + return null; } /** * Check if this function is a match candidate. @@ -2627,7 +3088,7 @@ class DocSearch { } }); if (simplifiedGenerics.length > 0) { - simplifiedGenerics = [...simplifiedGenerics, ...binds]; + simplifiedGenerics = [...binds, ...simplifiedGenerics]; } else { simplifiedGenerics = binds; } @@ -3285,10 +3746,11 @@ class DocSearch { innerRunQuery(); } + const isType = parsedQuery.foundElems !== 1 || parsedQuery.hasReturnArrow; const [sorted_in_args, sorted_returned, sorted_others] = await Promise.all([ - sortResults(results_in_args, true, currentCrate), - sortResults(results_returned, true, currentCrate), - sortResults(results_others, false, currentCrate), + sortResults(results_in_args, "elems", currentCrate), + sortResults(results_returned, "returned", currentCrate), + sortResults(results_others, (isType ? "query" : null), currentCrate), ]); const ret = createQueryResults( sorted_in_args, @@ -3315,6 +3777,7 @@ class DocSearch { } } + // ==================== Core search logic end ==================== let rawSearchIndex; @@ -3446,15 +3909,18 @@ function focusSearchResult() { * @param {Array<?>} array - The search results for this tab * @param {ParsedQuery} query * @param {boolean} display - True if this is the active tab + * @param {"sig"|"elems"|"returned"|null} typeInfo */ async function addTab(array, query, display) { const extraClass = display ? " active" : ""; - const output = document.createElement("div"); + const output = document.createElement( + array.length === 0 && query.error === null ? "div" : "ul", + ); if (array.length > 0) { output.className = "search-results " + extraClass; - for (const item of array) { + const lis = Promise.all(array.map(async item => { const name = item.name; const type = itemTypes[item.ty]; const longType = longItemTypes[item.ty]; @@ -3464,7 +3930,7 @@ async function addTab(array, query, display) { link.className = "result-" + type; link.href = item.href; - const resultName = document.createElement("div"); + const resultName = document.createElement("span"); resultName.className = "result-name"; resultName.insertAdjacentHTML( @@ -3487,10 +3953,73 @@ ${item.displayPath}<span class="${type}">${name}</span>\ const description = document.createElement("div"); description.className = "desc"; description.insertAdjacentHTML("beforeend", item.desc); + if (item.displayTypeSignature) { + const {type, mappedNames, whereClause} = await item.displayTypeSignature; + const displayType = document.createElement("div"); + type.forEach((value, index) => { + if (index % 2 !== 0) { + const highlight = document.createElement("strong"); + highlight.appendChild(document.createTextNode(value)); + displayType.appendChild(highlight); + } else { + displayType.appendChild(document.createTextNode(value)); + } + }); + if (mappedNames.size > 0 || whereClause.size > 0) { + let addWhereLineFn = () => { + const line = document.createElement("div"); + line.className = "where"; + line.appendChild(document.createTextNode("where")); + displayType.appendChild(line); + addWhereLineFn = () => {}; + }; + for (const [name, qname] of mappedNames) { + // don't care unless the generic name is different + if (name === qname) { + continue; + } + addWhereLineFn(); + const line = document.createElement("div"); + line.className = "where"; + line.appendChild(document.createTextNode(` ${qname} matches `)); + const lineStrong = document.createElement("strong"); + lineStrong.appendChild(document.createTextNode(name)); + line.appendChild(lineStrong); + displayType.appendChild(line); + } + for (const [name, innerType] of whereClause) { + // don't care unless there's at least one highlighted entry + if (innerType.length <= 1) { + continue; + } + addWhereLineFn(); + const line = document.createElement("div"); + line.className = "where"; + line.appendChild(document.createTextNode(` ${name}: `)); + innerType.forEach((value, index) => { + if (index % 2 !== 0) { + const highlight = document.createElement("strong"); + highlight.appendChild(document.createTextNode(value)); + line.appendChild(highlight); + } else { + line.appendChild(document.createTextNode(value)); + } + }); + displayType.appendChild(line); + } + } + displayType.className = "type-signature"; + link.appendChild(displayType); + } link.appendChild(description); - output.appendChild(link); - } + return link; + })); + lis.then(lis => { + for (const li of lis) { + output.appendChild(li); + } + }); } else if (query.error === null) { output.className = "search-failed" + extraClass; output.innerHTML = "No results :(<br/>" + @@ -3507,7 +4036,7 @@ ${item.displayPath}<span class="${type}">${name}</span>\ "href=\"https://docs.rs\">Docs.rs</a> for documentation of crates released on" + " <a href=\"https://crates.io/\">crates.io</a>.</li></ul>"; } - return [output, array.length]; + return output; } function makeTabHeader(tabNb, text, nbElems) { @@ -3564,24 +4093,18 @@ async function showResults(results, go_to_first, filterCrates) { currentResults = results.query.userQuery; - const [ret_others, ret_in_args, ret_returned] = await Promise.all([ - addTab(results.others, results.query, true), - addTab(results.in_args, results.query, false), - addTab(results.returned, results.query, false), - ]); - // Navigate to the relevant tab if the current tab is empty, like in case users search // for "-> String". If they had selected another tab previously, they have to click on // it again. let currentTab = searchState.currentTab; - if ((currentTab === 0 && ret_others[1] === 0) || - (currentTab === 1 && ret_in_args[1] === 0) || - (currentTab === 2 && ret_returned[1] === 0)) { - if (ret_others[1] !== 0) { + if ((currentTab === 0 && results.others.length === 0) || + (currentTab === 1 && results.in_args.length === 0) || + (currentTab === 2 && results.returned.length === 0)) { + if (results.others.length !== 0) { currentTab = 0; - } else if (ret_in_args[1] !== 0) { + } else if (results.in_args.length) { currentTab = 1; - } else if (ret_returned[1] !== 0) { + } else if (results.returned.length) { currentTab = 2; } } @@ -3610,14 +4133,14 @@ async function showResults(results, go_to_first, filterCrates) { }); output += `<h3 class="error">Query parser error: "${error.join("")}".</h3>`; output += "<div id=\"search-tabs\">" + - makeTabHeader(0, "In Names", ret_others[1]) + + makeTabHeader(0, "In Names", results.others.length) + "</div>"; currentTab = 0; } else if (results.query.foundElems <= 1 && results.query.returned.length === 0) { output += "<div id=\"search-tabs\">" + - makeTabHeader(0, "In Names", ret_others[1]) + - makeTabHeader(1, "In Parameters", ret_in_args[1]) + - makeTabHeader(2, "In Return Types", ret_returned[1]) + + makeTabHeader(0, "In Names", results.others.length) + + makeTabHeader(1, "In Parameters", results.in_args.length) + + makeTabHeader(2, "In Return Types", results.returned.length) + "</div>"; } else { const signatureTabTitle = @@ -3625,7 +4148,7 @@ async function showResults(results, go_to_first, filterCrates) { results.query.returned.length === 0 ? "In Function Parameters" : "In Function Signatures"; output += "<div id=\"search-tabs\">" + - makeTabHeader(0, signatureTabTitle, ret_others[1]) + + makeTabHeader(0, signatureTabTitle, results.others.length) + "</div>"; currentTab = 0; } @@ -3647,11 +4170,17 @@ async function showResults(results, go_to_first, filterCrates) { `Consider searching for "${targ}" instead.</h3>`; } + const [ret_others, ret_in_args, ret_returned] = await Promise.all([ + addTab(results.others, results.query, currentTab === 0), + addTab(results.in_args, results.query, currentTab === 1), + addTab(results.returned, results.query, currentTab === 2), + ]); + const resultsElem = document.createElement("div"); resultsElem.id = "results"; - resultsElem.appendChild(ret_others[0]); - resultsElem.appendChild(ret_in_args[0]); - resultsElem.appendChild(ret_returned[0]); + resultsElem.appendChild(ret_others); + resultsElem.appendChild(ret_in_args); + resultsElem.appendChild(ret_returned); search.innerHTML = output; if (searchState.rustdocToolbar) { @@ -3933,4 +4462,3 @@ if (typeof window !== "undefined") { // exports. initSearch(new Map()); } -})(); |
