From e09dd6e78d7d87b4ed8dfa8b82b5c73afcf30d22 Mon Sep 17 00:00:00 2001 From: Michael Howell Date: Mon, 23 Oct 2023 19:27:56 -0700 Subject: [PATCH] rustdoc-search: show type signature on type-driven SERP This displays the function signature, as rustdoc understands it, on the In Parameters, In Return Types, and In Function Signature pages, but not in the In Names page, since it's not used there. It also highlights the matching parts, to clarify why a function is considered a good match. --- src/librustdoc/html/static/css/rustdoc.css | 8 +- src/librustdoc/html/static/js/search.js | 551 +++++++++++++++---- src/tools/rustdoc-js/tester.js | 26 +- tests/rustdoc-js/generics-impl.js | 60 +- tests/rustdoc-js/generics-match-ambiguity.js | 24 +- tests/rustdoc-js/type-parameters.js | 46 +- 6 files changed, 554 insertions(+), 161 deletions(-) diff --git a/src/librustdoc/html/static/css/rustdoc.css b/src/librustdoc/html/static/css/rustdoc.css index e2b4cc50dd580..58898247a9f40 100644 --- a/src/librustdoc/html/static/css/rustdoc.css +++ b/src/librustdoc/html/static/css/rustdoc.css @@ -328,10 +328,11 @@ details:not(.toggle) summary { margin-bottom: .6em; } -code, pre, a.test-arrow, .code-header { +code, pre, a.test-arrow, .code-header, .search-results .type-signature { font-family: "Source Code Pro", monospace; } -.docblock code, .docblock-short code { +.docblock code, .docblock-short code, +.search-results .type-signature strong { border-radius: 3px; padding: 0 0.125em; } @@ -681,7 +682,8 @@ ul.block, .block li { } .docblock code, .docblock-short code, -pre, .rustdoc.src .example-wrap { +pre, .rustdoc.src .example-wrap, +.search-results .type-signature strong { background-color: var(--code-block-background-color); } diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index 48c9a53a28310..a2878be2817df 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -262,6 +262,14 @@ function initSearch(rawSearchIndex) { * Special type name IDs for searching by both array and slice (`[]` syntax). */ let typeNameIdOfArrayOrSlice; + /** + * Special type name IDs for searching never (the `!` type). + */ + let typeNameIdOfNever; + /** + * Special type name IDs for searching tuple (the `()` type). + */ + let typeNameIdOfTuple; /** * Add an item to the type Name->ID map, or, if one already exists, use it. @@ -467,12 +475,13 @@ function initSearch(rawSearchIndex) { * @param {ParsedQuery} query * @param {ParserState} parserState * @param {string} name - Name of the query element. + * @param {string} normalizedName - Name of the query element will be used for searching. * @param {Array} generics - List of generics of this query element. * * @return {QueryElement} - The newly created `QueryElement`. */ - function createQueryElement(query, parserState, name, generics, isInGenerics) { - const path = name.trim(); + function createQueryElement(query, parserState, name, normalizedName, generics, isInGenerics) { + const path = normalizedName.trim(); if (path.length === 0 && generics.length === 0) { throw ["Unexpected ", parserState.userQuery[parserState.pos]]; } else if (path === "*") { @@ -483,7 +492,7 @@ function initSearch(rawSearchIndex) { } const typeFilter = parserState.typeFilter; parserState.typeFilter = null; - if (name === "!") { + if (normalizedName === "!") { if (typeFilter !== null && typeFilter !== "primitive") { throw [ "Invalid search type: primitive never type ", @@ -502,6 +511,7 @@ function initSearch(rawSearchIndex) { } return { name: "never", + normalizedName: "never", id: null, fullPath: ["never"], pathWithoutLast: [], @@ -544,6 +554,7 @@ function initSearch(rawSearchIndex) { } return { name: name.trim(), + normalizedName: normalizedName.trim(), id: null, fullPath: pathSegments, pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), @@ -673,6 +684,7 @@ function initSearch(rawSearchIndex) { } elems.push({ name: "[]", + normalizedName: "[]", id: null, fullPath: ["[]"], pathWithoutLast: [], @@ -709,6 +721,7 @@ function initSearch(rawSearchIndex) { createQueryElement( query, parserState, + parserState.original.slice(start, end), parserState.userQuery.slice(start, end), generics, isInGenerics @@ -771,7 +784,7 @@ function initSearch(rawSearchIndex) { // The type filter doesn't count as an element since it's a modifier. const typeFilterElem = elems.pop(); checkExtraTypeFilterCharacters(start, parserState); - parserState.typeFilter = typeFilterElem.name; + parserState.typeFilter = typeFilterElem.normalizedName; parserState.pos += 1; parserState.totalElems -= 1; query.literalSearch = false; @@ -894,7 +907,7 @@ function initSearch(rawSearchIndex) { // The type filter doesn't count as an element since it's a modifier. const typeFilterElem = query.elems.pop(); checkExtraTypeFilterCharacters(start, parserState); - parserState.typeFilter = typeFilterElem.name; + parserState.typeFilter = typeFilterElem.normalizedName; parserState.pos += 1; parserState.totalElems -= 1; query.literalSearch = false; @@ -1063,6 +1076,7 @@ function initSearch(rawSearchIndex) { totalElems: 0, genericsElems: 0, typeFilter: null, + original: userQuery, userQuery: userQuery.toLowerCase(), }; let query = newParsedQuery(userQuery); @@ -1129,29 +1143,35 @@ function initSearch(rawSearchIndex) { * marked for removal. * * @param {[ResultObject]} results + * @param {boolean} isType - True will include function signature string * @returns {[ResultObject]} */ - function transformResults(results) { + function transformResults(results, isType) { const duplicates = new Set(); const out = []; for (const result of results) { if (result.id !== -1) { const obj = searchIndex[result.id]; - obj.dist = result.dist; const res = buildHrefAndPath(obj); - obj.displayPath = pathSplitter(res[0]); - obj.fullPath = obj.displayPath + obj.name; + const displayPath = pathSplitter(res[0]); + let fullPath = displayPath + obj.name; // To be sure than it some items aren't considered as duplicate. - obj.fullPath += "|" + obj.ty; + fullPath += "|" + obj.ty; - if (duplicates.has(obj.fullPath)) { + if (!isType && duplicates.has(fullPath)) { continue; } - duplicates.add(obj.fullPath); - - obj.href = res[1]; - out.push(obj); + duplicates.add(fullPath); + + out.push(Object.assign({ + dist: result.dist, + index: result.index, + displayTypeSignature: result.displayTypeSignature, + displayPath, + fullPath, + href: res[1], + }, obj)); if (out.length >= MAX_RESULTS) { break; } @@ -1160,6 +1180,119 @@ function initSearch(rawSearchIndex) { return out; } + /** + * Convert a type signature to a readable string. + * + * @param {FunctionSearchType?} results + * + * @returns string? + */ + function formatResultTypeSignature(type) { + if (type === null) { + return null; + } + + let result = []; + + function pushNotHighlighted(text) { + if (result.length % 2 === 0) { + result.push(""); + } + result[result.length - 1] = `${result[result.length - 1]}${text}`; + } + function pushHighlighted(text) { + if (result.length === 0) { + result.push(""); + } + if (result.length % 2 !== 0) { + result.push(""); + } + result[result.length - 1] = `${result[result.length - 1]}${text}`; + } + function push(text, highlighted) { + if (highlighted) { + pushHighlighted(text); + } else { + pushNotHighlighted(text); + } + } + + function isTransitivelyHighlighted(fnType) { + // Caching the flag is necessary to avoid quadratic complexity + // in this function. + if (Object.prototype.hasOwnProperty.call(fnType, "isTransitivelyHighlighted")) { + return fnType.isTransitivelyHighlighted; + } + const isHighlighted = fnType.highlighted || + (fnType.generics && fnType.generics.length > 0 && + fnType.generics.some(isTransitivelyHighlighted)) || + (type.where_clause && type.where_clause[(-fnType.id) - 1] && + type.where_clause[(-fnType.id) - 1].some(isTransitivelyHighlighted)); + fnType.isTransitivelyHighlighted = isHighlighted; + return isHighlighted; + } + + function formatTypeList(fnTypes, delim) { + if (!fnTypes) { + return; + } + let first = true; + for (const fnType of fnTypes) { + if (first) { + first = false; + } else { + pushNotHighlighted(delim); + } + if (fnType.ty === TY_PRIMITIVE && + (fnType.id === typeNameIdOfSlice || fnType.id === typeNameIdOfArray) + ) { + push("[", fnType.highlighted); + if (fnType.generics && fnType.generics.some(isTransitivelyHighlighted)) { + formatTypeList(fnType.generics, ", "); + } + push("]", fnType.highlighted); + } else if (fnType.ty === TY_PRIMITIVE && fnType.id === typeNameIdOfNever) { + push("!", fnType.highlighted); + } else if (fnType.name === null) { + if (fnType.generics && fnType.generics.some(t => t.name !== null)) { + push("impl ", fnType.highlighted); + formatTypeList(fnType.generics, " + "); + } else if (type.where_clause && + type.where_clause[(-fnType.id) - 1].some(isTransitivelyHighlighted) + ) { + push("impl ", fnType.highlighted); + formatTypeList(type.where_clause[(-fnType.id) - 1], " + "); + } else { + push("_", fnType.highlighted); + } + } else { + push(fnType.name, fnType.highlighted); + if (fnType.generics && fnType.generics.some(isTransitivelyHighlighted)) { + pushNotHighlighted("<"); + formatTypeList(fnType.generics, ", "); + pushNotHighlighted(">"); + } + } + } + } + + formatTypeList(type.inputs, ", "); + if (result.every(f => f === "")) { + result = []; + } + const returnsUnit = type.output.length === 1 && + type.output[0].id === typeNameIdOfTuple && + type.output[0].generics.length === 0; + if (type.output.length !== 0 && !returnsUnit) { + pushNotHighlighted(result.length === 0 ? "-> " : " -> "); + formatTypeList(type.output, ", "); + } + if (result.every(f => f === "")) { + return null; + } + return result; + } + /** * This function takes a result map, and sorts it by various criteria, including edit * distance, substring match, and the crate it comes from. @@ -1305,39 +1438,37 @@ function initSearch(rawSearchIndex) { result.id = -1; } } - return transformResults(result_list); + return transformResults(result_list, isType); } /** - * This function checks generics in search query `queryElem` can all be found in the - * search index (`fnType`), - * - * This function returns `true` if it matches, and also writes the results to mgensInout. - * It returns `false` if no match is found, and leaves mgensInout untouched. + * The type tree on each function is annotated with information about which + * node to highlight. This is then used to generate the formatted type + * signature for the result. * - * @param {FunctionType} fnType - The object to check. - * @param {QueryElement} queryElem - The element from the parsed query. - * @param {[FunctionType]} whereClause - Trait bounds for generic items. - * @param {Map|null} mgensInout - Map functions generics to query generics. - * - * @return {boolean} - Returns true if a match, false otherwise. + * Between each result, it gets cleared out. This function clears it. */ - function checkGenerics(fnType, queryElem, whereClause, mgensInout) { - return unifyFunctionTypes( - fnType.generics, - queryElem.generics, - whereClause, - mgensInout, - mgens => { - if (mgensInout) { - for (const [fid, qid] of mgens.entries()) { - mgensInout.set(fid, qid); - } - } - return true; + function clearHighlighted(fnType) { + function doClearHighlighted(fnTypeList) { + if (!fnTypeList) { + return; } - ); + for (const fnType of fnTypeList) { + fnType.highlighted = false; + delete fnType.isTransitivelyHighlighted; + delete fnType.matchedGenerics; + doClearHighlighted(fnType.generics); + } + } + doClearHighlighted(fnType.inputs); + doClearHighlighted(fnType.output); + if (fnType.where_clause) { + for (const where of fnType.where_clause) { + doClearHighlighted(where); + } + } } + /** * This function checks if a list of search query `queryElems` can all be found in the * search index (`fnTypes`). @@ -1353,9 +1484,10 @@ function initSearch(rawSearchIndex) { * @param {[FunctionType]} whereClause - Trait bounds for generic items. * @param {Map|null} mgensIn * - Map functions generics to query generics (never modified). - * @param {null|Map -> bool} solutionCb - Called for each `mgens` solution. + * @param {(null|Map, Array) -> bool} solutionCb + * - Called for each `mgens` solution. * - * @return {boolean} - Returns true if a match, false otherwise. + * @return {Array|false} - Returns array of matched parts, false otherwise. */ function unifyFunctionTypes(fnTypesIn, queryElems, whereClause, mgensIn, solutionCb) { /** @@ -1363,7 +1495,7 @@ function initSearch(rawSearchIndex) { */ let mgens = new Map(mgensIn); if (queryElems.length === 0) { - return !solutionCb || solutionCb(mgens); + return !solutionCb || solutionCb(mgens, []); } if (!fnTypesIn || fnTypesIn.length === 0) { return false; @@ -1411,6 +1543,7 @@ function initSearch(rawSearchIndex) { queryElemsOffset, fnTypesOffset, unbox, + matchedGenerics, } = backtracking.pop(); mgens = new Map(mgensScratch); const fnType = fnTypesScratch[fnTypesOffset]; @@ -1440,6 +1573,7 @@ function initSearch(rawSearchIndex) { fl = fnTypes.length; const tmp = fnTypes[queryElemsOffset]; fnTypes[queryElemsOffset] = fnTypes[fnTypesOffset]; + fnTypes[queryElemsOffset].matchedGenerics = matchedGenerics; fnTypes[fnTypesOffset] = tmp; // this is known as a good match; go to the next one i = queryElemsOffset; @@ -1470,10 +1604,11 @@ function initSearch(rawSearchIndex) { queryElem.generics, whereClause, mgens, - mgensScratch => { + (mgensScratch, matchedGenerics) => { matchCandidates.push({ fnTypesScratch, mgensScratch, + matchedGenerics, queryElemsOffset: i, fnTypesOffset: j, unbox: false, @@ -1492,6 +1627,7 @@ function initSearch(rawSearchIndex) { backtracking.push({ fnTypesScratch, mgensScratch, + matchedGenerics: [], queryElemsOffset: i, fnTypesOffset: j, unbox: true, @@ -1506,7 +1642,8 @@ function initSearch(rawSearchIndex) { } } // use the current candidate - const {fnTypesOffset: candidate, mgensScratch: mgensNew} = matchCandidates.pop(); + const {fnTypesOffset: candidate, mgensScratch: mgensNew, matchedGenerics} + = matchCandidates.shift(); if (fnTypes[candidate].id < 0 && queryElems[i].id < 0) { mgens.set(fnTypes[candidate].id, queryElems[i].id); } @@ -1523,9 +1660,22 @@ function initSearch(rawSearchIndex) { for (const otherCandidate of matchCandidates) { backtracking.push(otherCandidate); } + fnTypes[i].matchedGenerics = matchedGenerics; // If we're on the last item, check the solution with the callback // backtrack if the callback says its unsuitable - while (i === (ql - 1) && solutionCb && !solutionCb(mgens)) { + while (i === (ql - 1) && solutionCb) { + // The above loop partitions the list into "matched" and "unmatched" parts + // by swapping things into place when they match. + let highlightable = fnTypes.slice(0, ql); + for (let j = 0; j < highlightable.length; ++j) { + if (highlightable[j].matchedGenerics.length === 0) { + continue; + } + highlightable = [...highlightable, ...highlightable[j].matchedGenerics]; + } + if (solutionCb(mgens, highlightable)) { + break; + } if (!backtrack()) { return false; } @@ -1649,38 +1799,15 @@ function initSearch(rawSearchIndex) { * @return {boolean} - Returns true if the type matches, false otherwise. */ function checkType(row, elem, whereClause) { - if (row.id === null) { - // This is a pure "generic" search, no need to run other checks. - return row.generics.length > 0 - ? checkIfInList(row.generics, elem, whereClause) - : false; - } - - if (row.id < 0 && elem.id >= 0) { - const gid = (-row.id) - 1; - return checkIfInList(whereClause[gid], elem, whereClause); - } - - if (row.id < 0 && elem.id < 0) { - return true; - } - - const matchesExact = row.id === elem.id; - const matchesArrayOrSlice = elem.id === typeNameIdOfArrayOrSlice && - (row.id === typeNameIdOfSlice || row.id === typeNameIdOfArray); - - if ((matchesExact || matchesArrayOrSlice) && - typePassesFilter(elem.typeFilter, row.ty)) { - if (elem.generics.length > 0) { - return checkGenerics(row, elem, whereClause, new Map()); - } - return true; + if (row.id > 0 && elem.id > 0 && elem.pathWithoutLast.length === 0 && + row.generics.length === 0 && elem.generics.length === 0 && + // special case + elem.id !== typeNameIdOfArrayOrSlice + ) { + return row.id === elem.id && typePassesFilter(elem.typeFilter, row.ty); + } else { + return unifyFunctionTypes([row], [elem], whereClause); } - - // If the current item does not match, try [unboxing] the generic. - // [unboxing]: - // https://ndmitchell.com/downloads/slides-hoogle_fast_type_searching-09_aug_2008.pdf - return checkIfInList(row.generics, elem, whereClause); } function checkPath(contains, ty, maxEditDistance) { @@ -1831,8 +1958,19 @@ function initSearch(rawSearchIndex) { * @param {integer} index * @param {integer} dist * @param {integer} path_dist + * @param {integer} maxEditDistance + * @param {boolean} isType */ - function addIntoResults(results, fullId, id, index, dist, path_dist, maxEditDistance) { + function addIntoResults( + results, + fullId, + id, + index, + dist, + path_dist, + maxEditDistance, + isType + ) { const inBounds = dist <= maxEditDistance || index !== -1; if (dist === 0 || (!parsedQuery.literalSearch && inBounds)) { if (results.has(fullId)) { @@ -1841,12 +1979,16 @@ function initSearch(rawSearchIndex) { return; } } + const displayTypeSignature = isType ? + formatResultTypeSignature(searchIndex[id].type) : + null; results.set(fullId, { id: id, index: index, dontValidate: parsedQuery.literalSearch, dist: dist, path_dist: path_dist, + displayTypeSignature, }); } } @@ -1885,14 +2027,46 @@ function initSearch(rawSearchIndex) { const in_args = row.type && row.type.inputs && checkIfInList(row.type.inputs, elem, row.type.where_clause); if (in_args) { + // Generate type signature information for search result display. + clearHighlighted(row.type); + unifyFunctionTypes( + row.type.inputs, + parsedQuery.elems, + row.type.where_clause, + null, + (mgens, parts) => { + writeNamesToTypeParameters(mgens, row.type, parsedQuery); + for (const part of parts) { + part.highlighted = true; + } + return true; + } + ); // path_dist is 0 because no parent path information is currently stored // in the search index - addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance); + addIntoResults(results_in_args, fullId, pos, -1, 0, 0, maxEditDistance, true); } const returned = row.type && row.type.output && checkIfInList(row.type.output, elem, row.type.where_clause); if (returned) { - addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance); + // Generate type signature information for search result display. + if (!in_args) { + clearHighlighted(row.type); + unifyFunctionTypes( + row.type.output, + parsedQuery.elems, + row.type.where_clause, + null, + (mgens, parts) => { + writeNamesToTypeParameters(mgens, row.type, parsedQuery); + for (const part of parts) { + part.highlighted = true; + } + return true; + } + ); + } + addIntoResults(results_returned, fullId, pos, -1, 0, 0, maxEditDistance, true); } if (!typePassesFilter(elem.typeFilter, row.ty)) { @@ -1922,8 +2096,17 @@ function initSearch(rawSearchIndex) { } if (parsedQuery.literalSearch) { - if (searchWord === elem.name) { - addIntoResults(results_others, fullId, pos, index, 0, path_dist); + if (searchWord === elem.normalizedName) { + addIntoResults( + results_others, + fullId, + pos, + index, + 0, + path_dist, + -1, + false + ); } return; } @@ -1934,7 +2117,16 @@ function initSearch(rawSearchIndex) { return; } - addIntoResults(results_others, fullId, pos, index, dist, path_dist, maxEditDistance); + addIntoResults( + results_others, + fullId, + pos, + index, + dist, + path_dist, + maxEditDistance, + false + ); } /** @@ -1951,25 +2143,80 @@ function initSearch(rawSearchIndex) { return; } - // If the result is too "bad", we return false and it ends this search. if (!unifyFunctionTypes( row.type.inputs, parsedQuery.elems, row.type.where_clause, null, - mgens => { + (mgens, matchedPartsParams) => { return unifyFunctionTypes( row.type.output, parsedQuery.returned, row.type.where_clause, - mgens + mgens, + (completeMgens, matchedPartsReturn) => { + writeNamesToTypeParameters(completeMgens, row.type, parsedQuery); + clearHighlighted(row.type); + for (const part of matchedPartsParams) { + part.highlighted = true; + } + for (const part of matchedPartsReturn) { + part.highlighted = true; + } + return true; + } ); } )) { return; } - addIntoResults(results, row.id, pos, 0, 0, 0, Number.MAX_VALUE); + addIntoResults(results, row.id, pos, 0, 0, 0, Number.MAX_VALUE, true); + } + + function writeNamesToTypeParameters(mgens, rowType, parsedQuery) { + const queryTypeToName = new Map(); + function getQueryTypeFromList(list) { + if (!list) { + return; + } + for (const queryElem of list) { + if (queryElem.generics && queryElem.generics.length > 0) { + getQueryTypeFromList(queryElem.generics); + } + if (!queryTypeToName.has(queryElem.id)) { + queryTypeToName.set(queryElem.id, queryElem.name); + } + } + } + getQueryTypeFromList(parsedQuery.elems); + getQueryTypeFromList(parsedQuery.returned); + function assignFunctionNameFromList(list) { + if (!list) { + return; + } + for (const fnType of list) { + if (fnType.id < 0) { + assignFunctionNameFromList(rowType.where_clause[(-fnType.id) - 1]); + if (mgens.has(fnType.id)) { + const queryId = mgens.get(fnType.id); + if (queryTypeToName.has(queryId) && queryId < 0) { + const queryName = queryTypeToName.get(queryId); + fnType.name = queryName; + } else { + fnType.name = null; + } + } else { + fnType.name = null; + } + } + if (fnType.generics && fnType.generics.length > 0) { + assignFunctionNameFromList(fnType.generics); + } + } + } + assignFunctionNameFromList(rowType.inputs); + assignFunctionNameFromList(rowType.output); } function innerRunQuery() { @@ -1977,10 +2224,10 @@ function initSearch(rawSearchIndex) { let queryLen = 0; for (const elem of parsedQuery.elems) { - queryLen += elem.name.length; + queryLen += elem.normalizedName.length; } for (const elem of parsedQuery.returned) { - queryLen += elem.name.length; + queryLen += elem.normalizedName.length; } const maxEditDistance = Math.floor(queryLen / 3); @@ -2027,21 +2274,21 @@ function initSearch(rawSearchIndex) { if ((elem.id === null && parsedQuery.totalElems > 1 && elem.typeFilter === -1 && elem.generics.length === 0) || elem.typeFilter === TY_GENERIC) { - if (genericSymbols.has(elem.name)) { - elem.id = genericSymbols.get(elem.name); + if (genericSymbols.has(elem.normalizedName)) { + elem.id = genericSymbols.get(elem.normalizedName); } else { elem.id = -(genericSymbols.size + 1); - genericSymbols.set(elem.name, elem.id); + genericSymbols.set(elem.normalizedName, elem.id); } - if (elem.typeFilter === -1 && elem.name.length >= 3) { + if (elem.typeFilter === -1 && elem.normalizedName.length >= 3) { // Silly heuristic to catch if the user probably meant // to not write a generic parameter. We don't use it, // just bring it up. - const maxPartDistance = Math.floor(elem.name.length / 3); + const maxPartDistance = Math.floor(elem.normalizedName.length / 3); let matchDist = maxPartDistance + 1; let matchName = ""; for (const name of typeNameIdMap.keys()) { - const dist = editDistance(name, elem.name, maxPartDistance); + const dist = editDistance(name, elem.normalizedName, maxPartDistance); if (dist <= matchDist && dist <= maxPartDistance) { if (dist === matchDist && matchName > name) { continue; @@ -2051,7 +2298,7 @@ function initSearch(rawSearchIndex) { } } if (matchName !== "") { - parsedQuery.proposeCorrectionFrom = elem.name; + parsedQuery.proposeCorrectionFrom = elem.normalizedName; parsedQuery.proposeCorrectionTo = matchName; } } @@ -2061,7 +2308,7 @@ function initSearch(rawSearchIndex) { // Rust does not have HKT parsedQuery.error = [ "Generic type parameter ", - elem.name, + elem.normalizedName, " does not accept generic parameters", ]; } @@ -2098,10 +2345,22 @@ function initSearch(rawSearchIndex) { elem = parsedQuery.returned[0]; for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { row = searchIndex[i]; - in_returned = row.type && unifyFunctionTypes( + if (!row.type) { + continue; + } + in_returned = unifyFunctionTypes( row.type.output, parsedQuery.returned, - row.type.where_clause + row.type.where_clause, + null, + (mgens, parts) => { + writeNamesToTypeParameters(mgens, row.type, parsedQuery); + clearHighlighted(row.type); + for (const part of parts) { + part.highlighted = true; + } + return true; + } ); if (in_returned) { addIntoResults( @@ -2110,7 +2369,9 @@ function initSearch(rawSearchIndex) { i, -1, 0, - Number.MAX_VALUE + Number.MAX_VALUE, + undefined, + true ); } } @@ -2126,10 +2387,11 @@ function initSearch(rawSearchIndex) { innerRunQuery(); } + const isOthersTypeSearch = parsedQuery.foundElems > 1 || parsedQuery.returned.length > 0; const ret = createQueryResults( sortResults(results_in_args, true, currentCrate), sortResults(results_returned, true, currentCrate), - sortResults(results_others, false, currentCrate), + sortResults(results_others, isOthersTypeSearch, currentCrate), parsedQuery); handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates, currentCrate); if (parsedQuery.error !== null && ret.others.length !== 0) { @@ -2264,11 +2526,12 @@ function initSearch(rawSearchIndex) { /** * Render a set of search results for a single tab. - * @param {Array} array - The search results for this tab + * @param {Array} array - The search results for this tab * @param {ParsedQuery} query * @param {boolean} display - True if this is the active tab + * @param {boolean} isType - True shows the function signature */ - function addTab(array, query, display) { + function addTab(array, query, display, isType) { let extraClass = ""; if (display === true) { extraClass = " active"; @@ -2313,6 +2576,20 @@ ${item.displayPath}${name}\ const description = document.createElement("div"); description.className = "desc"; + if (isType) { + const displayTypeSignature = document.createElement("div"); + item.displayTypeSignature.forEach((value, index) => { + if (index % 2 !== 0) { + const highlight = document.createElement("strong"); + highlight.appendChild(document.createTextNode(value)); + displayTypeSignature.appendChild(highlight); + } else { + displayTypeSignature.appendChild(document.createTextNode(value)); + } + }); + displayTypeSignature.className = "type-signature"; + description.appendChild(displayTypeSignature); + } description.insertAdjacentHTML("beforeend", item.desc); link.appendChild(description); @@ -2392,9 +2669,24 @@ ${item.displayPath}${name}\ currentResults = results.query.userQuery; - const ret_others = addTab(results.others, results.query, true); - const ret_in_args = addTab(results.in_args, results.query, false); - const ret_returned = addTab(results.returned, results.query, false); + const ret_others = addTab( + results.others, + results.query, + true, + results.query.foundElems > 1 || results.query.returned.length > 0 + ); + const ret_in_args = addTab( + results.in_args, + results.query, + false, + true + ); + const ret_returned = addTab( + results.returned, + results.query, + false, + true + ); // 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 @@ -2426,6 +2718,9 @@ ${item.displayPath}${name}\ let output = `

Results${crates}

`; if (results.query.error !== null) { const error = results.query.error; + if (!error.forEach) { + throw error; + } error.forEach((value, index) => { value = value.split("<").join("<").split(">").join(">"); if (index % 2 !== 0) { @@ -2569,19 +2864,22 @@ ${item.displayPath}${name}\ * * @param {null|Array} types * @param {Array<{name: string, ty: number}>} lowercasePaths + * @param {Array<{name: string, ty: number}>} paths * * @return {Array} */ - function buildItemSearchTypeAll(types, lowercasePaths) { - return types.map(type => buildItemSearchType(type, lowercasePaths)); + function buildItemSearchTypeAll(types, lowercasePaths, paths) { + return types.map(type => buildItemSearchType(type, lowercasePaths, paths)); } /** * Converts a single type. * * @param {RawFunctionType} type + * @param {Array<{name: string, ty: number}>} lowercasePaths + * @param {Array<{name: string, ty: number}>} paths */ - function buildItemSearchType(type, lowercasePaths) { + function buildItemSearchType(type, lowercasePaths, paths) { const PATH_INDEX_DATA = 0; const GENERICS_DATA = 1; let pathIndex, generics; @@ -2592,7 +2890,8 @@ ${item.displayPath}${name}\ pathIndex = type[PATH_INDEX_DATA]; generics = buildItemSearchTypeAll( type[GENERICS_DATA], - lowercasePaths + lowercasePaths, + paths ); } if (pathIndex < 0) { @@ -2600,6 +2899,7 @@ ${item.displayPath}${name}\ // the actual names of generic parameters aren't stored, since they aren't API return { id: pathIndex, + name: null, ty: TY_GENERIC, path: null, generics, @@ -2609,6 +2909,7 @@ ${item.displayPath}${name}\ // `0` is used as a sentinel because it's fewer bytes than `null` return { id: null, + name: null, ty: null, path: null, generics, @@ -2617,6 +2918,7 @@ ${item.displayPath}${name}\ const item = lowercasePaths[pathIndex - 1]; return { id: buildTypeMapIndex(item.name), + name: paths[pathIndex - 1].name, ty: item.ty, path: item.path, generics, @@ -2635,11 +2937,11 @@ ${item.displayPath}${name}\ * * @param {RawFunctionSearchType} functionSearchType * @param {Array<{name: string, ty: number}>} lowercasePaths - * @param {Map} + * @param {Array<{name: string, ty: number}>} paths * * @return {null|FunctionSearchType} */ - function buildFunctionSearchType(functionSearchType, lowercasePaths) { + function buildFunctionSearchType(functionSearchType, lowercasePaths, paths) { const INPUTS_DATA = 0; const OUTPUT_DATA = 1; // `0` is used as a sentinel because it's fewer bytes than `null` @@ -2648,20 +2950,26 @@ ${item.displayPath}${name}\ } let inputs, output; if (typeof functionSearchType[INPUTS_DATA] === "number") { - inputs = [buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths)]; + inputs = [buildItemSearchType(functionSearchType[INPUTS_DATA], lowercasePaths, paths)]; } else { inputs = buildItemSearchTypeAll( functionSearchType[INPUTS_DATA], - lowercasePaths + lowercasePaths, + paths ); } if (functionSearchType.length > 1) { if (typeof functionSearchType[OUTPUT_DATA] === "number") { - output = [buildItemSearchType(functionSearchType[OUTPUT_DATA], lowercasePaths)]; + output = [buildItemSearchType( + functionSearchType[OUTPUT_DATA], + lowercasePaths, + paths + )]; } else { output = buildItemSearchTypeAll( functionSearchType[OUTPUT_DATA], - lowercasePaths + lowercasePaths, + paths ); } } else { @@ -2671,8 +2979,8 @@ ${item.displayPath}${name}\ const l = functionSearchType.length; for (let i = 2; i < l; ++i) { where_clause.push(typeof functionSearchType[i] === "number" - ? [buildItemSearchType(functionSearchType[i], lowercasePaths)] - : buildItemSearchTypeAll(functionSearchType[i], lowercasePaths)); + ? [buildItemSearchType(functionSearchType[i], lowercasePaths, paths)] + : buildItemSearchTypeAll(functionSearchType[i], lowercasePaths, paths)); } return { inputs, output, where_clause, @@ -2697,6 +3005,8 @@ ${item.displayPath}${name}\ typeNameIdOfArray = buildTypeMapIndex("array"); typeNameIdOfSlice = buildTypeMapIndex("slice"); typeNameIdOfArrayOrSlice = buildTypeMapIndex("[]"); + typeNameIdOfNever = buildTypeMapIndex("never"); + typeNameIdOfTuple = buildTypeMapIndex("tuple"); for (const crate in rawSearchIndex) { if (!hasOwnPropertyRustdoc(rawSearchIndex, crate)) { @@ -2856,7 +3166,8 @@ ${item.displayPath}${name}\ parent: itemParentIdxs[i] > 0 ? paths[itemParentIdxs[i] - 1] : undefined, type: buildFunctionSearchType( itemFunctionSearchTypes[i], - lowercasePaths + lowercasePaths, + paths ), id: id, normalizedName: word.indexOf("_") === -1 ? word : word.replace(/_/g, ""), diff --git a/src/tools/rustdoc-js/tester.js b/src/tools/rustdoc-js/tester.js index c7e6dd3615e94..c1930e59c2d4d 100644 --- a/src/tools/rustdoc-js/tester.js +++ b/src/tools/rustdoc-js/tester.js @@ -373,8 +373,32 @@ function loadSearchJS(doc_folder, resource_suffix) { return { doSearch: function(queryStr, filterCrate, currentCrate) { - return searchModule.execQuery(searchModule.parseQuery(queryStr), searchWords, + const results = searchModule.execQuery(searchModule.parseQuery(queryStr), searchWords, filterCrate, currentCrate); + for (const key in results) { + if (results[key]) { + for (const resultKey in results[key]) { + const entry = results[key][resultKey]; + if (!entry) { + continue; + } + if (entry.hasOwnProperty("displayTypeSignature") && + entry.displayTypeSignature !== null && + entry.displayTypeSignature instanceof Array + ) { + entry.displayTypeSignature.forEach((value, index) => { + if (index % 2 === 1) { + entry.displayTypeSignature[index] = "*" + value + "*"; + } else { + entry.displayTypeSignature[index] = value; + } + }); + entry.displayTypeSignature = entry.displayTypeSignature.join(""); + } + } + } + } + return results; }, getCorrections: function(queryStr, filterCrate, currentCrate) { const parsedQuery = searchModule.parseQuery(queryStr); diff --git a/tests/rustdoc-js/generics-impl.js b/tests/rustdoc-js/generics-impl.js index 5e33e224876fe..1e9d57dcf6e85 100644 --- a/tests/rustdoc-js/generics-impl.js +++ b/tests/rustdoc-js/generics-impl.js @@ -4,33 +4,61 @@ const EXPECTED = [ { 'query': 'Aaaaaaa -> u32', 'others': [ - { 'path': 'generics_impl::Aaaaaaa', 'name': 'bbbbbbb' }, + { + 'path': 'generics_impl::Aaaaaaa', + 'name': 'bbbbbbb', + 'displayTypeSignature': '*Aaaaaaa* -> *u32*' + }, ], }, { 'query': 'Aaaaaaa -> bool', 'others': [ - { 'path': 'generics_impl::Aaaaaaa', 'name': 'ccccccc' }, + { + 'path': 'generics_impl::Aaaaaaa', + 'name': 'ccccccc', + 'displayTypeSignature': '*Aaaaaaa* -> *bool*' + }, ], }, { 'query': 'Aaaaaaa -> usize', 'others': [ - { 'path': 'generics_impl::Aaaaaaa', 'name': 'read' }, + { + 'path': 'generics_impl::Aaaaaaa', + 'name': 'read', + 'displayTypeSignature': '*Aaaaaaa*, [] -> Result<*usize*>' + }, ], }, { 'query': 'Read -> u64', 'others': [ - { 'path': 'generics_impl::Ddddddd', 'name': 'eeeeeee' }, - { 'path': 'generics_impl::Ddddddd', 'name': 'ggggggg' }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'eeeeeee', + 'displayTypeSignature': 'impl *Read* -> *u64*' + }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'ggggggg', + 'displayTypeSignature': 'Ddddddd -> *u64*' + }, ], }, { 'query': 'trait:Read -> u64', 'others': [ - { 'path': 'generics_impl::Ddddddd', 'name': 'eeeeeee' }, - { 'path': 'generics_impl::Ddddddd', 'name': 'ggggggg' }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'eeeeeee', + 'displayTypeSignature': 'impl *Read* -> *u64*' + }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'ggggggg', + 'displayTypeSignature': 'Ddddddd -> *u64*' + }, ], }, { @@ -40,19 +68,31 @@ const EXPECTED = [ { 'query': 'bool -> u64', 'others': [ - { 'path': 'generics_impl::Ddddddd', 'name': 'fffffff' }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'fffffff', + 'displayTypeSignature': '*bool* -> *u64*' + }, ], }, { 'query': 'Ddddddd -> u64', 'others': [ - { 'path': 'generics_impl::Ddddddd', 'name': 'ggggggg' }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'ggggggg', + 'displayTypeSignature': '*Ddddddd* -> *u64*' + }, ], }, { 'query': '-> Ddddddd', 'others': [ - { 'path': 'generics_impl::Ddddddd', 'name': 'hhhhhhh' }, + { + 'path': 'generics_impl::Ddddddd', + 'name': 'hhhhhhh', + 'displayTypeSignature': '-> *Ddddddd*' + }, ], }, ]; diff --git a/tests/rustdoc-js/generics-match-ambiguity.js b/tests/rustdoc-js/generics-match-ambiguity.js index edce4268c5ac7..247e66697d5d9 100644 --- a/tests/rustdoc-js/generics-match-ambiguity.js +++ b/tests/rustdoc-js/generics-match-ambiguity.js @@ -8,15 +8,31 @@ const EXPECTED = [ { 'query': 'Wrap', 'in_args': [ - { 'path': 'generics_match_ambiguity', 'name': 'bar' }, - { 'path': 'generics_match_ambiguity', 'name': 'foo' }, + { + 'path': 'generics_match_ambiguity', + 'name': 'bar', + 'displayTypeSignature': '*Wrap*, Wrap' + }, + { + 'path': 'generics_match_ambiguity', + 'name': 'foo', + 'displayTypeSignature': '*Wrap*, Wrap' + }, ], }, { 'query': 'Wrap', 'in_args': [ - { 'path': 'generics_match_ambiguity', 'name': 'bar' }, - { 'path': 'generics_match_ambiguity', 'name': 'foo' }, + { + 'path': 'generics_match_ambiguity', + 'name': 'bar', + 'displayTypeSignature': '*Wrap*<*i32*, u32>, Wrap' + }, + { + 'path': 'generics_match_ambiguity', + 'name': 'foo', + 'displayTypeSignature': '*Wrap*<*i32*>, Wrap' + }, ], }, { diff --git a/tests/rustdoc-js/type-parameters.js b/tests/rustdoc-js/type-parameters.js index e695f189bb672..e637cb4fb254e 100644 --- a/tests/rustdoc-js/type-parameters.js +++ b/tests/rustdoc-js/type-parameters.js @@ -5,79 +5,79 @@ const EXPECTED = [ { query: '-> trait:Some', others: [ - { path: 'foo', name: 'alef' }, - { path: 'foo', name: 'alpha' }, + { path: 'foo', name: 'alef', displayTypeSignature: '-> impl *Some*' }, + { path: 'foo', name: 'alpha', displayTypeSignature: '-> impl *Some*' }, ], }, { query: '-> generic:T', others: [ - { path: 'foo', name: 'bet' }, - { path: 'foo', name: 'alef' }, - { path: 'foo', name: 'beta' }, + { path: 'foo', name: 'bet', displayTypeSignature: '_ -> *T*' }, + { path: 'foo', name: 'alef', displayTypeSignature: '-> *T*' }, + { path: 'foo', name: 'beta', displayTypeSignature: 'T -> *T*' }, ], }, { query: 'A -> B', others: [ - { path: 'foo', name: 'bet' }, + { path: 'foo', name: 'bet', displayTypeSignature: '*A* -> *B*' }, ], }, { query: 'A -> A', others: [ - { path: 'foo', name: 'beta' }, + { path: 'foo', name: 'beta', displayTypeSignature: '*A* -> *A*' }, ], }, { query: 'A, A', others: [ - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'alternate', displayTypeSignature: '*A*, *A*' }, ], }, { query: 'A, B', others: [ - { path: 'foo', name: 'other' }, + { path: 'foo', name: 'other', displayTypeSignature: '*A*, *B*' }, ], }, { query: 'Other, Other', others: [ - { path: 'foo', name: 'other' }, - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'other', displayTypeSignature: 'impl *Other*, impl *Other*' }, + { path: 'foo', name: 'alternate', displayTypeSignature: 'impl *Other*, impl *Other*' }, ], }, { query: 'generic:T', in_args: [ - { path: 'foo', name: 'bet' }, - { path: 'foo', name: 'beta' }, - { path: 'foo', name: 'other' }, - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'bet', displayTypeSignature: '*T* -> _' }, + { path: 'foo', name: 'beta', displayTypeSignature: '*T* -> T' }, + { path: 'foo', name: 'other', displayTypeSignature: '*T*, _' }, + { path: 'foo', name: 'alternate', displayTypeSignature: '*T*, T' }, ], }, { query: 'generic:Other', in_args: [ - { path: 'foo', name: 'bet' }, - { path: 'foo', name: 'beta' }, - { path: 'foo', name: 'other' }, - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'bet', displayTypeSignature: '*Other* -> _' }, + { path: 'foo', name: 'beta', displayTypeSignature: '*Other* -> Other' }, + { path: 'foo', name: 'other', displayTypeSignature: '*Other*, _' }, + { path: 'foo', name: 'alternate', displayTypeSignature: '*Other*, Other' }, ], }, { query: 'trait:Other', in_args: [ - { path: 'foo', name: 'other' }, - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'other', displayTypeSignature: '_, impl *Other*' }, + { path: 'foo', name: 'alternate', displayTypeSignature: 'impl *Other*, impl *Other*' }, ], }, { query: 'Other', in_args: [ - { path: 'foo', name: 'other' }, - { path: 'foo', name: 'alternate' }, + { path: 'foo', name: 'other', displayTypeSignature: '_, impl *Other*' }, + { path: 'foo', name: 'alternate', displayTypeSignature: 'impl *Other*, impl *Other*' }, ], }, {